# PRD: 메인 홈 추천 AI 캐릭터 스냅샷 산식 변경 ## 1. Overview 메인 홈 추천 탭의 AI 캐릭터 스냅샷 생성과 조회가 전날 데이터 기반 추천 점수를 사용하도록 수정한다. --- ## 2. Problem - 기존 메인 홈 추천 AI 캐릭터 스냅샷은 최근 7일 채팅 데이터와 기존 신규 부스트를 기준으로 계산되어, 전날 성과를 빠르게 반영하기 어렵다. - 기존 PRD에서는 AI 캐릭터 팔로우 증가량을 산식에서 제외했으나, 이번 요구사항에서는 전날 신규 팔로우 수를 점수에 포함해야 한다. - AI 캐릭터에는 별도 데뷔 날짜가 없으므로 신규 부스트 기준일을 캐릭터 생성 날짜/시간으로 명확히 고정해야 한다. - 산식 변경이 추천 노출 순서에 직접 영향을 주므로 계산 기간, 집계 대상, 부스트 구간, 조회 정렬 기준을 문서로 고정해야 한다. --- ## 3. Goals - AI 캐릭터 추천 점수를 전날 데이터 기반으로 산출한다. - AI 캐릭터 점수 산식을 `(AI 채팅 수 * 45% + 최근 활성 사용자 수 * 35% + 팔로우 증가 수 * 20%) * 신규 부스트`로 변경한다. - AI 채팅 수는 전날 AI 캐릭터가 답변한 메시지 수로 계산한다. - 최근 활성 사용자 수는 전날 해당 캐릭터와 1회 이상 채팅한 중복 없는 사용자 수로 계산한다. - 팔로우 증가 수는 전날 발생한 AI 캐릭터 신규 팔로우 수로 계산한다. - 신규 부스트는 `ChatCharacter.createdAt` 기준으로 10일 이내 1.15, 20일 이내 1.10, 30일 이내 1.05, 그 외 1.0을 적용한다. - 홈 통합 조회와 AI 캐릭터 전체보기 조회는 최신 AI 캐릭터 스냅샷의 점수순 결과를 사용한다. --- ## 4. Non-Goals - 메인 홈 추천 API URL, 응답 필드, 응답 JSON 스키마는 변경하지 않는다. - AI 캐릭터 상세, AI 채팅방, 크리에이터 채널 API의 공개 스키마는 변경하지 않는다. - 추천 결과 수동 편집, 관리자 화면, A/B 테스트, 머신러닝 개인화는 이번 범위에 포함하지 않는다. - AI 캐릭터 팔로우 생성/취소 동작 자체를 변경하지 않는다. - 기존 AI 캐릭터 `characterId`와 `creatorId`의 의미를 변경하지 않는다. --- ## 5. Target Users - 회원: 메인 홈 추천 탭에서 전날 반응이 좋은 AI 캐릭터를 발견하고 채팅 또는 크리에이터 채널로 이동하는 사용자 - 앱 클라이언트: 기존 메인 홈 추천 응답 계약을 유지한 채 AI 캐릭터 추천 순서만 최신 스냅샷 기준으로 사용하는 클라이언트 - 운영자: 전날 채팅/사용자/팔로우 반응이 추천 순서에 반영되는지 확인해야 하는 운영 담당자 --- ## 6. User Stories - 사용자는 메인 홈 추천 탭에서 전날 채팅 반응이 많았던 AI 캐릭터를 우선 보고 싶다. - 사용자는 여러 사용자가 실제로 대화한 AI 캐릭터를 추천받고 싶다. - 사용자는 최근 팔로우가 늘어난 AI 캐릭터를 추천 목록에서 발견하고 싶다. - 사용자는 신규 AI 캐릭터가 일정 기간 동안 적절한 노출 기회를 받기를 기대한다. - 앱 클라이언트는 기존 API 응답 구조를 바꾸지 않고 추천 순서만 변경된 결과를 받고 싶다. --- ## 7. Core Features ### Feature A. AI 캐릭터 스냅샷 점수 산식 변경 #### Requirements - AI 캐릭터 점수는 아래 산식으로 계산한다. - `score = ((aiChatCount * 0.45) + (activeUserCount * 0.35) + (followIncreaseCount * 0.20)) * newBoost` - `aiChatCount`는 집계 기간 안에 AI 캐릭터가 답변한 활성 메시지 수다. - AI가 답변한 메시지는 기존 AI 캐릭터 추천 집계와 동일하게 `chat_participant.participant_type = 'CHARACTER'`인 활성 participant가 작성한 활성 `chat_message`로 판정한다. - `activeUserCount`는 집계 기간 안에 해당 캐릭터와 1회 이상 채팅한 중복 없는 활성 사용자 수다. - `followIncreaseCount`는 집계 기간 안에 해당 AI 캐릭터에 대응하는 creator member를 신규 팔로우한 수다. - AI 캐릭터에 대응하는 creator member는 기존 홈 추천 응답의 `creatorId`와 동일한 `ChatCharacter.creatorMember.id`다. - 팔로우 증가는 `creator_following.creator_id = ChatCharacter.creatorMember.id`인 활성 팔로우 이력 중 집계 기간에 생성된 row 기준으로 계산한다. - 재팔로우가 기존 비활성 row를 다시 활성화하는 방식이면 신규 팔로우 수에 포함하지 않는다. - 점수 산식 상수는 코드와 테스트에서 재사용 가능한 형태로 관리한다. #### Edge Cases - `aiChatCount`, `activeUserCount`, `followIncreaseCount`가 모두 0인 캐릭터는 스냅샷 후보에서 제외한다. - AI 답변은 있지만 사용자 메시지가 없는 경우 `activeUserCount = 0`으로 계산한다. - 사용자 메시지는 있지만 AI 답변이 없는 경우에도 `activeUserCount`와 `followIncreaseCount`가 있으면 산식에 따라 점수를 계산할 수 있다. - 비활성 메시지, 비활성 participant, 비활성 사용자, 비활성 캐릭터는 집계에서 제외한다. - `creatorMember`가 없거나 연결된 member가 비활성/비 `CREATOR`/비 `AI_CHARACTER`이면 스냅샷 후보와 조회 결과에서 제외한다. ### Feature B. 전날 데이터 기반 집계 기간 #### Requirements - 스냅샷 점수 입력값은 전날 데이터만 사용한다. - 전날 기준은 KST 기준 전일 00:00:00 이상, 다음날 00:00:00 미만의 half-open 범위로 정의한다. - 운영 DB 시간은 UTC이므로, 실제 조회와 계산에서는 KST 기준 전일 00:00:00 이상, 다음날 00:00:00 미만 범위를 UTC 기준 `windowStartUtc <= createdAt < windowEndExclusiveUtc`로 변환해 사용한다. - 스케줄러 실행 시각이 KST 06:00이면, 실행일 전날 KST 일자를 집계 대상으로 삼는다. - 스냅샷 생성 시 `snapshotAt`은 기존 `RecommendationSnapshot` 저장/조회 계약을 유지하되, 점수 입력 집계 기간은 전날 하루로 제한한다. #### Edge Cases - 스케줄러 재실행으로 같은 `sectionType`, `snapshotAt` 스냅샷을 다시 만들 때 기존 최신 스냅샷 대체 정책을 유지한다. - 집계 기간에 해당하는 데이터가 없으면 AI 캐릭터 스냅샷은 빈 결과가 될 수 있으며, 홈 조회 API는 기존처럼 빈 배열을 반환한다. ### Feature C. 신규 부스트 변경 #### Requirements - AI 캐릭터 신규 부스트는 데뷔 날짜가 아니라 `ChatCharacter.createdAt` 기준으로 계산한다. - 신규 부스트 구간은 다음과 같다. - 캐릭터 생성 후 10일 이내: `1.15` - 캐릭터 생성 후 20일 이내: `1.10` - 캐릭터 생성 후 30일 이내: `1.05` - 그 외: `1.0` - 경계일 계산은 기존 추천 점수 정책의 `ChronoUnit.DAYS.between(baseAt.toLocalDate(), 기준일.toLocalDate())` 방식과 일관되게 한다. - 부스트 기준일은 스냅샷 생성 기준 시각인 `snapshotAt`으로 한다. #### Edge Cases - `ChatCharacter.createdAt`이 null인 캐릭터는 스냅샷 산식에 포함하지 않는다. - 생성일이 기준 시각보다 미래인 데이터는 데이터 오류로 보고 스냅샷 후보에서 제외한다. ### Feature D. AI 캐릭터 스냅샷 조회 #### Requirements - 홈 통합 조회 `GET /api/v2/home/recommendations`의 AI 캐릭터 섹션은 최신 `AI_CHARACTER` 스냅샷 순서를 사용한다. - AI 캐릭터 전체보기 조회도 최신 `AI_CHARACTER` 스냅샷 순서를 사용한다. - AI 캐릭터 스냅샷은 최종 점수 기준 20위까지 저장한다. - 기존 노출 정보는 유지한다. - `characterId` - `creatorId` - 캐릭터 이름 - 캐릭터 소개 - 프로필 이미지 - 작품명 - 사용자들이 친 전체 채팅 수 - 스냅샷 정렬은 점수 내림차순, 동일 점수면 더 늦게 생성된 캐릭터를 우선한다. - 조회 시점에도 비활성 또는 노출 제한 캐릭터는 제외한다. #### Edge Cases - 스냅샷에는 존재하지만 조회 시점에 캐릭터 또는 `creatorMember`가 비활성화된 경우 응답에서 제외한다. - fallback refresh 후에도 최신 스냅샷이 없으면 기존 홈 추천 API 동작과 동일하게 빈 배열을 반환한다. - 상세 조회 결과가 스냅샷 저장 개수보다 적어도 홈 조회 전체는 성공 처리한다. ### Feature E. 스냅샷 없음 fallback refresh #### Requirements - 최신 `AI_CHARACTER` 스냅샷이 없으면 fallback으로 스케줄러와 동일한 AI 캐릭터 스냅샷 refresh 로직을 재사용한다. - fallback refresh는 홈 조회 API가 직접 점수를 계산해 응답하지 않고, 스케줄러와 동일한 저장 로직으로 스냅샷을 만든 뒤 저장된 값을 다시 조회해 반환한다. - fallback refresh는 AI 캐릭터 섹션 단위 lock 또는 single-flight 처리를 적용해 동시에 여러 요청이 같은 refresh를 중복 실행하지 않도록 한다. - lock 획득 후에는 다른 요청이 이미 스냅샷을 생성했을 수 있으므로 최신 스냅샷을 다시 조회하고, 여전히 없을 때만 refresh를 실행한다. - fallback refresh가 성공하면 최신 스냅샷을 다시 조회해 Feature D의 조회 로직과 동일한 방식으로 응답한다. - fallback refresh timeout은 refresh 작업의 강제 중단 시간이 아니라 홈 API 요청이 refresh 완료를 기다리는 최대 대기 시간이다. - fallback refresh의 lock 대기 시간은 최대 300ms로 제한한다. - fallback refresh를 시작한 홈 API 요청은 refresh 완료를 최대 1,500ms까지 기다린다. - fallback refresh가 홈 API 대기 시간을 초과하면 현재 홈 요청은 AI 캐릭터 섹션을 빈 배열로 반환한다. - 홈 API 대기 시간이 초과되어도 이미 시작된 refresh 작업은 가능한 경우 취소하지 않고 백그라운드에서 완료되도록 한다. - 백그라운드 refresh가 완료되면 이후 홈 조회는 저장된 최신 스냅샷을 조회해 반환한다. - fallback refresh가 진행 중인 동안 lock 또는 single-flight 상태를 유지해 후속 요청이 중복 refresh를 시작하지 않도록 한다. #### Edge Cases - lock을 획득하지 못했지만 다른 요청이 refresh 중이면 짧게 대기한 뒤 최신 스냅샷을 다시 조회한다. - 대기 후에도 refresh가 완료되지 않아 최신 스냅샷이 없으면 해당 요청도 AI 캐릭터 섹션을 빈 배열로 반환하고 중복 refresh는 시작하지 않는다. - fallback refresh가 실패하면 실패 로그를 남기고 홈 조회 전체를 실패시키지 않으며 AI 캐릭터 섹션은 빈 배열로 반환한다. - fallback refresh가 성공했지만 집계 대상 데이터가 없어 스냅샷이 생성되지 않으면 AI 캐릭터 섹션은 빈 배열로 반환한다. - fallback refresh 중 예외가 발생해도 다른 추천 섹션 조회 결과는 가능한 범위에서 유지한다. --- ## 8. Technical Constraints - Kotlin, Spring Boot 2.7.14, Java 17, Gradle Wrapper 구조를 유지한다. - 기존 `kr.co.vividnext.sodalive.v2.recommendation` 패키지 경계와 `v2.api.home`에서 `v2.recommendation`을 호출하는 의존 방향을 유지한다. - 기존 `RecommendationSnapshot` 기반 저장/조회 구조를 재사용한다. - 기존 공개 API 응답 DTO는 필드 추가 없이 유지한다. - AI 캐릭터 스냅샷 생성 쿼리는 candidate pre-limit 없이 산식 계산과 정렬을 수행한 뒤 최종 20개 저장 limit을 적용한다. - 스케줄러 refresh와 fallback refresh는 산식, 기간, 저장 limit, 정렬 기준이 갈라지지 않도록 같은 application service 경로를 사용한다. - 홈 API 대기 timeout과 refresh 작업/DB query timeout은 분리한다. 홈 API 대기 timeout이 발생해도 refresh 작업이 반드시 같은 timeout으로 중단되면 안 된다. - fallback refresh의 lock은 단일 서버 환경에서는 JVM local lock을 사용할 수 있지만, 다중 인스턴스 운영 환경에서는 DB lock 또는 분산 lock 등 인스턴스 간 중복 실행을 막을 수 있는 방식을 구현 계획에서 확정한다. - 신규 DDL이 필요한 경우 운영 DB 반영용 SQL 문서는 MySQL 기준으로 별도 작성한다. - 점수 산식과 신규 부스트 값은 테스트에서 경계값을 검증할 수 있도록 상수화한다. --- ## 9. Metrics - AI 캐릭터 스냅샷 생성 성공/실패 로그 - 스냅샷 최종 저장 수 - fallback refresh 실행/성공/실패/timeout 로그 - fallback refresh lock 획득 성공/실패 로그 - `aiChatCount`, `activeUserCount`, `followIncreaseCount` 입력값 분포 - AI 캐릭터 홈 섹션 응답 성공/실패 로그 - AI 캐릭터 홈 섹션 노출 수