fix: feedback author display, sidebar scroll, stage sort by start date

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-05 22:55:11 +09:00
parent fa8ed76e22
commit 4960fe7352
6 changed files with 26 additions and 17 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE "task_details" ADD COLUMN IF NOT EXISTS "authorName" TEXT;

View File

@@ -99,6 +99,7 @@ model TaskDetail {
taskId String
milestoneId String?
content String
authorName String? // 피드백 작성자 표시명 (자유 입력)
updatedBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -5,15 +5,6 @@ import { AppError } from '../middleware/errorHandler';
const router = Router();
async function resolveAuthorId(taskId: string, authorName?: string): Promise<string> {
const name = authorName?.toString().trim();
if (name) {
const user = await prisma.user.findFirst({ where: { name } });
if (user) return user.id;
}
return resolveTaskActorId(taskId);
}
// POST /api/details/:taskId
router.post('/:taskId', async (req, res, next) => {
try {
@@ -30,13 +21,15 @@ router.post('/:taskId', async (req, res, next) => {
if (!ms) throw new AppError(400, '유효하지 않은 업무 단계입니다.');
}
const updatedBy = await resolveAuthorId(taskId, authorName);
const displayName = authorName?.toString().trim() || null;
const updatedBy = await resolveTaskActorId(taskId);
const detail = await prisma.taskDetail.create({
data: {
taskId,
milestoneId: milestoneId || null,
content: content.toString().trim(),
authorName: displayName,
updatedBy,
},
include: { author: { select: { id: true, name: true } } },

View File

@@ -26,7 +26,7 @@ export function FeedbackModal({
}: FeedbackModalProps) {
const [form, setForm] = useState<FeedbackFormData>({
content: detail?.content ?? '',
authorName: detail?.author?.name ?? defaultAuthorName,
authorName: detail?.authorName ?? detail?.author?.name ?? defaultAuthorName,
});
const handleSubmit = async (e: React.FormEvent) => {

View File

@@ -58,6 +58,19 @@ function sortByIsoDesc<T>(items: T[], pick: (item: T) => string) {
return [...items].sort((a, b) => new Date(pick(b)).getTime() - new Date(pick(a)).getTime());
}
/** 시작일이 늦은 단계가 상단 (시작일 없으면 종료일 → 생성일 순) */
function sortStagesByStartDesc(stages: Milestone[]) {
const pickStart = (m: Milestone) =>
m.startDate ?? m.dueDate ?? m.createdAt;
return [...stages].sort(
(a, b) => new Date(pickStart(b)).getTime() - new Date(pickStart(a)).getTime(),
);
}
function feedbackAuthorName(detail: TaskDetail) {
return detail.authorName?.trim() || detail.author?.name || '—';
}
function milestoneProgress(m: Milestone) {
if (m.completedAt) return 100;
const p = m.progress ?? 0;
@@ -215,7 +228,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
const details = task.details ?? [];
const sortedStages = useMemo(
() => sortByIsoDesc(milestones, (m) => m.updatedAt),
() => sortStagesByStartDesc(milestones),
[milestones],
);
@@ -404,7 +417,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
+
</button>
</div>
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pr-1">
{sortedStages.length === 0 && (
<p className="text-lg text-slate-400">+ .</p>
)}
@@ -450,7 +463,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
<LeftSection>
<PanelLabel></PanelLabel>
<ul
className="min-h-0 flex-1 space-y-2 overflow-hidden"
className="min-h-0 flex-1 space-y-2 overflow-y-auto pr-1"
onContextMenu={(e) => {
if (!selected) return;
e.preventDefault();
@@ -495,7 +508,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
</button>
</div>
<div
className="flex min-h-0 flex-1 flex-col overflow-hidden"
className="flex min-h-0 flex-1 flex-col overflow-y-auto pr-1"
onContextMenu={(e) => {
if (!selectedId) return;
e.preventDefault();
@@ -505,7 +518,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
{sortedFeedbacks.length === 0 ? (
<p className="text-lg text-slate-400"> + .</p>
) : (
<div className="mb-2 min-h-0 flex-1 space-y-2 overflow-hidden">
<div className="mb-2 min-h-0 flex-1 space-y-2">
{sortedFeedbacks.map((f) => (
<div
key={f.id}
@@ -518,7 +531,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
>
<p className="truncate text-2xl font-black leading-snug text-slate-700">
{f.content}
<span className="font-bold text-slate-400"> {f.author?.name ?? '—'}</span>
<span className="font-bold text-slate-400"> {feedbackAuthorName(f)}</span>
</p>
</div>
))}

View File

@@ -47,6 +47,7 @@ export interface TaskDetail {
taskId: string;
milestoneId: string | null;
content: string;
authorName: string | null;
updatedBy: string;
createdAt: string;
updatedAt: string;