import { useEffect, useState, useRef } from 'react' import { View, Text, ScrollView, StyleSheet, TouchableOpacity, RefreshControl, Animated } from 'react-native' import { COLORS } from '../../constants/Config' import { getNotifications, markNotificationRead } from '../../services/api' import { useWebSocket } from '../../hooks/useWebSocket' const TYPE_ICON: Record = { SR_CREATED:'πŸ“‹', SR_UPDATED:'πŸ”„', SR_COMPLETED:'βœ…', INCIDENT:'🚨', SLA_BREACH:'⏰', DEPLOY_SUCCESS:'πŸš€', DEPLOY_FAIL:'❌', LICENSE:'πŸ”‘', SYSTEM:'βš™οΈ', DEFAULT:'πŸ””', sr_created:'πŸ“‹', sr_updated:'πŸ”„', sr_completed:'βœ…', deploy_notify:'πŸš€', incident_created:'🚨', sla_breach:'⏰', anomaly_alert:'⚠️', batch_notify:'βš™οΈ', } interface NotifItem { id: string | number; type: string; title: string message: string; is_read: boolean; created_at: string source: 'api' | 'ws' } const SAMPLE: NotifItem[] = [ { id:'s1', type:'SR_CREATED', title:'[μƒ˜ν”Œ] SR μ‹ κ·œ μ ‘μˆ˜', message:'μ„œλ²„ μž¬μ‹œμž‘ μš”μ²­μ΄ μ ‘μˆ˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', is_read:false, created_at:new Date().toISOString(), source:'api' }, { id:'s2', type:'DEPLOY_SUCCESS', title:'[μƒ˜ν”Œ] 배포 μ™„λ£Œ', message:'zioinfo-web 배포가 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', is_read:true, created_at:new Date(Date.now()-3600000).toISOString(), source:'api' }, ] const fmtTitle = (t: string) => ({ sr_created:'SR μ‹ κ·œ μ ‘μˆ˜', sr_updated:'SR μƒνƒœ λ³€κ²½', sr_completed:'SR μ™„λ£Œ', deploy_notify:'배포 μ•Œλ¦Ό', incident_created:'μΈμ‹œλ˜νŠΈ λ°œμƒ', sla_breach:'SLA μœ„λ°˜ κ²½κ³ ', SR_CREATED:'SR μ‹ κ·œ μ ‘μˆ˜', INCIDENT:'μΈμ‹œλ˜νŠΈ', SLA_BREACH:'SLA μœ„λ°˜', DEPLOY_SUCCESS:'배포 성곡', DEPLOY_FAIL:'배포 μ‹€νŒ¨', } as Record)[t] ?? t const fmtMsg = (evt: { event_type:string; data:Record }) => { const d = evt.data ?? {} if (d.title) return String(d.title) if (d.message) return String(d.message) if (d.sr_id) return `SR ${d.sr_id}: ${d.status ?? ''}` return `${evt.event_type} 이벀트 μˆ˜μ‹ ` } export default function NotificationsScreen() { const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const [refresh, setRefresh] = useState(false) const pulseAnim = useRef(new Animated.Value(1)).current const { connected, lastEvent, events: wsEvents } = useWebSocket(['sr','deploy','sla','incident']) useEffect(() => { if (connected) { Animated.loop(Animated.sequence([ Animated.timing(pulseAnim, { toValue:0.4, duration:900, useNativeDriver:true }), Animated.timing(pulseAnim, { toValue:1.0, duration:900, useNativeDriver:true }), ])).start() } else { pulseAnim.setValue(1) } }, [connected]) useEffect(() => { if (!lastEvent) return const item: NotifItem = { id:`ws-${Date.now()}`, type:lastEvent.event_type, title:fmtTitle(lastEvent.event_type), message:fmtMsg(lastEvent), is_read:false, created_at:(lastEvent.timestamp as string) || new Date().toISOString(), source:'ws', } setItems(prev => [item, ...prev]) }, [lastEvent]) const load = async (r = false) => { r ? setRefresh(true) : setLoading(true) try { const res = await getNotifications() const api: NotifItem[] = (res.data.content ?? res.data.items ?? res.data ?? []) .map((n: any) => ({ ...n, source:'api' as const })) setItems(prev => [...prev.filter(i => i.source==='ws'), ...api]) } catch { if (!r) setItems(prev => prev.length===0 ? SAMPLE : prev) } finally { setLoading(false); setRefresh(false) } } useEffect(() => { load() }, []) const markRead = async (id: string | number) => { setItems(prev => prev.map(n => n.id===id ? {...n, is_read:true} : n)) if (!String(id).startsWith('ws-')) try { await markNotificationRead(Number(id)) } catch {} } const unread = items.filter(n => !n.is_read).length return ( μ•Œλ¦Ό {connected ? 'GUARDiA μ‹€μ‹œκ°„ μ—°κ²°' : 'μ—°κ²° 쀑...'} {unread>0 && {unread}} {wsEvents.length>0 && wsEvents[0] && ( {TYPE_ICON[wsEvents[0]?.event_type] ?? '⚑'} ⚑ μ‹€μ‹œκ°„: {fmtTitle(wsEvents[0]?.event_type ?? '')} {fmtMsg(wsEvents[0])} )} load(true)} />}> {!loading && items.length===0 && ( πŸ”” μ•Œλ¦Όμ΄ μ—†μŠ΅λ‹ˆλ‹€. SR 등둝 λ˜λŠ” 배포 μ‹€ν–‰ μ‹œ{'\n'}μ•Œλ¦Όμ΄ ν‘œμ‹œλ©λ‹ˆλ‹€. )} {items.map(n => ( markRead(n.id)}> {TYPE_ICON[n.type] ?? TYPE_ICON.DEFAULT} {n.source==='ws' && } {n.title} {n.message} {new Date(n.created_at).toLocaleString('ko-KR')} {n.source==='ws' && ⚑ μ‹€μ‹œκ°„} {!n.is_read && } ))} ) } const s = StyleSheet.create({ header: { flexDirection:'row', alignItems:'center', justifyContent:'space-between', padding:16, backgroundColor:'#fff', borderBottomWidth:1, borderBottomColor:COLORS.border }, title: { fontSize:17, fontWeight:'700', color:COLORS.text }, wsRow: { flexDirection:'row', alignItems:'center', gap:5, marginTop:3 }, dot: { width:7, height:7, borderRadius:4 }, wsLabel: { fontSize:11, fontWeight:'500' }, badge: { backgroundColor:COLORS.danger, borderRadius:10, paddingHorizontal:7, paddingVertical:2 }, badgeText: { color:'#fff', fontSize:11, fontWeight:'700' }, wsBanner: { flexDirection:'row', alignItems:'center', gap:10, padding:12, backgroundColor:'#eff2ff', borderBottomWidth:1, borderBottomColor:'#c7d2fe' }, wsIcon: { fontSize:20 }, wsBannerT: { fontSize:12, fontWeight:'700', color:COLORS.accent }, wsBannerM: { fontSize:11, color:COLORS.muted }, item: { flexDirection:'row', backgroundColor:'#fff', padding:14, borderBottomWidth:1, borderBottomColor:'#f1f5f9', gap:10, alignItems:'flex-start' }, unread: { backgroundColor:'#f8f9ff' }, icon: { fontSize:24, marginTop:2 }, wsPin: { position:'absolute', top:-2, right:-4, width:8, height:8, borderRadius:4, backgroundColor:COLORS.accent }, iT: { fontSize:14, color:COLORS.text }, iM: { fontSize:12, color:COLORS.muted, marginTop:3, lineHeight:17 }, iTime: { fontSize:11, color:COLORS.muted }, readDot: { width:8, height:8, borderRadius:4, backgroundColor:COLORS.accent, marginTop:6 }, empty: { alignItems:'center', marginTop:60, gap:10, paddingHorizontal:32 }, emptyT: { fontSize:15, fontWeight:'600', color:COLORS.muted }, emptyH: { fontSize:12, color:COLORS.muted, textAlign:'center', lineHeight:18 }, })