docs(banner): 언어 필터 계약과 검증 결과를 기록한다

This commit is contained in:
2026-11-03 00:33:39 +09:00
parent c4c61e3615
commit 5666802f61
8 changed files with 262 additions and 29 deletions
@@ -4,7 +4,7 @@
**Goal:** 관리자가 언어를 지정해 이벤트를 등록·식별하고, 앱 이벤트 목록과 팝업이 기존 국가 판정에 맞는 언어만 반환하게 한다.
**Architecture:** 기존 `Lang`과 `MemberContentPreferenceService.resolveCountryCode(member)`를 재사용한다. `EventRepository`에 nullable `lang` 조건을 추가해 두 앱 API는 KO/JA를 전달하고, 관리자 목록과 콘텐츠 메인 탭의 기존 내부 조회는 null을 전달해 언어 전체 조회를 유지한다.
**Architecture:** 기존 `Lang`과 `MemberContentPreferenceService.resolveCountryCode(member)`를 재사용한다. 앱 목록·팝업은 KO/JA를 조회하고, Phase 3에서 콘텐츠 메인 탭 6곳도 회원 국가 판정에 따른 필수 언어를 조회한다. 관리자 목록은 언어 전체 조회를 유지한다.
**Tech Stack:** Kotlin, Java 17, Spring Boot 2.7.14, JPA/QueryDSL, JUnit 5, Mockito, MockMvc, Gradle Wrapper, MySQL
@@ -12,11 +12,11 @@
| 문서 항목 | 내용 |
|---|---|
| 상태 | 구현 및 회귀 수정 완료 |
| 상태 | Phase 3 후속 언어 필터 구현·전체 검증·독립 리뷰 완료, 미배포 |
| 작성일 | 2026-08-19 |
| 요구사항 기준 | `docs/20260819_이벤트_접속국가별_언어필터/prd.md` |
| API 기준 | PRD 8절 |
| 현재 Phase | Phase 2 완료 |
| 현재 Phase | Phase 1~3 완료 |
| 현재 활성 Goal | 없음 |
| 다음 Goal | 없음 |
@@ -27,9 +27,9 @@
- 이벤트 수정 API와 레거시 `POST /event`의 요청 계약은 변경하지 않는다.
- 기존 이벤트와 레거시 등록의 기본 언어는 `KO`다.
- 관리자 목록은 기존 활성·종료 시각 조건을 유지하고 언어 조건을 추가하지 않는다.
- `GET /event`, `GET /event/popup`만 `JP -> Lang.JA`, 그 외 `Lang.KO`를 사용한다.
- `GET /event`, `GET /event/popup`과 Phase 3의 메인 탭 이벤트 배너는 `JP -> Lang.JA`, 그 외 `Lang.KO`를 사용한다.
- 로그인 회원은 기존 강제 KR/JP 매핑을 유지하고, 비로그인·국가 누락은 기존 정규화·KR 기본값을 사용한다.
- 콘텐츠 메인 탭 6곳의 `EventService.getEventList(isAdult)`는 언어 필터 없이 유지한다.
- [과거 기준, `DEC-009`에서 정정] 콘텐츠 메인 탭 6곳은 원래 언어 필터 없이 조회했으나 이번 후속 Phase에서 국가별 언어 필터를 적용한다. 과거 P2 구현·검증 기록은 당시 상태로 보존한다.
- 언어 필터는 QueryDSL `where`에서 정렬·`fetchFirst()` 전에 적용하고 메모리 후처리를 추가하지 않는다.
- 신규 dependency·cache·resolver abstraction·관련 없는 refactoring을 추가하지 않는다.
- 모든 구현 Task는 RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR 순서를 지킨다.
@@ -43,6 +43,7 @@
|---:|---|---:|---|---|
| 1 | 완료 | `3/3` | 없음 | 없음 |
| 2 | 완료 | `2/2` + Gate + `P2-R1` | 없음 | 없음 |
| 3 | 완료 | `1/1` + Gate | 없음 | 운영 배포 제외 |
- 동시에 하나의 미완료 goal만 운용한다.
- 구현 완료 즉시 해당 Task 체크박스와 현재 상태를 갱신한다.
@@ -58,13 +59,13 @@
- 관리자 이벤트 목록 `lang` 응답과 언어 전체 조회
- 앱 이벤트 목록·팝업의 접속 국가별 KO/JA 필터
- 기존 강제 국가 매핑과 비로그인 KR 기본값 재사용
- 콘텐츠 메인 탭 6곳의 언어 전체 조회 비회귀
- P2 당시 콘텐츠 메인 탭 6곳의 언어 전체 조회 비회귀 기록. `DEC-009`로 후속 언어 필터가 승인되어 현재 Phase 3에서 대체한다.
### 제외
- 이벤트 수정 시 언어 변경
- 레거시 `POST /event`의 `lang` request parameter
- 콘텐츠 메인 탭 6곳의 국가별 이벤트 필터
- 과거 제외 범위: 콘텐츠 메인 탭 6곳의 국가별 이벤트 필터. `DEC-009` 후속 승인으로 현재 범위에 포함하며 과거 체크박스·검증은 당시 이력으로 유지한다.
- EN 이벤트를 반환할 앱 국가 정책
- 다른 언어 fallback, `Accept-Language` 판정, 신규 dependency·공통 abstraction
- 기존 이벤트 성인·활성·게시 기간·정렬·URL 정책 변경
@@ -82,6 +83,7 @@
| `EVENT-LANG-007` | `P2-T2` | `lang = null`의 KO/EN/JA 전체 조회 mutation RED·GREEN |
| `EVENT-LANG-008` | `P1-T1`, `P1-GATE` | KO 기본 저장 테스트와 KO backfill DDL 대조 |
| `EVENT-LANG-009` | `P2-T1`, `P2-GATE` | QueryDSL `where` 조건과 정렬·`fetchFirst()` 선행 검증 |
| `EVENT-LANG-010` | `P3-T1`, `P3-GATE` | 여섯 메인 탭의 JP→JA/그 외→KO HTTP·service 검증 및 기존 `isAdult` 계산 비회귀 |
## 파일 구조
@@ -832,3 +834,67 @@ P1-T1 → P1-T2 → P1-GATE → P2-T1 → P2-T2 → P2-GATE → P2-R1
- 전체 회귀를 실행하지 않았으면 생략 근거와 대신 실행한 focused·직접 영향 명령을 적는다.
- 남은 항목과 갱신한 PRD·plan·review 경로를 적는다.
- 성공을 추정하지 않고 실제 실행한 최신 검증 결과와 완료되지 않은 범위를 함께 전달한다.
## Phase 3: 콘텐츠 메인 탭 이벤트 언어 필터 후속
2026-11-02 승인에 따라 과거 `EVENT-LANG-007`의 언어 미필터 요구를 정정한다. P2 완료 체크박스와 검증 기록은 당시 요구 범위의 이력이며 되돌리지 않는다.
### Task 3.1 여섯 메인 탭의 국가별 이벤트 언어 조회
**Goal `P3-T1`:** 콘텐츠 메인 탭 여섯 `eventBannerList`가 기존 회원 국가 판정에 따른 JA 또는 KO 이벤트만 조회하고 기존 성인 계산을 유지한다.
- 시작 조건: `DEC-009`, `EVENT-LANG-010` 승인.
- 완료 증거: 기준 테스트의 사전 통과 기록, RED에서 국가별 언어·기존 성인 조건 assertion 실패, GREEN focused 통과, 실제 HTTP 경로 검증, 전체 회귀·ktlint·bootJar 및 독립 리뷰 결과.
- 범위 밖: 이벤트 상세·팝업 정책, 메인 탭 응답 DTO/URL 변경, 링크·이벤트 파싱, 새 dependency, 운영 DDL 실행, UI 및 기존 성인 계산 변경.
- **Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt`
- Verify/reuse (변경 불필요): `src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/event/EventServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/event/EventRepositoryTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/event/EventBannerLanguageIntegrationTest.kt` (new; actual HTTP coverage for the six main-tab endpoints)
- Baseline/related HTTP regression: `src/test/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeServiceIntegrationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/event/EventControllerIntegrationTest.kt`
- **Interfaces:** reuse `MemberContentPreferenceService.resolveCountryCode(member)` and existing `Lang`; each current caller supplies member-derived language while retaining its existing `isAdult` value. Repository language is non-null on these six paths; existing low-level callers without requested filtering retain their nullable contract. No public response field is added.
- [x] **Baseline:** 부모의 사전 통과 113개 중 기존 `EventServiceTest`, `EventRepositoryTest`, `EventControllerIntegrationTest`, `AudioContentMainTabHomeServiceIntegrationTest` 15개를 재사용했다. 원본: `/tmp/soda-banner-ulw.HDkpg4/baseline.log`.
- [x] **RED:** 여섯 실제 HTTP 경로가 실제 EventService/Repository를 사용하도록 mixed KO/JA/EN 및 성인 true/false/null 행을 추가했다. 국가별 언어와 기존 성인 조건을 응답 ID로 검증한다.
- [x] **RED 확인:** 신규 HTTP 7개 모두 언어 미필터 assertion으로 실패했다. 컴파일·설정 실패가 아니다. `/tmp/soda-banner-impl.CfAkhD/event-red.log`, `event-red.xml`.
- [x] **GREEN:** 여섯 기존 호출부에 member 국가로 선택한 `Lang`만 추가했다. 기존 성인 계산식·EventService/Repository production code·공개 DTO는 변경하지 않았다.
- [x] **GREEN 확인:** focused 22개가 실패·오류·무시 0으로 통과했다. JP/KR/US/누락, 성인 설정 false/true, 강제 JP/KR 회원 및 익명 홈을 실제 HTTP 64건으로 확인했다.
- [x] **REFACTOR:** 전체 2,928개 테스트, 공백 수정 후 영향 26개 HTTP 테스트·전체 ktlint·bootJar 및 독립 리뷰를 확인했다. 운영 DDL은 실행하지 않았다.
```bash
export TMPDIR=/tmp/soda-banner-impl.CfAkhD NODE_COMPILE_CACHE=/tmp/soda-banner-impl.CfAkhD/node-compile-cache BUN_RUNTIME_TRANSPILER_CACHE_PATH=/tmp/soda-banner-impl.CfAkhD/bun-transpiler
./gradlew test --tests 'kr.co.vividnext.sodalive.event.EventServiceTest' --tests 'kr.co.vividnext.sodalive.event.EventRepositoryTest' --tests 'kr.co.vividnext.sodalive.event.EventBannerLanguageIntegrationTest' --tests 'kr.co.vividnext.sodalive.event.EventControllerIntegrationTest' --tests 'kr.co.vividnext.sodalive.content.main.tab.home.AudioContentMainTabHomeServiceIntegrationTest' --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process
./gradlew test ktlintCheck bootJar --continue --console=plain
```
**Expected:** focused tests exercise real country-to-language and HTTP paths; full suite, lint, and bootJar exit 0. LSP diagnostics are unavailable in this environment and must be reported as not run, not as passing.
### Phase 3 Gate `P3-GATE`
- 시작 조건: `P3-T1` 완료.
- 완료 증거: six-caller diff confirms preserved `isAdult`, real HTTP tests show country language filtering, focused/full tests and lint/build pass, independent review has no unresolved blocker.
- 범위 밖: UI, production DB/DDL execution, event detail/adult policy changes.
- [x] 여섯 호출부·반환 언어·공개 DTO 불변을 확인하고 P1/P2 이력을 보존했다. 전체 검증 및 최종 Oracle 승인으로 Gate를 완료했다.
### Phase 3 구현 Progress, 2026-11-02
- 현재 `test` 브랜치/HEAD `7fce26ad`에서 구현했으며 worktree·commit·stage·push는 하지 않았다. 기존 P1/P2 기록과 부모가 승인한 PRD 변경은 보존했다.
- RED 명령은 위 focused 명령에서 `--tests 'kr.co.vividnext.sodalive.event.EventBannerLanguageIntegrationTest'` 하나만 선택했다. JP의 예상 2개 대신 KO/EN까지 6개가 반환되는 assertion 실패 7건을 확인했다. 로그 `event-red.log`, 원본 HTTP/XML `event-red.xml`은 모두 `/tmp/soda-banner-impl.CfAkhD/` 아래에 있다.
- GREEN은 위 focused 명령이며 `event-green-retry.log`에 `BUILD SUCCESSFUL in 4m 15s`가 남는다. Service 4, Repository 5, 기존 Controller 5, 신규 Banner HTTP 7, 기존 Home HTTP 1로 총 22개가 통과했다. 첫 `event-green.log` 실행은 test compilation 중 600초 도구 제한으로 종료되어 통과 근거로 사용하지 않았다. 프로세스 종료 확인 후 제한을 늘려 순차 재실행했다.
- 실제 JWT/랜덤 포트/H2 Event 행/기존 resolver를 사용했다. 여섯 경로 각각 10개 국가·성인·강제 매핑 조합과 익명 홈 4개를 확인했고 `Accept-Language: en`에도 국가 기준 언어가 선택됐다. `event-http-evidence.log`의 64건과 `event-green.xml`에 실제 상태·본문이 남으며 토큰은 출력하지 않았다.
- 구현 및 focused/HTTP 범위만 완료했다. 사용자 지정 종료 경계에 따라 전체 suite·ktlint·bootJar·독립 리뷰와 `P3-GATE`는 부모 작업으로 미완료 유지한다. LSP 진단 도구는 없어 실행하지 않았다. 구현 보고서: `/tmp/soda-banner-impl.CfAkhD/implementation-report.md`.
- 최종 source 상태에서 위 5개 클래스와 Phase 7의 6개 클래스를 하나의 focused 명령으로 순차 재검증했다. `final-focused.log`: `BUILD SUCCESSFUL in 8m 8s`, 11개 클래스 145개, 실패·오류·무시 0. 최종 원본은 같은 root의 `final-TEST-*.xml`, 실제 HTTP 본문은 `final-http-db-evidence.log`다. 전체 suite 실행은 아니다.
### Phase 3 최종 검증 Progress, 2026-11-03
- 부모 실행: `./gradlew test ktlintCheck bootJar --continue --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`. 전체 XML 집계는 336 suites / 2,928 tests / failures=0 / errors=0 / skipped=0이며 `bootJar`도 성공했다. 최초 명령은 서브 배너 테스트 팩토리 인자 줄바꿈 한 곳의 ktlint 오류로 exit 1 (15m 50s)이었다. 테스트 실패나 제품 코드 오류는 아니었다.
- 해당 테스트 인자 여섯 개를 값·순서 변경 없이 줄별로 배치한 뒤 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subbanner.adapter.in.web.RecommendationSubBannerIntegrationTest' ktlintCheck bootJar --continue --console=plain --no-daemon -Pkotlin.compiler.execution.strategy=in-process`를 실행했다. exit 0, `BUILD SUCCESSFUL in 4m 32s`, 26 tests / failures=0 / errors=0 / skipped=0, 전체 lint 통과, bootJar UP-TO-DATE다.
- 전체 회귀 후 변경은 테스트의 공백뿐이므로 전체 2,928개를 다시 실행하지 않고 영향받은 26개 HTTP 시나리오를 재실행했다. 최신 `build/test-results/test`와 HTML은 이 26개 실행으로 갱신됐다. 전체 집계는 갱신 전 부모가 직접 확인한 기록이다.
- 부모는 전체 실행으로 실제 HTTP 테스트를 다시 구동하고 두 추천의 JP/KR/US/누락·강제 국가, 기간·소프트 삭제·공개 6필드 재검증 결과를 확인했다. 제품 JAR: `build/libs/sodalive-0.0.1-SNAPSHOT.jar`.
- LSP 도구 부재로 미실행이며 Gradle Kotlin compile 및 ktlint를 대체 진단으로 사용했다. 기존 무관 파일의 컴파일/Gradle 경고와 H2 scheduler의 MySQL 문법 로그는 테스트 실패와 구분하며 수정하지 않았다.
- 원본 로그는 실행 당시 `/tmp/soda-banner-ulw.HDkpg4/full-verification.log`, `final-verification.log`에 보관했다. 소유 임시 자료는 종료 시 정리하고 핵심 결과는 본 기록과 [공동 리뷰](../20261029_가로형서브배너/reviews/banner-language-review.md)에 보존한다.
- 독립 Oracle 리뷰는 명세·코드·시나리오를 승인했고, 후속 공백 delta와 최종 검증 증거도 APPROVE했다. 미해결 발견 사항 없음. `P3-T1/P3-GATE` 완료이며 운영 배포는 별도다.
- 메인 탭 구현·테스트 커밋 `255da2b6`, hook/메시지 검사 통과. 소유 QA 프로세스와 임시 경로 정리는 공동 리뷰의 종료 증거에 기록했다. 운영 배포·push는 하지 않았다.