From 09558e2669213d8032e36b93770961be6f83bd41 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 12 Jul 2026 12:13:50 +0900 Subject: [PATCH] =?UTF-8?q?docs(home):=20=EC=9D=91=EC=9B=90=20=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=20=EC=A7=91=EA=B3=84=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9D=84=20=EB=AC=B8=EC=84=9C=ED=99=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 40 ++++++++--------- .../prd.md | 45 ++++++++++--------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md b/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md index e1dff182..cdae3212 100644 --- a/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md +++ b/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md @@ -1,8 +1,8 @@ # 메인 홈 추천 응원 크리에이터 스냅샷 수정 Plan/Task ## 시나리오 계약 -- Happy path: KST 전날 하루 데이터를 UTC half-open 범위로 변환해 `CHEER_CREATOR` 점수를 계산하고, 점수순 상위 16개 스냅샷을 저장한다. Real surface: `DefaultHomeRecommendationQueryRepositoryTest`, `RecommendationSnapshotRefreshServiceTest`. -- Score: 응원 점수는 `((channelDonationAmount * 0.45) + (fanTalkCount * 0.30) + (channelDonationCount * 0.10)) * newBoost`다. 채널 후원 금액은 `use_can_calculate.can` 그대로 사용하고, 채널 후원 수는 `UseCanCalculate.useCan` 기준으로 중복 제거한다. Real surface: `RecommendationScorePolicyTest`, `DefaultHomeRecommendationQueryRepositoryTest`. +- Happy path: 인기 커뮤니티와 동일한 최근 7일 UTC half-open 범위로 `CHEER_CREATOR` 점수를 계산하고, 점수순 상위 16개 스냅샷을 저장한다. Real surface: `DefaultHomeRecommendationQueryRepositoryTest`, `RecommendationSnapshotRefreshServiceTest`. +- Score: 응원 점수는 `((donationAmount * 0.45) + (fanTalkCount * 0.30) + (donationCount * 0.10)) * newBoost`다. 후원 금액은 `CHANNEL_DONATION`과 `DONATION`의 `use_can_calculate.can`을 그대로 사용하고, 후원 수는 `UseCanCalculate.useCan` 기준으로 중복 제거한다. Real surface: `RecommendationScorePolicyTest`, `DefaultHomeRecommendationQueryRepositoryTest`. - Boost: 신규 부스트는 크리에이터 데뷔일 기준 0~10일 `1.15`, 11~20일 `1.10`, 21~30일 `1.05`, 31일 이상 `1.0`이다. Real surface: `RecommendationScorePolicyTest`, `DefaultHomeRecommendationQueryRepositoryTest`. - Fallback: 최신 `CHEER_CREATOR` 스냅샷이 없으면 lock, double-check, 동일 refresh 로직 재사용, refresh 후 재조회 순서로 fallback을 실행한다. lock 대기는 최대 300ms, 홈 API refresh 완료 대기는 최대 1,500ms다. Real surface: fallback service test, `HomeRecommendationQueryServiceTest`. - Empty marker: `CHEER_CREATOR` refresh 결과가 0건이면 `targetId = 0` marker를 저장해 정상 refresh 완료 상태를 남기고, 조회 응답에서는 marker를 제외한다. Real surface: `RecommendationSnapshotPersistenceAdapterTest`, `HomeRecommendationQueryServiceTest`. @@ -13,7 +13,7 @@ - 신규 공개 API, 신규 응답 필드, 운영 DDL 추가는 범위에 포함하지 않는다. - 기존 `recommendation_snapshot` 테이블과 `RecommendedSectionType.CHEER_CREATOR`를 재사용한다. - `CHEER_CREATOR` 집계는 현재 구조와 성능 특성을 유지해 DB-side exact scoring을 기본으로 한다. Kotlin 단에는 산식/부스트 근거 테스트용 정책 함수를 둔다. -- 전날 기준은 KST 전날 00:00:00 이상, 다음날 00:00:00 미만이고, DB 조회에는 UTC half-open window를 사용한다. +- 집계 기간은 인기 커뮤니티와 동일하게 KST 전날을 포함한 최근 7일이며, DB 조회에는 UTC half-open window를 사용한다. - fallback orchestration은 AI 캐릭터 전용 구현을 그대로 복사하지 않고, 섹션별 lock key와 refresh action을 받을 수 있는 최소 공통 runner를 우선 적용한다. - 다른 스냅샷 섹션으로 empty marker를 확장하는 작업은 이번 구현 범위에서 제외한다. 단, `CHEER_CREATOR`에 적용할 때 이후 공통화가 가능하도록 조건문/상수명을 명확히 둔다. @@ -22,11 +22,11 @@ - 유지: 홈 응원 크리에이터 응답 필드인 `creatorId`, `creatorNickname`, `creatorProfileImage`는 변경하지 않는다. - 유지: 홈 첫 화면 응답은 최대 8명, 스냅샷 후보 조회는 최대 16개를 사용한다. - 유지: 상세 조회 시점의 활성 크리에이터 필터와 차단 필터는 유지한다. -- 유지: 채널 후원 금액은 `use_can_calculate.can` 값을 그대로 합산한다. -- 변경: 점수 가중치는 후원 금액 45%, 팬Talk 수 30%, 채널 후원 수 10%로 바꾼다. -- 변경: 채널 후원 수는 `UseCanCalculate.useCan` 기준 distinct count로 계산한다. +- 유지: 후원 금액은 `use_can_calculate.can` 값을 그대로 합산한다. +- 변경: 점수 가중치는 후원 금액 45%, 팬Talk 수 30%, 후원 수 10%로 바꾼다. +- 변경: 후원 수는 `UseCanCalculate.useCan` 기준 distinct count로 계산한다. - 변경: 팬Talk 수는 `CreatorCheers.isActive == true` row 수로 계산한다. -- 변경: 집계 기간은 최근 7일에서 KST 전날 하루로 바꾼다. +- 변경: 집계 기간은 인기 커뮤니티와 동일한 최근 7일로 한다. - 변경: 신규 부스트는 기존 크리에이터 공통 부스트 `1.5/1.3/1.2`가 아니라 `CHEER_CREATOR` 전용 `1.15/1.10/1.05`를 사용한다. - 추가: `CHEER_CREATOR` 최신 스냅샷이 없을 때 fallback refresh를 실행한다. - 추가: `CHEER_CREATOR` refresh 결과 0건이면 empty snapshot marker를 저장한다. @@ -51,7 +51,7 @@ - Verify: `docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md` - Create: `docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md` - RED: 문서 작업은 TDD 예외. TDD 예외 사유: 코드 동작 변경 전 요구사항과 구현 순서를 고정하는 작업이다. - - GREEN: PRD의 산식, KST 전날 UTC half-open 범위, 후원 수 distinct 기준, empty marker, fallback timeout/lock 정책을 task로 분해한다. + - GREEN: PRD의 산식, 최근 7일 UTC half-open 범위, 후원 수 distinct 기준, empty marker, fallback timeout/lock 정책을 task로 분해한다. - REFACTOR: 기존 홈 추천 구현 파일과 테스트 파일 기준으로 task별 수정/검증 경로를 맞춘다. - 기대 결과: 구현 시작 전에 PRD와 plan-task가 같은 디렉터리에 준비된다. @@ -90,39 +90,39 @@ --- -### Phase 3: 전날 집계 window와 DB 스냅샷 query +### Phase 3: 최근 7일 집계 window와 DB 스냅샷 query -- [x] **Task 3.1: `CHEER_CREATOR` refresh window를 KST 전날 하루로 변경** +- [x] **Task 3.1: `CHEER_CREATOR` refresh window를 인기 커뮤니티와 동일한 최근 7일로 변경** - 파일 경로: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.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/RecommendationSnapshotRefreshServiceTest.kt` - - RED: `refreshCheerCreatorSnapshots(nowUtc)`가 `RecommendationSnapshotWindowPolicy.previousKstDayUtcWindow(nowUtc)`로 얻은 `windowStartUtc`, `windowEndExclusiveUtc`, `snapshotAt`을 사용하도록 실패 테스트를 작성한다. + - RED: `refreshCheerCreatorSnapshots(nowUtc)`가 `RecommendationSnapshotWindowPolicy.previousKstSevenDayUtcWindow(nowUtc)`로 얻은 `windowStartUtc`, `windowEndExclusiveUtc`, `snapshotAt`을 사용하도록 실패 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest` - - GREEN: `CHEER_CREATOR` 단일 refresh 경로를 분리하고, 기존 일괄 refresh에서 최근 7일 window 대신 전날 UTC half-open window를 넘긴다. - - REFACTOR: `POPULAR_COMMUNITY`의 기존 7일 window는 변경하지 않는다. `HomeRecommendationQueryPort.findCheerCreatorSnapshots(...)` 시그니처는 `windowEndExclusiveUtc` 의미가 드러나도록 정리한다. - - 기대 결과: 스케줄러와 fallback이 같은 `CHEER_CREATOR` 전날 refresh 경로를 호출할 수 있다. + - GREEN: `CHEER_CREATOR` 단일 refresh 경로를 분리하고, 기존 일괄 refresh에서 인기 커뮤니티와 같은 최근 7일 UTC half-open window를 넘긴다. + - REFACTOR: `POPULAR_COMMUNITY`의 기존 7일 window와 동일한 window를 사용한다. `HomeRecommendationQueryPort.findCheerCreatorSnapshots(...)` 시그니처는 `windowEndExclusiveUtc` 의미가 드러나도록 정리한다. + - 기대 결과: 스케줄러와 fallback이 같은 `CHEER_CREATOR` 최근 7일 refresh 경로를 호출할 수 있다. -- [x] **Task 3.2: 채널 후원 금액/후원 수 집계 기준 변경** +- [x] **Task 3.2: 채널 후원과 일반 후원 금액/후원 수 집계 기준 변경** - 파일 경로: - 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/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt` - - RED: 같은 `UseCanCalculate.useCan`을 참조하는 여러 row가 있을 때 `channelDonationAmount`는 `use_can_calculate.can` 값을 그대로 합산하고, `channelDonationCount`는 distinct `use_can` 기준 1건으로 계산되는 실패 테스트를 작성한다. + - RED: 같은 `UseCanCalculate.useCan`을 참조하는 여러 row가 있을 때 `donationAmount`는 `use_can_calculate.can` 값을 그대로 합산하고, `donationCount`는 distinct `use_can` 기준 1건으로 계산되는 실패 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest` - GREEN: donation stats query에서 금액은 기존 `sum(ucc.can)`을 유지하고, 후원 수는 `count(distinct ucc.use_can_id)` 또는 엔티티 매핑에 맞는 동일 의미 컬럼으로 변경한다. - - REFACTOR: `CanUsage.CHANNEL_DONATION`, `status = RECEIVED`, `is_refund = false` 등 기존 제외 조건은 유지한다. + - REFACTOR: `CanUsage.CHANNEL_DONATION`과 `CanUsage.DONATION`을 포함하고, `status = RECEIVED`, `is_refund = false` 등 기존 제외 조건은 유지한다. - 기대 결과: 후원 이벤트 단위 중복 제거가 점수의 후원 수 항목에만 적용된다. - [x] **Task 3.3: 팬Talk 수와 half-open 시간 조건 적용** - 파일 경로: - 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/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt` - - RED: `CreatorCheers.isActive == true` row만 집계하고, `created_at >= windowStartUtc and created_at < windowEndExclusiveUtc` 조건으로 전날 경계를 검증하는 실패 테스트를 작성한다. `windowEndExclusiveUtc`와 같은 시각의 row는 제외되어야 한다. + - RED: `CreatorCheers.isActive == true` row만 집계하고, `created_at >= windowStartUtc and created_at < windowEndExclusiveUtc` 조건으로 집계 경계를 검증하는 실패 테스트를 작성한다. `windowEndExclusiveUtc`와 같은 시각의 row는 제외되어야 한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest` - GREEN: `creator_cheers` 집계 조건을 active row와 half-open time range로 맞춘다. - REFACTOR: 후원 집계 조건도 같은 half-open range를 사용해 `<= :snapshotAt` 방식이 남지 않게 정리한다. - - 기대 결과: 전날 KST 하루 경계가 후원과 팬Talk 집계에 동일하게 적용된다. + - 기대 결과: 최근 7일 KST 경계가 후원과 팬Talk 집계에 동일하게 적용된다. - [x] **Task 3.4: 응원 크리에이터 DB-side 점수와 부스트 적용** - 파일 경로: @@ -266,7 +266,7 @@ ## Coverage Check - Feature A: Task 2.1, Task 3.2, Task 3.4에서 후원 금액 45%, 팬Talk 30%, 후원 수 10%, 후원 수 distinct 기준을 검증한다. -- Feature B: Task 3.1, Task 3.3에서 KST 전날 하루를 UTC half-open 조회 범위로 변환하고 `windowEndExclusiveUtc` 경계를 검증한다. +- Feature B: Task 3.1, Task 3.3에서 최근 7일 KST 범위를 UTC half-open 조회 범위로 변환하고 `windowEndExclusiveUtc` 경계를 검증한다. - Feature C: Task 2.2, Task 3.4에서 데뷔일 기준 응원 전용 신규 부스트와 경계값을 검증한다. - Feature D: Task 3.5, Task 5.4, Task 6.1에서 최신 `CHEER_CREATOR` 스냅샷 순서, 후보 16개/응답 8개, 기존 응답 스키마 유지를 검증한다. - Feature E: Task 5.1, Task 5.2, Task 5.3, Task 5.4에서 fallback refresh 재사용, double-check, 300ms lock 대기, 1,500ms 홈 API 대기, timeout 후 background 완료, 중복 refresh 방지를 검증한다. diff --git a/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md b/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md index 547572db..1b2fb30b 100644 --- a/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md +++ b/docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md @@ -1,12 +1,13 @@ # PRD: 메인 홈 추천 응원 크리에이터 스냅샷 수정 ## 1. Overview -메인 홈 추천 탭의 `CHEER_CREATOR` 스냅샷 생성과 조회를 전날 데이터 기반의 응원 점수로 수정하고, 스냅샷이 없을 때 홈 API가 동일 refresh 로직을 안전하게 재사용하도록 fallback 흐름을 보강한다. +메인 홈 추천 탭의 `CHEER_CREATOR` 스냅샷 생성과 조회를 최근 7일 데이터 기반의 응원 점수로 수정하고, 스냅샷이 없을 때 홈 API가 동일 refresh 로직을 안전하게 재사용하도록 fallback 흐름을 보강한다. --- ## 2. Problem -- 기존 `CHEER_CREATOR` 산식은 최근 7일 데이터를 기반으로 하며, 후원 금액 가중치와 신규 부스트 값이 이번 요구사항과 다르다. +- 기존 `CHEER_CREATOR` 스냅샷은 전날 KST 하루 데이터만 사용해, 인기 커뮤니티와 동일한 최근 7일 집계 기준과 다르다. +- 기존 후원 집계는 `CanUsage.CHANNEL_DONATION`만 대상으로 하며, 일반 후원 `CanUsage.DONATION`을 함께 반영하지 않는다. - 현재 일괄 refresh와 홈 API fallback refresh가 섹션별로 동일한 생성 로직을 공유하지 않으면 산식 drift가 발생할 수 있다. - 스냅샷이 없는 초기 배포, 운영 데이터 삭제, 배치 실패 상황에서 홈 조회가 매 요청마다 무거운 집계를 중복 실행하면 API 지연과 DB 부하가 커질 수 있다. - 집계 산식이 추천 노출 순서를 직접 바꾸므로 DB-side 계산과 Kotlin-side 계산 중 어떤 방식을 선택하더라도 산식/부스트 경계값 테스트가 필요하다. @@ -14,8 +15,8 @@ --- ## 3. Goals -- `CHEER_CREATOR` 스냅샷은 전날 KST 하루 데이터를 기반으로 생성한다. -- 응원 점수 산식을 `((채널 후원 금액 * 0.45) + (팬Talk 수 * 0.30) + (채널 후원 수 * 0.10)) * 신규 부스트`로 변경한다. +- `CHEER_CREATOR` 스냅샷은 인기 커뮤니티와 동일하게 스냅샷 생성 시점 기준 최근 7일 데이터를 기반으로 생성한다. +- 응원 점수 산식을 `((후원 금액 * 0.45) + (팬Talk 수 * 0.30) + (후원 수 * 0.10)) * 신규 부스트`로 변경한다. - 신규 부스트는 크리에이터 데뷔일 기준 10일 이내 1.15, 20일 이내 1.10, 30일 이내 1.05, 그 외 1.0을 적용한다. - 스케줄러 refresh와 fallback refresh는 동일한 `CHEER_CREATOR` refresh 로직을 재사용한다. - 스냅샷이 없을 때 fallback refresh는 lock, double-check, single-flight 대기로 중복 refresh를 방지한다. @@ -35,14 +36,14 @@ --- ## 5. Target Users -- 회원/비회원: 메인 홈 추천 탭에서 전날 응원 반응이 많았던 크리에이터를 발견하는 사용자 +- 회원/비회원: 메인 홈 추천 탭에서 최근 7일 응원 반응이 많았던 크리에이터를 발견하는 사용자 - 앱 클라이언트: 기존 응답 계약을 유지한 채 `CHEER_CREATOR` 추천 순서만 변경된 결과를 받는 클라이언트 -- 운영자: 전날 후원/팬Talk 반응이 추천 노출에 반영되는지 확인해야 하는 운영 담당자 +- 운영자: 최근 7일 후원/팬Talk 반응이 추천 노출에 반영되는지 확인해야 하는 운영 담당자 --- ## 6. User Stories -- 사용자는 메인 홈 추천 탭에서 전날 응원이 많았던 크리에이터를 우선 보고 싶다. +- 사용자는 메인 홈 추천 탭에서 최근 7일 응원이 많았던 크리에이터를 우선 보고 싶다. - 사용자는 후원 금액뿐 아니라 팬Talk와 후원 참여 횟수도 함께 반영된 추천을 보고 싶다. - 사용자는 신규 크리에이터가 일정 기간 동안 적절한 노출 기회를 받기를 기대한다. - 앱 클라이언트는 스냅샷이 없는 상황에서도 홈 API가 실패하지 않고 안정적으로 빈 배열 또는 생성된 스냅샷을 받기를 원한다. @@ -56,11 +57,11 @@ #### Requirements - `CHEER_CREATOR` 점수는 아래 산식으로 계산한다. - - `score = ((channelDonationAmount * 0.45) + (fanTalkCount * 0.30) + (channelDonationCount * 0.10)) * newBoost` -- `channelDonationAmount`는 집계 기간 안에 발생한 채널 후원 금액 합계다. -- `channelDonationCount`는 집계 기간 안에 발생한 채널 후원 건수다. -- 채널 후원 수는 `UseCanCalculate.useCan`이 같은 row를 1개 후원 이벤트로 보고 중복 제거해 계산한다. -- 채널 후원은 기존 요구사항과 동일하게 `CanUsage.CHANNEL_DONATION`이며, 환불/미수령/비정상 상태는 기존 채널 후원 집계 제외 조건을 유지한다. + - `score = ((donationAmount * 0.45) + (fanTalkCount * 0.30) + (donationCount * 0.10)) * newBoost` +- `donationAmount`는 집계 기간 안에 발생한 채널 후원과 일반 후원 금액 합계다. +- `donationCount`는 집계 기간 안에 발생한 채널 후원과 일반 후원 건수다. +- 후원 수는 `UseCanCalculate.useCan`이 같은 row를 1개 후원 이벤트로 보고 중복 제거해 계산한다. +- 후원은 `CanUsage.CHANNEL_DONATION`과 일반 후원 `CanUsage.DONATION`을 포함하며, 환불/미수령/비정상 상태는 기존 후원 집계 제외 조건을 유지한다. - `fanTalkCount`는 집계 기간 안에 생성된 활성 팬Talk 수다. - 팬Talk 수는 `CreatorCheers.isActive == true`인 row 수로 계산한다. - 팬Talk는 기존 `CreatorCheers` 또는 현재 구현의 `creator_cheers` 기반 데이터를 의미한다. @@ -69,20 +70,20 @@ - 최종 저장 수는 기존 홈 노출 안정성을 위해 `CHEER_CREATOR` 최대 16개를 유지한다. #### Edge Cases -- `channelDonationAmount`, `fanTalkCount`, `channelDonationCount`가 모두 0인 크리에이터는 스냅샷 후보에서 제외한다. +- `donationAmount`, `fanTalkCount`, `donationCount`가 모두 0인 크리에이터는 스냅샷 후보에서 제외한다. - 후원 금액은 없지만 팬Talk가 있으면 산식에 따라 점수를 계산한다. -- 팬Talk는 없지만 채널 후원이 있으면 산식에 따라 점수를 계산한다. +- 팬Talk는 없지만 채널 후원 또는 일반 후원이 있으면 산식에 따라 점수를 계산한다. - 비활성 크리에이터, 차단 필터에 의해 조회 시 제외되는 크리에이터는 최종 홈 응답에서 제외한다. - 스냅샷에는 존재하지만 조회 시점에 크리에이터가 비활성화된 경우 응답에서 제외한다. -### Feature B. 전날 데이터 기반 집계 기간 +### Feature B. 최근 7일 데이터 기반 집계 기간 #### Requirements -- 점수 입력값은 전날 KST 하루 데이터만 사용한다. -- 전날 기준은 KST 기준 전일 00:00:00 이상, 다음날 00:00:00 미만의 half-open 범위로 정의한다. +- 점수 입력값은 인기 커뮤니티와 동일하게 스냅샷 생성 시점 기준 최근 7일 데이터를 사용한다. +- 최근 7일 기준은 KST 기준 전날을 포함한 7일의 시작 시각 이상, 다음날 00:00:00 미만의 half-open 범위로 정의한다. - 운영 DB 시간이 UTC 기준이면, 실제 조회는 `windowStartUtc <= createdAt < windowEndExclusiveUtc`로 변환해 사용한다. - `snapshotAt`은 해당 KST 전날의 종료 시각을 UTC로 변환한 `windowEndExclusiveUtc.minusSeconds(1)`을 사용한다. -- 스케줄러가 KST 06:00에 실행되면 실행일 전날 KST 일자를 집계 대상으로 삼는다. +- 스케줄러가 KST 06:00에 실행되면 실행일 전날 KST 일자를 포함한 최근 7일을 집계 대상으로 삼는다. - 기존 코드에 남아 있는 `created_at <= :snapshotAt` 방식은 이번 범위에서 half-open end-exclusive 조건으로 맞춘다. #### Edge Cases @@ -180,7 +181,7 @@ - DB-side scoring을 유지하는 경우 Kotlin 단에는 산식 parity 검증용 정책 함수를 두고, DB expression과 같은 상수를 공유한다. - Kotlin-side scoring으로 변경하는 경우 DB에서 필요한 원천 metric을 정확히 집계하고, service에서 최종 점수/정렬/limit을 적용한다. 이 경우 후보 전체를 메모리에 올리는 비용과 데이터량을 구현 계획에서 검토한다. - 기본 권장안은 현재 구조와 성능 특성을 유지하는 DB-side exact scoring이다. 다만 산식/부스트 계산은 Kotlin 정책 테스트로 검증 가능한 형태를 둔다. -- 시간 범위는 KST 전날을 UTC half-open window로 변환하는 `RecommendationSnapshotWindowPolicy`를 재사용한다. +- 시간 범위는 인기 커뮤니티와 동일한 최근 7일 UTC half-open window를 반환하는 `RecommendationSnapshotWindowPolicy`를 재사용한다. - 신규 DDL은 만들지 않는 것을 기본으로 한다. 불가피하게 필요하면 운영 DB 반영용 SQL 문서는 MySQL 기준으로 별도 작성한다. --- @@ -190,7 +191,7 @@ - `CHEER_CREATOR` 스냅샷 최종 저장 수 - fallback refresh 실행/성공/실패/timeout 로그 - fallback refresh lock 획득 성공/실패 로그 -- `channelDonationAmount`, `fanTalkCount`, `channelDonationCount` 입력값 분포 +- `donationAmount`, `fanTalkCount`, `donationCount` 입력값 분포 - empty snapshot marker 저장 횟수 - 홈 API `CHEER_CREATOR` 섹션 빈 응답 비율 - 홈 API fallback refresh 대기 시간 @@ -203,8 +204,8 @@ --- ## 11. Decisions -- 채널 후원 금액은 `use_can_calculate.can` 값을 그대로 사용한다. -- 채널 후원 수는 `UseCanCalculate.useCan`이 같은 row를 1개 후원 이벤트로 보고 중복 제거한다. +- 후원 금액은 `use_can_calculate.can` 값을 그대로 사용한다. +- 후원 수는 `UseCanCalculate.useCan`이 같은 row를 1개 후원 이벤트로 보고 중복 제거한다. - 팬Talk 수는 `CreatorCheers.isActive == true`인 row 수로 계산한다. - 빈 결과 marker 정책은 다른 스냅샷 섹션에도 확장하는 것이 맞지만, 이번 구현 범위에서는 `CHEER_CREATOR`에만 적용한다.