import { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react'; import { createPortal } from 'react-dom'; import { BarChart3, CircleAlert, GitBranch, Loader2, Plus, Search, Trash2, UserRound, Wifi, X, type LucideIcon } from 'lucide-react'; import { PageHeader } from '../../components/PageHeader'; import { PaginationBar } from '../../components/PaginationBar'; import { useToast } from '../../components/ToastProvider'; import { listInstructorGroups, type InstructorGroup } from '../../lib/groups'; import { createInstructorStudent, listInstructorStudents, removeInstructorStudent, type InstructorStudent, type InstructorStudentsSummary } from '../../lib/students'; import type { AuthSession } from '../../types/auth'; const emptySummary: InstructorStudentsSummary = { totalStudents: 0, onlineStudents: 0, averageProgress: 0, unresolvedIssues: 0, }; export function StudentsPage({ session, selectedCourseId }: { session: AuthSession; selectedCourseId: string }) { const { showToast } = useToast(); const [students, setStudents] = useState([]); const [summary, setSummary] = useState(emptySummary); const [query, setQuery] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [studentModalOpen, setStudentModalOpen] = useState(false); const [newStudentEmail, setNewStudentEmail] = useState(''); const [newStudentName, setNewStudentName] = useState(''); const [newStudentGroupId, setNewStudentGroupId] = useState('unassigned'); const [studentSaving, setStudentSaving] = useState(false); const [studentFormError, setStudentFormError] = useState(null); const [removingStudentId, setRemovingStudentId] = useState(null); const [removeTarget, setRemoveTarget] = useState(null); const [issueTarget, setIssueTarget] = useState(null); const loadStudents = useCallback(async () => { setLoading(true); setError(null); try { const payload = await listInstructorStudents(session.accessToken, selectedCourseId || null); setStudents(payload.students); setSummary(payload.summary); } catch (reason) { setStudents([]); setSummary(emptySummary); setError(reason instanceof Error ? reason.message : '수강생 현황을 불러오지 못했습니다.'); } finally { setLoading(false); } }, [session.accessToken, selectedCourseId]); useEffect(() => { let cancelled = false; loadStudents(); listInstructorGroups(session.accessToken, selectedCourseId || null) .then((payload) => { if (!cancelled) setGroups(payload.groups); }) .catch(() => { if (!cancelled) setGroups([]); }); return () => { cancelled = true; }; }, [loadStudents, session.accessToken, selectedCourseId]); const filteredStudents = useMemo(() => { const keyword = query.trim().toLowerCase(); if (!keyword) return students; return students.filter((student) => [student.name, student.email, student.group, student.week, student.courseTitle] .filter(Boolean) .join(' ') .toLowerCase() .includes(keyword), ); }, [query, students]); const pagedStudents = useMemo(() => filteredStudents.slice((page - 1) * pageSize, page * pageSize), [filteredStudents, page, pageSize]); useEffect(() => { setPage(1); }, [query]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(filteredStudents.length / pageSize)); if (page > totalPages) setPage(totalPages); }, [filteredStudents.length, page, pageSize]); useEffect(() => { if (!studentModalOpen && !removeTarget && !issueTarget) return; const originalOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = originalOverflow; }; }, [issueTarget, removeTarget, studentModalOpen]); const handlePageSizeChange = (nextPageSize: number) => { setPageSize(nextPageSize); setPage(1); }; const openStudentModal = () => { setNewStudentEmail(''); setNewStudentName(''); setNewStudentGroupId('unassigned'); setStudentFormError(null); setStudentModalOpen(true); }; const closeStudentModal = () => { if (studentSaving) return; setStudentModalOpen(false); }; const handleCreateStudent = async (event: FormEvent) => { event.preventDefault(); const email = newStudentEmail.trim(); const name = newStudentName.trim(); if (!email || !name) { setStudentFormError('이메일과 이름을 입력해 주세요.'); return; } setStudentSaving(true); setStudentFormError(null); try { const payload = await createInstructorStudent(session.accessToken, { courseId: selectedCourseId || null, email, name, groupId: newStudentGroupId === 'unassigned' ? null : newStudentGroupId, }); setStudents(payload.students); setSummary(payload.summary); setStudentModalOpen(false); setPage(1); showToast({ title: '수강생이 추가되었습니다.', message: name, }); } catch (reason) { const message = reason instanceof Error ? reason.message : '수강생을 등록하지 못했습니다.'; setStudentFormError(message); showToast({ title: '수강생 추가에 실패했습니다.', message, variant: 'error', }); } finally { setStudentSaving(false); } }; const handleRemoveStudent = async (student: InstructorStudent) => { setRemovingStudentId(student.id); setError(null); try { const payload = await removeInstructorStudent(session.accessToken, student.id); setStudents(payload.students); setSummary(payload.summary); showToast({ title: '수강생이 제외되었습니다.', message: student.name, }); } catch (reason) { const message = reason instanceof Error ? reason.message : '수강생을 제외하지 못했습니다.'; setError(message); showToast({ title: '수강생 제외에 실패했습니다.', message, variant: 'error', }); } finally { setRemovingStudentId(null); setRemoveTarget(null); } }; const summaryItems = [ { label: '전체 수강생', value: `${summary.totalStudents}명`, icon: UserRound, tone: 'bg-[#f0efff] text-[#4f46e5]' }, { label: '현재 접속', value: `${summary.onlineStudents}명`, icon: Wifi, tone: 'bg-[#eff6ff] text-[#2563eb]' }, { label: '평균 진도', value: `${summary.averageProgress}%`, icon: BarChart3, tone: 'bg-[#fff7ed] text-[#f97316]' }, { label: '이슈', value: `${summary.unresolvedIssues}건`, icon: CircleAlert, tone: 'bg-[#fff1f2] text-[#e11d48]' }, ]; return (
); } function EmptyState({ message }: { message: string }) { return (

{message}

); } function Cell({ label, children }: { label: string; children: React.ReactNode }) { return (

{label}

{children}
); } function StudentMetric({ icon: Icon, label, value, danger = false }: { icon: typeof GitBranch; label: string; value: number; danger?: boolean }) { return (

{value}

); } function StudentIssueMetric({ student, onOpen }: { student: InstructorStudent; onOpen: (student: InstructorStudent) => void }) { const hasIssues = student.issues > 0; return (
); } function IssueModal({ student, onClose }: { student: InstructorStudent; onClose: () => void }) { return (

이슈 상세

{student.name} · {student.email}

{student.issueItems.length ? (
{student.issueItems.map((issue, index) => (

{issue.title}

{issue.detail}

))}
) : (
표시할 이슈가 없습니다.
)}
); } function SummaryCard({ label, value, icon: Icon, tone }: { label: string; value: string; icon: LucideIcon; tone: string }) { return (

{label}

{value}

); }