feat: 업무 상세 부속·활동 피드·내 업무 집계 + 페이지네이션 (5~8단계)

- 5단계(업무 상세 부속): 체크리스트(상세 읽기전용/수정폼 추가·삭제)·댓글/답글·파일 첨부(서버 로컬 업로드 폴더)·승인 워크플로우(요청/승인/수정요청 노트)
- 6단계(활동 피드): 저장소/멤버/업무 행위를 이벤트로 자동 적재, 저장소 활동 피드·업무 활동 탭 실연동(프론트가 type+payload로 문구 조립)
- 7단계(내 업무): 담당/지시 저장소 횡단 집계, MyTasksPage 실연동, TaskAssigneePage(/view) 제거
- 8단계(진행): 목록 페이지네이션 통일 — 백엔드 전체(공통 유틸 + 5개 목록, 저장소 업무는 세그먼트/검색/카운트) + 프론트 더보기(저장소 목록·내 업무 완료, 멤버·저장소 업무 대기)
- UI 보강: 업무 목록 댓글/첨부 분리표기, 첨부 배지 색상·라벨 통일, 완료 업무 D-Day 배지, 활동 이벤트 점검·보강(업무 수정/삭제·답글·첨부 삭제)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 18:13:17 +09:00
parent 875eddf659
commit a056d7f55a
51 changed files with 3437 additions and 1732 deletions
+14
View File
@@ -0,0 +1,14 @@
import { useApi } from './useApi'
import type { ApiActivity } from '@/types/activity'
// 활동 피드 API Composable — 인터셉터가 success/data 를 언래핑
export function useActivity() {
const api = useApi()
// 저장소 활동 피드(최신순)
async function list(repoId: string): Promise<ApiActivity[]> {
return (await api.get(`/repos/${repoId}/activity`)) as unknown as ApiActivity[]
}
return { list }
}
+24
View File
@@ -0,0 +1,24 @@
import { useApi } from './useApi'
import type { ApiMyTaskRow } from '@/types/my-task'
import type { Paginated } from '@/types/pagination'
// 내 업무 API Composable — 저장소 횡단 집계(담당/지시), 오프셋 페이지네이션
export function useMyTask() {
const api = useApi()
// 내가 담당인 업무
async function assigned(page = 1, size = 20): Promise<Paginated<ApiMyTaskRow>> {
return (await api.get('/tasks/assigned', {
params: { page, size },
})) as unknown as Paginated<ApiMyTaskRow>
}
// 내가 지시한 업무
async function issued(page = 1, size = 20): Promise<Paginated<ApiMyTaskRow>> {
return (await api.get('/tasks/issued', {
params: { page, size },
})) as unknown as Paginated<ApiMyTaskRow>
}
return { assigned, issued }
}
+5 -2
View File
@@ -5,13 +5,16 @@ import type {
RepoCreateMeta,
UpdateRepoPayload,
} from '@/types/repo'
import type { Paginated } from '@/types/pagination'
// 저장소 도메인 API Composable — 인터셉터가 success/data 를 언래핑
export function useRepo() {
const api = useApi()
async function list(): Promise<ApiRepo[]> {
return (await api.get('/repos')) as unknown as ApiRepo[]
async function list(page = 1, size = 20): Promise<Paginated<ApiRepo>> {
return (await api.get('/repos', {
params: { page, size },
})) as unknown as Paginated<ApiRepo>
}
// 저장소 생성 폼 메타(소유자/템플릿) — 생성 전 미리 조회
+97 -1
View File
@@ -1,5 +1,8 @@
import { useApi } from './useApi'
import type {
ApiAttachment,
ApiComment,
ApiCommentReply,
ApiRepoTask,
ApiTaskDetail,
CreateTaskPayload,
@@ -58,5 +61,98 @@ export function useTask() {
})) as unknown as ApiTaskDetail
}
return { list, get, create, update, remove, changeStatus }
/* ===================== 댓글 / 답글 (5단계) ===================== */
async function addComment(
repoId: string,
taskId: number,
text: string,
fileId?: string,
): Promise<ApiComment> {
return (await api.post(`/repos/${repoId}/tasks/${taskId}/comments`, {
text,
...(fileId ? { fileId } : {}),
})) as unknown as ApiComment
}
async function addReply(
repoId: string,
taskId: number,
commentId: string,
text: string,
): Promise<ApiCommentReply> {
return (await api.post(
`/repos/${repoId}/tasks/${taskId}/comments/${commentId}/replies`,
{ text },
)) as unknown as ApiCommentReply
}
/* ===================== 파일 첨부 (5단계) ===================== */
async function uploadFile(
repoId: string,
taskId: number,
file: File,
): Promise<ApiAttachment> {
const form = new FormData()
form.append('file', file)
return (await api.post(`/repos/${repoId}/tasks/${taskId}/files`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
})) as unknown as ApiAttachment
}
async function removeFile(
repoId: string,
taskId: number,
fileId: string,
): Promise<void> {
await api.delete(`/repos/${repoId}/tasks/${taskId}/files/${fileId}`)
}
/* ===================== 승인 워크플로우 (5단계) ===================== */
async function requestApproval(
repoId: string,
taskId: number,
note?: string,
): Promise<ApiTaskDetail> {
return (await api.post(`/repos/${repoId}/tasks/${taskId}/approval/request`, {
...(note ? { note } : {}),
})) as unknown as ApiTaskDetail
}
async function approve(
repoId: string,
taskId: number,
): Promise<ApiTaskDetail> {
return (await api.post(
`/repos/${repoId}/tasks/${taskId}/approval/approve`,
)) as unknown as ApiTaskDetail
}
async function requestChanges(
repoId: string,
taskId: number,
note: string,
): Promise<ApiTaskDetail> {
return (await api.post(`/repos/${repoId}/tasks/${taskId}/approval/changes`, {
note,
})) as unknown as ApiTaskDetail
}
return {
list,
get,
create,
update,
remove,
changeStatus,
addComment,
addReply,
uploadFile,
removeFile,
requestApproval,
approve,
requestChanges,
}
}