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:
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user