16 KiB
16 KiB
이벤트 접속 국가별 언어 필터 PRD
문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 기준 확정 |
| 작성일 | 2026-08-19 |
| 최종 수정일 | 2026-08-20 |
| 대상 제품 | 관리자 이벤트 배너, 앱 이벤트 목록·팝업 |
| 작성자·결정권자 | 사용자 |
| 관련 API Contract | 별도 문서 없음. 이 문서 8절을 기준으로 사용 |
| 관련 구현 계획 | docs/20260819_이벤트_접속국가별_언어필터/plan-task.md |
| 관련 기존 문서 | docs/20260819_추천탭_배너_접속국가별_언어필터/prd.md |
1. Overview
관리자가 이벤트를 등록할 때 기존 콘텐츠 배너처럼 언어를 지정한다. 관리자 목록은 언어로 필터링하지 않고 각 이벤트의 언어를 표시한다. 앱의 GET /event, GET /event/popup은 기존 접속 국가 판정 결과가 JP이면 일본어, 그 외에는 한국어 이벤트만 반환한다.
2. Problem Statement
Event에는 언어 필드가 없어 관리자가 한국어·일본어 이벤트를 구분해 등록할 수 없다.- 앱 이벤트 목록과 팝업 조회는 접속 국가를 고려하지 않아 여러 언어의 이벤트가 섞일 수 있다.
- 팝업을 조회한 뒤 메모리에서 언어를 걸러내면 다른 언어의 최신 팝업이
fetchFirst()를 차지해 필요한 팝업을 놓칠 수 있다.
문제를 해결했다는 판단은 관리자가 허용된 언어로 이벤트를 등록·식별할 수 있고, 두 앱 API가 기존 조회 조건을 유지하면서 DB 조회 단계에서 판정 언어만 반환하는 것으로 한다.
3. Goals
- 관리자는 이벤트 등록 시
KO,EN,JA중 하나를 필수로 지정한다. - 관리자 이벤트 목록은 기존 활성·종료 시각 조건 안에서 모든 언어를 반환하고
lang을 포함한다. - 기존
MemberContentPreferenceService.resolveCountryCode(member)로 강제 KR/JP 회원 매핑, 요청 국가 정규화, 누락 시 KR 기본값을 그대로 재사용한다. - 판정 국가가
JP이면Lang.JA, 그 외에는Lang.KO를 선택한다. - 언어 조건을 QueryDSL
where에 적용해 정렬과fetchFirst()보다 먼저 필터링한다. - 기존 이벤트 데이터와 레거시 등록 경로의 기본 언어는
KO로 유지한다.
4. Non-Goals
- 이벤트 수정 API에서
lang을 변경하지 않는다. EventController.createEvent의POST /event요청 계약을 변경하지 않는다.- 콘텐츠 메인 탭 6곳이
EventService.getEventList(isAdult)로 조립하는eventBannerList에 언어 필터를 적용하지 않는다. Lang에 새 enum을 추가하거나EN을 노출할 접속 국가 정책을 추가하지 않는다.Accept-Language를 이벤트 언어 판정에 사용하지 않는다.- 요청 언어의 이벤트가 없을 때 다른 언어로 fallback하지 않는다.
- 기존 이벤트의 성인·활성·시작·종료·정렬 정책과 응답 이미지 URL 처리를 변경하지 않는다.
5. 대상 사용자와 권한
| 사용자 | 주요 동작 | 국가·언어 정책 |
|---|---|---|
| 관리자 | 이벤트 등록, 언어 전체 목록 조회 | 등록 시 KO/EN/JA 중 하나를 지정, 목록에서 lang 확인 |
| 강제 JP 매핑 회원 | 앱 목록·팝업 조회 | 요청 헤더보다 우선하는 JP 판정, JA 조회 |
| 강제 KR 매핑 회원 | 앱 목록·팝업 조회 | 요청 헤더보다 우선하는 KR 판정, KO 조회 |
| 일반 로그인 회원 | 앱 목록·팝업 조회 | 정규화한 CloudFront-Viewer-Country가 JP이면 JA, 그 외 KO |
| 비로그인 사용자 | 앱 목록 조회 | 정규화한 요청 국가가 JP이면 JA, 누락·그 외 KO |
- 관리자 이벤트 API의 기존
ROLE_ADMIN인가를 유지한다. - 앱 이벤트 목록은 기존 비로그인 접근을 유지하고, 팝업은 기존처럼 인증 사용자만 접근한다.
6. 핵심 흐름
6.1 관리자 등록·조회
- 관리자가
POST /admin/event/bannermultipart 요청에lang을 필수로 보낸다. - 서버는 기존
Lang.fromCode규칙으로ko/KO,en/EN,ja/JA를 처리한다. - 허용되지 않는 값은
common.error.invalid_request로 거부하고 이벤트를 저장하지 않는다. GET /admin/event/banner는 기존처럼 활성 상태이고 종료 시각이 지나지 않은 이벤트를 언어 조건 없이 반환한다.- 각 관리자 응답 항목은
lang을 포함한다.
6.2 앱 목록·팝업 조회
CountryInterceptor가CloudFront-Viewer-Country를CountryContext에 저장한다.EventController는 인증 사용자Member?를EventService에 전달한다.EventService는MemberContentPreferenceService.resolveCountryCode(member)를 호출한다.- 국가 결과가
JP이면Lang.JA, 그 외에는Lang.KO를 선택한다. EventRepository는 기존 성인·활성·게시 기간 조건과 언어 조건을 DB에서 함께 적용한다.- 해당 언어의 이벤트가 없으면 목록은 빈
eventList, 팝업은null을 반환한다.
6.3 기존 내부 조회
- 콘텐츠 메인 탭 6곳은 기존
EventService.getEventList(isAdult)를 유지한다. - 이 경로는 Repository에
lang = null을 전달해 언어 조건을 추가하지 않는다. - 기존 응답 조립 범위와 순서를 변경하지 않는다.
7. 기능 요구사항
| ID | 상태 | 요구사항 | 수용 기준 | Goal 연결 |
|---|---|---|---|---|
EVENT-LANG-001 |
확정 | 관리자 이벤트 등록은 lang을 필수로 받는다. |
KO, EN, JA가 저장되고 누락·잘못된 값은 저장 전 거부된다. |
P1-T2 |
EVENT-LANG-002 |
확정 | 이벤트 언어는 등록 후 수정하지 않는다. | 두 수정 API의 요청·서비스 시그니처에 lang이 추가되지 않는다. |
P1-GATE |
EVENT-LANG-003 |
확정 | 관리자 목록은 모든 언어를 조회하고 lang을 반환한다. |
기존 활성·종료 시각 조건을 만족하는 KO/EN/JA 항목이 언어 필터 없이 반환되고 각 항목에 lang이 있다. |
P1-T2 |
EVENT-LANG-004 |
확정 | GET /event는 판정 국가가 JP이면 JA, 그 외에는 KO 이벤트만 반환한다. |
서로 다른 언어의 활성 이벤트가 함께 있어도 판정 언어만 eventList에 있다. |
P2-T1 |
EVENT-LANG-005 |
확정 | GET /event/popup은 판정 국가가 JP이면 JA, 그 외에는 KO 팝업만 반환한다. |
다른 언어 팝업의 ID가 더 최신이어도 언어 필터 후 선택된 팝업을 반환한다. | P2-T1 |
EVENT-LANG-006 |
확정 | 로그인 회원은 기존 강제 KR/JP 매핑을 포함한 국가 판정을 사용한다. | 강제 JP 회원은 비JP 헤더에서도 JA, 강제 KR 회원은 JP 헤더에서도 KO를 선택한다. | P2-T1 |
EVENT-LANG-007 |
확정 | 콘텐츠 메인 탭의 기존 eventBannerList는 언어 필터 없이 조회한다. |
getEventList(isAdult) 경로가 lang = null을 유지하고 KO/JA/EN 모두를 조회할 수 있다. |
P2-T2 |
EVENT-LANG-008 |
확정 | 기존 이벤트와 레거시 등록 경로의 언어는 KO다. | 기존 row를 KO로 backfill하고 NOT NULL DEFAULT 'KO'를 적용하며 POST /event에 lang을 추가하지 않는다. |
P1-T1 |
EVENT-LANG-009 |
확정 | 앱 언어 필터는 DB 조회 조건으로 적용한다. | QueryDSL where 내 event.lang.eq(lang)이 정렬·fetchFirst() 전에 적용된다. |
P2-T1 |
8. API 계약
8.1 관리자 등록
| 항목 | 내용 |
|---|---|
| Method / Path | POST /admin/event/banner |
| 인증 | ROLE_ADMIN |
| Content-Type | multipart/form-data |
| 신규 필수 파라미터 | lang: ko, en, ja 또는 대소문자를 달리한 동일 enum 코드 |
| 잘못된 값 | common.error.invalid_request, 저장·업로드 없음 |
| 기존 파라미터 | 스키마와 필수·선택 정책 유지 |
8.2 관리자 목록
| 항목 | 내용 |
|---|---|
| Method / Path | GET /admin/event/banner |
| 조회 조건 | 기존 is_active = true, end_date >= now; 언어 조건 없음 |
| 응답 변경 | GetAdminEventResponse.lang: Lang 추가 |
| 기존 응답 필드 | 이름·타입·URL 처리 유지 |
8.3 앱 조회
| Method / Path | 변경 내용 | 빈 결과 |
|---|---|---|
GET /event |
기존 응답 스키마를 유지하고 판정 언어 조건만 추가 | eventList = [] |
GET /event/popup |
기존 응답 스키마를 유지하고 판정 언어 조건만 추가 | data = null |
- 신규 request header를 추가하지 않고 기존
CloudFront-Viewer-Country를 사용한다. GET /event는 기존처럼 비로그인 접근을 허용하고,GET /event/popup은 기존 인증 필수 정책을 유지한다.- 앱 응답에
lang을 추가하지 않는다. EN이벤트는 관리자가 등록·조회할 수 있지만 현재 두 앱 API의 국가 정책으로는 노출되지 않는다.
9. 데이터 정책
Event.lang은 기존kr.co.vividnext.sodalive.i18n.Lang을EnumType.STRING으로 저장한다.- 엔티티 기본값은
Lang.KO로 둔다. - 운영 DB DDL은
event.lang VARCHAR(10)을 nullable로 추가한 뒤 기존NULL을KO로 backfill하고NOT NULL DEFAULT 'KO'로 변경한다. - DDL은 두 번 실행해도 이미 적용된 단계를 건너뛸는 기존
information_schema+PREPARE패턴을 따른다. - 기존 row의 언어를 별도로 추론하거나 이미지·제목을 분석해 자동 분류하지 않는다.
10. 성능·품질·보안 요구사항
- 언어 조건은 Repository QueryDSL
where에서 적용하고 메모리 후처리를 추가하지 않는다. - 기존
Lang,MemberContentPreferenceService,CountryContext를 재사용하고 신규 dependency·resolver abstraction을 추가하지 않는다. - 언어 파라미터는
Lang.fromCode로 정규화·검증하고 잘못된 값을 엔티티 생성과 S3 업로드 전에 거부한다. - 민감정보·헤더·파일 본문을 신규 로그에 기록하지 않는다.
- TDD는 관리자 등록·목록, 앱 국가별 목록·팝업, 기존 내부 전체 언어 조회 비회귀를 포함한다.
- focused test 후 직접 영향 회귀와
ktlintCheck를 실행한다. 전체 회귀는 targeted test로 영향 범위를 판단할 수 없거나 공통 경계 회귀가 발생할 때만 확장한다.
11. 성공 기준
- 관리자가
KO,EN,JA이벤트를 등록하고 목록에서 각 언어를 확인한다. (EVENT-LANG-001,EVENT-LANG-003) - 수정 API와 레거시
POST /event의 요청 계약이 변경되지 않는다. (EVENT-LANG-002,EVENT-LANG-008) - 기존 이벤트 모두가 KO로 이관되고 신규 언어 누락 row가 생성되지 않는다. (
EVENT-LANG-008) - 판정 국가 JP에서
GET /event,GET /event/popup이 JA만 반환한다. (EVENT-LANG-004~006) - JP 이외와 국가 누락에서 두 API가 KO만 반환한다. (
EVENT-LANG-004,EVENT-LANG-005) - 다른 언어의 최신 팝업이 있어도 언어 필터 후 선택된 팝업이 반환된다. (
EVENT-LANG-005,EVENT-LANG-009) - 콘텐츠 메인 탭 6곳의 기존
eventBannerList는 언어 필터 없이 조회된다. (EVENT-LANG-007) - 기존 성인·활성·게시 기간·정렬·URL·응답 스키마 정책이 유지된다.
11.1 구현 완료 검증 — 2026-08-20
- 관리자 등록·목록:
AdminEventBannerControllerIntegrationTest4개와EventRepositoryTest의 관리자 전체 언어 조회가 통과했다.Lang.fromCode는 기존KO,EN,JAenum의 code/name을 대소문자 무시로 처리하며, 누락·잘못된 값은 저장·업로드 없이 거부된다. - 레거시 계약: controller·service diff에서 관리자 수정·삭제와
POST /event,PUT /event,DELETE /event/{id}에lang이 추가되지 않았다. - DB 이관:
20260819_event_lang_ddl.sql이 nullable 컬럼 추가 →NULL의 KO backfill →NOT NULL DEFAULT 'KO'순서를 갖고, 엔티티 기본값과 JA/KO 영속성 테스트도 통과했다. 다만 운영 DDL은 실행하지 않았으므로 실제 기존 row 전체 이관 완료는 증명하지 않고 체크하지 않았다. - JP·강제 매핑: security-on 익명 JP 목록과 인증 JP 팝업 통합 테스트,
EventServiceTest의 JP→JA 전달,MemberContentPreferenceIntegrationTest의 강제 JP/KR 매핑이 통과했다. - 비JP·누락: 국가 누락의 익명 KO 목록과 인증 KO 팝업 통합 테스트, 일반 회원의 US·누락 판정과 비로그인 JP 정규화·누락 KR 테스트가 통과했다. service의
JP이외→KO분기도 diff로 확인했다. - 팝업 선필터: 더 최신 KO와 성인 JA 팝업이 있어도 비성인 JA 조회가 대상 JA를 반환하는 Repository 테스트가 통과했고, 언어 predicate가
where에서 정렬·fetchFirst()전에 적용됨을 확인했다. - 콘텐츠 메인: 저수준 service의
lang = null전달과 Repository의 KO/EN/JA 3건 전체 조회 테스트가 통과했다. 6개 production 호출자와 해당 응답 DTO 디렉터리 diff는 없었다. - 기존 정책: Repository 테스트가 활성·시작·종료·성인·ID 내림차순 조건을 함께 검증했고,
EventServiceURL 변환 및GetEventResponse·EventItemdiff가 없음을 확인했다. 구현 리뷰 후 익명 팝업 401과 인증 팝업의 국가별 언어를 함께 검증했고 기준 HEAD 대비SecurityConfigdiff가 없음을 확인했다.
12. Open Questions
- 없음.
13. 요구사항 추적표
| 요구사항 범위 | 계획 Phase | Goal | 자동 검증 |
|---|---|---|---|
EVENT-LANG-001~003, EVENT-LANG-008 |
1 | P1-T1, P1-T2, P1-GATE |
엔티티·관리자 controller/service/repository 통합 테스트 |
EVENT-LANG-004~006, EVENT-LANG-009 |
2 | P2-T1, P2-GATE |
EventServiceTest, EventRepositoryTest, EventControllerIntegrationTest |
EVENT-LANG-007 |
2 | P2-T2, P2-GATE |
언어 없는 기존 서비스 호출과 Repository null 조건 비회귀 테스트 |
14. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·Goal |
|---|---|---|---|---|---|
| 2026-08-19 | DEC-001 |
확정 | 앱 국가 판정은 기존 MemberContentPreferenceService.resolveCountryCode(member)를 사용한다. |
사용자 인터뷰 A | EVENT-LANG-004~006, P2-T1 |
| 2026-08-19 | DEC-002 |
확정 | 등록 언어는 기존 Lang의 KO, EN, JA 모두를 허용한다. |
사용자가 A를 철회하고 B로 확정, 신규 언어 타입·검증 중복 방지 | EVENT-LANG-001, P1-T2 |
| 2026-08-19 | DEC-003 |
확정 | 언어는 등록할 때만 지정하고 수정하지 않는다. | 사용자 인터뷰 A, 콘텐츠 배너의 현재 패턴 | EVENT-LANG-002, P1-GATE |
| 2026-08-19 | DEC-004 |
확정 | 관리자 목록은 언어 전체를 조회하고 lang을 응답한다. |
사용자 인터뷰 A | EVENT-LANG-003, P1-T2 |
| 2026-08-19 | DEC-005 |
확정 | 국가별 필터는 EventController.getEventList, getEventPopup에만 적용한다. |
사용자 인터뷰 A | EVENT-LANG-004, EVENT-LANG-005, EVENT-LANG-007, P2-T1~T2 |
| 2026-08-19 | DEC-006 |
확정 | 기존 이벤트는 KO로 backfill하고 NOT NULL DEFAULT 'KO'를 적용한다. |
사용자 인터뷰 A, 기존 배너 DDL 패턴 | EVENT-LANG-008, P1-T1 |
| 2026-08-19 | DEC-007 |
확정 | 언어 입력은 AdminEventBannerController에만 추가하고 레거시 POST /event는 KO 기본값을 사용한다. |
사용자 인터뷰 A | EVENT-LANG-002, EVENT-LANG-008, P1-T1~T2 |
| 2026-08-20 | DEC-008 |
확정 | GET /event의 익명 접근은 유지하고 GET /event/popup은 기준 HEAD의 인증 필수 정책을 유지한다. |
구현 리뷰에서 언어 필터와 무관한 익명 공개 확장을 확인 | EVENT-LANG-005, P2-R1 |
15. 변경 관리
요구사항이 변경되면 다음 순서로 갱신한다.
- 이 문서의 Decision Log에 변경 이유와 날짜를 추가한다.
- 관련 요구사항·수용 기준·API 계약을 갱신한다.
plan-task.md의 범위·Files·Interfaces·체크박스를 코드 변경 전에 먼저 갱신한다.- 기존 Progress·검증 기록을 삭제하거나 덮어쓰지 않는다.