136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
/**
|
|
* DailySummary (#26) — 일일 AI 요약 카드
|
|
*
|
|
* GET /api/tasks/stats/mine?period=today → Ollama로 자연어 요약 생성.
|
|
* 매일 1회 갱신 (SecureStore 캐시, 날짜 키 비교). 홈 대시보드 상단 카드.
|
|
*/
|
|
import { useState, useEffect } from 'react'
|
|
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'
|
|
import * as SecureStore from 'expo-secure-store'
|
|
import { COLORS, API_BASE } from '../constants/Config'
|
|
import { authFetch } from '../utils/auth'
|
|
import { generate, DEFAULT_TEXT_MODEL } from '../lib/ollama'
|
|
|
|
const CACHE_KEY = 'grd_daily_summary'
|
|
|
|
interface Stats {
|
|
processed?: number
|
|
pending?: number
|
|
sla_risk?: number
|
|
}
|
|
|
|
function todayKey(): string {
|
|
return new Date().toISOString().slice(0, 10)
|
|
}
|
|
|
|
export function DailySummary() {
|
|
const [summary, setSummary] = useState('')
|
|
const [stats, setStats] = useState<Stats>({})
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
let alive = true
|
|
;(async () => {
|
|
// 1) 캐시 확인 (오늘 날짜)
|
|
try {
|
|
const cached = await SecureStore.getItemAsync(CACHE_KEY)
|
|
if (cached) {
|
|
const c = JSON.parse(cached)
|
|
if (c.date === todayKey() && c.summary) {
|
|
if (alive) {
|
|
setSummary(c.summary)
|
|
setStats(c.stats ?? {})
|
|
setLoading(false)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
} catch {
|
|
/* 캐시 무시 */
|
|
}
|
|
|
|
// 2) 통계 조회
|
|
let s: Stats = {}
|
|
try {
|
|
const res = await authFetch(`${API_BASE}/api/tasks/stats/mine?period=today`)
|
|
if (res.ok) {
|
|
const d = await res.json()
|
|
s = {
|
|
processed: d.processed ?? d.completed ?? d.done ?? 0,
|
|
pending: d.pending ?? d.open ?? d.in_progress ?? 0,
|
|
sla_risk: d.sla_risk ?? d.at_risk ?? 0,
|
|
}
|
|
}
|
|
} catch {
|
|
/* 통계 실패 → 기본값 */
|
|
}
|
|
if (alive) setStats(s)
|
|
|
|
// 3) Ollama 요약
|
|
const prompt =
|
|
`다음 ITSM 일일 통계를 운영자에게 보고하듯 한국어 한 문장으로 요약하세요. ` +
|
|
`처리: ${s.processed ?? 0}건, 미처리: ${s.pending ?? 0}건, SLA 위험: ${s.sla_risk ?? 0}건. ` +
|
|
`격려 한마디 포함.`
|
|
const aiText = await generate(DEFAULT_TEXT_MODEL, prompt)
|
|
const finalText =
|
|
aiText ||
|
|
`오늘 처리 ${s.processed ?? 0}건, 미처리 ${s.pending ?? 0}건, SLA 위험 ${s.sla_risk ?? 0}건입니다.`
|
|
if (alive) {
|
|
setSummary(finalText)
|
|
setLoading(false)
|
|
}
|
|
// 4) 캐시 저장
|
|
try {
|
|
await SecureStore.setItemAsync(
|
|
CACHE_KEY,
|
|
JSON.stringify({ date: todayKey(), summary: finalText, stats: s }),
|
|
)
|
|
} catch {
|
|
/* 캐시 저장 실패 무시 */
|
|
}
|
|
})()
|
|
return () => {
|
|
alive = false
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<View style={S.wrap}>
|
|
<Text style={S.title}>🤖 오늘의 AI 브리핑</Text>
|
|
{loading ? (
|
|
<ActivityIndicator color="#fff" style={{ marginTop: 10 }} />
|
|
) : (
|
|
<>
|
|
<Text style={S.summary}>{summary}</Text>
|
|
<View style={S.statsRow}>
|
|
<Stat label="처리" value={stats.processed ?? 0} color="#7ee787" />
|
|
<Stat label="미처리" value={stats.pending ?? 0} color="#ffd866" />
|
|
<Stat label="SLA 위험" value={stats.sla_risk ?? 0} color="#ff7b72" />
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function Stat({ label, value, color }: { label: string; value: number; color: string }) {
|
|
return (
|
|
<View style={S.stat}>
|
|
<Text style={[S.statVal, { color }]}>{value}</Text>
|
|
<Text style={S.statLabel}>{label}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export default DailySummary
|
|
|
|
const S = StyleSheet.create({
|
|
wrap: { backgroundColor: COLORS.gnbBg, borderRadius: 16, padding: 16, margin: 12 },
|
|
title: { fontSize: 13, fontWeight: '700', color: 'rgba(255,255,255,0.85)' },
|
|
summary: { fontSize: 14, color: '#fff', lineHeight: 20, marginTop: 8 },
|
|
statsRow: { flexDirection: 'row', marginTop: 14, gap: 8 },
|
|
stat: { flex: 1, backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 10, paddingVertical: 10, alignItems: 'center' },
|
|
statVal: { fontSize: 20, fontWeight: '800' },
|
|
statLabel: { fontSize: 11, color: 'rgba(255,255,255,0.7)', marginTop: 2 },
|
|
})
|