diff --git a/backend/src/modules/task/task.controller.ts b/backend/src/modules/task/task.controller.ts index 07b28c8..02fbe20 100644 --- a/backend/src/modules/task/task.controller.ts +++ b/backend/src/modules/task/task.controller.ts @@ -107,8 +107,9 @@ export class TaskController { findOne( @Param('projectId') projectId: string, @Param('taskId', ParseIntPipe) taskId: number, + @CurrentUser() user: PublicUser, ): Promise { - return this.taskService.findOne(projectId, taskId); + return this.taskService.findOne(projectId, taskId, user.id); } @Patch(':taskId') diff --git a/backend/src/modules/task/task.service.ts b/backend/src/modules/task/task.service.ts index 43f6919..0d25a19 100644 --- a/backend/src/modules/task/task.service.ts +++ b/backend/src/modules/task/task.service.ts @@ -73,6 +73,8 @@ export interface ChecklistItemResponse { export interface CheckpointResponse { id: string; checkAt: string; + // 보고 접수 여부(상태 표시용) — 내용 열람 권한과 무관하게 전원에게 노출 + reported: boolean; } // 중간 점검 보고 응답(스냅샷 포함) @@ -343,10 +345,14 @@ export class TaskService { } // 업무 단건 상세 — project 내 순번(seq)으로 조회 - async findOne(projectId: string, seq: number): Promise { + async findOne( + projectId: string, + seq: number, + viewerId: string, + ): Promise { const project = await this.getProjectOrThrow(projectId); const task = await this.getTaskOrThrow(project.id, seq); - return this.buildDetail(task, project); + return this.buildDetail(task, project, viewerId); } // 업무 생성 — admin(지시자) 권한. 담당자는 프로젝트 멤버만 지정 가능 @@ -413,6 +419,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, saved.seq), project, + actingUserId, ); } @@ -510,6 +517,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, seq), project, + actingUserId, ); } @@ -688,6 +696,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, seq), project, + actingUserId, ); } @@ -920,6 +929,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, seq), project, + actingUserId, ); } @@ -937,6 +947,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, seq), project, + actingUserId, ); } @@ -978,6 +989,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, seq), project, + actingUserId, ); } @@ -1422,6 +1434,7 @@ export class TaskService { return this.buildDetail( await this.getTaskOrThrow(project.id, seq), project, + actingUserId, ); } @@ -1476,6 +1489,7 @@ export class TaskService { private async buildDetail( task: Task, project: Project, + viewerId: string, ): Promise { const [comments, files, activities, checkpointRows, reportRows] = await Promise.all([ @@ -1505,23 +1519,35 @@ export class TaskService { workStatus: c.workStatus, })); + // 점검일별 보고 접수 여부(상태 배지용) — 전원 노출 + const reportedCheckpointIds = new Set( + reportRows + .map((r) => r.checkpoint?.id) + .filter((id): id is string => !!id), + ); const checkpoints: CheckpointResponse[] = checkpointRows.map((c) => ({ id: c.id, checkAt: c.checkAt.toISOString(), + reported: reportedCheckpointIds.has(c.id), })); - const checkpointReports: CheckpointReportResponse[] = reportRows.map( - (r) => ({ - id: r.id, - checkpointId: r.checkpoint?.id ?? '', - checkAt: r.checkpoint?.checkAt - ? r.checkpoint.checkAt.toISOString() - : null, - reporter: r.reporter ? UserService.toPublic(r.reporter) : null, - note: r.note, - items: r.itemsSnapshot ?? [], - createdAt: r.createdAt.toISOString(), - }), - ); + + // 보고 '내용'은 지시자 + 해당 업무 담당자만 열람 가능(그 외에는 빈 배열) + const canViewReports = + task.issuer?.id === viewerId || + (task.assignees ?? []).some((a) => a.id === viewerId); + const checkpointReports: CheckpointReportResponse[] = canViewReports + ? reportRows.map((r) => ({ + id: r.id, + checkpointId: r.checkpoint?.id ?? '', + checkAt: r.checkpoint?.checkAt + ? r.checkpoint.checkAt.toISOString() + : null, + reporter: r.reporter ? UserService.toPublic(r.reporter) : null, + note: r.note, + items: r.itemsSnapshot ?? [], + createdAt: r.createdAt.toISOString(), + })) + : []; // 승인 정보는 승인 대기(review) 상태이고 요청 메타가 있을 때만 노출 const approval: ApprovalInfo | null = diff --git a/frontend/src/mock/relay.mock.ts b/frontend/src/mock/relay.mock.ts index b18f24f..43aff59 100644 --- a/frontend/src/mock/relay.mock.ts +++ b/frontend/src/mock/relay.mock.ts @@ -55,10 +55,12 @@ export interface ChecklistItem { /** 중간 점검일(표시용) */ export interface TaskCheckpointView { id: string - /** ISO — 보고 가능 구간(점검일~마감) 계산용 */ + /** ISO — 점검일 당일 판정용 */ checkAt: string /** 표시 라벨 */ dateLabel: string + /** 보고 접수 여부(상태 배지용) */ + reported: boolean } /** 중간 점검 보고(스냅샷, 표시용) */ diff --git a/frontend/src/pages/relay/TaskDetailPage.vue b/frontend/src/pages/relay/TaskDetailPage.vue index f34115a..9287f21 100644 --- a/frontend/src/pages/relay/TaskDetailPage.vue +++ b/frontend/src/pages/relay/TaskDetailPage.vue @@ -322,10 +322,12 @@ const CP_STATE_LABEL: Record = { reported: '보고 완료', missed: '누락', } -// 체크포인트 상태 — 보고됨 / 당일 / 지남(누락) / 예정 +// 보고 '내용' 열람 권한 — 지시자 또는 해당 업무 담당자(백엔드도 동일하게 제한) +const canViewReports = computed(() => isIssuer.value || isAssignee.value) + +// 체크포인트 상태 — 보고됨 / 당일 / 지남(누락) / 예정. 보고됨은 reported 플래그(전원 공유) 기준 function checkpointState(cp: TaskCheckpointView): CheckpointState { - const reported = task.value?.checkpointReports.some((r) => r.checkpointId === cp.id) ?? false - if (reported) return 'reported' + if (cp.reported) return 'reported' const cpDay = kstDate(cp.checkAt) if (cpDay === todayKst.value) return 'today' if (cpDay < todayKst.value) return 'missed' @@ -1279,7 +1281,7 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { {{ reportingCpId === cp.id ? '취소' : '보고서 작성' }}