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:
@@ -2,8 +2,8 @@
|
||||
|
||||
> 프론트 UI(목업)를 NestJS 백엔드에 단계적으로 연동하는 작업의 진행 기록.
|
||||
> 계약/스키마 상세는 [`api-contract.md`](./api-contract.md) 참조.
|
||||
> 최종 갱신: 2026-06-18 · 현재 위치: **0~7단계 완료(모든 화면 실데이터 연동). 8단계(마무리·선택) 진행 중 — 페이지네이션 통일 작업 중.**
|
||||
> 8단계 진행: **페이지네이션** 백엔드 전체 완료 + 프론트 2/4 도메인(저장소 목록·내 업무) 완료, 멤버·저장소 업무 도메인 대기. 그 외 8단계 항목(소셜 로그인·이메일 인증·알림·AI 패널·Redis 캐싱)은 미착수.
|
||||
> 최종 갱신: 2026-06-19 · 현재 위치: **0~7단계 완료(모든 화면 실데이터 연동). 8단계(마무리·선택) 진행 중 — 페이지네이션 통일 + 실시간 알림 + Redis 캐싱 완료.**
|
||||
> 8단계 진행: **① 페이지네이션 전 도메인 완료**(더보기/전체 로드). **② 실시간 알림(인앱 + SSE) 완료**. **③ Redis 캐싱 완료** — CacheModule을 Redis(keyv)로 전환(미설정 시 인메모리 폴백), 저장소 목록(`GET /repos`)을 버전 네임스페이스 캐시. 그 외 8단계 항목(소셜 로그인·이메일 인증·AI 패널)은 미착수.
|
||||
|
||||
---
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
|
||||
### 8단계 — 마무리(선택) · **진행 중**
|
||||
|
||||
#### 8-1. 페이지네이션 통일 — 백엔드 ✅ / 프론트 2/4 도메인
|
||||
#### 8-1. 페이지네이션 통일 — 백엔드 ✅ / 프론트 전 도메인 ✅
|
||||
|
||||
**결정(2026-06-18)**: 전체 목록 통일 + 프론트는 **더보기 버튼** 방식(시안에 페이지 UI 없음).
|
||||
|
||||
@@ -218,13 +218,55 @@
|
||||
- ✅ **내 업무**: `useMyTask.assigned/issued(page,size)`, `my-task.store`(원시 행 누적 + 그룹핑은 computed 로 파생 → 더보기 시 같은 그룹에 합류, 담당/지시 각각 total/page/hasMore + `loadMoreAssigned`/`loadMoreIssued`), MyTasksPage 뷰별 더보기.
|
||||
- 검증: 프론트 type-check/lint/build 0 err.
|
||||
|
||||
**프론트 — 남은 2/4 도메인 ⏳**
|
||||
- ⏳ **멤버**(RepoMembersPage): 더보기 페이지네이션. 단 **TaskCreatePage 가 담당자 후보로 전체 멤버를 사용**하므로, 거기엔 전체 로드(모든 페이지 순회 `fetchAll`)를 별도 보장해야 함.
|
||||
- ⏳ **저장소 업무**(RepoDetailPage): 현재 세그먼트/검색이 **클라이언트 필터** → 백엔드(`segment`/`q`)로 이전 + 세그먼트 카운트(`counts`) 연동 + 더보기. `useTask.list`·`task.store.fetchList` 응답 형태 변경 반영. (가장 복잡)
|
||||
- ⚠️ **전환기 주의**: 백엔드 응답이 이미 `{items,...}` 로 바뀌어, 프론트 연동 전까지 **멤버·저장소 상세 화면은 일시적으로 동작이 어긋날 수 있음**(다음 작업에서 해소).
|
||||
**응답 계약 회귀 수정 ✅ (2026-06-19, `c128b19`)**
|
||||
- 백엔드 페이지네이션 도입으로 목록 API 가 `{items,total,...}` 를 반환하는데 프론트 멤버·업무 소비처가 **배열로 기대**해 `list.map` 이 깨지고 멤버 탭·업무 목록이 **비어 보이던 회귀**를 수정.
|
||||
- 프론트: `useMember.list`·`useTask.list` 가 `Paginated<T>` 반환하도록 맞추고, `member.store`·`task.store.fetchList` 에서 `.items` 로 언래핑(저장소·내업무와 동일 계약). → **첫 페이지(기본 20건)는 정상 표시.**
|
||||
- 백엔드(task): 업무 목록 쿼리가 to-many 조인 컬럼(`checklist.sort_order`)으로 정렬해 페이지네이션 시 TypeORM 이 크래시하던 문제 제거 → `task.seq` 만 정렬(목록은 체크리스트 [완료/전체] 수만 쓰므로 항목 순서 불필요).
|
||||
- ⚠️ 이전의 '전환기 화면 깨짐' 위험은 **이 커밋으로 해소**. 단 더보기/세그먼트 이전은 아직이라 **첫 페이지 이후 항목은 보이지 않음**(아래 ⏳).
|
||||
|
||||
#### 8-2. 그 외(미착수)
|
||||
- 소셜 로그인(Google/Kakao OAuth), 이메일 인증, 알림, AI 에이전트 패널 실연동, Redis 캐싱.
|
||||
**프론트 — 남은 2/4 도메인 ✅ (2026-06-19)**
|
||||
- ✅ **멤버**(RepoMembersPage): **더보기 대신 전체 로드 채택**. `member.store.fetchList` 가 페이지네이션 API를 끝까지 순회(`while items`)해 전체 멤버를 적재한다. 이유: ① 역할/검색 세그먼트 카운트(전체/관리자/멤버)가 **전수 기준**이어야 하고(부분 로드 시 합계 불일치), ② **TaskCreatePage 가 담당자 후보 풀로 같은 `fetchList` 를 사용**하므로 전체가 보장돼야 하며, ③ 멤버는 팀 규모로 한정적이라 전체 로드가 안전. → 두 소비처(멤버 페이지·업무 생성 폼) 모두 페이지 수정 없이 전체 셋 확보. (더보기 UI 불필요)
|
||||
- ✅ **저장소 업무**(RepoDetailPage): 세그먼트/검색을 **백엔드로 이전** — `task.store` 에 `total`/`page`/`counts`/`hasMore`/`loadingMore` + `loadMore` 추가, `useTask.list(repoId, {segment,q}, page, size)` 가 `{items,total,counts}` 반환. 페이지가 `segment`(즉시)·`keyword`(250ms 디바운스) 변경 시 1페이지 리셋 로드, 더보기로 다음 페이지 append. 세그먼트 배지 수는 응답 `counts`(검색어 반영) 사용. 타입 `TaskSegment`/`TaskSegmentCounts`/`ApiRepoTaskListResult` 신설, 기존 `TaskListQuery.status` → `{segment,q}` 로 교체.
|
||||
- 검증: 프론트 type-check/lint/build 0 err.
|
||||
|
||||
> **알려진 한계**: RepoDetailPage 진행률 스트립의 '지연 N건'은 **현재 로드된 목록 기준**(전역 정확 집계는 백엔드 미지원). 업무 삭제 시 세그먼트 카운트는 목록 재진입(재마운트) 시 재조회로 갱신(`remove` 는 총계만 보정).
|
||||
|
||||
#### 8-2. 실시간 알림 (인앱 + SSE) ✅ (2026-06-19)
|
||||
|
||||
**전송 방식 결정**: **SSE(Server-Sent Events)** 채택. 알림은 서버→클라이언트 단방향 푸시라 SSE 가 정확히 맞고, HttpOnly 쿠키 인증(EventSource `withCredentials`)과 호환되며 NestJS `@Sse()` 네이티브 지원이라 **새 의존성 없음**(WebSocket 은 socket.io 등 추가 필요). 단일 인스턴스 가정 → 인메모리 pub/sub(RxJS Subject).
|
||||
|
||||
**백엔드** `backend/src/modules/notification/`
|
||||
- `Notification` 엔티티: `recipient`(User, CASCADE)·`type`·`payload`(jsonb)·`read`·`createdAt`. 복합 인덱스 `[recipient, createdAt]`. (활동 피드와 별개 — 활동=저장소 전체 로그, 알림=개인 수신함)
|
||||
- `NotificationService`: `notify({recipientIds, actorId, type, payload})`(행위자 자동 제외 + 베스트 에포트 적재 후 스트림 푸시), `listFor`(페이지네이션 + `unreadCount`), `markRead`/`markAllRead`, `streamFor(userId)`(RxJS Subject 를 userId 로 필터 + 25초 하트비트). 인메모리 `Subject<{userId,data}>` 하나를 구독 시 필터.
|
||||
- `NotificationController`: `GET /notifications`(목록+미읽음), `POST /notifications/:id/read`, `POST /notifications/read-all`, **`@Sse('stream')`**(실시간). SSE 는 `@SkipTransform()` 로 표준 래퍼(`{success,data}`)를 건너뛴다(신규 `common/decorators/skip-transform.decorator.ts` + `TransformInterceptor` 가 Reflector 로 마커 확인).
|
||||
- **적재 지점**(다른 서비스가 `notify` 호출, **8종**): TaskService — 업무 생성→담당자(`task.assigned`), 상태전이 `transition` 단일 지점에서 review→지시자(`task.approval_requested`)·done→담당자(`task.approved`)·changes→담당자(`task.changes_requested`), 댓글/답글→관련자(`task.commented`, **답글은 원댓글 작성자 포함**), 업무 수정 시 새 담당자(`task.assigned`). MemberService — 초대→피초대자(`member.invited`), **역할 변경→당사자(`member.role_changed`)**, **제거→당사자(`member.removed`)**. NotificationModule 을 TaskModule·RepoModule 이 import(순환 없음 — Notification 은 User 만 참조).
|
||||
- *(2026-06-19 보완: P1[멤버 역할변경/제거, 답글→원댓글 작성자] + P2[담당 해제 `task.unassigned`·마감 변경 `task.due_changed`] + P3[삭제 `task.removed`·시작 `task.started`·답글 `isReply` 구분] → 알림 **12종**, gap 전건 해소. 전수 검토는 [`notification-review.md`](./notification-review.md))*
|
||||
|
||||
**프론트** `frontend/src/`
|
||||
- `types/notification.ts`, `composables/useNotification.ts`(list/markRead/markAllRead — SSE 는 axios 비대상이라 store 에서 EventSource 직접 처리), `stores/notification.store.ts`(items/unreadCount/total + `connect`(멱등, 최초 1회 목록 로드 + 스트림 오픈)/`disconnect`/`loadMore`/`markRead`(낙관적)/`markAllRead`, type+payload→톤/문구/이동경로 뷰 매핑, **사용자 입력 escapeHtml 후 신뢰 태그만 v-html**).
|
||||
- **AppShell**: 장식이던 상단바 종 버튼 → 실연동. **미읽음 뱃지**(99+ 캡), **드롭다운**(목록·모두 읽음·더보기·빈 상태), 항목 클릭=읽음 처리 + 대상(`/repos/:id/tasks/:seq` 또는 `/repos/:id`)으로 이동. 스트림은 store 싱글톤이 보유 → 화면 전환에도 끊기지 않고, **연결은 AppShell 마운트 시(로그인 상태) 1회, 해제는 로그아웃 시**.
|
||||
- 검증: 백엔드 build/lint 0 err·20 tests pass, 프론트 type-check/lint/build 0 err.
|
||||
|
||||
> **알려진 한계**: ① ~~단일 인스턴스 전용~~ → **다중 인스턴스 지원 완료(2026-06-19, R6)**: `REDIS_HOST` 설정 시 Redis pub/sub(ioredis)로 fan-out — `notify` 가 채널 `relay:notifications` 에 발행, 모든 인스턴스가 구독해 실시간 전달. 미설정 시 인메모리 폴백. ② access 토큰(15분) 만료 후 SSE 가 끊겨 재연결되는 시점에 쿠키가 만료돼 있으면, 다음 axios 호출이 토큰을 갱신할 때까지 5초 간격 무알림 재시도(유휴 상태 한정, 사용 재개 시 자동 복구). ③ 알림은 **도입 이후 발생분만**(소급 없음), 이메일/푸시는 범위 밖.
|
||||
|
||||
#### 8-3. Redis 캐싱 ✅ (2026-06-19)
|
||||
|
||||
**스택**: `@nestjs/cache-manager@3` + `cache-manager@6`(Keyv 기반) → Redis 연결은 신규 의존성 **`@keyv/redis`**. docker-compose 가 이미 redis 컨테이너 + `REDIS_HOST/PORT/PASSWORD` 주입.
|
||||
|
||||
**백엔드**
|
||||
- **CacheModule → Redis 전환**(`app.module.ts` `registerAsync`): `REDIS_HOST` 있으면 `createKeyv(redis://…)` 스토어, **없으면 인메모리 폴백**(단독 실행/테스트). keyv `error` 이벤트를 흡수해 Redis 장애가 프로세스를 죽이지 않음(캐시 미스→DB 폴백). 기본 TTL 60초.
|
||||
- **저장소 목록 캐싱**(`GET /repos`): 신규 `RepoCacheService`(repo 모듈) — **버전 네임스페이스 무효화**. 키 `repos:list:v{ver}:p{page}:s{size}`, 변경 시 `repos:list:ver` 증가로 기존 키 일괄 폐기(keyv 와일드카드 삭제 미지원 회피). 모든 캐시 접근은 try/catch 로 실패 시 DB 조회 폴백.
|
||||
- **무효화 지점**: RepoService 생성/수정/삭제 + RepoSyncService 동기화 + **MemberService 초대/제거**(목록 카드 멤버 수·아바타 반영, 역할 변경은 목록 불변이라 제외 — *2026-06-19 보강*). `RepoCacheService` 공유.
|
||||
- **보강(2026-06-19, 검토 [`redis-cache-review.md`](./redis-cache-review.md))**: ① C1 멤버 변경 무효화, ② 기동 시 캐시 모드 로그(Redis/인메모리 가시화), ③ `main.ts` `enableShutdownHooks()` — 종료 시 알림 Redis pub/sub 연결 graceful close(이전엔 훅 미설정으로 `onModuleDestroy` 가 死코드였음).
|
||||
- **업무 카운트 staleness**: 업무 상태 변경은 목록 버전을 올리지 않아 `doneCount/totalCount` 는 최대 60초(TTL) 지연 — 대시보드 특성상 허용(정확 무효화 대신 task↔repo 결합 회피).
|
||||
- **캐싱 제외**: `GET /repos/meta` 는 순수 인메모리 getter(DB 미접근)라 Redis 왕복이 오히려 손해 → 캐싱 안 함.
|
||||
|
||||
**검증**: 백엔드 build/lint 0 err·20 tests pass. (프론트 변경 없음 — API 계약 동일, 응답만 캐시.)
|
||||
|
||||
> **알려진 한계**: ① 캐싱 대상은 현재 저장소 목록 1종(가장 트래픽 큰 랜딩) — 업무 목록 등은 세그먼트/검색/페이지 조합이 많아 추후 확장 후보. ② 업무 카운트 ≤60초 지연(위). ③ **의존성 추가(`@keyv/redis`)라 dev 컨테이너는 `up -d --build -V`(익명 node_modules 볼륨 갱신) 필요.**
|
||||
|
||||
#### 8-4. 그 외(미착수)
|
||||
- 소셜 로그인(Google/Kakao OAuth), 이메일 인증, AI 에이전트 패널 실연동.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user