File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { useEffect, useState } from 'react'
import { getContacts, type Contact, type ContactCategory } from '../api/client'
const FOOTER_NOTE = '담당자 등록·수정은 담당자관리 메뉴에서 합니다.'
/** 채널관리에서 담당자 3종(신청기관/문정원/변호사)을 배정할 때 여는 선택 모달.
* 담당자 정보를 여기서 입력하지 않는다 - 등록/수정은 항상 담당자관리 화면에서만 한다. */
export default function ContactPickerModal({
category,
title,
onSelect,
onClose,
}: {
category: ContactCategory
title: string
onSelect: (contact: Contact) => void
onClose: () => void
}) {
const [contacts, setContacts] = useState<Contact[]>([])
const [loading, setLoading] = useState(true)
const [keyword, setKeyword] = useState('')
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
getContacts(category)
.then((data) => {
if (!cancelled) {
setContacts(data)
}
})
.catch((e) => {
if (!cancelled) {
setError(e instanceof Error ? e.message : '담당자 목록을 불러오지 못했습니다.')
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => {
cancelled = true
}
}, [category])
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose()
}
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [onClose])
const keywordTrimmed = keyword.trim()
const visible = contacts.filter((c) => {
if (keywordTrimmed === '') {
return true
}
return c.name.includes(keywordTrimmed) || (c.affiliation ?? '').includes(keywordTrimmed)
})
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/30">
<div className="w-[30rem] rounded-lg bg-white p-5">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold">{title}</h2>
<button
type="button"
onClick={onClose}
aria-label="닫기"
className="rounded-md p-1 text-gray-400 hover:bg-gray-100"
>
✕
</button>
</div>
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="이름·소속으로 검색"
aria-label="담당자 검색"
className="mt-3 w-full rounded-md border border-gray-300 px-2.5 py-1.5 text-sm"
/>
{error && <p className="mt-2 text-xs text-red-600">{error}</p>}
<div className="mt-3 max-h-72 overflow-y-auto">
{loading ? (
<p className="py-6 text-center text-sm text-gray-400">불러오는 중…</p>
) : visible.length === 0 ? (
<p className="py-6 text-center text-sm text-gray-300">등록된 담당자가 없습니다.</p>
) : (
<ul className="divide-y divide-gray-100">
{visible.map((contact) => (
<li key={contact.id} className="flex items-center justify-between gap-3 py-2 text-sm">
<div>
<p className="font-medium text-gray-900">
{contact.name}
{contact.affiliation && (
<span className="ml-1 text-gray-500">· {contact.affiliation}</span>
)}
</p>
<p className="text-xs text-gray-400">
{[contact.deptName, contact.phone, contact.email].filter(Boolean).join(' · ') || '-'}
</p>
</div>
<button
type="button"
onClick={() => onSelect(contact)}
className="shrink-0 rounded-md border border-gray-300 px-2.5 py-1 text-xs text-gray-700 hover:bg-gray-50"
>
선택
</button>
</li>
))}
</ul>
)}
</div>
<p className="mt-4 border-t border-gray-100 pt-3 text-xs text-gray-400">{FOOTER_NOTE}</p>
</div>
</div>
)
}