fix: stage save errors and add milestone progress field

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-05 17:10:41 +09:00
parent 2c4ad9c4b4
commit c3f620a7ac
8 changed files with 104 additions and 31 deletions

View File

@@ -6,6 +6,7 @@ export interface StageFormData {
title: string;
startDate: string;
dueDate: string;
progress: number;
description: string;
feedback: string;
links: MilestoneLink[];
@@ -39,6 +40,7 @@ export function StageModal({ mode, milestone, onSave, onClose, saving }: StageMo
title: milestone?.title ?? '',
startDate: toDateInput(milestone?.startDate),
dueDate: toDateInput(milestone?.dueDate),
progress: milestone?.progress ?? 0,
description: milestone?.description ?? '',
feedback: '',
links: parseLinks(milestone?.links),
@@ -92,6 +94,22 @@ export function StageModal({ mode, milestone, onSave, onClose, saving }: StageMo
/>
</label>
<label className="block">
<span className="mb-1 flex items-center justify-between text-sm font-bold text-slate-500">
<span></span>
<span className="text-lg font-black text-emerald-600">{form.progress}%</span>
</span>
<input
type="range"
min={0}
max={100}
step={5}
value={form.progress}
onChange={(e) => set('progress', Number(e.target.value))}
className="w-full accent-emerald-600"
/>
</label>
<div className="grid grid-cols-2 gap-3">
<label className="block">
<span className="mb-1 block text-sm font-bold text-slate-500"></span>

View File

@@ -49,7 +49,9 @@ function sortByIsoDesc<T>(items: T[], pick: (item: T) => string) {
}
function milestoneProgress(m: Milestone) {
return m.completedAt ? 100 : 0;
if (m.completedAt) return 100;
const p = m.progress ?? 0;
return Math.min(100, Math.max(0, p));
}
function parseContentLines(text: string | null | undefined) {
@@ -274,7 +276,6 @@ function DetailView({ task }: { task: TaskWithRelations }) {
const form = new FormData();
form.append('file', file);
form.append('milestoneId', milestoneId);
form.append('uploadedBy', task.creatorId);
await apiClient.post(`/files/upload/${task.id}`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
@@ -289,21 +290,41 @@ function DetailView({ task }: { task: TaskWithRelations }) {
description: data.description.trim() || undefined,
startDate: data.startDate || undefined,
dueDate: data.dueDate || undefined,
progress: data.progress,
feedback: data.feedback.trim() || undefined,
links: JSON.stringify(data.links),
links: data.links.length > 0 ? JSON.stringify(data.links) : undefined,
};
let milestoneId: string;
if (stageModal?.mode === 'add') {
const { data: created } = await apiClient.post<Milestone>(`/milestones/${task.id}`, payload);
if (fileList.length) await uploadFiles(created.id, fileList);
milestoneId = created.id;
setSelectedId(created.id);
} else if (stageModal?.milestone) {
await apiClient.patch(`/milestones/item/${stageModal.milestone.id}`, payload);
if (fileList.length) await uploadFiles(stageModal.milestone.id, fileList);
const { data: updated } = await apiClient.patch<Milestone>(
`/milestones/item/${stageModal.milestone.id}`,
payload,
);
milestoneId = updated.id;
} else {
return;
}
if (fileList.length > 0) {
try {
await uploadFiles(milestoneId, fileList);
} catch {
alert('단계는 저장됐지만 파일 업로드에 실패했습니다.');
}
}
await qc.invalidateQueries({ queryKey: ['task', task.id] });
setStageModal(null);
} catch (err: unknown) {
const ax = err as { response?: { data?: { message?: string } }; message?: string };
const msg = ax.response?.data?.message || ax.message || '단계 저장에 실패했습니다.';
alert(msg);
} finally {
setStageSaving(false);
}

View File

@@ -86,6 +86,7 @@ export interface Milestone {
description: string | null;
startDate: string | null;
dueDate: string | null;
progress: number;
links: string | null;
completedAt: string | null;
order: number;