docs(recommendation): 노출 정책과 구현 검증 결과를 기록한다
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
# 추천 화면 AI 캐릭터 및 이벤트 배너 노출 정책 PRD
|
||||
|
||||
## 문서 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 구현 및 검증 완료 · 운영 배포 제외 · 전체 빌드는 UTC 환경 기준 |
|
||||
| 작성일 | 2026-10-31 |
|
||||
| 최종 수정일 | 2026-10-31 |
|
||||
| 대상 제품 | 홈 추천 및 콘텐츠 추천 API의 AI 캐릭터·이벤트 배너 노출 |
|
||||
| 작성자·결정권자 | 사용자 결정 반영 |
|
||||
| 관련 API Contract | 별도 문서 없음. 기존 응답 계약 유지 |
|
||||
| 관련 구현 계획 | [`docs/20261031_추천화면AI캐릭터및이벤트배너노출정책/plan-task.md`](plan-task.md) |
|
||||
| 관련 review | 없음 |
|
||||
|
||||
### 요구사항 상태
|
||||
|
||||
사용자 정책은 확정이며 기술 범위 선택은 별도 Decision Log에 분리했다. 사용자 승인으로 오인하지 않는다. 미결 정책 질문 없음, 모호성 0.00.
|
||||
|
||||
## 1. 개요
|
||||
|
||||
홈 추천 aggregate 응답에서 AI 캐릭터 목록은 모든 조회자에게 빈 배열로 제공한다. 홈 및 오디오 추천의 기존 이벤트 배너는 배너 자체의 현재 노출 조건을 유지하면서 연결된 이벤트 상세 조회와 같은 대상자 정책을 만족할 때만 반환한다. 응답 구조와 비이벤트 추천 콘텐츠는 변경하지 않는다.
|
||||
|
||||
## 2. 문제 정의
|
||||
|
||||
- 홈 추천 aggregate에 AI 캐릭터 목록이 포함되어 있어 해당 노출을 중단해도 응답 계약은 유지할 필요가 있다.
|
||||
- 이벤트 배너는 배너 자체 조건만으로는 연결된 이벤트의 활성 상태, 기간, 국가 기반 언어, 인증 성인 대상 정책을 모두 보장하지 못한다.
|
||||
- 이벤트 상세 화면에서 볼 수 없는 이벤트 배너가 추천 응답에 노출되지 않도록 두 조회 경로의 정책을 일치시킨다.
|
||||
|
||||
## 3. 목표
|
||||
|
||||
- 홈 aggregate 응답에서 `aiCharacters`를 항상 `[]`로 반환한다.
|
||||
- EVENT 유형이며 event FK가 있는 추천 배너는 기존 이벤트 상세 조회의 노출 정책을 만족할 때만 반환한다.
|
||||
- 기존 endpoint, response schema, 다른 배너 조건, subBanner 응답을 보존한다.
|
||||
|
||||
## 4. 비목표
|
||||
|
||||
- `AudioRecommendationsResponse`에 `aiCharacters` 필드를 추가하지 않는다.
|
||||
- 전용 AI 캐릭터 endpoint의 정책이나 구현을 바꾸지 않는다.
|
||||
- AI 추천 snapshot, scheduler, 저장 데이터를 바꾸지 않는다.
|
||||
- LINK 유형 URL 파싱, 일반 배너, `subBanners`(event FK가 없는 데이터 포함)의 정책은 바꾸지 않는다.
|
||||
- 이벤트 상세 endpoint의 응답, 오류 정책, 보안 설정을 바꾸지 않는다.
|
||||
- legacy 배너 endpoint, DB schema, dependency, 전역 timezone 또는 formatter를 바꾸지 않는다.
|
||||
|
||||
## 5. 사용자와 권한
|
||||
|
||||
| 조회자 | 인증 판정 | 이벤트의 `isAdult` 노출 대상 |
|
||||
|---|---|---|
|
||||
| 익명 | 미인증 | `null`, `false` |
|
||||
| 로그인했으나 AuthRepository 인증 기록 없음 | 미인증 | `null`, `false` |
|
||||
| 인증 기록이 있는 회원 | 인증 | `null`, `true` |
|
||||
| ADMIN | AuthRepository 인증 여부와 무관 | 이벤트 대상자 조건만 우회 |
|
||||
|
||||
인증 여부는 `AuthRepository.getAuthIdByMemberId(memberId) != null`로 판정한다. `canViewAdultContent`는 preference·country·auth 규칙에 따라 계산된 성인 노출 가능 여부이며, raw preference나 이벤트 인증 여부와 동일하지 않다. 두 판정은 다를 수 있으므로 이벤트의 성인 대상 정책은 반드시 인증 기록으로 판단한다.
|
||||
|
||||
## 6. 핵심 동작
|
||||
|
||||
1. `GET /api/v2/home/recommendations`를 조회하면 기존 응답에 `aiCharacters: []`가 포함된다.
|
||||
2. 두 추천 endpoint에서 기존 배너 조건으로 후보 배너를 조회한다.
|
||||
3. EVENT 배너의 연결 이벤트가 존재하고 이벤트 상세 노출 정책을 통과하면 배너 후보를 유지한다.
|
||||
4. 정책에 맞지 않거나 연결 이벤트가 없는 EVENT 배너는 해당 배너만 제외한다. 응답의 나머지 영역은 정상 반환한다.
|
||||
5. 적격 배너 수가 0이면 `banners: []`를 반환한다.
|
||||
|
||||
## 7. 기능 요구사항
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `REC-AI-001` | 확정 | 홈 aggregate에서 AI 캐릭터를 제공하지 않고 기존 목록 필드를 빈 배열로 유지한다. | 익명, 일반 회원, ADMIN 요청 모두 `data.aiCharacters`가 존재하고 JSON 배열 `[]`이며 null이 아니고 생략되지 않는다. aggregate 경로에서 AI 추천 query/fallback이 호출되지 않는다. | `GET /api/v2/home/recommendations`, `P1-T1` |
|
||||
| `REC-AI-002` | 확정 | 오디오 추천 응답의 현재 schema를 유지한다. | `AudioRecommendationsResponse`에 `aiCharacters`가 없으며 이 변경으로 필드가 추가되지 않는다. | `GET /api/v2/audio/recommendations`, `P1-T3`, `P1-GATE` |
|
||||
| `REC-EVT-001` | 확정 | 두 추천 endpoint의 EVENT 배너는 연결 이벤트가 존재하고 활성일 때만 노출한다. | 연결 이벤트 부재 또는 비활성 이벤트이면 배너 전체를 제외한다. 다른 배너와 API 처리는 계속된다. | `P1-T2`, `P1-T3` |
|
||||
| `REC-EVT-002` | 확정 | 이벤트 시작·종료 시각 경계는 양끝을 포함한다. | 비교 의미는 상세 조회와 동일하다. `startDate == now` 및 `endDate == now`는 적격, `now`가 범위 밖이면 제외한다. 검증은 시각 정밀도를 포함해 결정적으로 수행한다. | `P1-T2`, `P1-T3` |
|
||||
| `REC-EVT-003` | 확정 | 연결 이벤트의 언어 적격성은 회원의 해석 국가로 판정한다. | `resolveCountryCode(member) == JP`이면 연결 이벤트 `Lang.JA`, 그 외는 `Lang.KO`여야 한다. request `LangContext`가 결과를 덮어쓰지 않는다. | `P1-T2`, `P1-T3` |
|
||||
| `REC-EVT-004` | 확정 | 이벤트 성인 대상은 인증 기록 여부로 판정한다. | `isAdult == null`은 모두, `true`는 인증 회원만, `false`는 미인증 조회자만 적격이다. 익명은 미인증이다. | `P1-T2`, `P1-T3` |
|
||||
| `REC-EVT-005` | 확정 | ADMIN은 이벤트 대상자 제한만 우회한다. | ADMIN도 이벤트 존재, 활성, 시간 범위, 국가 기반 언어 조건을 통과해야 한다. ADMIN 우회가 배너 자체 조건을 우회하지 않는다. | `P1-T2`, `P1-T3` |
|
||||
| `REC-EVT-006` | 확정 | 이벤트 정책은 기존 배너 정책과 교집합으로 적용한다. | banner-own 활성, tab, language, reservation, 성인 노출 가능 여부, creator-block 조건이 모두 기존대로 유지된다. 이벤트가 부적격이면 배너를 제외하고 기존 정렬 후 limit 20 적용 순서 및 동순위 random tie 동작을 유지한다. | `P1-T2`, `P1-T3` |
|
||||
| `REC-EVT-007` | 확정 | 이벤트 정책 필터링은 개별 후보에 적용한다. | 부적격 EVENT 배너 하나 때문에 전체 API가 실패하지 않는다. 모든 후보 제외 시 빈 배열을 반환한다. LINK 및 subBanner 결과는 이 필터로 변경되지 않는다. | `P1-T2`, `P1-T3` |
|
||||
|
||||
## 8. API 계약
|
||||
|
||||
| 요구사항 | Method | Path | 응답 계약 |
|
||||
|---|---|---|---|
|
||||
| `REC-AI-001`, `REC-EVT-001~007` | GET | `/api/v2/home/recommendations` | 기존 aggregate schema 유지. `aiCharacters`는 `[]`; `banners`에는 기존 조건과 이벤트 정책을 모두 통과한 배너만 포함 |
|
||||
| `REC-AI-002`, `REC-EVT-001~007` | GET | `/api/v2/audio/recommendations` | 기존 `AudioRecommendationsResponse` schema 유지. `aiCharacters` 필드 없음. 기존 배너 중 이벤트 정책 부적격 배너만 제외 |
|
||||
|
||||
응답 envelope, 오류 계약, 인증 공개 범위는 변경하지 않는다. 이벤트 대상자 판단에 필요한 이벤트 정보가 없거나 정책을 만족하지 않으면 해당 배너를 노출하지 않는다. 기존 상세 조회 endpoint의 오류 응답을 추천 API 오류로 전파하지 않는다.
|
||||
|
||||
## 9. 데이터 및 정책 규칙
|
||||
|
||||
- 배너별 기존 필터는 유지하며, 이벤트 필터는 EVENT 유형에만 적용한다.
|
||||
- 이벤트 상세 정책은 이벤트 존재, `isActive`, `startDate <= now <= endDate`, 해석 국가의 언어, 인증 상태에 따른 성인 대상 검사를 뜻한다.
|
||||
- 국가 해석은 `MemberContentPreferenceService.resolveCountryCode(member)`를 기준으로 한다. 요청 언어와 별개다.
|
||||
- 이벤트 `isAdult == null`은 인증 여부와 관계없이 허용한다. `true`와 `false`는 인증 기록의 존재 여부와 정확히 일치해야 한다.
|
||||
- 기존 배너 자체 필터에는 preference·country·auth 규칙으로 계산되는 `canViewAdultContent` 성인 노출 가능 여부를 그대로 사용한다. 이는 raw preference나 이벤트 인증 판정과 동일하지 않으며 이벤트 인증 검사를 대체하지 않는다.
|
||||
- 상세 조회와 같은 시간 비교 의미를 사용하되 전역 시간대·formatter 및 기존 banner reservation 시간 정밀도 의미는 변경하지 않는다.
|
||||
- 기존 배너 정렬과 무작위 동순위 처리를 보존하고 적격 결과에 현재 limit 20을 적용한다.
|
||||
|
||||
## 10. 보안 및 데이터 취급
|
||||
|
||||
새 인증 경로나 저장 값은 만들지 않는다. 이벤트 공개 판정은 기존 `AuthRepository` 기록을 사용한다. 익명 조회의 인증 상태는 false로 취급한다. 추천 응답에 이벤트 상세 오류나 다른 사용자의 민감 데이터를 추가하지 않는다.
|
||||
|
||||
## 11. 품질 및 검증 기준
|
||||
|
||||
- 이벤트의 시작/종료 정확한 경계, 한쪽이라도 범위를 벗어난 경우를 결정적으로 검증한다.
|
||||
- 인증 상태와 성인 선호 설정이 서로 다른 경우를 포함해 대상자 정책을 검증한다. 예: 비인증 JP 회원이 성인 콘텐츠 선호를 켠 경우와 인증 회원이 이를 끈 경우에도 이벤트 인증 판정은 Auth 기록을 따른다.
|
||||
- JP/비JP와 request `LangContext` 불일치를 검증한다.
|
||||
- EVENT 부적격 배너가 제거되면서 정상 배너, 순서, limit 20, 빈 목록 동작이 유지되는지 확인한다.
|
||||
- 홈 aggregate AI query/fallback 비호출, 모든 viewer의 빈 배열, audio schema 불변을 검증한다.
|
||||
- 구현 시 focused 테스트와 영향 범위 회귀 테스트를 수행한다. 이 PRD 작성 자체는 문서 검증만 대상으로 한다.
|
||||
|
||||
## 12. 성공 기준
|
||||
|
||||
### 12.1 기능 수용 기준
|
||||
|
||||
- [x] `REC-AI-001`: 모든 viewer의 홈 응답에 `aiCharacters: []`가 존재하며 aggregate AI query/fallback은 호출되지 않는다.
|
||||
- [x] `REC-AI-002`: 오디오 추천 schema에 AI 캐릭터 필드가 추가되지 않는다.
|
||||
- [x] `REC-EVT-001~005`: 이벤트 존재, 활성, 포함 경계 시간, 국가 언어, 인증 대상자 규칙이 검증된다.
|
||||
- [x] `REC-EVT-006~007`: 기존 배너 조건 교집합, limit/order 보존, 개별 제외 및 빈 배열이 검증된다.
|
||||
- [x] 배너 자체 성인 선호와 이벤트 인증 여부가 다른 조합에서도 각자의 조건이 독립적으로 적용된다.
|
||||
|
||||
### 12.2 추적성 완료 기준
|
||||
|
||||
- [x] 모든 확정 요구사항이 `plan-task.md`의 구현 Task와 자동 검증 증거에 연결된다.
|
||||
- [x] 기존 API schema, subBanner, LINK 및 상세 endpoint 비변경이 검증된다.
|
||||
- [x] 문서 간 정합성과 diff 공백 검사가 완료된다.
|
||||
|
||||
## 13. Open Questions
|
||||
|
||||
없음. 확정 요구사항의 최종 모호성은 0.00이다.
|
||||
|
||||
요구사항 명확성 자체 평가: Goal 1.00 · Scope 1.00 · Constraints 1.00 · Success 1.00 · Context 1.00.
|
||||
|
||||
## 14. 요구사항 추적표
|
||||
|
||||
| 요구사항 범위 | API | 계획 Goal | 자동 검증 | 수동 검증 |
|
||||
|---|---|---|---|---|
|
||||
| `REC-AI-001` | 홈 추천 | `P1-T1` | aggregate 응답/호출 검증 | 홈 aggregate와 전용 목록 비교 |
|
||||
| `REC-AI-002` | 오디오 추천 | `P1-T3`, `P1-GATE` | audio DTO 계약 검증 | 오디오 endpoint 응답에서 필드 부재 확인 |
|
||||
| `REC-EVT-001~007` | 홈·오디오 추천 | `P1-T2`, `P1-T3`, `P1-GATE` | 양쪽 repository 및 API 정책 회귀 검증 | viewer/국가/경계 조합별 응답 확인 |
|
||||
|
||||
## 15. Decision Log
|
||||
|
||||
### 15.1 사용자 결정
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항 |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-10-31 | `UD-001` | 확정 | 홈 aggregate 응답의 `aiCharacters`는 빈 배열로 둔다. null로 만들거나 필드를 생략하지 않는다. | 사용자 확인 | `REC-AI-001` |
|
||||
| 2026-10-31 | `UD-002` | 확정 | 이벤트 배너에도 기존 이벤트 상세 조회의 노출 정책을 적용한다. | 사용자 확인 | `REC-EVT-001~007` |
|
||||
|
||||
### 15.2 최소 기술 범위 선택
|
||||
|
||||
아래는 구현 범위 선택이며 사용자 승인으로 인용하지 않는다.
|
||||
|
||||
| 날짜 | ID | 상태 | 선택 | 근거 | 영향 요구사항·범위 |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-10-31 | `TD-001` | 범위 확정 | `AudioRecommendationsResponse`에 현재 없는 AI 필드를 만들지 않는다. 두 추천 endpoint의 배너 정책만 다루고 기존 스키마를 유지한다. | 현행 DTO와 API 계약 확인 | `REC-AI-002`, `REC-EVT-*` |
|
||||
| 2026-10-31 | `TD-002` | 범위 확정 | 이벤트 조건은 각 추천 배너 후보의 필터로 적용하고 부적격 배너만 제외한다. 기존 배너 조건과 결합하며 정렬·limit 전 적격성을 판정한다. | 기존 추천 쿼리의 배너 조건 및 limit 흐름 | `REC-EVT-006~007` |
|
||||
| 2026-10-31 | `TD-003` | 제외 | 전용 AI endpoint, AI snapshot/scheduler/data, LINK URL 파싱, subBanner, legacy endpoint, 상세 endpoint 오류·응답, 보안 설정, DB 및 dependency 변경은 이 문서의 구현 대상으로 삼지 않는다. | 요청 범위와 독립된 기존 동작 보호 | Non-Goals |
|
||||
| 2026-10-31 | `TD-004` | 보존 | 이벤트 상세 시간 비교 의미만 따른다. 전역 timezone/formatter와 기존 배너 reservation 정밀도는 바꾸지 않는다. | 상세 서비스 비교식 및 프로젝트 시간 규칙 | `REC-EVT-002`, `REC-EVT-006` |
|
||||
|
||||
## 16. 근거 및 관련 문서
|
||||
|
||||
### 16.1 저장소 근거
|
||||
|
||||
- `docs/sample/sample-prd.md`, `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`: PRD 형식, 사전 문서 절차, 문서 전용 검증.
|
||||
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/event/application/EventDetailService.kt`: 이벤트 존재, 활성·기간·언어, 관리자 대상 예외와 인증 기록 판정.
|
||||
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt`: aggregate 조립, AI query/fallback, 배너 limit 20.
|
||||
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`: 기존 추천 배너 조건과 정렬/limit.
|
||||
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponse.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/dto/AudioRecommendationsResponse.kt`: 현재 응답 필드와 오디오 응답의 AI 필드 부재.
|
||||
- `src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBanner.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthRepository.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceService.kt`: 배너 속성, 인증 기록, 국가 해석 계약.
|
||||
|
||||
### 16.2 관련 작업 문서
|
||||
|
||||
- `docs/20261028_일반이벤트상세조회/prd.md`
|
||||
- `docs/20261028_일반이벤트상세조회/plan-task.md`
|
||||
- `docs/20261027_콘텐츠배너예약기간/prd.md`
|
||||
- `docs/20261027_콘텐츠배너예약기간/plan-task.md`
|
||||
- `docs/20260819_추천탭_배너_접속국가별_언어필터/prd.md`
|
||||
- `docs/20260819_추천탭_배너_접속국가별_언어필터/plan-task.md`
|
||||
|
||||
## 17. 문서 검증 기록
|
||||
|
||||
- 2026-10-31: PRD 샘플, 작업 절차, 문서 유지보수 가이드 및 관련 구현 근거를 대조했다.
|
||||
- PRD 독립 검토 결과: 중요한 미결정 사항 및 명확한 소스 모순 없음. 구현 계획은 별도 자체 검토로 요구사항·파일·테스트·Goal 연결을 대조했다.
|
||||
- 두 신규 문서를 각각 `git diff --no-index --check /dev/null`의 비교 대상으로 검사했고 공백 오류 출력이 없었다. 최종 재확인한 종료 코드는 신규 파일 차이를 뜻하는 1이며, 이전의 종료 코드 0 기록은 정정한다. staging 후 `git diff --cached --check`로 별도 확인한다.
|
||||
- 구현 테스트와 Gradle 빌드·테스트·lint는 실행하지 않았다. 문서 전용 변경이므로 실행 검증은 구현 단계에 남기며 기능 수용 기준은 미완료로 유지한다.
|
||||
|
||||
### 구현 후 검증 — 2026-10-31
|
||||
|
||||
- 위 문서 전용 기록 이후 사용자 승인으로 구현했다. 제품 커밋은 `4a6b2ee9`, `1f24a4b7`, `969f999a`이며 개별 TDD·리뷰 기록은 `plan-task.md`를 따른다.
|
||||
- 실제 JWT 및 HTTP 검증은 [http-qa.md](http-qa.md): 104개 요청·216개 판정 모두 PASS.
|
||||
- `JAVA_TOOL_OPTIONS=-Duser.timezone=UTC ./gradlew build`는 16분 34초에 종료 코드 0. 전체 테스트 2891개, failures/errors/skipped 0이며 lint와 bootJar 산출물도 확인했다.
|
||||
- 최종 독립 구현 리뷰는 명세·품질 승인, 차단 사항 없음이다. 기본 KST의 월말 기존 후원 이름 테스트 실패는 수정 범위 밖으로 남겼고 UTC 검증을 기본 환경 성공으로 확대하지 않는다.
|
||||
- 운영 배포·운영 DB 정밀도 및 도구가 없는 LSP 진단은 수행하지 않았다. 운영/저장소 시간대 설정은 변경하지 않았다.
|
||||
Reference in New Issue
Block a user