From 98a2308c4e88d0f1b484e49d5bcb6afbc9f6fbcd Mon Sep 17 00:00:00 2001 From: ttipo Date: Mon, 22 Jun 2026 15:33:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A4=91=EA=B0=84=20=EC=A0=90=EA=B2=80?= =?UTF-8?q?=20=EB=B3=B4=EA=B3=A0=EB=A5=BC=20=EB=AA=A8=EB=8B=AC=20+=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=ED=83=AD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=97=B4=EB=9E=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 하단 토글 목록 제거 → 상단 '보고 완료' 체크포인트 클릭 시 모달로 보고 내용 표시. 모달은 업무 생성의 카테고리 탭(필수/관리/보안/부가) 방식으로 항목을 구분해 보여준다. 보고 스냅샷에 category 추가(백엔드 엔티티/서비스 + 프론트 타입/뷰/스토어). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../task/entities/checkpoint-report.entity.ts | 6 +- backend/src/modules/task/task.service.ts | 2 +- frontend/src/mock/relay.mock.ts | 2 +- frontend/src/pages/relay/TaskDetailPage.vue | 333 ++++++++++++------ frontend/src/stores/task.store.ts | 6 +- frontend/src/types/task.ts | 1 + 6 files changed, 234 insertions(+), 116 deletions(-) diff --git a/backend/src/modules/task/entities/checkpoint-report.entity.ts b/backend/src/modules/task/entities/checkpoint-report.entity.ts index 96b2216..5a25084 100644 --- a/backend/src/modules/task/entities/checkpoint-report.entity.ts +++ b/backend/src/modules/task/entities/checkpoint-report.entity.ts @@ -10,12 +10,16 @@ import { import { User } from '../../user/entities/user.entity'; import { Task } from './task.entity'; import { TaskCheckpoint } from './task-checkpoint.entity'; -import type { ChecklistWorkStatus } from './checklist-item.entity'; +import type { + ChecklistCategory, + ChecklistWorkStatus, +} from './checklist-item.entity'; // 보고 스냅샷의 항목 단위(보고 시점의 항목별 진행 상태를 동결 보관) export interface ReportItemSnapshot { itemId: string; text: string; + category: ChecklistCategory; workStatus: ChecklistWorkStatus; } diff --git a/backend/src/modules/task/task.service.ts b/backend/src/modules/task/task.service.ts index 8bb8773..06101be 100644 --- a/backend/src/modules/task/task.service.ts +++ b/backend/src/modules/task/task.service.ts @@ -1383,7 +1383,7 @@ export class TaskService { c.workStatus = ws; changed.push(c); } - return { itemId: c.id, text: c.text, workStatus: ws }; + return { itemId: c.id, text: c.text, category: c.category, workStatus: ws }; }); if (changed.length) { await this.checklistRepo.save(changed); diff --git a/frontend/src/mock/relay.mock.ts b/frontend/src/mock/relay.mock.ts index c85f687..b18f24f 100644 --- a/frontend/src/mock/relay.mock.ts +++ b/frontend/src/mock/relay.mock.ts @@ -67,7 +67,7 @@ export interface CheckpointReportView { checkpointId: string reporterName: string note: string | null - items: { text: string; workStatus: ChecklistWorkStatus }[] + items: { text: string; category: ChecklistCategory; workStatus: ChecklistWorkStatus }[] /** 점검일 표시 */ dateLabel: string /** 보고 시각(상대) */ diff --git a/frontend/src/pages/relay/TaskDetailPage.vue b/frontend/src/pages/relay/TaskDetailPage.vue index 87635d1..1963be0 100644 --- a/frontend/src/pages/relay/TaskDetailPage.vue +++ b/frontend/src/pages/relay/TaskDetailPage.vue @@ -17,6 +17,7 @@ import { type ChecklistCategory, type ChecklistItem, type ChecklistWorkStatus, + type CheckpointReportView, type Comment, type TaskCheckpointView, type TaskDetail, @@ -365,13 +366,21 @@ async function submitReport() { } } -// --- 보고 기록 펼치기(지시자 열람) --- -const openReports = ref>(new Set()) -function toggleReport(id: string) { - const s = new Set(openReports.value) - if (s.has(id)) s.delete(id) - else s.add(id) - openReports.value = s +// --- 보고 기록 열람(모달) --- +const openedReport = ref(null) +const reportTab = ref('필수') +// 보고 완료 체크포인트 클릭 → 해당 체크포인트의 최신 보고를 모달로 연다 +function openReportModal(cp: TaskCheckpointView) { + const r = task.value?.checkpointReports.find((x) => x.checkpointId === cp.id) + if (!r) return + openedReport.value = r + reportTab.value = '필수' +} +function reportItemsByCat(cat: ChecklistCategory) { + return openedReport.value?.items.filter((it) => it.category === cat) ?? [] +} +function reportCatCount(cat: ChecklistCategory): number { + return reportItemsByCat(cat).length } // 담당자 액션 카드 테마(시안): changes→빨강 / review→앰버 / done→초록 / 그 외→accent const assigneeCardClass = computed(() => { @@ -1253,6 +1262,15 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { > {{ reportingCpId === cp.id ? '취소' : '보고서 작성' }} + - - -
-
- -
-
- {{ r.note }} -
-
    -
  • - {{ WORK_STATUS_LABEL[it.workStatus] }} - {{ it.text }} -
  • -
-
-
-
@@ -1632,6 +1602,95 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { + + + + + @@ -3288,12 +3347,14 @@ a.file-info { .cp-d-date { font-weight: 600; } -/* 상태 배지 */ +/* 상태 배지(span/button 공용) */ .cp-state { margin-left: auto; + font-family: inherit; font-size: 0.75rem; font-weight: 700; padding: 0.1875rem 0.5625rem; + border: none; border-radius: 20px; color: var(--text-3); background: #eceef1; @@ -3310,6 +3371,12 @@ a.file-info { color: var(--red); background: var(--red-weak); } +.cp-state.clickable { + cursor: pointer; +} +.cp-state.reported.clickable:hover { + background: #cfe9d8; +} .cp-report-btn { margin-left: auto; border: none; @@ -3401,81 +3468,123 @@ a.file-info { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); } -/* 보고 기록(열람) */ -.cp-rep-list { +/* 보고 열람 모달 */ +.cp-modal { + width: min(34rem, calc(100vw - 2rem)); + max-height: calc(100vh - 4rem); display: flex; flex-direction: column; - gap: 0.375rem; - border-top: 1px solid var(--border); - padding-top: 0.625rem; -} -.cp-rep { - background: rgba(20, 24, 33, 0.03); - border-radius: var(--radius); + background: #fff; + border-radius: 0.875rem; + box-shadow: 0 1.5rem 4.375rem rgba(20, 24, 33, 0.3); overflow: hidden; } -.cp-rep-head { +.cp-modal-head { display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; + padding: 1.125rem 1.25rem 0.875rem; + border-bottom: 1px solid var(--border); +} +.cp-modal-title { + font-size: 1rem; + font-weight: 700; + color: var(--text); +} +.cp-modal-sub { + margin-top: 0.25rem; + font-size: 0.781rem; + color: var(--text-3); +} +.cp-modal-note { + font-size: 0.8125rem; + color: var(--text-2); + line-height: 1.6; + white-space: pre-wrap; + padding: 0.75rem 1.25rem; + background: rgba(20, 24, 33, 0.03); + border-bottom: 1px solid var(--border); +} +/* 카테고리 탭 */ +.cat-tabs { + display: flex; + gap: 0.25rem; + padding: 0.75rem 1.25rem 0; +} +.cat-tab { + display: inline-flex; align-items: center; - gap: 0.4375rem; - width: 100%; + gap: 0.375rem; border: none; background: transparent; font-family: inherit; - text-align: left; - cursor: pointer; - padding: 0.5rem 0.625rem; - font-size: 0.781rem; - color: var(--text); -} -.cp-rep-head:hover { - background: rgba(20, 24, 33, 0.04); -} -.cp-rep-caret { - color: var(--text-3); - transition: transform 0.15s ease; - font-size: 0.75rem; -} -.cp-rep-caret.open { - transform: rotate(90deg); -} -.cp-rep-meta { - margin-left: auto; - font-size: 0.6875rem; - color: var(--text-3); - white-space: nowrap; -} -.cp-rep-body { - padding: 0 0.625rem 0.5625rem 1.5rem; -} -.cp-rep-note { - font-size: 0.75rem; + font-size: 0.8125rem; + font-weight: 600; color: var(--text-2); - margin-bottom: 0.375rem; - white-space: pre-wrap; + padding: 0.375rem 0.625rem; + border-radius: var(--radius-sm); + cursor: pointer; } -.cp-rep-items { - list-style: none; - margin: 0; - padding: 0; +.cat-tab:hover { + background: #f1f2f4; +} +.cat-tab .cat-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--text-3); +} +.cat-tab.sec-1 .cat-dot { + background: var(--accent); +} +.cat-tab.sec-2 .cat-dot { + background: #1b7a47; +} +.cat-tab.sec-3 .cat-dot { + background: #c0394f; +} +.cat-tab.sec-4 .cat-dot { + background: #a96a13; +} +.cat-tab .cat-count { + font-size: 0.6875rem; + font-weight: 700; + color: var(--text-3); +} +.cat-tab.active { + background: var(--accent-weak); + color: var(--accent); +} +.cat-tab.active .cat-count { + color: var(--accent); +} +/* 모달 항목 목록 */ +.cp-modal-items { + padding: 0.75rem 1.25rem 1.25rem; + overflow-y: auto; display: flex; flex-direction: column; - gap: 0.25rem; + gap: 0.5rem; } -.cp-rep-items li { +.cp-modal-item { display: flex; align-items: center; - gap: 0.4375rem; + gap: 0.625rem; } -.cp-rep-items .ws-badge { +.cp-modal-item .ws-badge { margin-left: 0; + flex-shrink: 0; } -.cp-rep-itext { - font-size: 0.75rem; - color: var(--text-2); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.cp-mi-text { + font-size: 0.8125rem; + color: var(--text); +} +.cp-modal-empty { + font-size: 0.8125rem; + color: var(--text-3); + padding: 1rem 0; + text-align: center; } diff --git a/frontend/src/stores/task.store.ts b/frontend/src/stores/task.store.ts index daaddeb..4d0cc91 100644 --- a/frontend/src/stores/task.store.ts +++ b/frontend/src/stores/task.store.ts @@ -215,7 +215,11 @@ function toTaskDetailView(api: ApiTaskDetail): TaskDetailView { checkpointId: r.checkpointId, reporterName: r.reporter?.name ?? '알 수 없음', note: r.note, - items: r.items.map((it) => ({ text: it.text, workStatus: it.workStatus })), + items: r.items.map((it) => ({ + text: it.text, + category: it.category, + workStatus: it.workStatus, + })), dateLabel: r.checkAt ? monthDayKo(r.checkAt) : '', timeLabel: relativeTimeKo(r.createdAt), })), diff --git a/frontend/src/types/task.ts b/frontend/src/types/task.ts index 4bd5e88..baa23a9 100644 --- a/frontend/src/types/task.ts +++ b/frontend/src/types/task.ts @@ -46,6 +46,7 @@ export interface ApiCheckpoint { export interface ApiReportItem { itemId: string text: string + category: ChecklistCategory workStatus: ChecklistWorkStatus }