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
+62
View File
@@ -0,0 +1,62 @@
import { useApi } from './useApi'
import type {
ApiRepoTask,
ApiTaskDetail,
CreateTaskPayload,
TaskListQuery,
UpdateTaskPayload,
} from '@/types/task'
import type { TaskStatus } from '@/mock/relay.mock'
// 업무 도메인 API Composable — 인터셉터가 success/data 를 언래핑
// 경로는 모두 repoId(slugName) 하위: /repos/:repoId/tasks[/:taskId]
export function useTask() {
const api = useApi()
// 업무 목록 (상태/담당자 필터 선택)
async function list(repoId: string, query: TaskListQuery = {}): Promise<ApiRepoTask[]> {
return (await api.get(`/repos/${repoId}/tasks`, {
params: query,
})) as unknown as ApiRepoTask[]
}
// 업무 단건 상세 (taskId = 저장소 내 순번)
async function get(repoId: string, taskId: number): Promise<ApiTaskDetail> {
return (await api.get(`/repos/${repoId}/tasks/${taskId}`)) as unknown as ApiTaskDetail
}
// 업무 생성 (admin)
async function create(repoId: string, payload: CreateTaskPayload): Promise<ApiTaskDetail> {
return (await api.post(`/repos/${repoId}/tasks`, payload)) as unknown as ApiTaskDetail
}
// 업무 수정 (admin)
async function update(
repoId: string,
taskId: number,
payload: UpdateTaskPayload,
): Promise<ApiTaskDetail> {
return (await api.patch(
`/repos/${repoId}/tasks/${taskId}`,
payload,
)) as unknown as ApiTaskDetail
}
// 업무 삭제 (admin)
async function remove(repoId: string, taskId: number): Promise<void> {
await api.delete(`/repos/${repoId}/tasks/${taskId}`)
}
// 상태 변경 (상태 머신)
async function changeStatus(
repoId: string,
taskId: number,
status: TaskStatus,
): Promise<ApiTaskDetail> {
return (await api.post(`/repos/${repoId}/tasks/${taskId}/status`, {
status,
})) as unknown as ApiTaskDetail
}
return { list, get, create, update, remove, changeStatus }
}