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 taskId String
milestoneId String? milestoneId String?
content String content String
authorName String? // 피드백 작성자 표시명 (자유 입력)
updatedBy String updatedBy String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

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

View File

@@ -26,7 +26,7 @@ export function FeedbackModal({
}: FeedbackModalProps) { }: FeedbackModalProps) {
const [form, setForm] = useState<FeedbackFormData>({ const [form, setForm] = useState<FeedbackFormData>({
content: detail?.content ?? '', content: detail?.content ?? '',
authorName: detail?.author?.name ?? defaultAuthorName, authorName: detail?.authorName ?? detail?.author?.name ?? defaultAuthorName,
}); });
const handleSubmit = async (e: React.FormEvent) => { 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()); 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) { function milestoneProgress(m: Milestone) {
if (m.completedAt) return 100; if (m.completedAt) return 100;
const p = m.progress ?? 0; const p = m.progress ?? 0;
@@ -215,7 +228,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
const details = task.details ?? []; const details = task.details ?? [];
const sortedStages = useMemo( const sortedStages = useMemo(
() => sortByIsoDesc(milestones, (m) => m.updatedAt), () => sortStagesByStartDesc(milestones),
[milestones], [milestones],
); );
@@ -404,7 +417,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
+ +
</button> </button>
</div> </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 && ( {sortedStages.length === 0 && (
<p className="text-lg text-slate-400">+ .</p> <p className="text-lg text-slate-400">+ .</p>
)} )}
@@ -450,7 +463,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
<LeftSection> <LeftSection>
<PanelLabel></PanelLabel> <PanelLabel></PanelLabel>
<ul <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) => { onContextMenu={(e) => {
if (!selected) return; if (!selected) return;
e.preventDefault(); e.preventDefault();
@@ -495,7 +508,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
</button> </button>
</div> </div>
<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) => { onContextMenu={(e) => {
if (!selectedId) return; if (!selectedId) return;
e.preventDefault(); e.preventDefault();
@@ -505,7 +518,7 @@ 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 overflow-hidden"> <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}
@@ -518,7 +531,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
> >
<p className="truncate text-2xl font-black leading-snug text-slate-700"> <p className="truncate text-2xl font-black leading-snug text-slate-700">
{f.content} {f.content}
<span className="font-bold text-slate-400"> {f.author?.name ?? '—'}</span> <span className="font-bold text-slate-400"> {feedbackAuthorName(f)}</span>
</p> </p>
</div> </div>
))} ))}

View File

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