guardia-messenger/app/(tabs)/index.tsx
DESKTOP-TKLFCPRython 278f9639c9 feat(app): Messenger app Variant design applied
Config.ts:
- COLORS: accent #4f6ef7 -> #00A0C8(cyan), primary #003366(navy)
- gnbBg: deeper navy #001530

_layout.tsx:
- TabBar: elevated shadow, cyan active tint, bolder label

index.tsx (Dashboard):
- StatCard: top color bar + icon box (screenshot9 pattern)
- Header: deep navy gradient rounded bottom
- QuickBtn: bg-light card style
- Section: deeper shadow, navy title

login.tsx:
- Background: deep navy #001530
- Card: white + strong shadow
- Button: solid cyan with shadow
- Label: cyan uppercase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 20:25:47 +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 },
})