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 { createMemo, deleteMemo, getMemos, type Org, type WorkMemo } from '../api/client'
const CUSTOM_CONTACT_VALUE = '__custom__'
const dateFormatter = new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
function formatDateTime(createdAt: number): string {
return dateFormatter.format(new Date(createdAt))
}
function managerOptionLabel(org: Org): string | null {
if (!org.managerName) {
return null
}
return org.managerTitle ? `${org.managerName} ${org.managerTitle}` : org.managerName
}
/** 신청기관 담당자, 문정원 담당자, 담당 변호사 중 이름이 채워진 사람만 후보로 낸다.
* 이름이 같은 사람이 중복 등록돼 있어도 셀렉트 옵션은 한 번만 보이도록 중복을 없앤다. */
function contactOptions(org: Org): string[] {
const candidates = [managerOptionLabel(org), org.mjManagerName, org.lawyerName]
const seen = new Set<string>()
const options: string[] = []
for (const candidate of candidates) {
if (!candidate || candidate.trim() === '' || seen.has(candidate)) {
continue
}
seen.add(candidate)
options.push(candidate)
}
return options
}
/** 기관관리 상세의 업무메모 탭. 통화 등 담당자와의 연락 내용을 기록/조회/삭제한다. */
export default function WorkMemos({ org }: { org: Org }) {
const options = contactOptions(org)
const [memos, setMemos] = useState<WorkMemo[]>([])
const [loading, setLoading] = useState(false)
const [contactChoice, setContactChoice] = useState(options[0] ?? CUSTOM_CONTACT_VALUE)
const [customContactName, setCustomContactName] = useState('')
const [body, setBody] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
getMemos(org.id)
.then((data) => {
if (!cancelled) {
setMemos(data)
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => {
cancelled = true
}
}, [org.id])
const isCustom = contactChoice === CUSTOM_CONTACT_VALUE
const effectiveContactName = (isCustom ? customContactName : contactChoice).trim()
const canSubmit = effectiveContactName !== '' && body.trim() !== '' && !busy
async function handleSubmit() {
if (!canSubmit) {
return
}
setBusy(true)
setError(null)
try {
const created = await createMemo(org.id, { contactName: effectiveContactName, body })
setMemos((prev) => [created, ...prev])
setBody('')
} catch (e) {
setError(e instanceof Error ? e.message : '메모 등록에 실패했습니다.')
} finally {
setBusy(false)
}
}
async function handleDelete(memo: WorkMemo) {
if (!window.confirm('이 메모를 삭제할까요?')) {
return
}
try {
await deleteMemo(org.id, memo.id)
setMemos((prev) => prev.filter((m) => m.id !== memo.id))
} catch (e) {
setError(e instanceof Error ? e.message : '메모 삭제에 실패했습니다.')
}
}
return (
<div className="p-4">
<section className="rounded-lg border border-gray-200 p-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="flex flex-col gap-1 text-sm">
<label htmlFor="memo-contact-choice" className="text-xs text-gray-500">
담당자
</label>
<select
id="memo-contact-choice"
value={contactChoice}
onChange={(e) => setContactChoice(e.target.value)}
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm"
>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
<option value={CUSTOM_CONTACT_VALUE}>직접 입력</option>
</select>
</div>
{isCustom && (
<div className="flex flex-col gap-1 text-sm">
<label htmlFor="memo-contact-name" className="text-xs text-gray-500">
담당자 이름
</label>
<input
id="memo-contact-name"
type="text"
value={customContactName}
onChange={(e) => setCustomContactName(e.target.value)}
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm"
/>
</div>
)}
</div>
<div className="mt-3 flex flex-col gap-1 text-sm">
<label htmlFor="memo-body" className="text-xs text-gray-500">
메모 내용
</label>
<textarea
id="memo-body"
rows={3}
value={body}
onChange={(e) => setBody(e.target.value)}
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm"
/>
</div>
{error && <p className="mt-2 text-xs text-red-600">{error}</p>}
<div className="mt-3 flex justify-end">
<button
type="button"
disabled={!canSubmit}
onClick={() => void handleSubmit()}
className="rounded-md bg-blue-600 px-4 py-1.5 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-300"
>
등록
</button>
</div>
</section>
<div className="mt-4">
{loading ? (
<p className="py-6 text-center text-sm text-gray-400">불러오는 중…</p>
) : memos.length === 0 ? (
<p className="py-6 text-center text-sm text-gray-300">
등록된 메모가 없습니다. 통화 후 첫 메모를 남겨보세요.
</p>
) : (
<ul className="space-y-3">
{memos.map((memo) => (
<li key={memo.id} className="rounded-lg border border-gray-200 p-3">
<div className="flex items-center justify-between gap-2">
<div className="flex items-baseline gap-2">
<span className="text-xs text-gray-400">{formatDateTime(memo.createdAt)}</span>
<span className="text-xs text-gray-300">·</span>
<span className="text-sm font-medium text-gray-900">{memo.contactName}</span>
</div>
<button
type="button"
onClick={() => void handleDelete(memo)}
className="rounded-md border border-gray-200 px-2 py-0.5 text-xs text-gray-500 hover:bg-gray-50"
>
삭제
</button>
</div>
<p className="mt-1.5 whitespace-pre-wrap text-sm text-gray-800">{memo.body}</p>
</li>
))}
</ul>
)}
</div>
</div>
)
}