# 추천 탭 배너 접속 국가별 언어 필터 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` |