feat: 중간 점검(체크포인트) 백엔드 — 진행상태/점검일/보고

체크리스트에 작업자 진행상태(work_status: 미착수/진행중/완료) 필드 추가(지시자 검토와 분리).
업무당 중간 점검일(task_checkpoints) 여러 개 + 작업자 보고 스냅샷(checkpoint_reports) 도입.
- 생성/수정 DTO에 checkpoints(날짜 목록) 추가, 날짜 기준 동기화(보고 보존)
- PATCH .../work-status(담당자 진행상태 갱신), POST .../checkpoints/:id/report(점검일~마감 구간, 스냅샷+지시자 알림 task.checkpoint_reported)
- 상세 응답에 checkpoints/checkpointReports/항목 workStatus 포함
- 운영용 마이그레이션 추가(dev 는 synchronize 자동 반영)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 14:46:07 +09:00
parent c550d66826
commit 20664dab0f
13 changed files with 514 additions and 6 deletions
@@ -0,0 +1,58 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
// 중간 점검(체크포인트) 기능 — 체크리스트 작업자 진행상태 + 점검일/보고 테이블 추가.
// 운영(migrationsRun)에서만 실행되며, dev 는 synchronize 로 자동 반영된다.
export class AddTaskCheckpoints1782200000000 implements MigrationInterface {
name = 'AddTaskCheckpoints1782200000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// 체크리스트 작업자 진행상태(자가보고) — 미착수/진행중/완료
await queryRunner.query(
`ALTER TABLE "checklist_items" ADD "work_status" character varying NOT NULL DEFAULT 'todo'`,
);
// 중간 점검일
await queryRunner.query(
`CREATE TABLE "task_checkpoints" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "check_at" TIMESTAMP WITH TIME ZONE NOT NULL, "sort_order" integer NOT NULL DEFAULT '0', "created_at" TIMESTAMP NOT NULL DEFAULT now(), "task_id" uuid, CONSTRAINT "PK_task_checkpoints" PRIMARY KEY ("id"))`,
);
// 중간 점검 보고(스냅샷)
await queryRunner.query(
`CREATE TABLE "checkpoint_reports" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "note" text, "items_snapshot" jsonb NOT NULL DEFAULT '[]', "created_at" TIMESTAMP NOT NULL DEFAULT now(), "task_id" uuid, "checkpoint_id" uuid, "reporter_id" uuid, CONSTRAINT "PK_checkpoint_reports" PRIMARY KEY ("id"))`,
);
// FK
await queryRunner.query(
`ALTER TABLE "task_checkpoints" ADD CONSTRAINT "FK_task_checkpoints_task" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "checkpoint_reports" ADD CONSTRAINT "FK_checkpoint_reports_task" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "checkpoint_reports" ADD CONSTRAINT "FK_checkpoint_reports_checkpoint" FOREIGN KEY ("checkpoint_id") REFERENCES "task_checkpoints"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "checkpoint_reports" ADD CONSTRAINT "FK_checkpoint_reports_reporter" FOREIGN KEY ("reporter_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "checkpoint_reports" DROP CONSTRAINT "FK_checkpoint_reports_reporter"`,
);
await queryRunner.query(
`ALTER TABLE "checkpoint_reports" DROP CONSTRAINT "FK_checkpoint_reports_checkpoint"`,
);
await queryRunner.query(
`ALTER TABLE "checkpoint_reports" DROP CONSTRAINT "FK_checkpoint_reports_task"`,
);
await queryRunner.query(
`ALTER TABLE "task_checkpoints" DROP CONSTRAINT "FK_task_checkpoints_task"`,
);
await queryRunner.query(`DROP TABLE "checkpoint_reports"`);
await queryRunner.query(`DROP TABLE "task_checkpoints"`);
await queryRunner.query(
`ALTER TABLE "checklist_items" DROP COLUMN "work_status"`,
);
}
}