import { useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { Activity, BarChart3, Car, Edit3, Loader2, Plus, Search, Trash2, Users, X, type LucideIcon } from 'lucide-react'; import { PageHeader } from '../../components/PageHeader'; import { PaginationBar } from '../../components/PaginationBar'; import { useToast } from '../../components/ToastProvider'; import { createInstructorGroup, deleteInstructorGroup, listInstructorGroups, updateInstructorGroup, type GroupStudentOption, type GroupPlatformOption, type InstructorGroup, type InstructorGroupsResponse, type InstructorGroupsSummary, } from '../../lib/groups'; import type { AuthSession } from '../../types/auth'; const emptySummary: InstructorGroupsSummary = { totalGroups: 0, assignedPlatforms: 0, totalStudents: 0, averageProgress: 0, }; export function GroupsPage({ session, selectedCourseId }: { session: AuthSession; selectedCourseId: string }) { const { showToast } = useToast(); const [groups, setGroups] = useState([]); const [summary, setSummary] = useState(emptySummary); const [unassignedStudents, setUnassignedStudents] = useState([]); const [platformOptions, setPlatformOptions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [query, setQuery] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [modalMode, setModalMode] = useState<'create' | 'edit' | null>(null); const [editingGroup, setEditingGroup] = useState(null); const [groupName, setGroupName] = useState(''); const [selectedPlatform, setSelectedPlatform] = useState('unassigned'); const [selectedMemberIds, setSelectedMemberIds] = useState([]); const [formError, setFormError] = useState(null); const [saving, setSaving] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deletingGroupId, setDeletingGroupId] = useState(null); const filteredGroups = useMemo(() => { const keyword = query.trim().toLowerCase(); if (!keyword) return groups; return groups.filter((group) => [group.name, group.platform, ...group.members] .join(' ') .toLowerCase() .includes(keyword), ); }, [groups, query]); const pagedGroups = useMemo(() => filteredGroups.slice((page - 1) * pageSize, page * pageSize), [filteredGroups, page, pageSize]); const summaryItems = [ { label: '전체 그룹', value: `${summary.totalGroups}개`, icon: Users, tone: 'bg-[#f0efff] text-[#4f46e5]' }, { label: '배정 플랫폼', value: `${summary.assignedPlatforms}대`, icon: Car, tone: 'bg-[#eff6ff] text-[#2563eb]' }, { label: '수강생', value: `${summary.totalStudents}명`, icon: Activity, tone: 'bg-[#fff7ed] text-[#f97316]' }, { label: '평균 진도', value: `${summary.averageProgress}%`, icon: BarChart3, tone: 'bg-[#f8fafc] text-[#64748b]' } ]; const applyGroupsPayload = (payload: InstructorGroupsResponse) => { setError(null); setGroups(payload.groups); setSummary(payload.summary); setUnassignedStudents(payload.unassignedStudents ?? []); setPlatformOptions(payload.platformOptions ?? []); }; useEffect(() => { let cancelled = false; setLoading(true); listInstructorGroups(session.accessToken, selectedCourseId || null) .then((payload) => { if (cancelled) return; applyGroupsPayload(payload); }) .catch((reason: Error) => { if (cancelled) return; setGroups([]); setSummary(emptySummary); setError(reason.message); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [session.accessToken, selectedCourseId]); useEffect(() => { setPage(1); }, [query]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(filteredGroups.length / pageSize)); if (page > totalPages) setPage(totalPages); }, [filteredGroups.length, page, pageSize]); useEffect(() => { if (!modalMode && !deleteTarget) return; const originalOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = originalOverflow; }; }, [deleteTarget, modalMode]); const handlePageSizeChange = (nextPageSize: number) => { setPageSize(nextPageSize); setPage(1); }; const openCreateModal = () => { setModalMode('create'); setEditingGroup(null); setGroupName(''); setSelectedPlatform('unassigned'); setSelectedMemberIds([]); setFormError(null); }; const openEditModal = (group: InstructorGroup) => { setModalMode('edit'); setEditingGroup(group); setGroupName(group.name); setSelectedPlatform(group.instructorPlatformId ?? 'unassigned'); setSelectedMemberIds((group.membersDetailed ?? []).map((member) => member.id)); setFormError(null); }; const closeGroupModal = () => { if (saving) return; setModalMode(null); setEditingGroup(null); }; const availableStudents = useMemo(() => { const currentMembers = editingGroup?.membersDetailed ?? []; const byId = new Map(); [...currentMembers, ...unassignedStudents].forEach((student) => byId.set(student.id, student)); return [...byId.values()].sort((left, right) => left.name.localeCompare(right.name, 'ko')); }, [editingGroup, unassignedStudents]); const availablePlatforms = useMemo(() => { const byId = new Map(); if (editingGroup?.instructorPlatformId && editingGroup.platform !== '미배정') { byId.set(editingGroup.instructorPlatformId, { id: editingGroup.instructorPlatformId, name: editingGroup.platform, }); } platformOptions.forEach((platform) => byId.set(platform.id, platform)); return [...byId.values()].sort((left, right) => left.name.localeCompare(right.name, 'ko')); }, [editingGroup, platformOptions]); const toggleMember = (studentId: string) => { setSelectedMemberIds((current) => current.includes(studentId) ? current.filter((id) => id !== studentId) : [...current, studentId], ); }; const handleSaveGroup = async () => { const name = groupName.trim(); if (!name) { setFormError('그룹명을 입력해 주세요.'); return; } setSaving(true); setFormError(null); try { const input = { courseId: selectedCourseId || null, name, instructorPlatformId: selectedPlatform === 'unassigned' ? null : selectedPlatform, platformName: availablePlatforms.find((platform) => platform.id === selectedPlatform)?.name ?? null, memberIds: selectedMemberIds, }; const payload = modalMode === 'edit' && editingGroup ? await updateInstructorGroup(session.accessToken, editingGroup.id, input) : await createInstructorGroup(session.accessToken, input); applyGroupsPayload(payload); showToast({ title: modalMode === 'edit' ? '그룹이 수정되었습니다.' : '그룹이 추가되었습니다.', message: name, }); setModalMode(null); setEditingGroup(null); setPage(1); } catch (reason) { setFormError(reason instanceof Error ? reason.message : '그룹을 저장하지 못했습니다.'); } finally { setSaving(false); } }; const handleDeleteGroup = async (group: InstructorGroup) => { setDeletingGroupId(group.id); setError(null); try { const payload = await deleteInstructorGroup(session.accessToken, group.id, selectedCourseId || null); applyGroupsPayload(payload); showToast({ title: '그룹이 삭제되었습니다.', message: group.name, }); setDeleteTarget(null); setPage(1); } catch (reason) { setError(reason instanceof Error ? reason.message : '그룹을 삭제하지 못했습니다.'); } finally { setDeletingGroupId(null); } }; return (
); } function SummaryCard({ label, value, icon: Icon, tone }: { label: string; value: string; icon: LucideIcon; tone: string }) { return (

{label}

{value}

); }