/** * 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({}) 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 ( 🤖 오늘의 AI 브리핑 {loading ? ( ) : ( <> {summary} )} ) } function Stat({ label, value, color }: { label: string; value: number; color: string }) { return ( {value} {label} ) } 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 }, })