import { useEffect, useMemo, useState } from 'react'; import { Activity, BarChart3, Car, ChevronRight, CircleAlert, Loader2, Users, Wifi, type LucideIcon } from 'lucide-react'; import { PageHeader } from '../../components/PageHeader'; import { useToast } from '../../components/ToastProvider'; import { listInstructorChatThreads, type InstructorChatThread } from '../../lib/chat'; import { listInstructorGroups, type InstructorGroup, type InstructorGroupsSummary } from '../../lib/groups'; import { listInstructorPlatforms, type InstructorPlatform, type InstructorPlatformsSummary } from '../../lib/platforms'; import type { AppRoute } from '../../routes'; import type { AuthSession } from '../../types/auth'; const emptyPlatformSummary: InstructorPlatformsSummary = { totalPlatforms: 0, activePlatforms: 0, standbyPlatforms: 0, errorPlatforms: 0, }; const emptyGroupSummary: InstructorGroupsSummary = { totalGroups: 0, assignedPlatforms: 0, totalStudents: 0, averageProgress: 0, }; export function DashboardPage({ session, selectedCourseId, onNavigate, onOpenChatThread }: { session: AuthSession; selectedCourseId: string; onNavigate?: (route: AppRoute) => void; onOpenChatThread?: (threadId: string) => void }) { const { showToast } = useToast(); const [platforms, setPlatforms] = useState([]); const [platformSummary, setPlatformSummary] = useState(emptyPlatformSummary); const [groups, setGroups] = useState([]); const [groupSummary, setGroupSummary] = useState(emptyGroupSummary); const [threads, setThreads] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); setError(null); Promise.allSettled([ listInstructorPlatforms(session.accessToken), listInstructorGroups(session.accessToken, selectedCourseId || null), listInstructorChatThreads(session.accessToken), ]) .then(([platformResult, groupResult, chatResult]) => { if (cancelled) return; if (platformResult.status === 'fulfilled') { setPlatforms(platformResult.value.platforms); setPlatformSummary(platformResult.value.summary); } else { setPlatforms([]); setPlatformSummary(emptyPlatformSummary); } if (groupResult.status === 'fulfilled') { setGroups(groupResult.value.groups); setGroupSummary(groupResult.value.summary); } else { setGroups([]); setGroupSummary(emptyGroupSummary); } if (chatResult.status === 'fulfilled') { setThreads(chatResult.value); } else { setThreads([]); } const failed = [platformResult, groupResult].filter((result) => result.status === 'rejected'); setError(failed.length > 0 ? '대시보드 일부 데이터를 불러오지 못했습니다.' : null); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [session.accessToken, selectedCourseId]); const waitingThreads = useMemo(() => threads.filter((thread) => thread.status === 'WAITING_INSTRUCTOR'), [threads]); const activeUsers = useMemo(() => { const groupActiveUsers = groups.reduce((sum, group) => sum + group.activeUsers, 0); if (groupActiveUsers > 0) return groupActiveUsers; return platforms.filter((platform) => platform.user).length; }, [groups, platforms]); const metrics = [ { label: '활성 플랫폼', value: `${platformSummary.activePlatforms}/${platformSummary.totalPlatforms}`, icon: Activity, tone: 'bg-[#f0efff] text-[#4f46e5]' }, { label: '총 수강생', value: `${groupSummary.totalStudents}명`, icon: Users, tone: 'bg-[#eff6ff] text-[#2563eb]' }, { label: '현재 접속', value: `${activeUsers}명`, icon: Wifi, tone: 'bg-[#f8fafc] text-[#64748b]' }, { label: '평균 진도', value: `${groupSummary.averageProgress}%`, icon: BarChart3, tone: 'bg-[#fff7ed] text-[#f97316]' }, ]; return (
{error ?

{error}

: null}
{metrics.map((metric) => ( ))}

플랫폼 실시간 상태

하드웨어 모니터링

{loading ? ( ) : platforms.length > 0 ? ( platforms.map((platform) => ( { showToast({ title: '긴급 정지 요청이 전송되었습니다.', message: platform.name, variant: 'error', }); }} /> )) ) : ( )}

그룹 진행 요약

그룹 구성, 플랫폼 배정, 진행률 조회

{loading ? ( ) : groups.length > 0 ? ( groups.map((group) => ( )) ) : ( )}

AI 가이드 에스컬레이션

강사 지원이 필요한 질문

{waitingThreads.length} 건
{loading ? ( ) : waitingThreads.length > 0 ? ( waitingThreads.slice(0, 5).map((thread) => ( onOpenChatThread?.(thread.id)} /> )) ) : ( )}
); } function PlatformStatusCard({ platform, onEmergencyStop }: { platform: InstructorPlatform; onEmergencyStop: () => void }) { return (

{platform.name}

{platform.status}

{platform.user ? `사용자: ${platform.user} 배터리: ${platform.battery}% 속도: ${platform.speed}` : `배터리: ${platform.battery}%`}

{platform.emergencyStop ? ( ) : null}
); } function GroupSummaryCard({ group }: { group: InstructorGroup }) { return (

{group.name}

{group.status}

{group.members.length}명 | {group.week}

{group.platform}
진행률 {group.progress}%
완료 {group.completedMissions} 활동 0 ? 'text-[#4f46e5]' : 'text-[#94a3b8]'}>{group.activeUsers}
); } function EscalationRow({ thread, onRespond }: { thread: InstructorChatThread; onRespond?: () => void }) { return (

{thread.studentName}

{thread.groupName ?? (thread.week ? `${thread.week}주차` : '미배정')}

{formatRelativeTime(thread.lastMessageAt)}

{thread.lastMessage}

); } function MetricCard({ label, value, icon: Icon, tone, loading = false }: { label: string; value: string; icon: LucideIcon; tone: string; loading?: boolean }) { return (

{label}

{loading ? '-' : value}

); } function LoadingState({ message }: { message: string }) { return (
); } function EmptyState({ message }: { message: string }) { return (

{message}

); } function statusTone(status: string) { if (status === '활성') return 'bg-[#f0efff] text-[#4f46e5]'; if (status === '오류') return 'bg-[#fff1f2] text-[#e11d48]'; return 'bg-[#f1f5f9] text-[#64748b]'; } function formatRelativeTime(value: string | undefined) { if (!value) return '-'; const time = new Date(value).getTime(); if (Number.isNaN(time)) return '-'; const diffMinutes = Math.max(0, Math.floor((Date.now() - time) / 60000)); if (diffMinutes < 1) return '방금 전'; if (diffMinutes < 60) return `${diffMinutes}분 전`; const diffHours = Math.floor(diffMinutes / 60); if (diffHours < 24) return `${diffHours}시간 전`; const diffDays = Math.floor(diffHours / 24); return `${diffDays}일 전`; }