feat: feedback edit/delete buttons and author name on edit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-05 23:04:55 +09:00
parent c93df1ae40
commit b8975098ae
3 changed files with 43 additions and 17 deletions

View File

@@ -44,7 +44,7 @@ router.post('/:taskId', async (req, res, next) => {
// PATCH /api/details/item/:id // PATCH /api/details/item/:id
router.patch('/item/:id', async (req, res, next) => { router.patch('/item/:id', async (req, res, next) => {
try { try {
const { content } = req.body as Record<string, string>; const { content, authorName } = req.body as Record<string, string>;
if (!content?.toString().trim()) throw new AppError(400, '피드백 내용은 필수입니다.'); if (!content?.toString().trim()) throw new AppError(400, '피드백 내용은 필수입니다.');
const existing = await prisma.taskDetail.findUnique({ where: { id: req.params.id } }); const existing = await prisma.taskDetail.findUnique({ where: { id: req.params.id } });
@@ -52,7 +52,10 @@ router.patch('/item/:id', async (req, res, next) => {
const detail = await prisma.taskDetail.update({ const detail = await prisma.taskDetail.update({
where: { id: req.params.id }, where: { id: req.params.id },
data: { content: content.toString().trim() }, data: {
content: content.toString().trim(),
...(authorName !== undefined && { authorName: authorName?.toString().trim() || null }),
},
include: { author: { select: { id: true, name: true } } }, include: { author: { select: { id: true, name: true } } },
}); });

View File

@@ -66,18 +66,18 @@ export function FeedbackModal({
/> />
</label> </label>
{mode === 'add' && ( <label className="block">
<label className="block"> <span className="mb-1 block text-sm font-bold text-slate-500">
<span className="mb-1 block text-sm font-bold text-slate-500"> *</span> {mode === 'add' ? '*' : ''}
<input </span>
required <input
value={form.authorName} required={mode === 'add'}
onChange={(e) => setForm((p) => ({ ...p, authorName: e.target.value }))} value={form.authorName}
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-base focus:border-emerald-400 focus:outline-none" onChange={(e) => setForm((p) => ({ ...p, authorName: e.target.value }))}
placeholder="작성자 이름" className="w-full rounded-lg border border-slate-200 px-3 py-2 text-base focus:border-emerald-400 focus:outline-none"
/> placeholder="작성자 이름"
</label> />
)} </label>
</div> </div>
<div className="flex justify-end gap-2 border-t border-slate-100 px-6 py-4"> <div className="flex justify-end gap-2 border-t border-slate-100 px-6 py-4">

View File

@@ -373,6 +373,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
} else if (feedbackModal?.detail) { } else if (feedbackModal?.detail) {
await apiClient.patch(`/details/item/${feedbackModal.detail.id}`, { await apiClient.patch(`/details/item/${feedbackModal.detail.id}`, {
content: data.content.trim(), content: data.content.trim(),
authorName: data.authorName.trim() || null,
}); });
} }
await qc.invalidateQueries({ queryKey: ['task', task.id] }); await qc.invalidateQueries({ queryKey: ['task', task.id] });
@@ -516,23 +517,45 @@ function DetailView({ task }: { task: TaskWithRelations }) {
}} }}
> >
{sortedFeedbacks.length === 0 ? ( {sortedFeedbacks.length === 0 ? (
<p className="text-lg text-slate-400"> + .</p> <p className="text-lg text-slate-400">+ .</p>
) : ( ) : (
<div className="mb-2 min-h-0 flex-1 space-y-2"> <div className="mb-2 min-h-0 flex-1 space-y-2">
{sortedFeedbacks.map((f) => ( {sortedFeedbacks.map((f) => (
<div <div
key={f.id} key={f.id}
className="rounded-lg bg-slate-50 px-3 py-2" className="flex items-start justify-between gap-2 rounded-lg bg-slate-50 px-3 py-2"
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setFeedbackCtx({ x: e.clientX, y: e.clientY, detailId: f.id }); setFeedbackCtx({ x: e.clientX, y: e.clientY, detailId: f.id });
}} }}
> >
<p className="truncate text-2xl font-black leading-snug text-slate-700"> <p className="min-w-0 flex-1 truncate text-2xl font-black leading-snug text-slate-700">
{f.content} {f.content}
<span className="font-bold text-slate-400"> {feedbackAuthorName(f)}</span> <span className="font-bold text-slate-400"> {feedbackAuthorName(f)}</span>
</p> </p>
<div className="flex shrink-0 items-center gap-1 pt-0.5">
<button
type="button"
title="피드백 수정"
onClick={() => setFeedbackModal({ mode: 'edit', detail: f })}
className="rounded px-2 py-0.5 text-xs font-bold text-slate-500 hover:bg-slate-100"
>
</button>
<button
type="button"
title="피드백 삭제"
onClick={() => {
if (window.confirm('이 피드백을 삭제하시겠습니까?')) {
deleteFeedback.mutate(f.id);
}
}}
className="rounded px-2 py-0.5 text-xs font-bold text-red-400 hover:bg-red-50 hover:text-red-600"
>
🗑
</button>
</div>
</div> </div>
))} ))}
</div> </div>