zioinfo-mail/workspace/guardia-messenger/app/(tabs)/index.tsx
DESKTOP-TKLFCPR\ython 777e027553 refactor(structure): move app -> workspace/guardia-messenger
Permission denied on git mv, used robocopy instead.

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

194 lines
7.8 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[]
}
/* Variant 스타일 StatCard — screenshot9 패턴 */
function StatCard({ icon, label, value, color }: { icon: string; label: string; value: number | string; color: string }) {
return (
<View style={[s.statCard, { borderTopColor: color, borderTopWidth: 3 }]}>
{/* 아이콘 박스 — 연파랑 컨테이너 */}
<View style={[s.statIconBox, { backgroundColor: color + '18' }]}>
<Text style={s.statIcon}>{icon}</Text>
</View>
{/* 라벨 (위) */}
<Text style={s.statLabel}>{label}</Text>
{/* 수치 (아래) */}
<Text style={[s.statValue, { color }]}>{value}</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 },
/* 헤더 — Variant 딥네이비 그라디언트 */
header: {
backgroundColor: '#001530',
padding: 24, paddingTop: 20,
borderBottomLeftRadius: 20, borderBottomRightRadius: 20,
},
greeting: { fontSize: 20, fontWeight: '800', color: '#fff', letterSpacing: -0.5 },
subGreet: { fontSize: 13, color: 'rgba(0,160,200,.85)', marginTop: 4 },
licenseBanner: { backgroundColor: '#fff3cd', padding: 12, marginHorizontal: 16, marginTop: 12,
borderRadius: 10, borderLeftWidth: 3, borderLeftColor: COLORS.warning },
licenseBannerText: { fontSize: 12, color: '#856404' },
licenseBar: {
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
backgroundColor: COLORS.light, marginHorizontal: 16, marginTop: 10,
paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10,
borderWidth: 1, borderColor: 'rgba(0,160,200,.2)',
},
licenseEdition: { fontSize: 12, fontWeight: '700', color: COLORS.accent },
licenseDays: { fontSize: 12, color: COLORS.muted },
/* 통계 그리드 */
statsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 14, gap: 10 },
/* Variant StatCard — 상단 컬러 바 + 아이콘 박스 */
statCard: {
width: '47%', backgroundColor: '#fff', borderRadius: 12, padding: 14,
shadowColor: '#003366', shadowOffset: { width: 0, height: 2 },
shadowOpacity: .08, shadowRadius: 8, elevation: 3,
},
statIconBox: { width: 40, height: 40, borderRadius: 10, marginBottom: 10,
alignItems: 'center', justifyContent: 'center' },
statIcon: { fontSize: 20 },
statValue: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 },
statLabel: { fontSize: 11, fontWeight: '600', color: COLORS.muted,
letterSpacing: 0.3, textTransform: 'uppercase', marginBottom: 4 },
/* 섹션 */
section: {
backgroundColor: '#fff', marginHorizontal: 16, marginTop: 12,
borderRadius: 14, padding: 16,
shadowColor: '#003366', shadowOffset: { width: 0, height: 2 },
shadowOpacity: .05, shadowRadius: 6, elevation: 2,
},
sectionTitle: {
fontSize: 14, fontWeight: '800', color: COLORS.primary,
marginBottom: 12, letterSpacing: -0.3,
},
srItem: { flexDirection: 'row', alignItems: 'center', gap: 10,
paddingVertical: 9, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
priorityDot: { width: 8, height: 8, borderRadius: 4 },
srTitle: { fontSize: 13, fontWeight: '600', color: COLORS.text },
srMeta: { fontSize: 11, color: COLORS.muted, marginTop: 2 },
/* 빠른 실행 */
quickRow: { flexDirection: 'row', justifyContent: 'space-around' },
quickBtn: {
alignItems: 'center', padding: 12,
backgroundColor: COLORS.bg, borderRadius: 12,
flex: 1, marginHorizontal: 3,
},
quickIcon: { fontSize: 26, marginBottom: 5 },
quickLabel: { fontSize: 11, fontWeight: '600', color: COLORS.primary },
})