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
|
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
|
||||||
|
|||||||
@@ -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 } } },
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user