guardia-messenger/app/(tabs)/index.tsx
DESKTOP-TKLFCPRython f29f525c77 refactor: 101.79.17.164 → zioinfo.co.kr 전체 도메인 변환 + Manager UI 배포
- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스)
- Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포
- 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:09:17 +09:00

160 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'
import { COLORS, PRIORITY_COLOR } from '../../constants/Config'
import { getDashboard, getLicenseStatus } from '../../services/api'
import { useAuth } from '../../hooks/useAuth'
interface Stats {
total_tasks: number
open_tasks: number
in_progress_tasks: number
completed_today: number
critical_count: number
pending_approvals: number
recent_tasks?: any[]
}
function StatCard({ icon, label, value, color }: { icon: string; label: string; value: number | string; color: string }) {
return (
<View style={[s.statCard, { borderLeftColor: color, borderLeftWidth: 3 }]}>
<Text style={s.statIcon}>{icon}</Text>
<Text style={s.statValue}>{value}</Text>
<Text style={s.statLabel}>{label}</Text>
</View>
)
}
export default function Dashboard() {
const { user } = useAuth()
const [stats, setStats] = useState<Stats | null>(null)
const [license, setLicense] = useState<any>(null)
const [loading, setLoading] = useState(true)
const [refresh, setRefresh] = useState(false)
const load = async (isRefresh = false) => {
if (isRefresh) setRefresh(true)
else setLoading(true)
try {
const [d, l] = await Promise.allSettled([getDashboard(), getLicenseStatus()])
if (d.status === 'fulfilled') setStats(d.value.data)
if (l.status === 'fulfilled') setLicense(l.value.data)
} catch {}
setLoading(false)
setRefresh(false)
}
useEffect(() => { load() }, [])
if (loading) return (
<View style={s.center}>
<ActivityIndicator size="large" color={COLORS.accent} />
<Text style={{ color: COLORS.muted, marginTop: 12 }}> ...</Text>
</View>
)
return (
<ScrollView
style={s.scroll}
refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} />}
>
{/* 인사말 */}
<View style={s.header}>
<Text style={s.greeting}>, {user?.display_name ?? user?.username} 👋</Text>
<Text style={s.subGreet}>GUARDiA ITSM </Text>
</View>
{/* 라이선스 배너 */}
{license?.upgrade_banner?.show && (
<View style={s.licenseBanner}>
<Text style={s.licenseBannerText}> {license.upgrade_banner.message}</Text>
</View>
)}
{license?.valid && (
<View style={s.licenseBar}>
<Text style={s.licenseEdition}>{license.edition}</Text>
<Text style={s.licenseDays}>{license.days_remaining} </Text>
</View>
)}
{/* 통계 카드 */}
<View style={s.statsGrid}>
<StatCard icon="📋" label="전체 SR" value={stats?.total_tasks ?? '-'} color={COLORS.accent} />
<StatCard icon="🔄" label="진행 중" value={stats?.in_progress_tasks ?? '-'} color={COLORS.warning} />
<StatCard icon="🚨" label="긴급" value={stats?.critical_count ?? '-'} color={COLORS.danger} />
<StatCard icon="✅" label="오늘 완료" value={stats?.completed_today ?? '-'} color={COLORS.success} />
</View>
{/* 최근 SR */}
{stats?.recent_tasks && stats.recent_tasks.length > 0 && (
<View style={s.section}>
<Text style={s.sectionTitle}>📋 </Text>
{stats.recent_tasks.slice(0, 5).map((sr: any) => (
<View key={sr.id} style={s.srItem}>
<View style={[s.priorityDot, { backgroundColor: PRIORITY_COLOR[sr.priority] ?? COLORS.muted }]} />
<View style={{ flex: 1 }}>
<Text style={s.srTitle} numberOfLines={1}>{sr.title}</Text>
<Text style={s.srMeta}>{sr.sr_id} · {sr.status}</Text>
</View>
</View>
))}
</View>
)}
{/* 빠른 실행 */}
<View style={s.section}>
<Text style={s.sectionTitle}> </Text>
<View style={s.quickRow}>
{[
{ icon: '📝', label: 'SR 등록' },
{ icon: '🤖', label: 'AI 질문' },
{ icon: '📊', label: '리포트' },
{ icon: '🔒', label: '감사로그' },
].map(q => (
<TouchableOpacity key={q.label} style={s.quickBtn}>
<Text style={s.quickIcon}>{q.icon}</Text>
<Text style={s.quickLabel}>{q.label}</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={{ height: 24 }} />
</ScrollView>
)
}
const s = StyleSheet.create({
scroll: { flex: 1, backgroundColor: COLORS.bg },
center: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: COLORS.bg },
header: { backgroundColor: COLORS.primary, padding: 24, paddingTop: 16 },
greeting: { fontSize: 20, fontWeight: '700', color: '#fff' },
subGreet: { fontSize: 13, color: '#aac4e8', marginTop: 4 },
licenseBanner: { backgroundColor: '#fff3cd', padding: 12, marginHorizontal: 16, marginTop: 12,
borderRadius: 8, borderLeftWidth: 3, borderLeftColor: COLORS.warning },
licenseBannerText: { fontSize: 12, color: '#856404' },
licenseBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
backgroundColor: COLORS.light, marginHorizontal: 16, marginTop: 8,
paddingHorizontal: 14, paddingVertical: 8, borderRadius: 8 },
licenseEdition: { fontSize: 12, fontWeight: '700', color: COLORS.accent },
licenseDays: { fontSize: 12, color: COLORS.muted },
statsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 12, gap: 8 },
statCard: { width: '47%', backgroundColor: '#fff', borderRadius: 10, padding: 14,
shadowColor: '#000', shadowOffset: { width: 0, height: 2 },
shadowOpacity: .06, shadowRadius: 6, elevation: 2 },
statIcon: { fontSize: 22, marginBottom: 6 },
statValue: { fontSize: 26, fontWeight: '800', color: COLORS.text },
statLabel: { fontSize: 11, color: COLORS.muted, marginTop: 2 },
section: { backgroundColor: '#fff', marginHorizontal: 16, marginTop: 12,
borderRadius: 12, padding: 16, elevation: 1 },
sectionTitle: { fontSize: 14, fontWeight: '700', color: COLORS.text, marginBottom: 12 },
srItem: { flexDirection: 'row', alignItems: 'center', gap: 10,
paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
priorityDot: { width: 8, height: 8, borderRadius: 4 },
srTitle: { fontSize: 13, fontWeight: '500', color: COLORS.text },
srMeta: { fontSize: 11, color: COLORS.muted, marginTop: 2 },
quickRow: { flexDirection: 'row', justifyContent: 'space-around' },
quickBtn: { alignItems: 'center', padding: 12 },
quickIcon: { fontSize: 28, marginBottom: 4 },
quickLabel: { fontSize: 11, color: COLORS.muted },
})