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, updateMemo, type Org, type WorkMemo } from '../api/client'
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))
}
/**
* 기관관리 상세의 업무메모 탭. 통화 등 담당자와의 연락 내용을 기록/수정/삭제한다.
*
* 작성자는 화면에서 고르지 않는다 - 요청자 확정대로 로그인한 사용자를 서버가 직접
* 기록하므로, 목록에 보이는 작성자는 서버가 채워 준 값이다.
*/
export default function WorkMemos({ org }: { org: Org }) {
const [memos, setMemos] = useState<WorkMemo[]>([])
const [loading, setLoading] = useState(false)
const [body, setBody] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
/** 수정 중인 메모 id. null이면 새 메모 작성 모드다. */
const [editingId, setEditingId] = useState<number | null>(null)
const [editingBody, setEditingBody] = useState('')
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 canSubmit = body.trim() !== '' && !busy
async function handleSubmit() {
if (!canSubmit) {
return
}
setBusy(true)
setError(null)
try {
const created = await createMemo(org.id, { body })
setMemos((prev) => [created, ...prev])
setBody('')
} catch (e) {
setError(e instanceof Error ? e.message : '메모 등록에 실패했습니다.')
} finally {
setBusy(false)
}
}
async function handleUpdate(memo: WorkMemo) {
if (editingBody.trim() === '' || busy) {
return
}
setBusy(true)
setError(null)
try {
const saved = await updateMemo(org.id, memo.id, { body: editingBody })
setMemos((prev) => prev.map((m) => (m.id === memo.id ? saved : m)))
setEditingId(null)
setEditingBody('')
} 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">
<p className="text-xs text-gray-400">작성자는 로그인한 사용자로 자동 기록됩니다.</p>
<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>
<div className="flex gap-1">
{editingId === memo.id ? (
<>
<button
type="button"
disabled={busy || editingBody.trim() === ''}
onClick={() => void handleUpdate(memo)}
className="rounded-md bg-blue-600 px-2 py-0.5 text-xs text-white disabled:bg-gray-300"
>
저장
</button>
<button
type="button"
disabled={busy}
onClick={() => {
setEditingId(null)
setEditingBody('')
}}
className="rounded-md border border-gray-200 px-2 py-0.5 text-xs text-gray-500 hover:bg-gray-50"
>
취소
</button>
</>
) : (
<button
type="button"
onClick={() => {
setEditingId(memo.id)
setEditingBody(memo.body)
}}
className="rounded-md border border-gray-200 px-2 py-0.5 text-xs text-gray-500 hover:bg-gray-50"
>
수정
</button>
)}
<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>
</div>
{editingId === memo.id ? (
<textarea
aria-label="메모 수정"
rows={3}
value={editingBody}
onChange={(e) => setEditingBody(e.target.value)}
className="mt-1.5 w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm"
/>
) : (
<p className="mt-1.5 whitespace-pre-wrap text-sm text-gray-800">{memo.body}</p>
)}
</li>
))}
</ul>
)}
</div>
</div>
)
}