feat: 체크리스트를 필수·관리·보안·부가 카테고리 탭으로 구조화
업무 지시(생성/편집)·상세 화면의 '해야 할 일'을 AI 추천과 동일한 4개 영역(필수/관리/보안/부가)으로 분류한다. - 백엔드: ChecklistItem 에 category 컬럼(기본 '필수') 추가, 생성/동기화 DTO·서비스·응답에 category 반영 - 프론트: 생성/편집은 카테고리 탭 + 탭별 추가 입력, AI 추천 항목은 소속 영역으로 매핑해 일괄 추가, 상세는 카테고리별 그룹 표시 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
ArrayMaxSize,
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsISO8601,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
@@ -12,6 +13,10 @@ import {
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
CHECKLIST_CATEGORIES,
|
||||
type ChecklistCategory,
|
||||
} from '../entities/checklist-item.entity';
|
||||
|
||||
// 체크리스트 입력 항목 (생성 시 함께 등록)
|
||||
export class ChecklistInputDto {
|
||||
@@ -23,6 +28,15 @@ export class ChecklistInputDto {
|
||||
@IsNotEmpty({ message: '체크리스트 항목 내용을 입력해 주세요.' })
|
||||
@MaxLength(200)
|
||||
text!: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '카테고리(탭)',
|
||||
enum: CHECKLIST_CATEGORIES,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(CHECKLIST_CATEGORIES)
|
||||
category?: ChecklistCategory;
|
||||
}
|
||||
|
||||
// 업무 생성 요청
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ArrayMaxSize,
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsISO8601,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
@@ -12,6 +13,10 @@ import {
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
CHECKLIST_CATEGORIES,
|
||||
type ChecklistCategory,
|
||||
} from '../entities/checklist-item.entity';
|
||||
|
||||
// 수정 시 체크리스트 항목 — id 있으면 기존 항목 갱신(완료 상태 보존), 없으면 신규 추가
|
||||
export class UpdateChecklistItemInput {
|
||||
@@ -31,6 +36,15 @@ export class UpdateChecklistItemInput {
|
||||
@IsNotEmpty({ message: '체크리스트 항목 내용을 입력해 주세요.' })
|
||||
@MaxLength(200)
|
||||
text!: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '카테고리(탭)',
|
||||
enum: CHECKLIST_CATEGORIES,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(CHECKLIST_CATEGORIES)
|
||||
category?: ChecklistCategory;
|
||||
}
|
||||
|
||||
// 업무 수정 요청 — 제공된 필드만 갱신
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
} from 'typeorm';
|
||||
import { Task } from './task.entity';
|
||||
|
||||
// 체크리스트 카테고리(탭) — AI 추천 4개 영역과 동일
|
||||
export const CHECKLIST_CATEGORIES = ['필수', '관리', '보안', '부가'] as const;
|
||||
export type ChecklistCategory = (typeof CHECKLIST_CATEGORIES)[number];
|
||||
|
||||
// 업무 체크리스트 항목
|
||||
@Entity('checklist_items')
|
||||
export class ChecklistItem {
|
||||
@@ -23,6 +27,10 @@ export class ChecklistItem {
|
||||
@Column()
|
||||
text!: string;
|
||||
|
||||
// 카테고리(탭) — 필수/관리/보안/부가
|
||||
@Column({ type: 'varchar', default: '필수' })
|
||||
category!: ChecklistCategory;
|
||||
|
||||
// 완료 여부
|
||||
@Column({ default: false })
|
||||
done!: boolean;
|
||||
|
||||
@@ -11,7 +11,10 @@ import { BusinessException } from '../../common/exceptions/business.exception';
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import { ProjectMember } from '../project/entities/project-member.entity';
|
||||
import { Task, type TaskStatus } from './entities/task.entity';
|
||||
import { ChecklistItem } from './entities/checklist-item.entity';
|
||||
import {
|
||||
ChecklistItem,
|
||||
type ChecklistCategory,
|
||||
} from './entities/checklist-item.entity';
|
||||
import { Comment } from './entities/comment.entity';
|
||||
import { Attachment, type AttachmentKind } from './entities/attachment.entity';
|
||||
import { CreateTaskDto } from './dto/create-task.dto';
|
||||
@@ -52,6 +55,7 @@ export interface ChecklistItemResponse {
|
||||
id: string;
|
||||
text: string;
|
||||
done: boolean;
|
||||
category: ChecklistCategory;
|
||||
}
|
||||
|
||||
// 프로젝트 업무 목록의 세그먼트별 카운트(전체/진행/대기/완료) — 검색어 반영
|
||||
@@ -331,6 +335,7 @@ export class TaskService {
|
||||
checklist: (dto.checklist ?? []).map((c, idx) =>
|
||||
this.checklistRepo.create({
|
||||
text: c.text,
|
||||
category: c.category ?? '필수',
|
||||
done: false,
|
||||
sortOrder: idx,
|
||||
}),
|
||||
@@ -521,7 +526,7 @@ export class TaskService {
|
||||
// 반환: 의미 있는 변경(추가/삭제/내용변경)이 있었는지 — 활동 적재 판단용(순서만 변경은 제외)
|
||||
private async syncChecklist(
|
||||
task: Task,
|
||||
items: { id?: string; text: string }[],
|
||||
items: { id?: string; text: string; category?: ChecklistCategory }[],
|
||||
): Promise<boolean> {
|
||||
const existing = task.checklist ?? [];
|
||||
const byId = new Map(existing.map((c) => [c.id, c]));
|
||||
@@ -530,11 +535,15 @@ export class TaskService {
|
||||
let changed = false;
|
||||
|
||||
items.forEach((it, idx) => {
|
||||
const category = it.category ?? '필수';
|
||||
const found = it.id ? byId.get(it.id) : undefined;
|
||||
if (found) {
|
||||
// 기존 항목 — 내용/순서만 갱신(완료 상태 done 은 보존)
|
||||
if (found.text !== it.text) changed = true; // 내용 변경 감지(갱신 전 비교)
|
||||
// 기존 항목 — 내용/카테고리/순서만 갱신(완료 상태 done 은 보존)
|
||||
if (found.text !== it.text || found.category !== category) {
|
||||
changed = true; // 변경 감지(갱신 전 비교)
|
||||
}
|
||||
found.text = it.text;
|
||||
found.category = category;
|
||||
found.sortOrder = idx;
|
||||
keepIds.add(found.id);
|
||||
toSave.push(found);
|
||||
@@ -544,6 +553,7 @@ export class TaskService {
|
||||
toSave.push(
|
||||
this.checklistRepo.create({
|
||||
text: it.text,
|
||||
category,
|
||||
done: false,
|
||||
sortOrder: idx,
|
||||
task,
|
||||
@@ -1232,7 +1242,12 @@ export class TaskService {
|
||||
const checklist = (task.checklist ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((c) => ({ id: c.id, text: c.text, done: c.done }));
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
text: c.text,
|
||||
done: c.done,
|
||||
category: c.category,
|
||||
}));
|
||||
|
||||
// 승인 정보는 승인 대기(review) 상태이고 요청 메타가 있을 때만 노출
|
||||
const approval: ApprovalInfo | null =
|
||||
|
||||
Reference in New Issue
Block a user