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 { useCallback, useEffect, useState } from 'react'
import {
addReMemo,
deleteReMemo,
getReMemos,
updateReMemo,
type Org,
type ReMemo,
} from '../api/client'
/**
* 기관관리 상세의 RE 탭.
*
* 요구사항 [4]: 별도 양식 없이 메모장처럼 등록한다. 메모 1개가 RE 1개다.
* 예) ooo 계약서 및 제안요청서 송부 요청, 장영익, 2026.07.08
*
* 완료 체크는 회신에 없던 항목이다. 대시보드 '미해결 RE' 숫자를 세려면 해결된 건과 아닌 건을
* 가릴 수단이 필요해 한 칸 두었다.
*/
function today(): string {
const d = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
export default function ReMemos({ org }: { org: Org }) {
const [memos, setMemos] = useState<ReMemo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
const [content, setContent] = useState('')
const [author, setAuthor] = useState('')
const [memoDate, setMemoDate] = useState(today())
const [editingId, setEditingId] = useState<number | null>(null)
const load = useCallback(async () => {
setLoading(true)
try {
setMemos(await getReMemos(org.id))
setError(null)
} catch (e) {
setError((e as Error).message)
} finally {
setLoading(false)
}
}, [org.id])
useEffect(() => {
void load()
}, [load])
function resetForm() {
setContent('')
setAuthor('')
setMemoDate(today())
setEditingId(null)
}
async function handleSave() {
if (!content.trim() || !author.trim() || busy) {
return
}
setBusy(true)
setError(null)
try {
const body = { content: content.trim(), author: author.trim(), memoDate, resolved: false }
const next = editingId === null
? await addReMemo(org.id, body)
: await updateReMemo(org.id, editingId, {
...body,
resolved: memos.find((m) => m.id === editingId)?.resolved ?? false,
})
setMemos(next)
resetForm()
} catch (e) {
setError((e as Error).message)
} finally {
setBusy(false)
}
}
async function toggleResolved(memo: ReMemo) {
setBusy(true)
try {
setMemos(await updateReMemo(org.id, memo.id, {
content: memo.content,
author: memo.author,
memoDate: memo.memoDate,
resolved: !memo.resolved,
}))
} catch (e) {
setError((e as Error).message)
} finally {
setBusy(false)
}
}
async function handleDelete(memo: ReMemo) {
if (!window.confirm('이 RE를 지울까요?')) {
return
}
setBusy(true)
try {
await deleteReMemo(org.id, memo.id)
await load()
if (editingId === memo.id) {
resetForm()
}
} finally {
setBusy(false)
}
}
function startEdit(memo: ReMemo) {
setEditingId(memo.id)
setContent(memo.content)
setAuthor(memo.author)
setMemoDate(memo.memoDate)
}
const unresolved = memos.filter((m) => !m.resolved).length
return (
<div className="p-5">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold">
RE <span className="ml-1 text-xs font-normal text-gray-500">미해결 {unresolved}건 / 전체 {memos.length}건</span>
</h3>
</div>
<div className="mt-3 rounded-lg border border-gray-200 p-3">
<textarea
aria-label="RE 내용"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={2}
placeholder="예) ooo 계약서 및 제안요청서 송부 요청"
className="w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm"
/>
<div className="mt-2 flex flex-wrap items-center gap-2">
<input
aria-label="작성자"
value={author}
onChange={(e) => setAuthor(e.target.value)}
placeholder="작성자"
className="w-32 rounded-md border border-gray-300 px-2 py-1.5 text-sm"
/>
<input
type="date"
aria-label="일자"
value={memoDate}
onChange={(e) => setMemoDate(e.target.value)}
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm"
/>
<button
type="button"
disabled={!content.trim() || !author.trim() || busy}
onClick={() => void handleSave()}
className="rounded-md bg-blue-600 px-3 py-1.5 text-sm text-white disabled:bg-gray-300"
>
{editingId === null ? '등록' : '수정'}
</button>
{editingId !== null && (
<button
type="button"
onClick={resetForm}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600"
>
취소
</button>
)}
</div>
</div>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
{loading ? (
<p className="mt-4 text-sm text-gray-500">불러오는 중…</p>
) : memos.length === 0 ? (
<p className="mt-4 text-sm text-gray-500">등록된 RE가 없습니다.</p>
) : (
<ul className="mt-4 space-y-2">
{memos.map((memo) => (
<li
key={memo.id}
className={`rounded-lg border p-3 ${
memo.resolved ? 'border-gray-200 bg-gray-50' : 'border-amber-200 bg-amber-50/40'
}`}
>
<div className="flex items-start justify-between gap-3">
<p className={`whitespace-pre-wrap text-sm ${memo.resolved ? 'text-gray-400 line-through' : 'text-gray-900'}`}>
{memo.content}
</p>
<div className="flex shrink-0 gap-2">
<button
type="button"
disabled={busy}
onClick={() => void toggleResolved(memo)}
className="rounded-md border border-gray-300 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-50"
>
{memo.resolved ? '미해결로' : '해결'}
</button>
<button
type="button"
disabled={busy}
onClick={() => startEdit(memo)}
className="rounded-md border border-gray-300 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-50"
>
수정
</button>
<button
type="button"
disabled={busy}
onClick={() => void handleDelete(memo)}
className="rounded-md border border-gray-300 px-2 py-0.5 text-xs text-red-600 hover:bg-red-50"
>
삭제
</button>
</div>
</div>
<p className="mt-1.5 text-xs text-gray-500">
{memo.author} · {memo.memoDate}
</p>
</li>
))}
</ul>
)}
</div>
)
}