feat: 할 일 항목별 보완 내용 개별 입력·표시 추가
공용 수정 요청 메시지는 그대로 두고, 검토 모달에서 각 보완 항목 아래에 항목별 보완 내용을 개별 등록할 수 있게 하고 상세 화면에 함께 표시한다. - 백엔드: ChecklistItem.reviewNote 컬럼 추가, ChecklistReviewDto.note 수용, requestChanges 가 미완성 항목에 보완 내용 보관(완료/승인 시 비움), 상세 응답 동봉 - 프론트: 검토 모달 보완 항목 아래 보완 내용 textarea(개별), 상세 체크리스트 보완 항목 아래 보완 내용 표시. 공용 메시지 입력은 유지 - 테스트: 미완성 항목 보완 내용 보관·완료 항목 비움 검증(9 tests) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ export class ApprovalRequestDto {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
// 수정 요청 시 항목별 완료 검토 결과(지시자가 체크/X 표시)
|
||||
// 수정 요청 시 항목별 완료 검토 결과(지시자가 체크/X 표시 + 항목별 보완 내용)
|
||||
export class ChecklistReviewDto {
|
||||
@ApiProperty({ description: '체크리스트 항목 ID' })
|
||||
@IsUUID('4')
|
||||
@@ -34,6 +34,15 @@ export class ChecklistReviewDto {
|
||||
@ApiProperty({ description: '완료 여부 — true=완성(체크) / false=미완성(X)' })
|
||||
@IsBoolean()
|
||||
done!: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '항목별 보완 내용(미완성 항목에만 적용). 생략 시 보완 내용 없음',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
note?: string;
|
||||
}
|
||||
|
||||
// 수정 요청(지시자 → 담당자) — 보완 내용 필수 + 항목별 완료 체크(선택)
|
||||
|
||||
@@ -35,6 +35,10 @@ export class ChecklistItem {
|
||||
@Column({ default: false })
|
||||
done!: boolean;
|
||||
|
||||
// 보완 내용(수정 요청 시 항목별로 입력) — 완료 처리 시 비워짐
|
||||
@Column({ name: 'review_note', type: 'text', nullable: true })
|
||||
reviewNote!: string | null;
|
||||
|
||||
// 표시 순서(생성 순)
|
||||
@Column({ name: 'sort_order', type: 'int', default: 0 })
|
||||
sortOrder!: number;
|
||||
|
||||
@@ -189,15 +189,20 @@ describe('TaskService', () => {
|
||||
checklistRepo.save.mockResolvedValue(task.checklist);
|
||||
|
||||
const result = await svc.requestChanges('q3-launch', 'admin', 9, '보완 바랍니다', [
|
||||
{ id: 'c1', done: false }, // 완성 → 미완성(X)
|
||||
{ id: 'c2', done: true }, // 미완성 → 완성(체크)
|
||||
{ id: 'c1', done: false, note: '여백을 더 주세요' }, // 완성 → 미완성(X) + 보완 내용
|
||||
{ id: 'c2', done: true, note: '무시됨' }, // 미완성 → 완성(체크): 보완 내용 비움
|
||||
// c3 은 미전달 → 기존값(true) 유지
|
||||
]);
|
||||
|
||||
expect(checklistRepo.save).toHaveBeenCalledTimes(1);
|
||||
expect(task.checklist.find((c) => c.id === 'c1')?.done).toBe(false);
|
||||
expect(task.checklist.find((c) => c.id === 'c2')?.done).toBe(true);
|
||||
expect(task.checklist.find((c) => c.id === 'c3')?.done).toBe(true);
|
||||
const c1 = task.checklist.find((c) => c.id === 'c1');
|
||||
const c2 = task.checklist.find((c) => c.id === 'c2');
|
||||
const c3 = task.checklist.find((c) => c.id === 'c3');
|
||||
expect(c1?.done).toBe(false);
|
||||
expect(c1?.reviewNote).toBe('여백을 더 주세요'); // 미완성 항목은 보완 내용 보관
|
||||
expect(c2?.done).toBe(true);
|
||||
expect(c2?.reviewNote).toBeNull(); // 완료 항목은 보완 내용 비움
|
||||
expect(c3?.done).toBe(true);
|
||||
expect(result.status).toBe('changes');
|
||||
expect(result.changesNote).toBe('보완 바랍니다');
|
||||
});
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface ChecklistItemResponse {
|
||||
text: string;
|
||||
done: boolean;
|
||||
category: ChecklistCategory;
|
||||
// 보완 내용(수정 요청 시 항목별 입력) — 없으면 null
|
||||
reviewNote: string | null;
|
||||
}
|
||||
|
||||
// 프로젝트 업무 목록의 세그먼트별 카운트(전체/진행/대기/완료) — 검색어 반영
|
||||
@@ -891,7 +893,7 @@ export class TaskService {
|
||||
actingUserId: string,
|
||||
seq: number,
|
||||
note: string,
|
||||
reviews?: { id: string; done: boolean }[],
|
||||
reviews?: { id: string; done: boolean; note?: string }[],
|
||||
): Promise<TaskDetailResponse> {
|
||||
const project = await this.getProjectOrThrow(projectId);
|
||||
const membership = await this.assertMember(project.id, actingUserId);
|
||||
@@ -901,14 +903,18 @@ export class TaskService {
|
||||
task.approvalNote = null;
|
||||
task.approvalRequestedAt = null;
|
||||
|
||||
// 항목별 완료 검토 결과 반영(체크=완성 / X=미완성) — 전달된 항목만 갱신
|
||||
// 항목별 완료 검토 결과 반영(체크=완성 / X=미완성 + 항목별 보완 내용) — 전달된 항목만 갱신
|
||||
if (reviews?.length && task.checklist?.length) {
|
||||
const reviewMap = new Map(reviews.map((r) => [r.id, r.done]));
|
||||
const reviewMap = new Map(reviews.map((r) => [r.id, r]));
|
||||
let checklistChanged = false;
|
||||
for (const item of task.checklist) {
|
||||
const reviewed = reviewMap.get(item.id);
|
||||
if (reviewed !== undefined && item.done !== reviewed) {
|
||||
item.done = reviewed;
|
||||
if (!reviewed) continue;
|
||||
// 완료 처리된 항목은 보완 내용 비움, 미완성 항목만 보완 내용 보관
|
||||
const nextNote = reviewed.done ? null : (reviewed.note?.trim() || null);
|
||||
if (item.done !== reviewed.done || item.reviewNote !== nextNote) {
|
||||
item.done = reviewed.done;
|
||||
item.reviewNote = nextNote;
|
||||
checklistChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -1041,9 +1047,12 @@ export class TaskService {
|
||||
task.status = next;
|
||||
await this.taskRepo.save(task);
|
||||
|
||||
// 승인 완료(→done) 시 체크리스트 항목을 모두 완료 처리(부수효과로 영속)
|
||||
// 승인 완료(→done) 시 체크리스트 항목을 모두 완료 처리 + 보완 내용 비움(부수효과로 영속)
|
||||
if (next === 'done' && task.checklist?.length) {
|
||||
for (const item of task.checklist) item.done = true;
|
||||
for (const item of task.checklist) {
|
||||
item.done = true;
|
||||
item.reviewNote = null;
|
||||
}
|
||||
await this.checklistRepo.save(task.checklist);
|
||||
}
|
||||
|
||||
@@ -1262,6 +1271,7 @@ export class TaskService {
|
||||
text: c.text,
|
||||
done: c.done,
|
||||
category: c.category,
|
||||
reviewNote: c.reviewNote ?? null,
|
||||
}));
|
||||
|
||||
// 승인 정보는 승인 대기(review) 상태이고 요청 메타가 있을 때만 노출
|
||||
|
||||
Reference in New Issue
Block a user