From 20f593bd5aa6239d5786a814586a3ab949de71da Mon Sep 17 00:00:00 2001 From: ttipo Date: Sun, 21 Jun 2026 17:18:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=AC=EC=9D=84=20'=ED=95=A0=20=EC=9D=BC=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EA=B2=80=ED=86=A0'=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시안(수정 요청 모달.html)을 반영해 모달 UI를 재구성한다. - 헤더(그라데이션 배지·안내)·스크롤 본문·고정 푸터 구조로 변경 - 항목별 완료/보완을 세그먼트 토글(완료=초록 / 보완=빨강 좌측 바)로 표시, 카테고리 그룹·진행률 바·완료 카운트 동봉 - 보완 항목 수에 따라 안내 배너(warn/good)와 메시지 필수 여부가 전환 - 모든 항목 완료 시 버튼이 '승인 완료'(초록)로 바뀌어 곧바로 승인, 보완 항목이 있으면 '수정 요청 보내기 (N건)'(빨강)으로 메시지와 함께 반려 - 모달 진입 시 전 항목을 '완료'로 초기화(담당자가 완료를 주장하며 요청) Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/pages/relay/TaskDetailPage.vue | 780 ++++++++++++++++---- 1 file changed, 639 insertions(+), 141 deletions(-) diff --git a/frontend/src/pages/relay/TaskDetailPage.vue b/frontend/src/pages/relay/TaskDetailPage.vue index 1e522b5..a46c191 100644 --- a/frontend/src/pages/relay/TaskDetailPage.vue +++ b/frontend/src/pages/relay/TaskDetailPage.vue @@ -146,18 +146,38 @@ async function approveTask() { showApprove.value = false } } -// 수정 요청(review→changes) — 지시자 액션. 보완 메시지 필수 + 항목별 완료 체크 +// 할 일 완료 검토(지시자) — 모든 항목 '완료'면 승인, 보완 항목이 있으면 메시지와 함께 수정 요청 const changesNote = ref('') -// 항목 ID → 완성 여부(체크=true / X=false). 모달을 열 때 현재 완료 상태로 초기화 +// 항목 ID → 완료 여부(완료=true / 보완=false) const reviewChecks = ref>({}) +const reviewTotal = computed(() => task.value?.checklist.length ?? 0) const reviewDoneCount = computed( () => Object.values(reviewChecks.value).filter(Boolean).length, ) -// 수정 요청 모달 열기 — 체크리스트 검토 상태를 현재 완료값으로 초기화 +const reviewTodoCount = computed(() => reviewTotal.value - reviewDoneCount.value) +const reviewPct = computed(() => + reviewTotal.value + ? Math.round((reviewDoneCount.value / reviewTotal.value) * 100) + : 0, +) +// 보완 항목이 없으면 곧바로 승인 가능 +const reviewAllDone = computed(() => reviewTodoCount.value === 0) +// 제출 가능 여부 — 보완 항목이 있으면 메시지 필수 +const reviewSubmittable = computed( + () => reviewAllDone.value || changesNote.value.trim().length > 0, +) +// 담당자 표기(푸터) +const assigneeNames = computed(() => { + const names = task.value?.assignees.map((a) => a.name) ?? [] + if (names.length === 0) return '담당자' + if (names.length === 1) return names[0] + return `${names[0]} 외 ${names.length - 1}명` +}) +// 수정 요청 모달 열기 — 모든 항목을 '완료'로 초기화(담당자가 완료를 주장하며 요청) function openChanges() { const map: Record = {} for (const item of task.value?.checklist ?? []) { - if (item.id) map[item.id] = item.done + if (item.id) map[item.id] = true } reviewChecks.value = map changesNote.value = '' @@ -166,27 +186,37 @@ function openChanges() { function reviewState(item: ChecklistItem): boolean { return item.id ? !!reviewChecks.value[item.id] : false } -function toggleReview(item: ChecklistItem) { +function setReview(item: ChecklistItem, done: boolean) { if (!item.id) return - reviewChecks.value = { - ...reviewChecks.value, - [item.id]: !reviewChecks.value[item.id], - } + reviewChecks.value = { ...reviewChecks.value, [item.id]: done } } +// 카테고리 → 그룹 도트 색 클래스 +function groupClass(cat: ChecklistCategory): string { + const map: Record = { + 필수: 'g-req', + 관리: 'g-mng', + 보안: 'g-sec', + 부가: 'g-ext', + } + return map[cat] +} +// 검토 제출 — 전부 완료면 승인, 보완 항목이 있으면 수정 요청 async function submitChanges() { - if (!task.value) return - const note = changesNote.value.trim() - if (!note) return - const checklist: ChecklistReviewItem[] = (task.value.checklist ?? []) - .filter((c): c is ChecklistItem & { id: string } => !!c.id) - .map((c) => ({ id: c.id, done: !!reviewChecks.value[c.id] })) + if (!task.value || !reviewSubmittable.value) return try { - task.value = await taskStore.requestChanges( - projectId.value, - task.value.id, - note, - checklist.length ? checklist : undefined, - ) + if (reviewAllDone.value) { + task.value = await taskStore.approve(projectId.value, task.value.id) + } else { + const checklist: ChecklistReviewItem[] = (task.value.checklist ?? []) + .filter((c): c is ChecklistItem & { id: string } => !!c.id) + .map((c) => ({ id: c.id, done: !!reviewChecks.value[c.id] })) + task.value = await taskStore.requestChanges( + projectId.value, + task.value.id, + changesNote.value.trim(), + checklist.length ? checklist : undefined, + ) + } } catch { // 인터셉터가 알림 처리 } finally { @@ -1308,58 +1338,92 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { - +