fix: feedback author display, sidebar scroll, stage sort by start date
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "task_details" ADD COLUMN IF NOT EXISTS "authorName" TEXT;
|
||||
@@ -99,6 +99,7 @@ model TaskDetail {
|
||||
taskId String
|
||||
milestoneId String?
|
||||
content String
|
||||
authorName String? // 피드백 작성자 표시명 (자유 입력)
|
||||
updatedBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -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 } } },
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface TaskDetail {
|
||||
taskId: string;
|
||||
milestoneId: string | null;
|
||||
content: string;
|
||||
authorName: string | null;
|
||||
updatedBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
Reference in New Issue
Block a user