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
+33 -23
View File
@@ -42,10 +42,14 @@
status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프론트는 `BIZ_001` 코드일 때 서버 `message` 를 그대로 노출한다.
예: 로그인 실패 → `BusinessException('BIZ_001', '이메일 또는 비밀번호가 올바르지 않습니다.', 401)`.
### 페이지네이션
### 페이지네이션 — **8단계에서 통일(진행 중)**
목록 API 는 오프셋 기반: 쿼리 `?page=1&size=20`, 응답 `{ items: [...], total, page, size }`.
(1단계에서는 단순 배열 허용, 7단계에서 페이지네이션으로 통일)
목록 API 는 오프셋 기반: 쿼리 `?page=1&size=20`(기본 size 20, 최대 100), 응답 `{ items: [...], total, page, size }`.
프론트는 **더보기 버튼**으로 점진 로드(시안에 페이지 UI 없음).
- **백엔드 적용 완료**: `GET /repos`, `/repos/:id/members`, `/tasks/assigned`, `/tasks/issued`, `/repos/:id/tasks`(세그먼트/검색 + `counts` 동봉). 공통 유틸 `common/pagination`.
- **프론트 적용 완료**: 저장소 목록, 내 업무. **남음**: 멤버, 저장소 업무 목록(RepoDetailPage).
- 응답이 단순 배열 → `{ items, total, page, size }` 로 바뀌었으므로 프론트 composable/store 가 함께 갱신돼야 한다.
---
@@ -206,8 +210,8 @@ todo ─시작→ prog ─승인요청→ review ─승인→ done
- 생성 시 status 는 항상 `todo`. 저장소 `doneCount/totalCount` 는 업무 status 집계(`done` 수/전체)로 실시간 반영, 멤버 `taskCount` 는 사용자별 담당 업무 수로 집계.
`CreateTaskDto`: `{ title, content?: string[], assigneeIds: uuid[](≥1), dueDate?: ISO, checklist?: {text}[] }`
`UpdateTaskDto`: `{ title?, content?: string[], assigneeIds?: uuid[], dueDate?: ISO|null }` — 제공된 필드만 갱신(체크리스트 항목 편집은 5단계).
`RepoTask`(목록 행): `{ id(seq), title, status, dueDate: ISO|null, checklist: [done,total], commentCount, assignees: User[] }`
`UpdateTaskDto`: `{ title?, content?: string[], assigneeIds?: uuid[], dueDate?: ISO|null, checklist?: {id?, text}[] }` — 제공된 필드만 갱신. `checklist` 제공 시 **전체 동기화**(id 있으면 기존 유지·완료 보존, 없으면 신규, 빠진 기존 항목은 삭제).
`RepoTask`(목록 행): `{ id(seq), title, status, dueDate: ISO|null, checklist: [done,total], commentCount, fileCount, assignees: User[] }` — 목록 행은 댓글 수(말풍선)·첨부 수(클립)를 **분리 표기**(각 0이면 숨김).
`TaskDetail`: `{ id(seq), repoId, repoName, title, status, content[], checklist: {id,text,done}[], assignees[], issuer: User|null, dueDate: ISO|null, createdAt, comments[], activities[], files[] }`
> **comments/activities/files 는 5·6단계 영역** — 4단계 응답에서는 빈 배열로 내려간다. `approval` 객체도 5단계(현재 미포함).
@@ -219,14 +223,15 @@ todo ─시작→ prog ─승인요청→ review ─승인→ done
---
## 5. 업무 상세 부속 — **5단계**
## 5. 업무 상세 부속 — **5단계 ✅ 구현 완료**
### 체크리스트
| 메서드 | 경로 | 요청 | 응답 |
|---|---|---|---|
| POST | `.../tasks/:taskId/checklist` | `{ text }` | `ChecklistItem` |
| PATCH | `.../checklist/:itemId` | `{ text?, done? }` | `ChecklistItem` |
| DELETE | `.../checklist/:itemId` | — | `null` |
> 모든 엔드포인트 `JwtAuthGuard` + 저장소 멤버. `:taskId`=seq(정수), `:itemId`/`:fileId`/`:commentId`=uuid.
> 파일 저장은 **서버 로컬 업로드 폴더**(`UPLOAD_DIR`, 기본 `cwd/uploads`) 채택. 승인 워크플로우는 4단계 상태머신(`POST .../status`)을 노트와 함께 확장한 형태.
### 체크리스트 — **상세 화면 읽기 전용 / 정의는 생성·수정 폼에서**
- **완료 토글은 사용자가 하지 않는다** — 상세 화면에서는 표시만 하고, 완료는 승인 워크플로우에서 일괄 처리된다("승인 완료(→done) 시 체크리스트 항목 done 일괄 true", 4단계 부수효과).
- **항목 추가/삭제(정의)는 업무 생성·수정 폼에서** 한다: 생성=`CreateTaskDto.checklist`, 수정=`UpdateTaskDto.checklist`(id 기준 전체 동기화).
- (5단계 초기 설계의 `POST/PATCH/DELETE .../checklist` 항목별 토글 API 는 제거됨 — 항목 편집은 업무 수정 PATCH 로 통합.)
### 댓글 / 답글
| 메서드 | 경로 | 요청 | 응답 |
@@ -240,7 +245,7 @@ todo ─시작→ prog ─승인요청→ review ─승인→ done
| POST | `.../tasks/:taskId/files` | `multipart/form-data` | `Attachment` |
| DELETE | `.../files/:fileId` | — | `null` |
> **결정 필요**: 파일 저장소(로컬 볼륨 vs S3/MinIO). `Attachment`: `{ id, name, size, kind('pdf'|'zip'|'doc'|'img'), url }`.
> **결정**: 파일 저장소 = **서버 로컬 업로드 폴더**(S3/MinIO 후순위). `Attachment`: `{ id, name, size(바이트), kind('pdf'|'zip'|'doc'|'img'), url(다운로드 경로), uploader: User|null, createdAt }`. 업로드 필드명 `file`. 다운로드는 `GET .../files/:fileId/download`(인증 쿠키로 접근, 인라인 스트리밍). 크기 표기는 프론트 `formatBytes`.
### 승인 워크플로우
| 메서드 | 경로 | 요청 | 응답 |
@@ -251,29 +256,34 @@ todo ─시작→ prog ─승인요청→ review ─승인→ done
---
## 6. 활동 피드 — **6단계**
## 6. 활동 피드 — **6단계 ✅ 구현 완료**
활동은 **2~5단계 행위에서 이벤트로 자동 적재**(쓰기 API 없음, 읽기만).
| 메서드 | 경로 | 응답 |
|---|---|---|
| GET | `/repos/:repoId/activity` | `RepoActivityDay[]`(날짜 그룹) |
| (업무 활동) | `TaskDetail.activities` 에 포함 | `Activity[]` |
| GET | `/repos/:repoId/activity` | `ApiActivity[]`(최신순, 최대 200) — 날짜 그룹핑은 프론트 |
| (업무 활동) | `TaskDetail.activities` 에 포함 | `ApiActivity[]`(해당 업무 seq) |
`RepoActivityItem`: `{ tone, icon, actor: User, payload, createdAt }`
> 서버는 이벤트 타입 + 행위자 + 대상만 저장하고, **표시 HTML 문구는 프론트에서 조립**(현재 mock 은 html 통문자열 — 연동 시 구조화 권장). 매핑: RepoActivityPage, TaskDetail 활동 탭.
`ApiActivity`(원시): `{ id, type, actor: User|null, taskSeq: number|null, payload: object, createdAt }`
- `type`: 저장소 `repo.created`/`repo.renamed`/`repo.visibility_changed`, 멤버 `member.invited`/`member.role_changed`/`member.removed`, 업무 `task.created`/`task.status_changed`/`task.comment_added`(답글 포함)/`task.file_attached`/`task.assignees_changed`/`task.due_changed`/`task.edited`(제목·내용·체크리스트)/`task.file_removed`/`task.removed`.
- `payload`(타입별): `taskTitle`, `statusFrom`/`statusTo`, `targetName`, `roleType`, `newName`, `visibility`, `fileName`, `assigneeNames`, `dueDate` 등.
> 업무 수정(`update`)은 **실제로 바뀐 측면만** 골라 적재한다(담당자 변경=`assignees_changed`, 마감 변경=`due_changed`, 제목·내용·체크리스트 변경=`edited`). 답글은 댓글과 동일 `task.comment_added`. 저장소 삭제는 활동도 함께 사라져 미적재, 저장소 설명/브랜치 변경·체크리스트 개별 완료는 미적재(승인 완료로 일괄 표현).
> **서버는 이벤트 타입 + 행위자 + 대상(payload)만 저장**하고, 표시 문구/아이콘/톤·날짜 그룹은 **프론트가 type+payload 로 조립**(구조화 채택). 사용자 입력은 `escapeHtml` 후 신뢰 태그만 삽입(v-html XSS 방지). 적재는 베스트 에포트(실패해도 본 기능 안 막음), 6단계 도입 이후 발생분만 기록(소급 없음). 매핑: RepoActivityPage(저장소 활동), TaskDetail 활동 탭(업무 활동).
---
## 7. 내 업무 (`/api/tasks`) — **7단계**
## 7. 내 업무 (`/api/tasks`) — **7단계 ✅ 구현 완료**
| 메서드 | 경로 | 요청 | 응답 |
|---|---|---|---|
| GET | `/tasks/assigned` | — | `MyTaskGroup[]`(상태 그룹) — 내가 담당 |
| GET | `/tasks/issued` | — | `MyTaskGroup[]` — 내가 지시 |
| GET | `/tasks/assigned` | — | `MyTaskRow[]` — 내가 담당(저장소 횡단) |
| GET | `/tasks/issued` | — | `MyTaskRow[]` — 내가 지시(저장소 횡단) |
저장소 횡단 집계. `MyTaskGroup`/`MyTaskRow` 는 mock 인터페이스와 동일하되 라벨류 제외, 원시값(`dueDate`, `status`) 기반.
> 매핑 페이지: MyTasksPage(담당/지시 탭, 상태 그룹). `countActive` 는 프론트 유지.
- 백엔드는 **플랫 행 배열**(`MyTaskRowResponse[]`)을 마감 임박순(마감 없음 뒤로)으로 반환하고, **상태 그룹핑·순서·라벨은 프론트가 파생**(활동 피드와 동일 패턴).
- `MyTaskRowResponse`: `{ id(seq), repoId(slug), repoName, title, status, dueDate, checklist:[done,total], assignees: User[], issuer: User|null }`.
- 프론트 파생: 담당 뷰=지시자 표시·그룹순서 `[changes,prog,todo,review,done]`, 지시 뷰=담당자 미니아바타·승인대기 강조(`내 승인 필요`)·그룹순서 `[review,prog,changes,todo,done]`. `dueLabel`/`dday`/`ddayLevel`/`overdue``taskDueInfo` 로 파생.
> 매핑 페이지: MyTasksPage(담당/지시 탭, 상태 그룹). 행 클릭은 항상 `…/tasks/:taskId`(TaskDetailPage, 역할 기반 분기). **`TaskAssigneePage`(`/view`) 라우트·파일 제거 완료.**
---