docs(recommendation): 추천 배너 언어 필터 계획을 기록한다

This commit is contained in:
2026-08-19 17:10:42 +09:00
parent 1731b3a8c1
commit 2ad30f9069
2 changed files with 631 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
# 추천 탭 배너 접속 국가별 언어 필터 PRD
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 기준 확정 |
| 작성일 | 2026-08-19 |
| 최종 수정일 | 2026-08-19 |
| 대상 제품 | 메인 홈 추천 탭, 메인 콘텐츠 추천 탭 |
| 작성자·결정권자 | 사용자 |
| 관련 API Contract | 신규 없음. 기존 공개 API 계약 유지 |
| 관련 구현 계획 | `docs/20260819_추천탭_배너_접속국가별_언어필터/plan-task.md` |
| 관련 기존 문서 | `docs/20260805_추천탭_배너_조회조건/prd.md`, `docs/20260529_메인_홈_추천_API/prd.md`, `docs/20260623_메인_콘텐츠_추천_탭_API/prd.md` |
## 1. Overview
메인 홈 추천 API와 메인 콘텐츠 추천 API에서 접속 국가에 맞는 언어의 배너만 반환한다. 로그인 회원은 기존 강제 국가 매핑을 포함한 국가 판정 결과를 사용하고, 비로그인 사용자는 `CloudFront-Viewer-Country` 요청 헤더를 기준으로 판정한다.
## 2. Problem Statement
- 두 추천 API의 현재 배너 QueryDSL 조회에는 `content_banner.lang` 조건이 없다.
- 그 결과 한국어·일본어·영어 배너가 같은 응답에 섞일 수 있다.
- 언어 필터가 조회 후 적용되면 다른 언어 배너가 limit을 차지해 필요한 배너가 누락될 수 있다.
문제를 해결했다는 판단은 두 API가 배너 정렬과 limit 적용 전에 확정된 언어 조건을 적용하고, 기존 배너 조회 조건과 공개 응답 스키마를 유지하는 것으로 한다.
## 3. Goals
- 접속 국가 판정 결과가 `JP`이면 `Lang.JA` 배너만 반환한다.
- `JP` 이외의 국가와 국가 정보가 없는 경우에는 `Lang.KO` 배너만 반환한다.
- 로그인 회원은 `MemberContentPreferenceService.resolveCountryCode`의 기존 강제 국가 매핑을 유지한다.
- 비로그인 사용자도 요청 국가를 판정할 수 있도록 동일한 국가 정규화와 기본값 정책을 사용한다.
- 언어 조건을 DB 조회 단계에서 정렬·limit보다 먼저 적용한다.
## 4. Non-Goals
- `Lang.EN` 배너를 반환하는 국가 정책을 추가하지 않는다.
- `Accept-Language`를 배너 언어 판정에 사용하지 않는다.
- `CloudFront-Viewer-Country` 헤더 계약이나 `CountryInterceptor`를 변경하지 않는다.
- 공개 API endpoint, request/response DTO와 응답 스키마를 변경하지 않는다.
- 기존 배너의 탭·성인·활성·차단·대상 유효성·정렬·limit 정책을 변경하지 않는다.
- DB 스키마, 배너 데이터, 관리자 배너 API와 기존 v1 배너 조회를 변경하지 않는다.
- 배너 이외의 추천 섹션에 국가 또는 언어 필터를 추가하지 않는다.
## 5. 대상 사용자와 국가 판정
| 사용자 | 국가 판정 | 언어 선택 |
|---|---|---|
| 강제 JP 매핑 로그인 회원 | 기존 강제 매핑 결과 `JP` | `Lang.JA` |
| 강제 KR 매핑 로그인 회원 | 기존 강제 매핑 결과 `KR` | `Lang.KO` |
| 일반 로그인 회원 | 정규화한 `CloudFront-Viewer-Country`, 누락 시 `KR` | `JP`이면 `JA`, 그 외 `KO` |
| 비로그인 사용자 | 정규화한 `CloudFront-Viewer-Country`, 누락 시 `KR` | `JP`이면 `JA`, 그 외 `KO` |
국가 코드는 기존 정책처럼 앞뒤 공백을 제거하고 대문자로 정규화한다.
## 6. 핵심 조회 흐름
1. `CountryInterceptor``CloudFront-Viewer-Country``CountryContext`에 저장한다.
2. `HomeRecommendationFacade.getHomeRecommendations``AudioRecommendationQueryService.getRecommendations`가 회원과 요청 국가로 국가 코드를 판정한다.
3. 국가 코드가 `JP`이면 `Lang.JA`, 그 외에는 `Lang.KO`를 선택한다.
4. 선택한 `Lang`을 기존 application → port → persistence 경로로 전달한다.
5. Repository가 `content_banner.lang = :lang`을 기존 조건과 함께 적용한 뒤 정렬하고 limit을 적용한다.
## 7. 기능 요구사항
| ID | 상태 | 요구사항 | 수용 기준 | Goal 연결 |
|---|---|---|---|---|
| `BANNER-LANG-001` | 확정 | `GET /api/v2/home/recommendations`는 판정 국가가 `JP`이면 `JA`, 그 외에는 `KO` 홈 배너만 반환한다. | `tab_id IS NULL`인 활성 배너 중 선택 언어와 일치하는 배너만 응답한다. | `P1-T2` |
| `BANNER-LANG-002` | 확정 | `GET /api/v2/audio/recommendations`는 판정 국가가 `JP`이면 `JA`, 그 외에는 `KO` 콘텐츠 추천 배너만 반환한다. | `tab_id = 2`인 활성 배너 중 선택 언어와 일치하는 배너만 응답한다. | `P1-T3` |
| `BANNER-LANG-003` | 확정 | 로그인 회원의 국가 판정은 기존 강제 KR/JP 매핑을 요청 헤더보다 우선한다. | 강제 JP 회원은 비JP 헤더에서도 `JP`, 강제 KR 회원은 JP 헤더에서도 `KR`로 판정된다. | `P1-T1` |
| `BANNER-LANG-004` | 확정 | 비로그인 사용자는 요청 국가를 정규화해 사용하고, 헤더가 없거나 비어 있으면 `KR`로 판정한다. | `JP`·`jp`·공백 포함 JP는 `JP`, null·blank는 `KR`로 판정된다. | `P1-T1` |
| `BANNER-LANG-005` | 확정 | 언어 필터는 정렬·limit 전에 DB 조회 조건으로 적용한다. | 다른 언어 배너가 limit을 차지하지 않고, Repository 테스트가 언어별 결과를 검증한다. | `P1-T2`, `P1-T3` |
| `BANNER-LANG-006` | 확정 | 기존 배너 조회 정책과 공개 API 계약을 유지한다. | 기존 탭·성인·활성·차단·대상 유효성·정렬·limit 테스트와 controller/E2E 회귀가 통과한다. | `P1-GATE` |
## 8. API 계약
| Method | Path | 변경 내용 |
|---|---|---|
| `GET` | `/api/v2/home/recommendations` | 응답 스키마 변경 없이 접속 국가별 배너 언어 조회 조건만 추가한다. |
| `GET` | `/api/v2/audio/recommendations` | 응답 스키마 변경 없이 접속 국가별 배너 언어 조회 조건만 추가한다. |
- 신규 request header는 추가하지 않는다.
- 기존 `CloudFront-Viewer-Country` 처리 경로를 재사용한다.
- 배너가 없으면 기존처럼 빈 목록을 반환한다.
## 9. 성능과 품질 요구사항
- `content_banner.lang` 조건은 QueryDSL `where`에 포함하고 메모리 후처리를 사용하지 않는다.
- 신규 dependency, 캐시, 공통 resolver abstraction을 추가하지 않는다.
- 기존 `Lang`, `CountryContext`, `MemberContentPreferenceService`를 재사용한다.
- application 전달 테스트와 두 Repository 조회 테스트를 TDD로 보강한다.
- 직접 영향 controller/E2E 회귀와 `ktlintCheck`를 Phase Gate에서 검증한다.
## 10. 성공 기준
- [x] 강제 매핑을 포함한 판정 국가가 `JP`이면 두 API의 배너가 모두 `JA`로 제한된다. (`BANNER-LANG-001~003`)
- [x] 일반 국가와 국가 정보가 없는 경우 두 API의 배너가 모두 `KO`로 제한된다. (`BANNER-LANG-001`, `BANNER-LANG-002`, `BANNER-LANG-004`)
- [x] 다른 언어 배너가 정렬·limit 대상에 포함되지 않는다. (`BANNER-LANG-005`)
- [x] 기존 배너 필터와 공개 응답 계약이 유지된다. (`BANNER-LANG-006`)
## 11. Open Questions
- 없음.
## 12. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·Goal |
|---|---|---|---|---|---|
| 2026-08-19 | `DEC-001` | 확정 | `JP``JA`, 그 외 국가는 `KO` 배너만 조회한다. | 사용자 인터뷰 답변 B | `BANNER-LANG-001`, `BANNER-LANG-002`, `BANNER-LANG-005` |
| 2026-08-19 | `DEC-002` | 확정 | 로그인 회원은 기존 강제 국가 매핑을 포함한 국가 판정을 사용한다. | 사용자 인터뷰 답변 B | `BANNER-LANG-003`, `P1-T1` |
| 2026-08-19 | `DEC-003` | 확정 | 비로그인은 요청 국가를 사용하고 국가 정보가 없으면 기존 기본값 `KR`을 사용한다. | 승인된 설계 | `BANNER-LANG-004`, `P1-T1` |
| 2026-08-19 | `DEC-004` | 확정 | 언어 필터는 Repository 조회 조건으로 적용한다. | limit 이전 필터링과 기존 QueryDSL 패턴 유지 | `BANNER-LANG-005`, `P1-T2`, `P1-T3` |