import { MoreHorizontal, Pencil, Plus, Trash2 } 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 { createTerm, deleteTerm, loadTermsPage, updateTerm, type LauncherTerm } from './operations-api'; import { LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils'; type TermForm = { key: string; body: string; isRequired: boolean; }; const termKeyOptions = [ { value: 'terms', label: '이용약관' }, { value: 'privacy', label: '개인정보 처리방침' } ]; const emptyForm: TermForm = { key: 'terms', body: '', isRequired: true }; function termKeyLabel(key: string) { return termKeyOptions.find((option) => option.value === key)?.label ?? key; } function termKeyTone(key: string) { return key === 'privacy' ? 'blue' as const : 'violet' as const; } function TermCreator({ term }: { term: LauncherTerm }) { const [failed, setFailed] = useState(false); const name = term.createdByName || term.createdByEmail || '-'; const avatarUrl = term.createdByAvatarUrl || ''; const initial = name.trim().charAt(0).toUpperCase() || 'A'; return (
{avatarUrl && !failed ? ( setFailed(true)} /> ) : ( {initial} )}

{name}

{formatDateTime(term.createdAt)}

); } function toForm(term: LauncherTerm): TermForm { return { key: term.key, body: term.body, isRequired: term.isRequired }; } export function TermsPage() { const { toast } = useToast(); const [terms, setTerms] = useState([]); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [keyword, setKeyword] = useState(''); const [filterValues, setFilterValues] = 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 [menuTerm, setMenuTerm] = useState<{ id: string; x: number; y: number } | null>(null); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const loadingVisible = useMinimumLoading(loading); const refresh = () => { setLoading(true); loadTermsPage({ page, limit: 6, keyword, key: filterValues.key }) .then((result) => { setTerms(result.items); setTotal(result.total); setFailed(false); }) .catch((error: Error) => { setTerms([]); setTotal(0); setFailed(true); toast({ type: 'error', title: '약관 조회 실패', message: error.message }); }) .finally(() => setLoading(false)); }; useEffect(() => { refresh(); }, [page, keyword, filterValues.key]); useEffect(() => { if (!menuTerm) return; const close = () => setMenuTerm(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setMenuTerm(null); }; window.addEventListener('click', close); window.addEventListener('keydown', closeOnEscape); return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', closeOnEscape); }; }, [menuTerm]); const rows = useMemo( () => terms.map((term) => ({ ...term, no: term.no ?? 0, keyLabel: termKeyLabel(term.key) })), [terms] ); const openCreate = () => { setEditing(null); setForm(emptyForm); setModalOpen(true); }; const openEdit = (term: LauncherTerm) => { setMenuTerm(null); setEditing(term); setForm(toForm(term)); setModalOpen(true); }; const closeModal = () => { setModalOpen(false); setEditing(null); setForm(emptyForm); }; const payload = () => ({ key: form.key.trim(), body: form.body.trim(), isRequired: form.isRequired }); const submit = () => { if (!form.key.trim() || !form.body.trim()) return; setSaving(true); const request = editing ? updateTerm(editing.id, payload()) : createTerm(payload()); request .then(() => { toast({ type: 'success', message: editing ? '약관을 수정했습니다.' : '약관을 추가했습니다.' }); closeModal(); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: editing ? '약관 수정 실패' : '약관 추가 실패', message: error.message }); }) .finally(() => setSaving(false)); }; const removeTerm = (term: LauncherTerm) => { setMenuTerm(null); setDeleteTarget(term); }; const confirmRemoveTerm = () => { if (!deleteTarget) return; setSaving(true); deleteTerm(deleteTarget.id) .then(() => { toast({ type: 'success', message: '약관을 삭제했습니다.' }); setDeleteTarget(null); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: '약관 삭제 실패', message: error.message }); }) .finally(() => setSaving(false)); }; const columns = useMemo>>( () => [ { key: 'no', header: 'No.', className: 'w-14 px-3 py-3 text-center font-medium', cell: (row) => {row.no} }, { key: 'key', header: '약관명', width: 160, className: 'px-5 py-3', cell: (row) => }, { key: 'body', header: '본문', flexible: true, className: 'px-5 py-3 font-medium', cell: (row) =>

{row.body}

}, { key: 'required', header: '필수 여부', className: 'w-24 px-5 py-3', cell: (row) => }, { key: 'createdAt', header: '생성', className: 'w-48 px-5 py-3', cell: (row) => }, { key: 'actions', header: '', className: 'w-20 px-5 py-3 font-medium', cell: (row) => (
) } ], [] ); const filters = useMemo>>( () => [ { key: 'key', label: '약관명', options: termKeyOptions, getValue: (row) => row.key } ], [] ); return ( <> 추가 } /> {failed ? (
약관 데이터를 불러오지 못했습니다.
) : null} `${row.body} ${row.key} ${row.keyLabel}`} getRowKey={(row) => row.id} pageSize={6} minWidth={980} loading={loading} emptyMessage="등록된 약관이 없습니다." serverPagination={{ page, total, onPageChange: setPage, onSearchChange: (value) => { setKeyword(value); setPage(1); }, filterValues, onFilterChange: (key, value) => { setFilterValues((prev) => ({ ...prev, [key]: value })); setPage(1); } }} /> {menuTerm ? (() => { const row = rows.find((item) => item.id === menuTerm.id); if (!row) return null; return (
event.stopPropagation()} className="fixed z-[70] min-w-28 overflow-hidden rounded-md border border-border bg-card py-1 text-left shadow-xl" style={{ left: menuTerm.x, top: menuTerm.y, transform: 'translateX(-100%)' }}>
); })() : null} {editing ? '약관 수정' : '약관 추가'} } onClose={closeModal} className="!max-w-2xl" footer={ <> } >
{ event.preventDefault(); submit(); }}>