import { MoreHorizontal, Plus } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table'; import { useToast } from '../../components/toast'; import { Button, Modal } from '../../components/ui'; import { PageTitle } from '../../layout/AppLayout'; import { formatDateTime } from '../../lib/date-format'; import { createOrganization, deleteOrganization, loadOrganizations, updateOrganization, type Organization, type OrganizationType } from './operations-api'; import { LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils'; const organizationTypes: Array<{ value: OrganizationType; label: string }> = [ { value: 'SCHOOL', label: '학교' }, { value: 'COMPANY', label: '회사' }, { value: 'GOVERNMENT', label: '공공기관' }, { value: 'LAB', label: '연구소' }, { value: 'OTHER', label: '기타' } ]; const emptyForm = { name: '', type: 'SCHOOL' as OrganizationType, domain: '' }; function organizationTypeLabel(type: OrganizationType) { return organizationTypes.find((item) => item.value === type)?.label ?? type; } export function OrganizationsPage() { const { toast } = useToast(); const [organizations, setOrganizations] = useState([]); const [loading, setLoading] = useState(true); const [failed, setFailed] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [menuOrganization, setMenuOrganization] = useState<{ id: string; x: number; y: number } | null>(null); const [saving, setSaving] = useState(false); const [form, setForm] = useState(emptyForm); const loadingVisible = useMinimumLoading(loading); const refresh = () => { setLoading(true); loadOrganizations() .then((items) => { setOrganizations(items); setFailed(false); }) .catch((error: Error) => { setOrganizations([]); setFailed(true); toast({ type: 'error', title: '조직 조회 실패', message: error.message }); }) .finally(() => setLoading(false)); }; useEffect(() => { refresh(); }, []); useEffect(() => { if (!menuOrganization) return; const close = () => setMenuOrganization(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setMenuOrganization(null); }; window.addEventListener('click', close); window.addEventListener('keydown', closeOnEscape); return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', closeOnEscape); }; }, [menuOrganization]); const rows = useMemo( () => organizations.map((organization, index) => ({ ...organization, no: organizations.length - index })), [organizations] ); const columns = useMemo>>( () => [ { key: 'no', header: 'No.', className: 'w-14 px-3 py-3 text-center font-medium', cell: (row) => {row.no} }, { key: 'name', header: '조직명', className: 'px-5 py-3 font-medium', cell: (row) => (

{row.name}

{row.id}

) }, { key: 'type', header: '유형', className: 'w-32 px-5 py-3', cell: (row) => }, { key: 'domain', header: '도메인', className: 'w-56 px-5 py-3', cell: (row) => {row.domain || '-'} }, { key: 'createdAt', header: '등록일', className: 'w-40 px-5 py-3', cell: (row) => {formatDateTime(row.createdAt)} }, { key: 'actions', header: '', className: 'w-20 px-5 py-3 text-right font-medium', cell: (row) => (
) } ], [menuOrganization?.id] ); const filters = useMemo>>( () => [ { key: 'type', label: '유형', options: organizationTypes.map((type) => ({ label: type.label, value: type.value })), getValue: (row) => row.type } ], [] ); const closeModal = () => { setModalOpen(false); setEditing(null); setForm(emptyForm); setSaving(false); }; const openCreateModal = () => { setEditing(null); setForm(emptyForm); setModalOpen(true); }; const openEditModal = (organization: Organization) => { setEditing(organization); setForm({ name: organization.name, type: organization.type, domain: organization.domain ?? '' }); setModalOpen(true); }; const submit = () => { if (!form.name.trim()) return; setSaving(true); const payload = { name: form.name.trim(), type: form.type, domain: form.domain.trim() || null }; const request = editing ? updateOrganization(editing.id, payload) : createOrganization(payload); request .then(({ organization }) => { setOrganizations((prev) => editing ? prev.map((item) => item.id === organization.id ? organization : item) : [organization, ...prev]); toast({ type: 'success', title: editing ? '조직 수정 완료' : '조직 등록 완료', message: `${organization.name} 조직이 ${editing ? '수정' : '등록'}되었습니다.` }); closeModal(); }) .catch((error: Error) => { setSaving(false); toast({ type: 'error', title: editing ? '조직 수정 실패' : '조직 등록 실패', message: error.message }); }); }; const confirmDelete = () => { if (!deleteTarget) return; setSaving(true); deleteOrganization(deleteTarget.id) .then(() => { setOrganizations((prev) => prev.filter((organization) => organization.id !== deleteTarget.id)); toast({ type: 'success', title: '조직 삭제 완료', message: `${deleteTarget.name} 조직이 삭제되었습니다.` }); setDeleteTarget(null); }) .catch((error: Error) => { toast({ type: 'error', title: '조직 삭제 실패', message: error.message }); }) .finally(() => setSaving(false)); }; return ( <> 추가 } /> {failed ? (
조직 데이터를 불러오지 못했습니다.
) : null} `${row.name} ${row.type} ${organizationTypeLabel(row.type)} ${row.domain ?? ''}`} getRowKey={(row) => row.id} pageSize={10} minWidth={980} loading={loading} emptyMessage="표시할 조직 데이터가 없습니다." /> {menuOrganization ? (() => { const row = rows.find((item) => item.id === menuOrganization.id); if (!row) return null; return (
event.stopPropagation()} className="fixed z-[70] min-w-32 overflow-hidden rounded-md border border-border bg-card py-1 text-left shadow-xl" style={{ left: menuOrganization.x, top: menuOrganization.y, transform: 'translateX(-100%)' }}>
); })() : null} } >
{ if (!saving) setDeleteTarget(null); }} footer={ <> } >

{deleteTarget?.name ?? '-'} 조직을 삭제하시겠습니까? 삭제된 조직은 목록과 선택 항목에서 숨겨집니다.

); }