import { useEffect, useState } from 'react' import { provisionChannels, updateAssignments, type AssignmentInput, type Contact, type ContactCategory, type Org, type ProvisionResult, } from '../api/client' import ConfirmDialog from './ConfirmDialog' import ContactPickerModal from './ContactPickerModal' import StatusBadge from './StatusBadge' // 채널관리 상세. 담당자 정보는 여기서 입력하지 않는다(담당자관리에서만 입력) - 이 화면은 // 이미 등록된 담당자를 ContactPickerModal로 골라 신청기관/문정원/아이티앤/변호사 역할에 배정만 한다. interface RoleConfig { key: 'applicant' | 'mj' | 'itn' | 'lawyer' category: ContactCategory label: string pickerTitle: string field: keyof AssignmentInput } const ROLES: RoleConfig[] = [ { key: 'applicant', category: 'APPLICANT', label: '신청기관 담당자', pickerTitle: '신청기관 담당자 선택', field: 'applicantContactId', }, { key: 'mj', category: 'MJ', label: '문정원 담당자', pickerTitle: '문정원 담당자 선택', field: 'mjContactId', }, { key: 'itn', category: 'ITN', label: '아이티앤 담당자', pickerTitle: '아이티앤 담당자 선택', field: 'itnContactId', }, { key: 'lawyer', category: 'LAWYER', label: '담당 변호사', pickerTitle: '담당 변호사 선택', field: 'lawyerContactId', }, ] function contactOf(org: Org, role: RoleConfig): Contact | null { switch (role.key) { case 'applicant': return org.applicant case 'mj': return org.mj case 'itn': return org.itn case 'lawyer': return org.lawyer } } /** 배정된 담당자의 한 줄 요약: 이름 · 부서(또는 소속) · 연락처. 값이 없는 항목은 건너뛴다. */ function contactSummary(contact: Contact): string { const parts = [contact.name, contact.deptName ?? contact.affiliation ?? null, contact.phone] return parts.filter((p): p is string => Boolean(p)).join(' · ') } export default function OrgDetail({ org, onChanged }: { org: Org; onChanged: () => void }) { const [confirming, setConfirming] = useState(false) const [result, setResult] = useState(null) const [error, setError] = useState(null) const [busy, setBusy] = useState(false) const [pickerRole, setPickerRole] = useState(null) const [lawyerDate, setLawyerDate] = useState(org.lawyerAssignedDate ?? '') // org.id로만 키를 잡는다(org 객체 전체가 아니라) - App.reload()가 채널 생성/배정 뒤에 // 새 배열/새 객체를 만들어 넘기더라도, 같은 기관을 계속 보고 있는 한 방금 받은 // result/error를 이 effect가 지워버리면 안 되기 때문이다. useEffect(() => { setResult(null) setError(null) setLawyerDate(org.lawyerAssignedDate ?? '') // 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' function currentAssignment(): AssignmentInput { return { applicantContactId: org.applicant?.id ?? null, mjContactId: org.mj?.id ?? null, itnContactId: org.itn?.id ?? null, lawyerContactId: org.lawyer?.id ?? null, lawyerAssignedDate: org.lawyerAssignedDate ?? null, } } /** 매 호출이 네 배정값 + 배정일을 통째로 다시 보낸다(부분 갱신이 아니다). */ async function persistAssignments(overrides: Partial) { setError(null) setBusy(true) try { await updateAssignments(org.id, { ...currentAssignment(), ...overrides }) onChanged() } catch (e) { setError(e instanceof Error ? e.message : '저장에 실패했습니다.') } finally { setBusy(false) } } async function handleSelect(role: RoleConfig, contact: Contact) { setPickerRole(null) await persistAssignments({ [role.field]: contact.id }) } async function handleClear(role: RoleConfig) { await persistAssignments({ [role.field]: null }) } async function handleLawyerDateChange(value: string) { setLawyerDate(value) await persistAssignments({ lawyerAssignedDate: value === '' ? null : value }) } 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}

{ROLES.map((role) => { const contact = contactOf(org, role) return (
{role.label} {contact ? ( {contactSummary(contact)} ) : ( 미지정 )}
{role.key === 'lawyer' && (
void handleLawyerDateChange(e.target.value)} className="rounded-md border border-gray-300 px-2 py-1 text-sm" />
)} {contact && ( )}
) })}

신청기관 담당자를 지정해야 채널을 만들 수 있습니다.

{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)} /> )} {pickerRole && ( void handleSelect(pickerRole, contact)} onClose={() => setPickerRole(null)} /> )}
) } function outcomeLabel(outcome: ProvisionResult['mj']): string { switch (outcome) { case 'CACHED': return '이미 있음 (변경 없음)' case 'RECOVERED': return '기존 채널 연결됨' case 'CREATED': return '새로 생성됨' case 'FAILED': return '실패' } }