feat: 업무(Task) 모듈 4단계 — CRUD·상태머신·진행률 집계·역할 기반 상세 UI

백엔드
- Task/ChecklistItem 엔티티(저장소 내 순번 seq, jsonb content, assignees ManyToMany, checklist)
- GET/POST/PATCH/DELETE /repos/:repoId/tasks[/:taskId] + POST .../status
- 상태 머신(todo→prog→review→done, review→changes, changes→{review,prog}) + 전이별 역할 검증
- 담당자=저장소 멤버만 지정, 승인 완료(→done) 시 체크리스트 일괄 완료(영속)
- 저장소 doneCount/totalCount·멤버 taskCount 실집계(RepoModule→TaskModule)
- Task.repo FK 컬럼명 snake_case 고정(@JoinColumn), task.service 단위 테스트

프론트
- types/task·useTask·task.store, format.ts 라벨 파생(상태/마감/dday)
- RepoDetailPage 업무 목록, TaskCreatePage 생성/수정 겸용(편집 라우트)
- TaskDetailPage 역할 기반 액션 분기(지시자 승인/수정요청, 담당자 시작/승인요청)
- 액션 카드·상태 뱃지 디자인 시안 정렬, 작성폼/목록 UI 정리

문서: api-contract·integration-progress 4단계 반영

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 18:12:11 +09:00
parent c01f8e535c
commit 875eddf659
27 changed files with 2466 additions and 342 deletions
+67
View File
@@ -1,5 +1,7 @@
// 공통 포맷터 — 부수효과 없는 순수 함수만 작성
import type { TaskStatus } from '@/mock/relay.mock'
/**
* 숫자를 한국 통화 표기 (예: 12345 → "12,345원")
*/
@@ -75,3 +77,68 @@ export function progressOf(done: number, total: number): ProgressInfo {
const label = pct === 0 ? '시작 전' : `${pct}% 완료`
return { pct, level, label }
}
/* ============================================================
* 업무(Task) 표시용 파생 값
* ============================================================ */
const WEEKDAYS_KO = ['일', '월', '화', '수', '목', '금', '토']
/** 업무 상태 → 한국어 라벨 */
const TASK_STATUS_LABEL: Record<TaskStatus, string> = {
todo: '할 일',
prog: '진행 중',
review: '승인 대기',
done: '완료',
changes: '수정 요청',
}
export function taskStatusLabel(status: TaskStatus): string {
return TASK_STATUS_LABEL[status] ?? status
}
/** 'M월 D일' (예: '6월 8일') */
export function monthDayKo(input: Date | string): string {
const d = typeof input === 'string' ? new Date(input) : input
if (Number.isNaN(d.getTime())) return ''
return `${d.getMonth() + 1}${d.getDate()}`
}
/** 업무 마감 정보(원시 dueDate + 완료여부 → 표시 라벨들) */
export interface TaskDueInfo {
/** 전체 날짜 라벨 (예: '6월 18일 (목)') — 마감 없으면 '' */
dateLabel: string
/** D-day 라벨 (예: 'D-3', 'D-DAY', '2일 지남') — 마감 없으면 '' */
dday: string
/** 목록 행 라벨 (완료: '6월 9일 완료' / 진행: 'D-3' / 지남: '2일 지남') */
listLabel: string
/** 마감 지남 여부(완료 업무는 false) */
overdue: boolean
}
/** dueDate(ISO|null) + 완료여부로 마감 표시 정보 계산 */
export function taskDueInfo(dueDate: string | null, isDone: boolean): TaskDueInfo {
if (!dueDate) {
return { dateLabel: '', dday: '', listLabel: '', overdue: false }
}
const due = new Date(dueDate)
if (Number.isNaN(due.getTime())) {
return { dateLabel: '', dday: '', listLabel: '', overdue: false }
}
const dateLabel = `${due.getMonth() + 1}${due.getDate()}일 (${WEEKDAYS_KO[due.getDay()]})`
// 자정 기준 일 수 차이
const startDue = new Date(due.getFullYear(), due.getMonth(), due.getDate())
const now = new Date()
const startNow = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const days = Math.round((startDue.getTime() - startNow.getTime()) / 86_400_000)
if (isDone) {
return { dateLabel, dday: '', listLabel: `${monthDayKo(due)} 완료`, overdue: false }
}
if (days < 0) {
const passed = `${-days}일 지남`
return { dateLabel, dday: passed, listLabel: passed, overdue: true }
}
const dday = days === 0 ? 'D-DAY' : `D-${days}`
return { dateLabel, dday, listLabel: dday, overdue: false }
}