import { useEffect, useRef, useState } from 'react' import { createContact, deleteContact, getContacts, importMembers, updateContactInfo, type Contact, type ContactCategory, type ContactInput, type MemberImportReport, } from '../api/client' const CATEGORY_LABELS: Record = { APPLICANT: '신청기관', MJ: '문정원', LAWYER: '변호사', OPERATOR: '수행기관', ITN: '아이티앤 담당자', } const CATEGORY_BADGE_STYLES: Record = { APPLICANT: 'bg-blue-50 text-blue-700', MJ: 'bg-emerald-50 text-emerald-700', LAWYER: 'bg-violet-50 text-violet-700', OPERATOR: 'bg-sky-50 text-sky-700', ITN: 'bg-orange-50 text-orange-700', } const FILTERS: { key: ContactCategory | 'ALL'; label: string }[] = [ { key: 'ALL', label: '전체' }, { key: 'APPLICANT', label: '신청기관' }, { key: 'MJ', label: '문정원' }, { key: 'LAWYER', label: '변호사' }, { key: 'OPERATOR', label: '수행기관' }, { key: 'ITN', label: '아이티앤 담당자' }, ] function emptyForm(contact: Contact | null): ContactInput { return { category: contact?.category ?? 'APPLICANT', name: contact?.name ?? '', affiliation: contact?.affiliation ?? '', deptName: contact?.deptName ?? '', title: contact?.title ?? '', phone: contact?.phone ?? '', email: contact?.email ?? '', } } function ContactFormModal({ editing, onClose, onSaved, }: { editing: Contact | null onClose: () => void onSaved: () => void }) { const [form, setForm] = useState(emptyForm(editing)) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') { onClose() } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [onClose]) const canSave = form.category.trim() !== '' && form.name.trim() !== '' && !busy function setField(key: keyof ContactInput, value: string) { setForm((prev) => ({ ...prev, [key]: value })) } async function handleSave() { if (!canSave) { return } setBusy(true) setError(null) try { if (editing) { await updateContactInfo(editing.id, form) } else { await createContact(form) } onSaved() } catch (e) { setError(e instanceof Error ? e.message : '저장에 실패했습니다.') } finally { setBusy(false) } } return (

{editing ? '담당자 수정' : '담당자 추가'}

setField('name', e.target.value)} className="rounded-md border border-gray-300 px-2 py-1.5" />
setField('affiliation', e.target.value)} className="rounded-md border border-gray-300 px-2 py-1.5" />
setField('deptName', e.target.value)} className="rounded-md border border-gray-300 px-2 py-1.5" />
setField('title', e.target.value)} className="rounded-md border border-gray-300 px-2 py-1.5" />
setField('phone', e.target.value)} className="rounded-md border border-gray-300 px-2 py-1.5" />
setField('email', e.target.value)} className="rounded-md border border-gray-300 px-2 py-1.5" />
{error &&

{error}

}
) } /** 담당자관리 화면. 담당자 정보가 입력되는 유일한 곳이다 - 채널관리는 여기 등록된 * 담당자를 ContactPickerModal로 골라 배정만 한다. */ export default function ContactsPage() { const [contacts, setContacts] = useState([]) const [loading, setLoading] = useState(true) const [filter, setFilter] = useState('ALL') const [modalOpen, setModalOpen] = useState(false) const [editing, setEditing] = useState(null) const [error, setError] = useState(null) const importInputRef = useRef(null) const [importBusy, setImportBusy] = useState(false) const [importReport, setImportReport] = useState(null) const [importError, setImportError] = useState(null) async function load() { setLoading(true) try { setContacts(await getContacts()) } catch (e) { setError(e instanceof Error ? e.message : '담당자 목록을 불러오지 못했습니다.') } finally { setLoading(false) } } useEffect(() => { void load() }, []) const visible = filter === 'ALL' ? contacts : contacts.filter((c) => c.category === filter) function openAdd() { setEditing(null) setModalOpen(true) } function openEdit(contact: Contact) { setEditing(contact) setModalOpen(true) } function closeModal() { setModalOpen(false) setEditing(null) } async function handleSaved() { closeModal() await load() } async function handleDelete(contact: Contact) { if (!window.confirm('삭제하면 기관에 지정된 배정도 함께 해제됩니다. 삭제할까요?')) { return } try { await deleteContact(contact.id) await load() } catch (e) { setError(e instanceof Error ? e.message : '삭제에 실패했습니다.') } } async function handleImportFile(file: File) { setImportBusy(true) setImportError(null) try { const report = await importMembers(file) setImportReport(report) await load() } catch (e) { setImportError(e instanceof Error ? e.message : '회원명단 업로드에 실패했습니다.') } finally { setImportBusy(false) } } return (
{FILTERS.map((f) => ( ))}
{ const file = e.target.files?.[0] if (file) void handleImportFile(file) e.target.value = '' }} />
{importReport && (

{`등록 ${importReport.created} · 갱신 ${importReport.updated} · 기관지정 ${importReport.assigned} · 건너뜀 ${importReport.skipped}`}

)} {importError &&

{importError}

} {error &&

{error}

}
{loading ? ( ) : visible.length === 0 ? ( ) : ( visible.map((contact) => ( )) )}
구분 성명 소속 부서 직급/직함 연락처 이메일 관리
불러오는 중…
등록된 담당자가 없습니다.
{CATEGORY_LABELS[contact.category]} {contact.name} {contact.affiliation ?? '-'} {contact.deptName ?? '-'} {contact.title ?? '-'} {contact.phone ?? '-'} {contact.email ?? '-'}
{modalOpen && ( void handleSaved()} /> )}
) }