# 홈 팔로잉 최근 대화 팔로우 필터 구현 계획 > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` to implement this plan task-by-task. 체크박스는 구현 시 실제 진행 상태에 맞게 갱신한다. **Goal:** 홈 팔로잉 탭에서 현재 팔로우 중인 크리에이터의 DM 및 AI 채팅만 최근 대화로 제공한다. **Architecture:** 기존 `ChatRoomListService`의 병합·정렬·응답 변환을 재사용하고, 홈 팔로잉 호출만 `followedCreatorsOnly = true`를 전달한다. AI/DM 저장소 query는 활성 `CreatorFollowing` 존재 조건을 pagination 전에 적용하며, 기본값 `false`로 일반 채팅 목록 동작을 유지한다. **Tech Stack:** Kotlin, Java 17, Spring Boot 2.7.14, Spring Data JPA/JPQL, Gradle, JUnit 5, Mockito, MockMvc **Spec:** `docs/20260819_홈_팔로잉_최근대화_팔로우필터/prd.md` ## Global Constraints - 공개 API request/response schema를 변경하지 않는다. - DM과 AI 채팅을 모두 포함하고 현재 `CreatorFollowing.isActive = true`인 대상만 허용한다. - 팔로우 조건은 DB query에서 pagination보다 먼저 적용한다. - 일반 `GET /api/v2/chat/rooms`는 기존 전체 조회 동작을 유지한다. - DB schema, index, dependency를 추가하지 않는다. - 메시지 본문, 인증 정보와 식별자를 새 log에 기록하지 않는다. - 요청 범위 밖 리팩터링을 하지 않는다. --- | 문서 항목 | 내용 | |---|---| | 상태 | 구현 완료 | | 작성일 | 2026-08-19 | | 요구사항 기준 | `docs/20260819_홈_팔로잉_최근대화_팔로우필터/prd.md` | | API 기준 | 기존 `GET /api/v2/home/following`, `GET /api/v2/chat/rooms` 계약 유지 | | 현재 Phase | Phase 1 완료 | | 현재 활성 Goal | 없음 | ## 현재 상태 | Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| | 1 | 완료 | `1/1` | 없음 | 없음 | - 동시에 하나의 미완료 goal만 운용한다. - 사용자가 명시적으로 요청하지 않았으므로 goal token budget은 설정하지 않는다. - 이 문서 작성 단계에서는 production/test 코드를 수정하거나 테스트를 실행하지 않는다. ## 범위 ### 포함 - 홈 팔로잉 최근 대화에 활성 팔로우 DM/AI filter 적용 - 기존 정렬, cursor 해석, 최대 10개와 응답 변환 재사용 - 일반 채팅 목록의 전체 조회 기본값 유지 - facade, service, repository query와 홈 팔로잉 E2E 회귀 검증 ### 제외 - 공개 API schema, 채팅방 생성과 메시지 전송 정책 변경 - 팔로우/언팔로우 쓰기 로직과 알림 정책 변경 - 차단·계정 활성 상태 등 추가 노출 정책 - DB migration, index와 dependency 추가 - 홈 팔로잉의 최근 대화 외 섹션 변경 ## 파일 책임과 변경 범위 | 파일 | 계획된 책임 | |---|---| | `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt` | 홈 팔로잉 최근 대화 호출에 `followedCreatorsOnly = true` 전달 | | `src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/service/ChatRoomListService.kt` | 팔로우 필터 옵션을 AI/DM repository에 전달하고 기존 병합·정렬 유지 | | `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/ChatRoomRepository.kt` | AI 캐릭터 `creatorMember.id` 기준 활성 팔로우 조건 적용 | | `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt` | DM 상대 `member.id` 기준 활성 팔로우 조건 적용 | | `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt` | facade가 홈 전용 팔로우 필터를 요청하는지 검증 | | `src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt` | filter 옵션 전달과 기존 전체 조회 회귀 검증 | | `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt` | 활성 팔로우 DM/AI 포함과 미팔로우·비활성 팔로우 제외 검증 | | `src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListControllerTest.kt` | 수정 없이 일반 채팅 목록 위임 회귀 확인 | ## Phase 1: 최근 대화 팔로우 필터 적용 **Phase 결과:** 홈 팔로잉 탭은 활성 팔로우 크리에이터의 DM/AI 최근 대화만 반환하고 일반 채팅 목록은 기존 동작을 유지한다. **선행조건:** 승인된 PRD `HFC-001~008`. **Phase 완료 조건:** `P1-T1`과 `P1-GATE` 완료, 실제 검증 결과를 Progress에 기록한다. ### 구현 항목 #### Task 1.1 홈 팔로잉 최근 대화 필터 **Goal 실행 `P1-T1`:** 활성 팔로우 관계를 AI/DM query에 적용하고 홈 팔로잉 facade에서만 해당 필터를 활성화한다. - **시작 조건:** `HFC-001~008` 확정, 현재 코드와 공개 API 계약 재확인. - **완료 증거:** 아래 체크박스 전체 완료, focused test 성공, 변경 파일과 실제 결과를 Progress에 기록. - **범위 밖:** 채팅 목록 공개 filter enum 추가, 별도 query 복제, 메모리 후처리, 신규 abstraction·dependency·migration. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/service/ChatRoomListService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/ChatRoomRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt` **Interfaces:** - Consumes: 요청 회원 `Member.id`, DM 상대 `opponent.member.id`, AI 대상 `pc.character.creatorMember.id`, `CreatorFollowing.isActive`. - Produces: 기존 반환형 `ChatRoomListPageResponse`; 공개 DTO와 endpoint 계약 변경 없음. - `ChatRoomListService.getRooms`는 아래 parameter만 마지막에 추가하고 기본값으로 기존 호출을 보존한다. ```kotlin fun getRooms( member: Member, filter: String = ChatRoomListFilter.ALL.name, cursor: String? = null, limit: Int = DEFAULT_LIMIT, followedCreatorsOnly: Boolean = false ): ChatRoomListPageResponse ``` - `ChatRoomRepository.findAiChatListRooms`와 `UserCreatorChatRoomRepository.findDmChatListRooms`에는 `@Param("followedCreatorsOnly") followedCreatorsOnly: Boolean`을 추가한다. - [x] **RED:** `HomeFollowingEndToEndTest.shouldAssembleFollowingTabForMember` fixture에 활성 팔로우 DM, 활성 팔로우 AI, 더 최신인 미팔로우 DM 11개, `isActive = false` 팔로우 DM을 만든다. 응답에는 활성 팔로우 AI/DM room 두 개만 기존 최신순으로 남는 assertion을 추가해 저장소 filter가 pagination보다 먼저 적용되는지도 함께 검증한다. ```kotlin .andExpect(jsonPath("$.data.recentChats.length()").value(2)) .andExpect(jsonPath("$.data.recentChats[0].roomId").value(fixture.aiChatRoomId)) .andExpect(jsonPath("$.data.recentChats[0].chatType").value("AI")) .andExpect(jsonPath("$.data.recentChats[1].roomId").value(fixture.dmChatRoomId)) .andExpect(jsonPath("$.data.recentChats[1].chatType").value("DM")) .andExpect(jsonPath("$.data.recentChats[?(@.roomId == ${fixture.unfollowedDmRoomIds.first()})]").isEmpty) .andExpect(jsonPath("$.data.recentChats[?(@.roomId == ${fixture.inactiveFollowingDmRoomId})]").isEmpty) ``` - [x] **RED 확인:** 아래 명령을 실행해 현재 구현이 더 최신인 미팔로우·비활성 팔로우 DM을 최대 10개 반환하므로 `recentChats.length()` 또는 room 순서 assertion이 실패하는지 확인한다. ```bash ./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest.shouldAssembleFollowingTabForMember" ``` - [x] **GREEN:** AI/DM repository query parameter를 service에서 전달하고, query의 기존 방·참가자·메시지 활성 조건 뒤에 아래 활성 팔로우 존재 조건을 추가한다. 조건은 `Pageable` 적용 전에 평가한다. AI query: ```sql AND ( :followedCreatorsOnly = false OR EXISTS ( SELECT 1 FROM CreatorFollowing cf WHERE cf.member.id = :memberId AND cf.creator.id = pc.character.creatorMember.id AND cf.isActive = true ) ) ``` DM query: ```sql AND ( :followedCreatorsOnly = false OR EXISTS ( SELECT 1 FROM CreatorFollowing cf WHERE cf.member.id = :memberId AND cf.creator.id = opponent.member.id AND cf.isActive = true ) ) ``` - [x] **GREEN:** `HomeFollowingFacade`만 아래처럼 filter를 활성화한다. 일반 `ChatRoomListController` 호출은 기본값 `false`를 사용한다. ```kotlin val recentChats = chatRoomListService.getRooms( member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true ).rooms ``` - [x] **GREEN:** `ChatRoomListServiceTest`의 repository stub/verify에 `followedCreatorsOnly` 인자를 반영하고, `true`가 AI와 DM repository 양쪽에 전달되는 test를 추가한다. 기존 test는 `false` 전달과 병합·정렬·cursor·preview가 유지되는지 검증한다. - [x] **GREEN:** `HomeFollowingFacadeTest`가 `followedCreatorsOnly = true` 호출을 stub/verify하고 비로그인 시 무호출을 계속 검증하도록 수정한다. - [x] **GREEN 확인:** 아래 focused test를 실행해 facade, service, query와 endpoint 동작을 함께 확인한다. ```bash ./gradlew test \ --tests "kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest" \ --tests "kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacadeTest" \ --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest" ``` **Expected:** 모든 test가 통과하고 홈 팔로잉 E2E에서 활성 팔로우 AI/DM 두 방만 기존 최신순으로 반환된다. - [x] **REFACTOR:** 이번 Task에서 생긴 중복만 정리한다. 별도 filter enum, query method 복제와 공통 abstraction은 만들지 않는다. focused test와 아래 직접 영향 회귀·lint를 다시 실행하고 실제 결과를 Progress에 기록한다. ### 완료 조건 - [x] `P1-T1`의 체크박스와 완료 증거가 모두 충족됐다. - [x] `HFC-001~008`이 구현 또는 명시적 제외로 추적된다. - [x] 공개 API와 DB schema 변경이 없다. - [x] 실제 검증 결과가 Progress에 기록됐다. ### 검증 방법 #### Phase 1 Gate **Goal 실행 `P1-GATE`:** 홈 팔로잉 필터와 일반 채팅 목록 비회귀를 최종 판정한다. - **시작 조건:** `P1-T1` 완료. - **완료 증거:** 아래 test와 lint 성공, 전체 회귀 실행 여부와 근거를 Progress에 기록. - **범위 밖:** Gate 통과를 위한 test 삭제·skip·완화와 관련 없는 코드 수정. ```bash ./gradlew test \ --tests "kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest" \ --tests "kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest" \ --tests "kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacadeTest" \ --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest" ./gradlew ktlintCheck ``` **Expected:** 모든 focused/영향 범위 test와 `ktlintCheck`가 exit code `0`으로 끝난다. 홈 팔로잉은 활성 팔로우 AI/DM만 반환하고 일반 채팅 목록은 팔로우 filter 없이 기존 전체 조회를 위임한다. 수동 검증: - [x] `git diff --name-only`에 계획된 production/test 파일과 이 작업 문서 이외의 변경이 없는지 확인한다. - [x] `git diff`에서 endpoint, response DTO, DB schema와 dependency 변경이 없는지 확인한다. - [x] 새 log에 회원·채팅·팔로우 식별자 또는 메시지 본문이 추가되지 않았는지 확인한다. 전체 회귀 `./gradlew test`는 작은 조회 조건 변경이며 위 test가 service, repository query, facade, endpoint와 인접 일반 채팅 목록을 포함하므로 기본 생략한다. 위 명령으로 영향 범위를 판단할 수 없는 실패가 발생하거나 공통 코드로 범위가 확장되면 전체 회귀를 실행하고 결과를 Progress에 기록한다. ## 실행 순서와 의존성 | 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 | |---:|---|---|---|---| | 1 | `P1-T1` | 승인된 PRD | 아니요 | query mapping과 fixture 근거를 PRD·현재 코드와 다시 대조 | | 2 | `P1-GATE` | `P1-T1` 완료 | 아니요 | 실패를 소유한 Task에 회귀 수정 goal 추가 | ```text P1-T1 → P1-GATE ``` ## 변경 금지 항목 - 기존 완료 문서와 검증 기록을 삭제하거나 덮어쓰지 않는다. - 공개 endpoint, request/response DTO와 chat type filter enum을 변경하지 않는다. - `followingCreators` 최대 20개 결과로 최근 대화를 메모리 후처리하지 않는다. - AI/DM query 전체를 새 method로 복제하지 않는다. - 신규 dependency, DB migration과 index를 추가하지 않는다. - 관련 없는 리팩터링 또는 포맷 변경을 하지 않는다. - test를 삭제·skip·완화하지 않는다. ## 의사결정 및 중단 규칙 - PRD와 구현이 충돌하면 `prd.md`의 Decision Log를 먼저 갱신하고 이 계획을 동기화한 뒤 구현한다. - `ChatCharacter.creatorMember.id` 또는 DM `opponent.member.id`로 활성 팔로우를 판정할 수 없으면 추정 구현을 중단하고 새 근거를 확인한다. - 구현 범위가 공개 API, DB schema 또는 팔로우 쓰기 로직으로 확장되면 사용자 승인 전 진행하지 않는다. - 체크박스, test와 Progress 기록이 모두 충족된 뒤에만 goal을 완료 처리한다. ## Progress ### 2026-08-19 `P1-T1`, `P1-GATE` 완료 - RED: `HomeFollowingEndToEndTest.shouldAssembleFollowingTabForMember`를 실행해 `$.data.recentChats.length()`가 기대값 `2` 대신 `10`을 반환하는 실패를 확인했다. - GREEN: `ChatRoomListServiceTest`, `HomeFollowingFacadeTest`, `HomeFollowingEndToEndTest` focused test가 exit code `0`으로 통과했다. - Phase Gate: 위 focused test에 `ChatRoomListControllerTest`를 추가한 영향 범위 회귀와 `./gradlew ktlintCheck`가 exit code `0`으로 통과했다. - 전체 회귀: `./gradlew test`가 exit code `0`으로 통과했다. - 수동 검증: 변경 파일은 계획된 production/test 7개와 이 작업의 `prd.md`, `plan-task.md`뿐이며 endpoint, response DTO, DB schema, dependency와 log 변경이 없음을 `git diff`로 확인했다. - LSP: 로컬에 `kotlin-ls`가 설치되어 있지 않아 실행하지 못했으며, Kotlin compile, 전체 test와 ktlint 성공으로 대체 검증했다. ## Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | |---|---|---|---|---|---| | 2026-08-19 | `DEC-001` | 확정 | DM과 AI 채팅을 모두 활성 팔로우 대상으로 filter한다. | 사용자 인터뷰 승인 | `P1-T1`, PRD `HFC-001~003` | | 2026-08-19 | `DEC-002` | 확정 | 기존 service/query에 기본값 `false`인 최소 옵션을 추가하고 홈 facade만 활성화한다. | 일반 채팅 목록 회귀 없이 기존 병합·정렬을 재사용하는 최소 변경 | `P1-T1` | | 2026-08-19 | `DEC-003` | 확정 | filter는 DB query에서 pagination 전에 적용한다. | 후처리 시 최신 미팔로우 대화가 limit을 차지해 팔로우 대화가 누락될 수 있음 | `P1-T1`, PRD `HFC-004` | ## 발견된 문제 현재 확정된 범위 내 발견된 문제는 없다.