guardia-messenger/components/DailySummary.tsx

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 },
})