docs: 8단계 진행현황·API 계약 갱신 + 알림/Redis 검토 문서 추가
- integration-progress/api-contract: 8-1 페이지네이션·8-2 알림·8-3 Redis 캐싱 반영. - notification-review.md: 알림 시스템 전수 검토(유형/빠짐/주의) + 보완 이력. - redis-cache-review.md: Redis 캐시 전수 검토 + 보완 이력. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-6
@@ -42,14 +42,16 @@
|
||||
status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프론트는 `BIZ_001` 코드일 때 서버 `message` 를 그대로 노출한다.
|
||||
예: 로그인 실패 → `BusinessException('BIZ_001', '이메일 또는 비밀번호가 올바르지 않습니다.', 401)`.
|
||||
|
||||
### 페이지네이션 — **8단계에서 통일(진행 중)**
|
||||
### 페이지네이션 — **8단계에서 통일(완료)**
|
||||
|
||||
목록 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 가 함께 갱신돼야 한다.
|
||||
- **프론트 적용 완료(전 도메인)**:
|
||||
- **더보기(오프셋)**: 저장소 목록, 내 업무, **저장소 업무 목록(RepoDetailPage)**. 저장소 업무는 세그먼트(`segment`)·검색(`q`)도 백엔드로 이전하고 세그먼트 배지 수는 응답 `counts` 사용(`TaskListQuery={segment,q,assignee}`, `ApiRepoTaskListResult=Paginated<ApiRepoTask> & {counts}`).
|
||||
- **전체 로드**: **멤버(RepoMembersPage)** 는 `member.store.fetchList` 가 페이지네이션 API를 끝까지 순회해 전체를 적재(더보기 대신). 역할/검색 카운트가 전수 기준이어야 하고, TaskCreatePage 담당자 후보 풀이 같은 호출을 쓰므로 전체 셋을 보장한다(멤버는 팀 규모로 한정적).
|
||||
- (회귀 수정 `c128b19`: `useMember.list`·`useTask.list` 가 `Paginated<T>` 를 반환하도록 정합 — 위 작업의 선행 단계로 흡수됨.)
|
||||
|
||||
---
|
||||
|
||||
@@ -178,7 +180,7 @@ status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프
|
||||
|
||||
| 메서드 | 경로 | 요청 | 응답 | 권한 |
|
||||
|---|---|---|---|---|
|
||||
| GET | `.../tasks` | `?status&assignee` | `RepoTask[]` | 인증 |
|
||||
| GET | `.../tasks` | `?segment&q&assignee&page&size` | `{ items: RepoTask[], total, page, size, counts }` | 인증 |
|
||||
| POST | `.../tasks` | `CreateTaskDto` | `TaskDetail` | admin |
|
||||
| GET | `.../tasks/:taskId` | — | `TaskDetail` | 인증 |
|
||||
| PATCH | `.../tasks/:taskId` | `UpdateTaskDto` | `TaskDetail` | admin |
|
||||
@@ -186,7 +188,7 @@ status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프
|
||||
| POST | `.../tasks/:taskId/status` | `{ status }` | `TaskDetail` | 멤버(전이별 역할 검증) |
|
||||
|
||||
- `:taskId` 는 **저장소 내 순번(seq)** — 정수(`ParseIntPipe`). 전역 PK 는 별도 uuid.
|
||||
- 목록 정렬: seq 내림차순(최신 우선). `?status`/`?assignee`(userId) 필터.
|
||||
- 목록 정렬: seq 내림차순(최신 우선). `?segment`(all/prog/todo/done, prog=prog+review)·`?q`(제목 부분일치)·`?assignee`(userId) 필터 + `?page&size` 페이지네이션. 응답에 세그먼트 카운트(`counts{all,prog,todo,done}`, 검색어 반영) 동봉.
|
||||
|
||||
상태(`TaskStatus`): `todo | prog | review | done | changes`. 전이 규칙(상태 머신):
|
||||
|
||||
@@ -289,7 +291,32 @@ todo ─시작→ prog ─승인요청→ review ─승인→ done
|
||||
|
||||
## 8. 마무리(선택) — **8단계**
|
||||
|
||||
소셜 로그인(Google/Kakao OAuth), 이메일 인증, 알림, AI 에이전트 패널, Redis 캐싱, 전체 페이지네이션 통일.
|
||||
### 알림 (`/api/notifications`) — **✅ 구현 완료 (인앱 + 실시간 SSE)**
|
||||
|
||||
활동 피드와 별개로 **사용자별 수신함**을 두고, 도메인 행위에서 대상자에게 알림을 적재한다(쓰기 API 없음 — 다른 서비스가 내부에서 `NotificationService.notify` 호출). 실시간은 **SSE**(서버→클라이언트 단방향, 쿠키 인증).
|
||||
|
||||
| 메서드 | 경로 | 요청 | 응답 | 비고 |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/notifications` | `?page&size` | `{ items: ApiNotification[], total, page, size, unreadCount }` | 본인 수신함(최신순) |
|
||||
| POST | `/notifications/:id/read` | — | `{ unreadCount }` | 단건 읽음 |
|
||||
| POST | `/notifications/read-all` | — | `{ unreadCount: 0 }` | 전체 읽음 |
|
||||
| SSE | `/notifications/stream` | — (EventSource) | `ApiNotification` 스트림 | 실시간. 표준 래퍼 미적용(`@SkipTransform`), 25초 하트비트(`event: ping`) |
|
||||
|
||||
`ApiNotification`(원시): `{ id, type, payload, read, createdAt }`
|
||||
- `type`(12종): `task.assigned`(지시→담당자) / `task.unassigned`(담당 해제→제거된 담당자) / `task.approval_requested`(승인요청→지시자) / `task.approved`(승인→담당자) / `task.changes_requested`(수정요청→담당자) / `task.started`(시작·재개→지시자) / `task.due_changed`(마감 변경→담당자, payload `dueDate`) / `task.removed`(삭제→관련자, 이동=저장소) / `task.commented`(댓글·답글→관련자, **답글은 원댓글 작성자 포함** + payload `isReply`) / `member.invited`(초대→피초대자) / `member.role_changed`(역할 변경→당사자) / `member.removed`(제거→당사자).
|
||||
- `payload`: `repoId`(slug)·`repoName`·`taskSeq`·`taskTitle`·`actorName`·`roleType` 등. 표시 문구/톤/이동경로(`/repos/:repoId/tasks/:taskSeq` 또는 `/repos/:repoId`)는 **프론트가 type+payload 로 조립**(사용자 입력은 escapeHtml).
|
||||
- **행위자 본인은 수신 대상에서 자동 제외.** 적재는 베스트 에포트(실패해도 본 흐름 안 막음), 도입 이후 발생분만(소급 없음).
|
||||
- 인증: 모든 엔드포인트 `JwtAuthGuard`(본인 알림 전용). SSE 도 `access_token` 쿠키(EventSource `withCredentials`)로 연결 시점 검증.
|
||||
- 실시간 전송은 `REDIS_HOST` 설정 시 **Redis pub/sub**(ioredis, 채널 `relay:notifications`)로 fan-out → **다중 인스턴스 수평 확장에서도 실시간 도달**. 미설정 시 인메모리 폴백(단일 인스턴스).
|
||||
> 매핑: AppShell 상단바 종(미읽음 뱃지 + 드롭다운). 연결은 store 싱글톤(EventSource)이 보유 — 화면 전환에 끊기지 않고 로그아웃 시 해제.
|
||||
|
||||
### 캐싱 (Redis) — **✅ 완료**
|
||||
|
||||
전역 CacheModule 을 Redis(keyv)로 전환(`REDIS_*` 미설정 시 인메모리 폴백, 기본 TTL 60초). **`GET /repos`(저장소 목록)**을 버전 네임스페이스로 캐시 — 키 `repos:list:v{ver}:p{page}:s{size}`, **저장소 생성/수정/삭제·Gitea 동기화·멤버 초대/제거** 시 버전 증가로 일괄 무효화(멤버 역할 변경은 목록 응답 불변이라 제외). 버전 카운터가 공유 Redis 에 있어 **무효화가 다중 인스턴스에서도 정합**. 업무 카운트(`doneCount/totalCount`)는 ≤60초 지연 허용. 응답 계약 불변(캐시는 투명) — 프론트 영향 없음. `GET /repos/meta` 는 DB 미접근 인메모리 getter 라 캐싱 제외.
|
||||
|
||||
### 그 외(미착수)
|
||||
|
||||
소셜 로그인(Google/Kakao OAuth), 이메일 인증, AI 에이전트 패널. (전체 페이지네이션 통일·실시간 알림·Redis 캐싱은 완료.)
|
||||
|
||||
---
|
||||
|
||||
@@ -303,5 +330,6 @@ src/modules/
|
||||
task/ (4) ✅ — Task / ChecklistItem (Comment / Reply / Attachment 는 5단계)
|
||||
activity/ (6) — Activity 이벤트 (다른 모듈에서 emit)
|
||||
my-tasks/ (7) — 집계 전용 (task 모듈에 흡수 가능)
|
||||
notification/ (8) ✅ — Notification 엔티티 + SSE 스트림 (task/repo 모듈에서 notify)
|
||||
```
|
||||
> 실제 배치: `member` 는 `repo` 모듈에 흡수(`MemberService`/`MemberController` + `RepoMember` 재사용). `task` 모듈은 4단계 완료(엔티티·CRUD·상태머신·집계), 5단계에서 Comment/Reply/Attachment 추가 예정. `RepoModule` 이 진행률·taskCount 집계를 위해 `TaskModule` 을 import(엔티티만 참조해 순환 없음).
|
||||
|
||||
Reference in New Issue
Block a user