Files
sodalive-backend-spring-boot/docs/20260819_추천탭_배너_접속국가별_언어필터/plan-task.md

30 KiB

추천 탭 배너 접속 국가별 언어 필터 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

문서 항목 내용
상태 구현 완료
작성일 2026-08-19
요구사항 기준 docs/20260819_추천탭_배너_접속국가별_언어필터/prd.md
API 기준 기존 공개 API 계약 유지
현재 Phase Phase 2: 언어 필터 limit 선행 회귀 테스트 보완 완료
현재 활성 Goal 없음

목표

메인 홈 추천과 메인 콘텐츠 추천에서 로그인 여부와 기존 강제 국가 매핑을 반영해 일본은 일본어, 그 외 국가는 한국어 배너만 조회한다.

Architecture

기존 MemberContentPreferenceService.resolveCountryCode가 비로그인 사용자까지 처리하도록 입력만 확장하고 강제 국가 매핑과 국가 정규화를 재사용한다. 두 추천 application 진입점에서 국가를 Lang.JA 또는 Lang.KO로 변환해 기존 port에 전달하며, 두 QueryDSL Repository가 content_banner.lang을 정렬·limit 전 조회 조건으로 적용한다.

Tech Stack

Kotlin, Java 17, Spring Boot 2.7.14, JPA/QueryDSL, JUnit 5, Mockito, Gradle Wrapper

Global Constraints

  • JPLang.JA, 그 외 국가와 국가 정보 누락은 Lang.KO로 고정한다.
  • 로그인 회원의 기존 강제 KR/JP 매핑은 요청 헤더보다 우선한다.
  • Accept-Language, DB 스키마, 공개 API endpoint/DTO/응답 스키마를 변경하지 않는다.
  • 기존 탭·성인·활성·차단·대상 유효성·정렬·limit 조건을 유지한다.
  • 신규 dependency, 언어 resolver abstraction과 메모리 후처리를 추가하지 않는다.
  • 모든 구현 Task는 RED → GREEN → REFACTOR 순서로 진행한다.

현재 상태

Phase 상태 완료 Task 활성/다음 Goal 차단 또는 남은 조건
1 완료 3/3 없음 없음
2 완료 1/1 없음 없음
  • 동시에 하나의 미완료 goal만 운용한다.
  • 구현 완료 즉시 해당 Task 체크박스와 현재 상태를 갱신한다.
  • 실제 검증 결과는 기존 기록을 덮어쓰지 않고 Progress에 누적한다.

범위

포함

  • 로그인·비로그인 사용자 공통 국가 코드 판정
  • 홈 추천 tab_id IS NULL 배너의 KO/JA 필터
  • 콘텐츠 추천 tab_id = 2 배너의 KO/JA 필터
  • application → port → persistence 언어 전달
  • focused application/Repository 테스트와 직접 영향 controller/E2E 회귀

제외

  • Lang.EN을 선택하는 국가 정책
  • 배너 외 추천 섹션의 국가·언어 필터
  • 관리자/v1 배너 조회 변경
  • DB 데이터·스키마, API 계약과 dependency 변경
  • 기존 배너 정책의 리팩터링

파일 구조

책임 파일
공통 국가 판정 src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceService.kt, MemberContentPreferenceCountryResolver.kt
홈 application 언어 선택·전달 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt, v2/recommendation/application/HomeRecommendationQueryService.kt
홈 port·조회 조건 src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt, v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
콘텐츠 추천 application 언어 선택·전달 src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt
콘텐츠 추천 port·조회 조건 src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/port/out/AudioRecommendationQueryPort.kt, v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt

Phase 1: 접속 국가별 배너 언어 필터

Phase 결과: 두 추천 API가 판정 국가에 대응하는 한 언어의 배너만 기존 정책대로 반환한다.

선행조건: 승인된 PRD BANNER-LANG-001~006.

Phase 완료 조건: P1-T1~P1-T3P1-GATE 완료, 실제 검증 결과 누적.

구현 항목

Task 1.1 로그인·비로그인 공통 국가 판정 (P1-T1)

Goal 실행 P1-T1: 기존 강제 국가 매핑을 유지하면서 비로그인 사용자도 동일한 정규화와 기본값으로 국가 코드를 판정한다.

  • 시작 조건: PRD BANNER-LANG-003, BANNER-LANG-004 확정.
  • 완료 증거: nullable 회원 국가 판정 테스트가 RED 후 GREEN이고 기존 강제 KR/JP 매핑 테스트가 통과한다.
  • 범위 밖: 국가를 배너 언어로 변환하거나 배너를 조회하는 로직.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceCountryResolver.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceIntegrationTest.kt

Interfaces:

  • Consumes: CountryContext.countryCode, 기존 강제 KR/JP 회원 ID 매핑.

  • Produces: MemberContentPreferenceService.resolveCountryCode(member: Member?): String — 정규화한 국가 코드이며 null·blank는 KR.

  • RED: 비로그인 JP, 소문자/공백 포함 JP, 헤더 누락을 검증하는 테스트를 추가한다.

countryContext.setCountryCode(" jp ")
assertEquals("JP", service.resolveCountryCode(null))

countryContext.setCountryCode(null)
assertEquals("KR", service.resolveCountryCode(null))
  • RED 확인: 아래 focused test를 실행해 현재 Member non-null 계약 때문에 컴파일이 실패하는지 확인한다.
./gradlew test --tests "kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceIntegrationTest"

Expected: resolveCountryCode(null) 호출의 타입 불일치로 실패한다.

  • GREEN: 기존 helper가 nullable 회원을 받고, non-null 회원만 기존 ID 필수 조건을 검사하도록 최소 변경한다.
fun resolveCountryCode(member: Member?): String {
    if (member != null) requireMemberId(member)
    return resolveCountryCodeWithForcedMapping(member, countryContext.countryCode)
}

fun resolveCountryCodeWithForcedMapping(member: Member?, requestCountryCode: String?): String {
    val memberId = member?.id
    if (memberId != null && FORCED_KR_MEMBER_IDS.contains(memberId)) {
        return "KR"
    }
    if (memberId != null && FORCED_JP_MEMBER_IDS.contains(memberId)) {
        return "JP"
    }
    return requestCountryCode
        ?.trim()
        ?.takeIf { it.isNotBlank() }
        ?.uppercase()
        ?: "KR"
}
  • GREEN 확인: 같은 focused test를 다시 실행해 비로그인과 기존 강제 매핑 테스트가 모두 통과하는지 확인한다.
  • REFACTOR: 이번 변경에서 생긴 중복만 정리하고 공개 국가 정책 메서드를 추가하지 않는다. focused test를 재실행해 결과를 Progress에 기록한다.

Task 1.2 홈 추천 배너 언어 필터 (P1-T2)

Goal 실행 P1-T2: 홈 추천 배너가 판정 국가에 맞는 JA 또는 KO만 DB에서 조회한다.

  • 시작 조건: P1-T1 완료, PRD BANNER-LANG-001, BANNER-LANG-005 확정.
  • 완료 증거: facade·query service 전달 테스트와 홈 Repository 언어 필터 테스트가 RED 후 GREEN이다.
  • 범위 밖: 콘텐츠 추천 탭 배너와 홈의 다른 추천 섹션.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt

Interfaces:

  • Consumes: MemberContentPreferenceService.resolveCountryCode(member: Member?): String.

  • Produces: findHomeBanners(..., lang: Lang = Lang.KO) application/port 계약과 QueryDSL audioContentBanner.lang.eq(lang) 조건.

  • RED: facade가 JPLang.JA, 그 외를 Lang.KO로 전달하고 query service가 Lang을 port에 위임하는 테스트를 추가한다.

Mockito.doReturn("JP").`when`(preferenceService).resolveCountryCode(member)

facade.getHomeRecommendations(member)

assertEquals(Lang.JA, queryPort.bannerLang)
  • RED: 홈 Repository fixture helper에 lang: Lang = Lang.KO를 추가하고 같은 조건의 KO/JA 배너 중 요청 언어만 반환하는 테스트를 추가한다.
val koBanner = saveBanner("ko.png", AudioContentBannerType.LINK, 1, isActive = true, lang = Lang.KO)
val jaBanner = saveBanner("ja.png", AudioContentBannerType.LINK, 2, isActive = true, lang = Lang.JA)

assertEquals(listOf(koBanner.thumbnailImage), repository.findHomeBanners(20, lang = Lang.KO).map { it.thumbnailImage })
assertEquals(listOf(jaBanner.thumbnailImage), repository.findHomeBanners(20, lang = Lang.JA).map { it.thumbnailImage })
  • RED 확인: 아래 focused test를 실행해 Lang 전달 계약과 Repository 조건이 없어 발생하는 컴파일/assertion 실패를 확인한다.
./gradlew test \
  --tests "kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacadeTest" \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest" \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest"
  • GREEN: facade에서 국가를 언어로 변환해 전달한다. service와 port 시그니처 끝에는 기본값 Lang.KO인 인자를 추가하고 Repository override는 같은 Lang 인자를 받는다.
val bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO

queryService.findHomeBanners(
    limit = HOME_BANNER_LIMIT,
    memberId = member?.id,
    includeAdultBanners = includeAdult,
    lang = bannerLang
)
.where(
    audioContentBanner.isActive.isTrue,
    audioContentBanner.tab.isNull,
    audioContentBanner.lang.eq(lang),
    includeAdultBannerCondition(includeAdultBanners),
    activeBannerTargetCondition(memberId, bannerCreator, seriesOwner)
)
  • GREEN 확인: 같은 focused test를 다시 실행해 언어 전달·필터와 기존 홈 배너 조건이 통과하는지 확인한다.
  • REFACTOR: 국가→언어 변환을 별도 abstraction으로 만들지 않고 한 줄 정책으로 유지한다. 이번 Task가 만든 중복만 정리하고 focused test 결과를 Progress에 기록한다.

Task 1.3 콘텐츠 추천 배너 언어 필터 (P1-T3)

Goal 실행 P1-T3: 콘텐츠 추천 배너가 판정 국가에 맞는 JA 또는 KO만 DB에서 조회한다.

  • 시작 조건: P1-T1 완료, PRD BANNER-LANG-002, BANNER-LANG-005 확정.
  • 완료 증거: query service 전달 테스트와 콘텐츠 추천 Repository 언어 필터 테스트가 RED 후 GREEN이다.
  • 범위 밖: 홈 추천 배너와 오디오 추천의 배너 외 섹션.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/port/out/AudioRecommendationQueryPort.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt

Interfaces:

  • Consumes: MemberContentPreferenceService.resolveCountryCode(member: Member?): String.

  • Produces: AudioRecommendationQueryPort.findBanners(..., lang: Lang = Lang.KO)와 QueryDSL audioContentBanner.lang.eq(lang) 조건.

  • RED: 기존 shouldUseStoredPreferenceForMemberAdultVisibility에 JP 국가 stub과 배너 호출 검증을 추가하고, 비로그인 조립 테스트는 Lang.KO 전달을 검증하도록 보강한다.

Mockito.doReturn("JP").`when`(preferenceService).resolveCountryCode(member)

service.getRecommendations(member)

Mockito.verify(queryPort).findBanners(
    AudioRecommendationQueryService.BANNER_LIMIT,
    member.id,
    true,
    Lang.JA
)
  • RED: 콘텐츠 추천 Repository fixture helper에 lang: Lang = Lang.KO를 추가하고 동일 탭의 KO/JA 배너가 요청 언어별로 분리되는 테스트를 추가한다.
val koBanner = saveBanner(
    "ko.png",
    AudioContentBannerType.LINK,
    1,
    link = "https://ko.test",
    tab = recommendationTab,
    lang = Lang.KO
)
val jaBanner = saveBanner(
    "ja.png",
    AudioContentBannerType.LINK,
    2,
    link = "https://ja.test",
    tab = recommendationTab,
    lang = Lang.JA
)

assertEquals(listOf("https://cdn.test/${koBanner.thumbnailImage}"), repository.findBanners(20, null, false, Lang.KO).map { it.imageUrl })
assertEquals(listOf("https://cdn.test/${jaBanner.thumbnailImage}"), repository.findBanners(20, null, false, Lang.JA).map { it.imageUrl })
  • RED 확인: 아래 focused test를 실행해 Lang 인자와 Repository 필터가 없어 발생하는 컴파일/assertion 실패를 확인한다.
./gradlew test \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest" \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest"
  • GREEN: getRecommendations에서 국가를 언어로 변환해 배너 호출에만 전달한다. port에는 기본값 Lang.KO인 인자를 추가하고 Repository override는 같은 Lang 인자를 받는다.
val bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO

banners = queryPort.findBanners(BANNER_LIMIT, memberId, canViewAdultContent, bannerLang)
.where(
    audioContentBanner.isActive.isTrue,
    audioContentBanner.tab.id.eq(2L),
    audioContentBanner.lang.eq(lang),
    adultBannerCondition(canViewAdultContent),
    activeBannerTargetCondition(memberId, bannerCreator, seriesOwner)
)
  • GREEN 확인: 같은 focused test를 다시 실행해 언어 필터와 기존 탭·성인·차단 조건이 함께 통과하는지 확인한다.
  • REFACTOR: 배너 외 오디오 추천 로직을 변경하지 않는다. 이번 Task가 만든 중복만 정리하고 focused test 결과를 Progress에 기록한다.

완료 조건

  • P1-T1~P1-T3의 체크박스와 완료 증거가 모두 충족됐다.
  • BANNER-LANG-001~006이 구현 또는 명시적 제외로 추적된다.
  • 공개 API와 DB 스키마 변경이 없다.
  • 실제 검증 결과가 Progress에 기록됐다.

검증 방법

Phase 1 Gate

Goal 실행 P1-GATE: 두 추천 API의 국가별 배너 언어 필터와 기존 배너 정책의 비회귀를 최종 판정한다.

  • 시작 조건: P1-T1~P1-T3 완료.
  • 완료 증거: 아래 focused/영향 범위 test, lint, 문서 명령과 수동 검증 통과 및 Progress 기록.
  • 범위 밖: Gate 통과를 위한 test 삭제·skip·완화와 관련 없는 코드 수정.
./gradlew test \
  --tests "kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceIntegrationTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacadeTest" \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest" \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest" \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest" \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationControllerTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest"
./gradlew ktlintCheck
./gradlew tasks --all
git diff --check

Expected: 모든 명령이 exit code 0으로 끝난다. JP 판정은 두 API에서 JA 배너만, 그 외 판정은 KO 배너만 반환하며 기존 탭·성인·활성·차단·대상 유효성·정렬·limit와 응답 스키마가 유지된다.

수동 검증:

  • git diff --name-only에 계획된 production/test 파일과 이 작업 문서 이외의 변경이 없는지 확인한다.
  • git diff에서 endpoint, request/response DTO, DB 스키마, dependency와 Accept-Language 사용이 추가되지 않았는지 확인한다.
  • 두 Repository 모두 lang 조건이 orderBylimit보다 앞선 where에 있는지 확인한다.

전체 회귀 ./gradlew test는 변경이 국가 판정과 두 배너 조회 경로에 한정되고 위 명령이 application, persistence, controller/E2E를 포함하므로 기본 생략한다. focused test로 영향 범위를 판단할 수 없는 실패가 발생하거나 공통 국가 판정 변경의 회귀 범위가 확대되면 전체 회귀를 실행하고 결과를 Progress에 기록한다.

실행 순서와 의존성

순서 Goal 선행조건 병행 가능 차단 시 다음 행동
1 P1-T1 승인된 PRD 아니요 기존 국가 정규화·강제 매핑 테스트와 구현을 다시 대조
2 P1-T2 P1-T1 아니요 홈 application/port/repository 계약을 한 단계씩 대조
3 P1-T3 P1-T1 아니요 콘텐츠 추천 application/port/repository 계약을 한 단계씩 대조
4 P1-GATE P1-T1~P1-T3 아니요 실패를 소유한 Task에 회귀 수정 Goal 추가
P1-T1 → P1-T2 → P1-T3 → P1-GATE

변경 금지 항목

  • 기존 완료 문서와 검증 기록을 삭제하거나 덮어쓰지 않는다.
  • 공개 endpoint, request/response DTO와 API envelope를 변경하지 않는다.
  • DB schema, 배너 데이터, 관리자/v1 배너 API를 변경하지 않는다.
  • Accept-Language나 회원 저장 언어를 국가 판정 대신 사용하지 않는다.
  • 조회 후 컬렉션 filter로 언어를 제거하지 않는다.
  • 신규 dependency, enum, 공통 abstraction과 관련 없는 리팩터링을 추가하지 않는다.
  • test를 삭제·skip·완화하지 않는다.

의사결정 및 중단 규칙

  • PRD와 구현이 충돌하면 prd.md의 Decision Log와 이 계획을 먼저 갱신한 뒤 구현한다.
  • CloudFront-Viewer-Country 또는 기존 강제 국가 매핑 정책을 바꿔야 하면 범위 확장으로 보고 사용자 승인 전 중단한다.
  • Lang.EN, 다른 국가별 언어 또는 배너 외 추천 섹션이 필요해지면 별도 요구사항으로 분리한다.
  • 공개 API, DB schema 또는 관리자/v1 조회 변경이 필요해지면 사용자 승인 전 진행하지 않는다.
  • 체크박스, test와 Progress 기록이 모두 충족된 뒤에만 Goal을 완료 처리한다.

Progress

2026-08-19 문서 작성

  • 사용자 인터뷰로 JP → JA, 그 외 국가 → KO 정책과 로그인 회원의 기존 강제 국가 매핑 적용을 확정했다.
  • PRD와 구현 계획만 작성했으며 production/test 코드는 변경하지 않았다.
  • placeholder 검색과 git diff --check에서 문제가 없음을 확인했다.
  • ./gradlew tasks --all은 최초 sandbox의 Gradle wrapper lock 접근 제한으로 실패했으며, 승인된 동일 명령 재실행에서 BUILD SUCCESSFUL을 확인했다.

2026-08-19 P1-T1 완료

  • RED: ./gradlew test --tests "kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceIntegrationTest" 실행 결과 resolveCountryCode(null) 4곳이 Member non-null 타입 불일치로 :compileTestKotlin FAILED가 됨을 확인했다.
  • GREEN/REFACTOR: MemberContentPreferenceService.resolveCountryCoderesolveCountryCodeWithForcedMapping만 nullable 회원을 받도록 최소 변경했다. 같은 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.

2026-08-19 P1-T2 완료

  • RED: 홈 facade/service/repository 테스트에 Lang 전달과 content_banner.lang 조건 기대를 추가한 뒤 focused test를 실행했고, findHomeBannerslang 파라미터 부재로 :compileTestKotlin FAILED가 됨을 확인했다.
  • GREEN/REFACTOR: 홈 facade에서 JPLang.JA, 그 외 Lang.KO를 한 줄로 선택해 service/port/repository에 전달하고, QueryDSL whereaudioContentBanner.lang.eq(lang)을 추가했다. 같은 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.

2026-08-19 P1-T3 완료

  • RED: 콘텐츠 추천 service/repository 테스트에 Lang.KO 기본 전달, JPLang.JA 전달, 추천 탭 배너 언어 분리 기대를 추가한 뒤 focused test를 실행했고, findBannersLang 인자 부재로 :compileTestKotlin FAILED가 됨을 확인했다.
  • GREEN/REFACTOR: AudioRecommendationQueryService.getRecommendations에서 배너 호출에만 JP → Lang.JA, 그 외 Lang.KO를 전달하고, QueryDSL whereaudioContentBanner.lang.eq(lang)을 추가했다. 같은 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.

2026-08-19 P1-GATE 완료

  • Phase Gate focused/controller/E2E 명령 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • ./gradlew ktlintCheck는 최초 실행에서 테스트 import 순서 2곳으로 실패했고, import 정렬 후 재실행 결과 BUILD SUCCESSFUL을 확인했다.
  • ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • git diff --check 실행 결과 출력 없이 통과했다.
  • git diff --name-only와 diff 검토로 계획된 production/test 파일 및 이 작업 문서 외 변경이 없고, 공개 endpoint/request/response DTO, DB 스키마, dependency, Accept-Language 정책이 추가되지 않았음을 확인했다.
  • 두 Repository 모두 audioContentBanner.lang.eq(lang) 조건이 where 안에서 orderBylimit보다 먼저 적용됨을 확인했다.
  • Reviewer Gate에서 요구사항 차단 이슈 없음으로 승인받았다.

Decision Log

날짜 ID 상태 결정 근거 영향 Goal/문서
2026-08-19 DEC-001 확정 공통 국가 판정을 먼저 nullable 회원까지 확장한 뒤 두 배너 경로가 소비한다. 기존 강제 매핑·정규화 재사용과 중복 방지 P1-T1~P1-T3
2026-08-19 DEC-002 확정 두 port의 lang은 기존 직접 호출 호환을 위해 기본값 Lang.KO를 사용한다. 비JP 정책과 기존 KO fixture 일치 P1-T2, P1-T3
2026-08-19 DEC-003 확정 국가→언어 변환은 두 application 진입점의 한 줄 정책으로 두고 별도 abstraction을 만들지 않는다. 사용처가 두 곳뿐인 고정 정책의 최소 구현 P1-T2, P1-T3

발견된 문제

2026-08-19 2차 리뷰

  • production 호출 흐름과 QueryDSL 조건에는 요구사항 차단 문제가 없다.
  • 두 Repository 언어 테스트가 limit = 20으로 배너 2개를 모두 수용해 BANNER-LANG-005의 "다른 언어 배너가 limit을 차지하지 않음" 회귀를 직접 방어하지 못하는 테스트 공백을 확인했다.

Phase 2: 언어 필터 limit 선행 회귀 테스트 보완

Phase 결과: 두 Repository 테스트가 언어 조건이 제거되거나 limit 이후로 밀리는 회귀를 직접 검출한다.

선행조건: P1-GATE 완료와 2차 리뷰 테스트 공백 확정.

Phase 완료 조건: P2-T1P2-GATE 완료, mutation RED와 복구 후 GREEN 결과 누적.

구현 항목

Task 2.1 언어 필터가 limit보다 먼저 적용되는 회귀 테스트 (P2-T1)

Goal 실행 P2-T1: 낮은 orders의 반대 언어 배너가 있어도 limit = 1 조회는 요청 언어 배너를 반환함을 두 Repository에서 검증한다.

  • 시작 조건: PRD BANNER-LANG-005, 2차 리뷰 결과 확정.
  • 완료 증거: 두 테스트가 정상 구현에서 통과하고, 각 Repository의 언어 조건 제거 mutation에서 의도한 assertion 실패를 보인 뒤 복구 후 다시 통과한다.
  • 범위 밖: production query, 배너 정렬 정책, API 계약 변경.
  • TDD 예외 사유: production 동작은 이미 올바르고 이번 보완은 기존 테스트가 특정 회귀를 검출하는지 확인하는 테스트 전용 변경이다.
  • 대체 검증 방법: 테스트를 먼저 보강한 뒤 각 Repository의 audioContentBanner.lang.eq(lang)을 일시 제거해 실패를 확인하고 즉시 복구한 뒤 같은 테스트의 통과를 확인한다.

Files:

  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt

  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt

  • Mutation 후 원상 복구 확인: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt

  • Mutation 후 원상 복구 확인: src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt

  • RED: 각 언어 테스트에서 orders = 1인 반대 언어와 orders = 2인 요청 언어를 만들고 limit = 1 결과가 요청 언어 한 건인지 검증한다.

  • RED 확인: 홈 Repository의 언어 조건을 일시 제거하고 홈 단일 테스트가 반대 언어를 반환해 실패하는지 확인한 뒤 원상 복구한다.

  • RED 확인: 콘텐츠 추천 Repository의 언어 조건을 일시 제거하고 콘텐츠 추천 단일 테스트가 반대 언어를 반환해 실패하는지 확인한 뒤 원상 복구한다.

  • GREEN: production 변경 없이 두 테스트의 최종 fixture와 limit = 1 assertion만 유지한다.

  • GREEN 확인: 아래 두 focused test class가 통과하는지 확인한다.

  • REFACTOR: 테스트 이름·fixture 중복만 최소 정리하고 production diff가 Phase 1 구현과 동일한지 확인한다.

./gradlew test \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest" \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest"

Phase 2 Gate (P2-GATE)

  • 시작 조건: P2-T1 완료.
  • 완료 증거: Phase 1 Gate focused/controller/E2E 테스트, ktlintCheck, tasks --all, git diff --check 통과와 재리뷰 결과 기록.
  • 범위 밖: 새 production 동작과 관련 없는 테스트 확장.
./gradlew test \
  --tests "kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceIntegrationTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacadeTest" \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest" \
  --tests "kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest" \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest" \
  --tests "kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationControllerTest" \
  --tests "kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest"
./gradlew ktlintCheck
./gradlew tasks --all
git diff --check

Expected: 모든 명령이 exit code 0으로 끝나고, 두 언어 테스트는 반대 언어가 더 낮은 orders여도 요청 언어 배너를 한 건 반환한다.

Phase 2 Progress

2026-08-19 P2-T1 완료

  • 테스트 보완: 두 Repository 언어 테스트를 limit = 1로 강화하고 이름을 limit 선행 동작이 드러나도록 변경했다.
  • 홈 mutation RED: DefaultHomeRecommendationQueryRepository의 언어 조건을 일시 제거한 단일 테스트가 DefaultHomeRecommendationQueryRepositoryTest.kt:276에서 assertion 실패함을 확인하고 즉시 복구했다.
  • 콘텐츠 추천 mutation RED: DefaultAudioRecommendationQueryRepository의 언어 조건을 일시 제거한 단일 테스트가 DefaultAudioRecommendationQueryRepositoryTest.kt:126에서 assertion 실패함을 확인하고 즉시 복구했다.
  • GREEN: 복구 후 두 Repository focused test class를 함께 실행해 BUILD SUCCESSFUL을 확인했다.

2026-08-19 P2-GATE 완료

  • Phase 1 Gate와 같은 focused/application/Repository/controller/E2E 테스트 9개 class를 실행해 BUILD SUCCESSFUL을 확인했다.
  • ./gradlew ktlintCheck./gradlew tasks --all이 각각 BUILD SUCCESSFUL로 끝났다.
  • git diff --check 결과 오류가 없고, 두 production Repository에 audioContentBanner.lang.eq(lang)이 복구된 상태임을 확인했다.
  • 최종 독립 재리뷰에서 최초 Important 이슈 해소와 Critical/Important/Minor 추가 이슈 없음 판정을 받았다.
  • 전체 회귀 ./gradlew test는 실행하지 않았다. 변경이 두 기존 Repository 테스트의 limit 경계 강화에 한정되고 Phase Gate가 직접 영향 application/persistence/controller/E2E 범위를 포함하므로 생략했다.