import { useEffect, useState } from 'react' import { provisionChannels, updateContact, type Contact, type Org, type ProvisionResult, } from '../api/client' import ConfirmDialog from './ConfirmDialog' import StatusBadge from './StatusBadge' // 엑셀처럼 한 사람이 한 행이다. 셀이 null이면 그 역할에 해당 항목이 없다는 뜻(- 표시). // aria-label은 "행 제목 + 항목"으로 유일하게 만들어 라벨 중복 없이 접근 가능하게 한다. const CONTACT_COLUMNS = ['부서명', '담당자명', '직급/직함', '연락처', '이메일', '배정일'] interface ContactCell { key: keyof Contact label: string type?: string } const CONTACT_ROWS: { title: string; cells: (ContactCell | null)[] }[] = [ { title: '신청기관 담당자', cells: [ { key: 'deptName', label: '신청기관 담당자 부서명' }, { key: 'managerName', label: '신청기관 담당자 담당자명' }, { key: 'managerTitle', label: '신청기관 담당자 직급/직함' }, { key: 'managerPhone', label: '신청기관 담당자 연락처' }, { key: 'managerEmail', label: '신청기관 담당자 이메일' }, null, ], }, { title: '문정원 담당자', cells: [ { key: 'mjDeptName', label: '문정원 담당자 부서명' }, { key: 'mjManagerName', label: '문정원 담당자 담당자명' }, null, { key: 'mjManagerPhone', label: '문정원 담당자 연락처' }, { key: 'mjManagerEmail', label: '문정원 담당자 이메일' }, null, ], }, { title: '담당 변호사', cells: [ null, { key: 'lawyerName', label: '담당 변호사 성명' }, null, { key: 'lawyerPhone', label: '담당 변호사 연락처' }, { key: 'lawyerEmail', label: '담당 변호사 이메일' }, { key: 'lawyerAssignedDate', label: '담당 변호사 배정일', type: 'date' }, ], }, ] function emptyContact(org: Org): Contact { return { deptName: org.deptName ?? '', managerName: org.managerName ?? '', managerTitle: org.managerTitle ?? '', managerPhone: org.managerPhone ?? '', managerEmail: org.managerEmail ?? '', mjDeptName: org.mjDeptName ?? '', mjManagerName: org.mjManagerName ?? '', mjManagerPhone: org.mjManagerPhone ?? '', mjManagerEmail: org.mjManagerEmail ?? '', lawyerName: org.lawyerName ?? '', lawyerPhone: org.lawyerPhone ?? '', lawyerEmail: org.lawyerEmail ?? '', lawyerAssignedDate: org.lawyerAssignedDate ?? '', } } const REQUIRED_FIELDS: (keyof Contact)[] = [ 'deptName', 'managerName', 'managerPhone', 'managerEmail', ] function ContactEditTable({ contact, onChange, }: { contact: Contact onChange: (key: keyof Contact, value: string) => void }) { return (
{CONTACT_COLUMNS.map((col) => ( ))} {CONTACT_ROWS.map((row) => ( {row.cells.map((cell, i) => ( ))} ))}
구분 {col}
{row.title} {cell ? ( onChange(cell.key, e.target.value)} /> ) : ( - )}
) } export default function OrgDetail({ org, onChanged }: { org: Org; onChanged: () => void }) { const [contact, setContact] = useState(emptyContact(org)) const [confirming, setConfirming] = useState(false) const [result, setResult] = useState(null) const [error, setError] = useState(null) const [busy, setBusy] = useState(false) // org.id로만 키를 잡는다(org 객체 전체가 아니라) - App.reload()가 채널 생성 뒤에 // 새 배열/새 객체를 만들어 넘기더라도, 같은 기관을 계속 보고 있는 한 방금 받은 // result/error를 이 effect가 지워버리면 안 되기 때문이다(같은 이유로 입력 중이던 // contact 값도 다른 기관으로 전환할 때만 초기화한다). useEffect(() => { setContact(emptyContact(org)) setResult(null) setError(null) // eslint-disable-next-line react-hooks/exhaustive-deps }, [org.id]) const displayMj = `${org.orgNo}_${org.orgName} (문정원)` const displayLaw = `${org.orgNo}_${org.orgName} (법률검토)` const canProvision = org.status !== 'INFO_PENDING' const requiredFilled = REQUIRED_FIELDS.every((key) => contact[key].trim() !== '') function setField(key: keyof Contact, value: string) { setContact((prev) => ({ ...prev, [key]: value })) } async function save() { setError(null) setBusy(true) try { await updateContact(org.id, contact) onChanged() } catch (e) { setError((e as Error).message) } finally { setBusy(false) } } async function provision() { setError(null) setBusy(true) try { const provisionResult = await provisionChannels(org.id) setResult(provisionResult) onChanged() } catch (e) { setError((e as Error).message) } finally { setBusy(false) setConfirming(false) } } return (

{org.orgNo} {org.orgName}

신청기관 담당자의 부서명·담당자명·연락처·이메일은 채널 생성을 위해 필수입니다.

{error &&

{error}

} {result && (

문정원 채널: {outcomeLabel(result.mj)}

법률검토 채널: {outcomeLabel(result.law)}

{result.mj === 'FAILED' && result.law === 'FAILED' ? (

문정원 채널과 법률검토 채널 모두 생성에 실패했습니다. 아직 만들어진 채널이 없으니 [채널 생성]을 다시 눌러도 안전합니다.

) : ( <> {result.law === 'FAILED' && (

법률검토 채널 생성에 실패했습니다. 문정원 채널은 그대로 남아 있으니 [채널 생성]을 다시 눌러도 안전합니다. 없는 채널만 다시 만듭니다.

)} {result.mj === 'FAILED' && (

문정원 채널 생성에 실패했습니다. 법률검토 채널은 그대로 남아 있으니 [채널 생성]을 다시 눌러도 안전합니다. 없는 채널만 다시 만듭니다.

)} )} {result.message && (
{result.message}
)}
)} {confirming && ( void provision()} onCancel={() => setConfirming(false)} /> )}
) } function outcomeLabel(outcome: ProvisionResult['mj']): string { switch (outcome) { case 'CACHED': return '이미 있음 (변경 없음)' case 'RECOVERED': return '기존 채널 연결됨' case 'CREATED': return '새로 생성됨' case 'FAILED': return '실패' } }