feat(home): AI 캐릭터 부족분을 랜덤 보충한다

This commit is contained in:
2026-07-22 22:14:26 +09:00
parent 075ca88f01
commit a0bf71be47
10 changed files with 230 additions and 4 deletions

View File

@@ -295,6 +295,30 @@
- 2026-07-09: GREEN에서 `HomeRecommendationFacade.HOME_AI_CHARACTER_LIMIT``HomeRecommendationQueryService.DEFAULT_AI_CHARACTER_LIMIT`를 20으로 변경하고, 홈 통합 응답이 AI 캐릭터를 최대 20개 반환하는 controller 테스트를 추가했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest` 실행 결과 `BUILD SUCCESSFUL`로 통과했다.
- 2026-07-09: 회귀 검증으로 `./gradlew ktlintCheck``./gradlew tasks --all`을 실행해 모두 `BUILD SUCCESSFUL`로 통과했다. 두 명령은 최초 sandbox 실행에서 Gradle wrapper의 `~/.gradle` lock 파일 접근 권한 문제로 실패했고, 권한 상승 재실행으로 통과했다. `./gradlew :app:ktlintCheck`는 단일 루트 프로젝트에 `app` 프로젝트가 없어 실패했으며, 저장소 기준 명령인 `./gradlew ktlintCheck`로 대체 검증했다.
- [x] **Task 9.2: 홈 첫 화면 AI 캐릭터 부족분 랜덤 보충**
- Files:
- Modify: `docs/20260529_메인_홈_추천_API/prd.md`
- Modify: `docs/20260529_메인_홈_추천_API/plan-task.md`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- RED: 스냅샷 상세 조립 결과가 요청 limit 미만이면 스냅샷 target id를 제외하고 랜덤 AI 캐릭터 id를 부족분만큼 조회한 뒤 기존 상세 조회를 재사용하는 service 테스트와, 랜덤 id 조회가 활성 `ChatCharacter` 및 활성 CREATOR/AI_CHARACTER `creatorMember`만 반환하고 제외 id를 빼는 repository 테스트를 작성한다.
- 실패 확인:
```bash
./gradlew test \
--tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest \
--tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
```
- GREEN: 홈 통합 조회에서 `backfillRandom = true`로 호출하고, `HomeRecommendationQueryService.findAiCharacterRecommendations`에서 보충이 명시적으로 켜져 있으며 상세 결과가 `limit`보다 적을 때만 `HomeRecommendationQueryPort.findRandomAiCharacterRecommendationIds(excludeCharacterIds, limit)`로 부족분 id를 먼저 뽑은 뒤 기존 `findAiCharacterRecommendationDetails(...)`로 상세와 채팅 수를 조회한다. `excludeCharacterIds`는 최신 스냅샷 target id 전체를 사용해 stale 스냅샷 후보를 다시 뽑지 않는다.
- REFACTOR: 전체보기 paging 조회는 랜덤 보충을 적용하지 않고, AI 캐릭터 추천 점수 산식/스냅샷 생성/공개 API schema는 변경하지 않는다.
- 기대 결과: `GET /api/v2/home/recommendations`의 `aiCharacters`는 가능한 경우 20개까지 채워지고, 랜덤 후보가 부족하면 중복 없이 가능한 AI 캐릭터만 반환한다.
- 검증 기록:
- 2026-07-22: RED에서 `HomeRecommendationQueryPort.findRandomAiCharacterRecommendationIds(...)` 계약을 추가하고 service/repository 테스트를 작성했으며, 기존 구현은 `DefaultHomeRecommendationQueryRepository`가 신규 port 메서드를 구현하지 않아 `compileKotlin`에서 실패했다.
- 2026-07-22: GREEN에서 홈 통합 조회에만 `backfillRandom = true`를 전달하고, AI 캐릭터 스냅샷 상세 결과가 limit보다 적을 때 랜덤 AI 캐릭터 id를 먼저 조회한 뒤 기존 상세 조회로 상세와 채팅 수를 재사용하도록 구현했다. 빈 스냅샷 refresh 완료 marker와 전체보기 paging 조회는 랜덤 보충하지 않도록 기존 계약을 유지했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest` 실행 결과 `BUILD SUCCESSFUL`로 통과했다.
- 2026-07-22: 리뷰 지적에 따라 전체보기 첫 페이지가 `offset == 0`으로 랜덤 보충되는 문제를 `backfillRandom` 명시 플래그로 보정했다. 재검증으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest`, `./gradlew ktlintCheck`, `git diff --check`, `./gradlew test`를 실행해 모두 `BUILD SUCCESSFUL` 또는 무출력 통과를 확인했다.
### Phase 4: 콘텐츠 조회 이력 기록
- [x] **Task 4.1: 콘텐츠 조회 이력 엔티티/서비스 작성**

View File

@@ -164,6 +164,7 @@
#### Requirements
- AI 캐릭터 리스트를 조회한다.
- 홈 첫 화면은 20개를 조회한다.
- 홈 첫 화면은 최신 스냅샷 상세 조회 결과가 20개 미만이면 이미 조회한 스냅샷 캐릭터를 제외한 활성 AI 캐릭터 랜덤 조회로 부족분을 채운다.
- 전체 리스트 API는 페이징으로 조회할 수 있어야 한다.
- 노출 정보는 캐릭터 id, AI 캐릭터에 대응하는 creator id, 캐릭터 이름, 캐릭터 소개, 프로필 이미지, 작품명, 사용자들이 친 전체 채팅 수를 포함한다.
- AI 캐릭터에 대응하는 creator id는 `ChatCharacter.creatorMember.id`이며, 해당 Member는 `role = CREATOR`, `memberKind = AI_CHARACTER`인 내부 크리에이터 Member다.
@@ -182,6 +183,8 @@
#### Edge Cases
- 비활성 또는 노출 제한 캐릭터는 제외한다.
- 활성 `ChatCharacter``creatorMember`가 없거나 연결된 Member가 비활성/비 CREATOR/비 AI_CHARACTER이면 해당 AI 캐릭터는 홈 추천 응답에서 제외한다.
- 랜덤 보충 후보가 부족하면 중복 없이 조회 가능한 AI 캐릭터만 내려준다.
- 전체 리스트 API의 paging 조회는 페이지별 랜덤 보충을 적용하지 않는다.
### Feature H. 장르의 크리에이터

View File

@@ -20,6 +20,7 @@
- 유지: `DefaultHomeRecommendationQueryRepository.findAiCharacterRecommendationDetails(...)`의 상세 조립, `characterId`, `creatorId`, 원작명, 전체 채팅 수, 활성 `creatorMember` 필터는 변경하지 않는다.
- 유지: `HomeRecommendationFacade`, `HomeRecommendationController`, `HomeRecommendationResponse.HomeAiCharacterItem`의 공개 API URL과 응답 필드는 변경하지 않는다.
- 유지: 최근 응원/인기 커뮤니티 스냅샷 산식, window, 저장 limit, random tie-breaker 정렬은 변경하지 않는다.
- 추가: 홈 통합 조회에서 최신 스냅샷 상세 결과가 20개 미만이면 스냅샷 캐릭터 id를 제외한 활성 AI 캐릭터 랜덤 조회로 부족분을 채운다.
- 변경: `findAiCharacterSnapshots(...)`의 입력 window, AI 캐릭터 점수 산식, 신규 부스트 값, 팔로우 증가 수 포함, 후보 제외 조건, top 20 동점 정렬 기준만 변경한다.
- 변경: `RecommendationSnapshotRefreshService`에는 AI 캐릭터 단일 refresh 경로를 추가하되, 기존 전체 일 refresh가 최근 응원/인기 커뮤니티까지 함께 갱신하는 동작은 유지한다.
- 추가: 최신 AI 캐릭터 스냅샷이 없을 때만 fallback refresh orchestration을 추가한다.
@@ -217,6 +218,21 @@
### Phase 7: 최종 검증과 문서 갱신
- [x] **Task 6.3: 홈 통합 AI 캐릭터 부족분 랜덤 보충**
- 파일 경로:
- Modify: `docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/prd.md`
- Modify: `docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/plan-task.md`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- RED: 새 `findRandomAiCharacterRecommendationIds(...)` port 계약을 추가하고, 스냅샷 상세 결과가 limit 미만이면 부족분만큼 랜덤 id를 조회한 뒤 기존 상세 조회를 재사용하는 service 테스트와 제외 id/활성 AI creator member 조건을 검증하는 repository 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`
- GREEN: 홈 통합 조회에서 `backfillRandom = true`로 호출하고, 보충이 명시적으로 켜져 있으며 스냅샷이 존재하고 상세 결과가 limit 미만인 경우에만 랜덤 id를 먼저 조회한 뒤 기존 상세 조회로 상세와 채팅 수를 재사용한다.
- REFACTOR: 빈 스냅샷 refresh 완료 marker와 전체보기 paging 조회는 기존 빈 결과/paging 계약을 유지한다.
- 기대 결과: 홈 통합 AI 캐릭터 섹션은 가능한 경우 20개까지 채워지고, 공개 API 응답 필드는 변경되지 않는다.
- [x] **Task 7.1: focused regression 실행**
- 파일 경로:
- Modify: `docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/plan-task.md`
@@ -246,7 +262,7 @@
- Feature A: Task 2.1, Task 3.1에서 AI 채팅 수 45%, 활성 사용자 수 35%, 팔로우 증가 수 20% 산식을 검증한다.
- Feature B: Task 2.3, Task 4.1에서 KST 전날 하루를 UTC 조회 범위로 변환하는 정책을 검증한다.
- Feature C: Task 2.2, Task 3.2에서 `ChatCharacter.createdAt` 기준 AI 전용 신규 부스트와 null/future 제외를 검증한다.
- Feature D: Task 3.3, Task 5.3, Task 6.1에서 top 20, 동점 생성일 최신순, 기존 응답 스키마 유지를 검증한다.
- Feature D: Task 3.3, Task 5.3, Task 6.1, Task 6.3에서 top 20, 동점 생성일 최신순, 홈 통합 부족분 랜덤 보충, 기존 응답 스키마 유지를 검증한다.
- Feature E: Task 5.1, Task 5.2, Task 5.3에서 fallback refresh 재사용, double-check, 300ms lock 대기, 1,500ms 홈 API 대기, timeout 후 background 완료, 중복 refresh 방지를 검증한다.
- Non-Goals: Task 6.1과 Task 7.2에서 공개 API URL/응답 필드 변경 없음, AI 캐릭터 팔로우 생성/취소 동작 변경 없음, 관리자/ML/A-B 제외를 확인한다.
@@ -258,6 +274,9 @@
- 2026-07-10: 구현 검증으로 산식/윈도우 focused 테스트 `RecommendationScorePolicyTest`, `RecommendationSnapshotWindowPolicyTest`를 실행해 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-10: AI 캐릭터 스냅샷 집계 focused 테스트 `DefaultHomeRecommendationQueryRepositoryTest`를 실행해 전날 UTC window, 팔로우 증가량, 후보 제외, top 20/동점 정렬이 `BUILD SUCCESSFUL`임을 확인했다.
- 2026-07-10: refresh/fallback/query focused 테스트 `RecommendationSnapshotRefreshServiceTest`, `RecommendationSnapshotFallbackServiceTest`, `HomeRecommendationQueryServiceTest`를 실행해 section lock, 300ms lock wait, 1,500ms home wait, timeout 후 background 완료, double-check, fallback 연결이 `BUILD SUCCESSFUL`임을 확인했다.
- 2026-07-22: 사용자 피드백에 따라 홈 통합 AI 캐릭터 섹션의 스냅샷 상세 결과가 20개 미만이면 랜덤 활성 AI 캐릭터로 부족분을 보충하도록 PRD와 plan-task를 보강했다. RED에서 신규 port 계약 미구현으로 `DefaultHomeRecommendationQueryRepository` 컴파일이 실패했고, GREEN에서 홈 통합 조회에만 `backfillRandom = true`를 전달해 랜덤 id 조회와 기존 상세 조회 재사용 조건을 추가했다. focused 검증으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`를 실행해 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-22: 리뷰 지적에 따라 랜덤 보충 쿼리가 전체 AI 캐릭터의 채팅 메시지를 집계한 뒤 `RAND()`/`LIMIT`하지 않도록 2단계 조회로 보정했다. 랜덤 쿼리는 활성 AI 캐릭터 id만 limit만큼 선택하고, 상세/전체 채팅 수는 기존 `findAiCharacterRecommendationDetails(...)` 경로를 재사용한다. RED에서 기존 production 구현의 `findRandomAiCharacterRecommendationDetails(...)` 참조로 `compileKotlin`이 실패했고, GREEN 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest``./gradlew ktlintCheck``BUILD SUCCESSFUL`로 통과했다.
- 2026-07-22: 리뷰 게이트에서 전체보기 첫 페이지가 `offset == 0` 조건으로 랜덤 보충되는 blocker를 확인해 `backfillRandom` 명시 플래그로 홈 통합 조회와 전체보기 조회를 분리했다. 재검증으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest`, `./gradlew ktlintCheck`, `git diff --check`, `./gradlew test`를 실행해 모두 `BUILD SUCCESSFUL` 또는 무출력 통과를 확인했고, 재리뷰에서 blocker 없이 승인받았다.
- 2026-07-10: API 회귀 focused 테스트 `HomeRecommendationControllerTest`, `HomeRecommendationResponseTest`를 실행해 공개 응답 스키마 유지가 `BUILD SUCCESSFUL`임을 확인했다.
- 2026-07-10: 전체 검증으로 `./gradlew ktlintCheck`, `./gradlew test`, `./gradlew tasks --all`, `git diff --check`를 실행했다. `./gradlew test`는 120초 제한에서 1회 timeout되어 600초 제한으로 재실행했고, 모든 명령이 최종 `BUILD SUCCESSFUL` 또는 무출력 통과했다.
- 2026-07-10: 리뷰 게이트에서 발견된 fallback 조건/단일 실행/section lock 해제 시점 이슈를 수정했다. 최신 스냅샷 존재 여부는 요청 page가 아니라 `offset=0, limit=1` 존재 확인으로 분리했고, fallback refresh는 단일 in-flight future로 제한했으며, scheduler AI section lock은 트랜잭션 완료 후 해제되도록 보강했다.

View File

@@ -21,6 +21,7 @@
- 팔로우 증가 수는 전날 발생한 AI 캐릭터 신규 팔로우 수로 계산한다.
- 신규 부스트는 `ChatCharacter.createdAt` 기준으로 10일 이내 1.15, 20일 이내 1.10, 30일 이내 1.05, 그 외 1.0을 적용한다.
- 홈 통합 조회와 AI 캐릭터 전체보기 조회는 최신 AI 캐릭터 스냅샷의 점수순 결과를 사용한다.
- 홈 통합 조회는 최신 스냅샷 상세 조회 결과가 20개 미만이면 스냅샷 캐릭터를 제외한 활성 AI 캐릭터 랜덤 조회로 부족분을 채운다.
---
@@ -117,11 +118,14 @@
- 사용자들이 친 전체 채팅 수
- 스냅샷 정렬은 점수 내림차순, 동일 점수면 더 늦게 생성된 캐릭터를 우선한다.
- 조회 시점에도 비활성 또는 노출 제한 캐릭터는 제외한다.
- 홈 통합 조회에서 스냅샷 상세 조회 결과가 20개 미만이면 이미 조회한 스냅샷 캐릭터 id를 제외하고 활성 AI 캐릭터를 랜덤으로 조회해 부족분을 뒤에 붙인다.
#### Edge Cases
- 스냅샷에는 존재하지만 조회 시점에 캐릭터 또는 `creatorMember`가 비활성화된 경우 응답에서 제외한다.
- fallback refresh 후에도 최신 스냅샷이 없으면 기존 홈 추천 API 동작과 동일하게 빈 배열을 반환한다.
- 상세 조회 결과가 스냅샷 저장 개수보다 적어도 홈 조회 전체는 성공 처리한다.
- 랜덤 보충 후보가 부족하면 중복 없이 조회 가능한 AI 캐릭터만 반환한다.
- 빈 스냅샷 refresh 완료 marker가 있거나 전체보기 paging 조회이면 랜덤 보충을 적용하지 않는다.
### Feature E. 스냅샷 없음 fallback refresh