feat: 중간 점검 보고 내용은 지시자·담당자만 열람(상태는 전원 노출)

보고 '내용' 열람을 지시자 + 해당 업무 담당자로 제한:
- 백엔드 buildDetail: viewerId 기준으로 권한 없으면 checkpointReports 를 빈 배열로 응답(findOne 에 현재 사용자 전달). 점검일에는 reported 플래그를 두어 상태는 전원 공유
- 프론트: checkpointState 는 reported 플래그 기준, '보고 완료' 클릭(모달)은 지시자·담당자만. 그 외는 비클릭 상태 배지

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:01:50 +09:00
parent b717ecd9a5
commit 6577550937
6 changed files with 55 additions and 21 deletions
+2 -1
View File
@@ -107,8 +107,9 @@ export class TaskController {
findOne(
@Param('projectId') projectId: string,
@Param('taskId', ParseIntPipe) taskId: number,
@CurrentUser() user: PublicUser,
): Promise<TaskDetailResponse> {
return this.taskService.findOne(projectId, taskId);
return this.taskService.findOne(projectId, taskId, user.id);
}
@Patch(':taskId')
+41 -15
View File
@@ -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<TaskDetailResponse> {
async findOne(
projectId: string,
seq: number,
viewerId: string,
): Promise<TaskDetailResponse> {
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<TaskDetailResponse> {
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 =