import { useEffect, useRef, useState } from 'react' import { createContact, deleteContact, getContacts, importMembers, updateContactInfo, type Contact, type ContactCategory, type ContactInput, type MemberImportReport, } from '../api/client' import { useCodes } from '../codes/useCodes' import StatusChip from '../ui/StatusChip' import HelpButton from '../help/HelpButton' // 구분 5종(표시명·뱃지색·필터·드롭다운)은 예전에 이 파일 안에서만 네 번 따로 적혀 있었다. // 지금은 전부 코드표(CONTACT_CATEGORY)에서 온다. 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) const categories = useCodes('CONTACT_CATEGORY') 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) const categories = useCodes('CONTACT_CATEGORY') /** 코드표에 없는 구분값이 저장돼 있어도 뱃지가 사라지지 않도록 undefined를 허용한다. */ function categoryOf(category: ContactCategory) { return categories.find((candidate) => candidate.code === category) } // '전체'는 코드가 아니라 화면에서만 쓰는 필터 항목이라 코드표에 넣지 않고 여기서 앞에 붙인다. const filters: { key: ContactCategory | 'ALL'; label: string }[] = [ { key: 'ALL', label: '전체' }, ...categories.map((category) => ({ key: category.code as ContactCategory, label: category.label, })), ] 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) => ( )) )}
구분 성명 소속 부서 직급/직함 연락처 이메일 관리
불러오는 중…
등록된 담당자가 없습니다.
{contact.name} {contact.affiliation ?? '-'} {contact.deptName ?? '-'} {contact.title ?? '-'} {contact.phone ?? '-'} {contact.email ?? '-'}
{modalOpen && ( void handleSaved()} /> )}
) }