docs(content): 오디오 스냅샷 폴백 계획을 문서화한다

This commit is contained in:
2026-07-12 22:42:36 +09:00
parent 729e3e8c45
commit 90b1ff22fb
2 changed files with 398 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
# 메인 콘텐츠 추천 오디오 스냅샷 폴백 Plan/Task
## 시나리오 계약
- Happy path: `GET /api/v2/audio/recommendations`가 사용자 visibility에 맞는 `NEW_AND_HOT`, `MOST_COMMENTED`, `RECOMMENDED_AUDIO` 스냅샷을 각각 조회하고, 비어 있는 섹션만 fallback refresh 후 재조회한다. Real surface: `AudioRecommendationQueryServiceTest`, `AudioRecommendationEndToEndTest`.
- Independent fallback: `NEW_AND_HOT` 스냅샷이 존재해도 `MOST_COMMENTED` 또는 `RECOMMENDED_AUDIO`가 비어 있으면 해당 섹션 fallback을 독립적으로 실행한다. Real surface: `AudioRecommendationQueryServiceTest`, 신규 fallback service test.
- Visibility: 비회원/19금 노출 불가 회원은 `*_SAFE`, 19금 노출 가능 회원은 `*_ALL` section type으로 fallback을 수행한다. Real surface: `AudioRecommendationQueryServiceTest`.
- Lock and wait: fallback은 `lock:audio-recommendation-snapshot-refresh:{SECTION_TYPE}` lock, 300ms lock wait, 1,500ms API wait, JVM single-flight, lock 안 double-check를 사용한다. Real surface: 신규 fallback service test.
- Empty marker: 오디오 snapshot-backed 6개 section type은 refresh 결과 0건이면 `targetId = 0` marker를 저장하고, snapshot 조회 응답에서는 marker를 제외한다. Real surface: `RecommendationSnapshotPersistenceAdapterTest`.
- Refresh reuse: fallback refresh는 `AudioRecommendationSnapshotRefreshService`의 스케줄러와 동일한 snapshot 생성 로직을 사용한다. Real surface: `AudioRecommendationSnapshotRefreshServiceTest`, 신규 fallback service test.
- Adjacent regression: `GET /api/v2/audio/recommendations` URL, response field, 오디오 점수 산식, 배너/오리지널/최신/무료/포인트 섹션 조회 정책은 변경하지 않는다. Real surface: `AudioRecommendationFacadeTest`, `AudioRecommendationEndToEndTest`.
## 범위와 전제
- 이번 문서는 `docs/20260712_메인_콘텐츠_추천_오디오_스냅샷_폴백/prd.md`의 구현 계획이다.
- 신규 공개 API, 신규 응답 필드, 운영 DDL 추가는 범위에 포함하지 않는다.
- 기존 `recommendation_snapshot` 테이블과 오디오 관련 `RecommendedSectionType` 6개를 재사용한다.
- `AudioRecommendationQueryService.getRecommendations`의 snapshot-backed 3개 섹션만 이번 fallback 보강의 대상이다.
- `findNewAndHotAudios` 전체보기 fallback은 기존 lazy refresh 동작을 유지하되, 공통 helper 변경 시 회귀 테스트로 보호한다.
- 오디오 스냅샷은 기존 `findLatestSnapshots(...)` 정책을 유지한다. 홈 추천처럼 대상일 `snapshotAt` exact 조회로 바꾸는 것은 이번 범위에 포함하지 않는다.
- 기존 `AudioRecommendationSnapshotRefreshService`는 일괄 refresh만 제공하므로, 단일 섹션 refresh 추가와 일괄 refresh 재사용 중 더 작은 변경을 구현 단계에서 선택한다.
- 우선 권장안은 오디오 전용 fallback service를 두고, 홈 추천 fallback service의 lock/timeout/double-check/single-flight 패턴을 복제보다 작은 형태로 재사용 가능한 helper로 분리할지 검토하는 것이다.
## 기존 오디오 추천 로직 유지/변경 경계
- 유지: `GET /api/v2/audio/recommendations` endpoint와 응답 JSON 필드.
- 유지: `NEW_AND_HOT`, `MOST_COMMENTED`, `RECOMMENDED_AUDIO` 점수 산식과 limit.
- 유지: `SAFE`/`ALL` visibility variant 선택 기준.
- 유지: `AudioRecommendationSnapshotScheduler`의 매일 00:00 KST refresh.
- 유지: `banners`, `originalSeries`, `latestAudios`, `freeAudios`, `pointAudios` 조회 경로.
- 변경: `MOST_COMMENTED``RECOMMENDED_AUDIO`만 비어 있어도 fallback refresh를 시도한다.
- 변경: `NEW_AND_HOT` fallback도 다른 두 섹션과 같은 공통 fallback 경로를 사용하도록 정리한다.
- 추가: 오디오 snapshot-backed 6개 section type에 empty marker를 적용한다.
- 추가: 오디오 fallback refresh lock/timeout/single-flight 로그를 남긴다.
## 실행 명령
- 문서 명령 확인: `./gradlew tasks --all`
- 오디오 조회 service 테스트: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`
- 오디오 refresh service 테스트: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshServiceTest`
- snapshot marker 테스트: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest`
- 오디오 API 회귀 테스트: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacadeTest`
- 포맷 검증: `./gradlew ktlintCheck`
- 전체 회귀: `./gradlew test`
---
### Phase 1: 문서와 현재 동작 고정
- [x] **Task 1.1: PRD와 구현 계획 문서 작성**
- 파일 경로:
- Create: `docs/20260712_메인_콘텐츠_추천_오디오_스냅샷_폴백/prd.md`
- Create: `docs/20260712_메인_콘텐츠_추천_오디오_스냅샷_폴백/plan-task.md`
- RED: 문서 작업은 TDD 예외. TDD 예외 사유: 코드 동작 변경 전 요구사항과 구현 순서를 고정하는 작업이다.
- GREEN: 오디오 snapshot-backed 3개 섹션의 독립 fallback, lock, timeout, empty marker, refresh 재사용 요구사항을 문서화한다.
- REFACTOR: 기존 메인 콘텐츠 추천 탭 PRD와 홈 추천 snapshot fallback 문서의 정책 차이를 반영해 범위/비범위를 정리한다.
- 기대 결과: 구현 시작 전에 PRD와 plan-task가 같은 디렉터리에 준비된다.
- [x] **Task 1.2: 현재 오디오 snapshot fallback 부재를 회귀 테스트로 고정**
- 파일 경로:
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt`
- RED: `NEW_AND_HOT_AUDIO_SAFE`는 기존 스냅샷이 있지만 `MOST_COMMENTED_AUDIO_SAFE`만 비어 있을 때, `MOST_COMMENTED` fallback port가 호출되어야 한다는 실패 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`
- GREEN: 아직 구현하지 않는다. 이 task는 구현 단계에서 실패를 확인한 뒤 Phase 3에서 통과시킨다.
- REFACTOR: `RECOMMENDED_AUDIO_SAFE`만 비어 있는 케이스도 별도 테스트로 추가해 두 섹션이 `NEW_AND_HOT` 존재 여부에 묶이지 않음을 고정한다.
- 기대 결과: 현재 결함이 테스트로 재현된다.
---
### Phase 2: empty marker 저장 정책
- [x] **Task 2.1: 오디오 스냅샷 section type empty marker 지원 추가**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt`
- RED: `replaceSnapshots(NEW_AND_HOT_AUDIO_SAFE, snapshotAt, emptyList())`, `replaceSnapshots(MOST_COMMENTED_AUDIO_SAFE, snapshotAt, emptyList())`, `replaceSnapshots(RECOMMENDED_AUDIO_SAFE, snapshotAt, emptyList())` 호출 시 marker가 저장되고 `findSnapshots(...)`는 빈 배열, `existsSnapshot(...)`는 true인 실패 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest`
- GREEN: `supportsEmptySnapshotMarker(...)`에 오디오 snapshot-backed 6개 section type을 추가한다.
- REFACTOR: 홈 추천 `AI_CHARACTER`, `CHEER_CREATOR`, `POPULAR_COMMUNITY` marker 동작 회귀 assertion을 유지한다.
- 기대 결과: 데이터가 없는 오디오 섹션도 정상 refresh 완료 상태를 저장한다.
- [x] **Task 2.2: marker 대체와 latest 조회 제외 검증**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt`
- RED: 오디오 marker가 있는 같은 `sectionType`, `snapshotAt`에 실제 row를 저장하면 marker가 제거되는 실패 테스트를 작성한다. `findLatestSnapshots(...)`에서도 marker가 반환되지 않음을 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest`
- GREEN: 기존 delete 후 save 흐름이 marker 대체를 보장하는지 확인하고, 조회 query의 `target_id <> 0` 조건이 latest 조회에도 적용되게 유지한다.
- REFACTOR: marker 관련 상수와 지원 section 판정 함수 이름이 오디오/홈 모두에 어색하지 않은지 정리한다.
- 기대 결과: marker가 사용자 응답이나 이후 실제 스냅샷을 오염시키지 않는다.
---
### Phase 3: 오디오 fallback orchestration
- [x] **Task 3.1: 오디오 fallback port/service 추가**
- 파일 경로:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationSnapshotFallbackService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationSnapshotFallbackServiceTest.kt`
- RED: 특정 오디오 `sectionType`의 latest snapshot이 없고 marker도 없으면 lock을 획득하고 refresh service를 호출한 뒤 같은 section latest snapshot을 재조회하는 실패 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackServiceTest`
- GREEN: `RecommendationSnapshotPort`, `AudioRecommendationSnapshotRefreshService`, `RedissonClient`를 사용하는 오디오 전용 fallback service를 추가한다.
- REFACTOR: 홈 추천 fallback의 lock wait 300ms, API wait 1,500ms, single-flight, double-check 패턴을 맞추되, 공통화가 과하면 오디오 전용 최소 구현으로 유지한다.
- 기대 결과: 오디오 섹션 하나를 입력받아 fallback refresh와 재조회를 수행할 수 있다.
- [x] **Task 3.2: lock miss, timeout, 실패, marker 케이스 검증**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationSnapshotFallbackService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationSnapshotFallbackServiceTest.kt`
- RED: lock 획득 실패, 1,500ms timeout, refresh 예외, marker 존재, 동시 요청 single-flight 케이스를 실패 테스트로 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackServiceTest`
- GREEN: 각 실패/대기 상황에서 빈 배열을 반환하고 warn/info log를 남기며 전체 API 예외로 전파하지 않게 구현한다.
- REFACTOR: fallback service가 상세 DTO 조립이나 점수 계산을 직접 하지 않도록 유지한다.
- 기대 결과: fallback이 요청 지연과 중복 refresh를 제한한다.
- [x] **Task 3.3: refresh service에 섹션 단위 refresh 경로 추가 또는 일괄 refresh 재사용 확정**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationSnapshotRefreshService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationSnapshotRefreshServiceTest.kt`
- RED: `refreshSection(sectionType, now)` 또는 동등한 경로가 입력 section type에 맞는 queryPort 함수와 limit을 사용해 `replaceSnapshots(...)`를 호출하는 실패 테스트를 작성한다. 일괄 refresh 재사용을 선택하면 fallback service test에서 `refreshDailySnapshots()` 호출을 명시적으로 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshServiceTest`
- GREEN: 가장 작은 변경으로 스케줄러와 fallback이 같은 snapshot 생성 로직을 공유하게 한다.
- REFACTOR: `SAFE`/`ALL` section type 매핑 중복이 커지면 기존 `AudioRecommendationVisibility` extension 또는 작은 helper로 정리한다.
- 기대 결과: fallback refresh와 scheduler refresh 사이에 산식 drift가 생기지 않는다.
---
### Phase 4: `AudioRecommendationQueryService` 연결
- [x] **Task 4.1: `getRecommendations`의 3개 snapshot 조회를 fallback 경로로 변경**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt`
- RED: `MOST_COMMENTED_AUDIO_SAFE`만 비어 있을 때 `mostCommentedAudios`가 fallback 재조회 결과로 조립되는 실패 테스트를 작성한다. `RECOMMENDED_AUDIO_SAFE`만 비어 있는 케이스도 추가한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`
- GREEN: `NEW_AND_HOT`, `MOST_COMMENTED`, `RECOMMENDED_AUDIO` 각각에 대해 `findSnapshotsWithFallback(sectionType, offset, limit)` 형태의 경로를 사용한다.
- REFACTOR: 기존 `refreshMissingNewAndHotSnapshots(...)`와 Redis 날짜 marker는 새 fallback 경로로 대체하거나 전체보기 전용으로만 남긴다. 사용하지 않게 되면 관련 의존성/상수를 제거한다.
- 기대 결과: 3개 오디오 스냅샷 섹션이 서로 독립적으로 fallback을 실행한다.
- [x] **Task 4.2: visibility별 fallback section type 검증**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt`
- RED: 비회원은 `*_SAFE`, 성인 콘텐츠 노출 가능 회원은 `*_ALL` section type으로 fallback service가 호출되는 실패 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`
- GREEN: 기존 `newAndHotSectionType`, `mostCommentedSectionType`, `recommendedAudioSectionType` 매핑을 fallback 호출에도 그대로 사용한다.
- REFACTOR: 기존 성인 preference 조회 정책과 `initializeDefaultPreference` 미호출 회귀 테스트를 유지한다.
- 기대 결과: fallback이 현재 사용자 visibility와 다른 variant를 잘못 갱신하거나 조회하지 않는다.
- [x] **Task 4.3: `findNewAndHotAudios` 전체보기 회귀 정리**
- 파일 경로:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt`
- RED: 전체보기 `findNewAndHotAudios(member, offset, limit)`가 기존처럼 offset/limit snapshot 순서를 유지하고, snapshot이 없을 때 새 fallback 경로 또는 기존 lazy refresh 정책 중 결정된 경로를 사용하는 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`
- GREEN: 전체보기 동작을 구현 결정에 맞춰 최소 수정한다.
- REFACTOR: 홈 첫 화면 limit 12와 전체보기 paging limit이 섞이지 않도록 helper 인자를 명확히 유지한다.
- 기대 결과: 첫 화면 fallback 보강이 전체보기 paging을 깨지 않는다.
---
### Phase 5: API 회귀와 최종 검증
- [x] **Task 5.1: 오디오 추천 API 응답 스키마 회귀 검증**
- 파일 경로:
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt`
- RED: `newAndHotAudios`, `mostCommentedAudios`, `recommendedAudios` 필드명이 유지되고 신규 필드가 추가되지 않는 회귀 테스트를 확인/보강한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacadeTest`
- GREEN: controller/facade/DTO 변경 없이 application service 결과가 기존 response로 매핑되게 한다.
- REFACTOR: 공개 API URL과 JSON field name 변경이 없음을 assertion으로 유지한다.
- 기대 결과: 클라이언트 공개 스키마는 변경되지 않는다.
- [x] **Task 5.2: focused regression 실행**
- 파일 경로:
- Modify: `docs/20260712_메인_콘텐츠_추천_오디오_스냅샷_폴백/plan-task.md`
- RED: 구현 task 완료 후 계획 문서에 기록할 focused command 목록을 확정한다.
- 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
- GREEN: 아래 명령을 실행하고 결과를 이 문서 하단 검증 기록에 누적한다.
- REFACTOR: 실패한 명령이 있으면 원인과 재실행 결과를 같은 task 아래에 기록한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshServiceTest`
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackServiceTest`
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest`
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacadeTest`
- 기대 결과: 오디오 fallback, marker, refresh, API 회귀가 최소 명령으로 검증된다.
- [ ] **Task 5.3: 전체 회귀와 문서 검증**
- 파일 경로:
- Modify: `docs/20260712_메인_콘텐츠_추천_오디오_스냅샷_폴백/plan-task.md`
- RED: 구현 완료 후 전체 회귀 명령 실행 전에는 검증 기록이 구현 전 상태여야 한다.
- 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
- GREEN: `./gradlew ktlintCheck`, `./gradlew test`, `./gradlew tasks --all`을 실행하고 결과를 문서 하단 검증 기록에 누적한다.
- REFACTOR: PRD와 plan-task가 구현 결과와 어긋나면 먼저 문서를 갱신하고 필요한 focused test를 재실행한다.
- 기대 결과: 포맷, 전체 테스트, 문서 명령 유효성을 모두 확인한다.
---
## 검증 기록
- 2026-07-12: PRD와 구현 계획 문서만 작성했다. 코드 변경은 수행하지 않았다.
- 2026-07-12: 문서 변경 검증을 수행했다.
- `git diff --check` 성공.
- `./gradlew tasks --all`은 최초 샌드박스 실행에서 `~/.gradle` lock 파일 권한 오류로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL`로 통과했다.
- 2026-07-12: 오디오 snapshot-backed 3개 섹션의 독립 fallback 구현을 수행했다.
- RED 확인:
- `RecommendationSnapshotPersistenceAdapterTest.shouldSaveAudioEmptySnapshotMarkerWhenReplacingWithEmptySnapshots`는 오디오 section marker 미지원으로 실패했다.
- `AudioRecommendationSnapshotRefreshServiceTest.shouldRefreshRequestedAudioSnapshotSectionOnly``refreshSection` 미정의 컴파일 오류로 실패했다.
- `AudioRecommendationSnapshotFallbackServiceTest``AudioRecommendationSnapshotFallbackService` 미정의 컴파일 오류로 실패했다.
- `AudioRecommendationQueryServiceTest`는 기존 생성자/조회 경로가 fallback service를 사용하지 않아 컴파일 오류로 실패했다.
- GREEN 확인:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshServiceTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackServiceTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest --tests kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacadeTest` 성공.
- 포맷/문서 명령:
- `./gradlew ktlintCheck` 성공.
- `./gradlew tasks --all` 성공.
- `git diff --check` 성공.
- 전체 회귀:
- `./gradlew test`는 최초 300초 제한에 걸렸고, 재실행은 사용자 요청으로 중단했다. 전체 테스트는 사용자가 별도로 재실행하기로 했다.
- 2026-07-12: post-implementation review에서 blocking issue 2건을 확인하고 수정했다.
- `AudioRecommendationQueryService`의 fallback 기준 시간을 `Asia/Seoul` 기준으로 분리해 JVM 기본 timezone 의존을 제거했다.
- `AudioRecommendationSnapshotScheduler`가 fallback과 동일한 6개 section lock을 획득한 뒤 일괄 refresh를 실행하도록 수정했다.
- 추가 RED 확인:
- `AudioRecommendationSnapshotSchedulerTest.shouldSkipWhenSectionLockNotAcquired`는 section lock miss에도 일 배치를 실행해 실패했다.
- `AudioRecommendationSnapshotSchedulerTest.shouldRefreshOnlyWhenLockAcquired`는 section lock unlock 검증 실패로 실패했다.
- 수정 후 검증:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.scheduler.AudioRecommendationSnapshotSchedulerTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackServiceTest` 성공.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshServiceTest` 성공.
- `./gradlew ktlintCheck` 성공.