# 20260424 Yandex 광고 추가 구현 계획 ## 작업 체크리스트 - [x] 대상 화면 3곳의 광고 삽입 위치와 기존 Yandex 광고 패턴을 조사한다. QA: `LiveFragment`, `LiveRoomDetailFragment`, `AudioContentDetailActivity`, `MyPageFragment`, `LiveRoomActivity`, 공식 Yandex 문서를 근거로 각 광고 형식과 삽입 위치를 설명할 수 있어야 한다. - [x] AD_UNIT_ID 운영 방식을 기존 Yandex 광고와 같은 구조로 정하고, 지면별 분리 여부를 판단한다. QA: 기존 `BuildConfig` 주입 패턴을 유지하면서 어떤 지면은 반드시 분리하고 어떤 경우에만 공용이 가능한지 문서에 남아 있어야 한다. - [x] `app/build.gradle`에 신규 광고 지면용 ad unit id를 buildType별로 추가한다. QA: `debug`/`release` 모두에서 라이브 탭 배너, 라이브 상세 배너, 콘텐츠 상세 전면 광고, 콘텐츠 상세 배너용 `BuildConfig` 값이 생성되어야 한다. - [x] 라이브 탭의 최근 종료한 라이브와 라이브 다시 듣기 사이에 Yandex adaptive inline banner를 추가한다. QA: `fragment_live.xml`의 `rv_latest_finished_live_channel` 다음, `ll_replay_live` 이전에 배너 컨테이너가 추가되고, 배너 최대 높이는 90dp를 넘지 않아야 한다. - [x] 라이브 상세 bottom sheet의 참여자 목록과 크리에이터 프로필 사이에 Yandex adaptive inline banner를 추가한다. QA: `fragment_live_room_detail.xml`의 `ll_participate_wrapper` 다음, 크리에이터 프로필 `RelativeLayout` 이전에 배너가 배치되고, bottom sheet 해제 시 배너 리소스가 정리되어야 한다. - [x] 콘텐츠 상세에서 무료 콘텐츠 재생 또는 미리듣기 시작 시 Yandex interstitial 광고를 추가한다. QA: `AudioContentDetailActivity.setupPlayArea()`의 실제 재생 시작 클릭 경로에서만 광고를 1회 시도하고, 광고 실패 여부와 무관하게 기존 재생 흐름이 유지되어야 한다. - [x] 콘텐츠 상세의 오픈예정/theme 표시 영역과 이전화/다음화 영역 사이에 Yandex adaptive inline banner를 추가한다. QA: `activity_audio_content_detail.xml`의 `ll_previous_next_content`와 theme/open 예정 `RelativeLayout` 사이에 배너가 추가되고, 최대 높이는 90dp를 넘지 않아야 한다. - [x] 각 화면 생명주기에 맞는 광고 로드/정리 코드를 반영한다. QA: 배너는 화면 종료 시 `destroy()`가 호출되고, 전면 광고는 listener와 ad 참조가 화면 종료 시 정리되어야 한다. - [x] 검증 결과를 문서 하단에 누적 기록한다. QA: 최소 빌드, 테스트, 수동 확인 계획과 실제 실행 결과가 문서 하단에 남아 있어야 한다. ## 범위 메모 - 이번 요청 범위는 아래 4개 광고 지면 추가로 한정한다. - 라이브 탭 배너 1개 - 라이브 상세 배너 1개 - 콘텐츠 상세 전면 광고 1개 - 콘텐츠 상세 배너 1개 - AD_UNIT_ID는 기존 Yandex 광고와 동일하게 `app/build.gradle`의 `debug`/`release` `buildConfigField`로 관리한다. - 배너 광고는 모두 Yandex adaptive inline banner를 기준으로 구현한다. - 사용자가 명시한 제약에 따라 모든 배너의 최대 높이는 90dp를 상한으로 둔다. - 전면 광고는 `AudioContentDetailActivity`에서 무료 콘텐츠 재생 또는 미리듣기 시작 시점에만 노출을 시도한다. - 앱 전역 Yandex SDK 의존성과 기본 초기화는 이미 `app/build.gradle`, `SodaLiveApp.kt`에 존재하므로 이번 작업에서 신규 SDK 도입이나 앱 초기화 구조 변경은 제외한다. - 기존 구현 패턴은 배너는 `MyPageFragment`, 전면 광고는 `LiveRoomActivity`를 우선 따른다. - 같은 포맷이라도 페이지 목적과 노출 맥락이 다르면 AD_UNIT_ID를 분리하는 방향을 기본값으로 둔다. - 실제 ad unit id 값은 아직 전달되지 않아 신규 4개 지면은 `BuildConfig`에 교체용 placeholder 문자열로 추가하고, 코드 구조와 주입 경로를 먼저 확정한다. ## 조사 근거 - 기존 배너 구현 - `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt` - `app/src/main/res/layout/fragment_my.xml` - 기존 전면 광고 구현 - `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt` - 앱 초기화 - `app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt` - 대상 화면 - `app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt` - `app/src/main/res/layout/fragment_live.xml` - `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt` - `app/src/main/res/layout/fragment_live_room_detail.xml` - `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` - `app/src/main/res/layout/activity_audio_content_detail.xml` - 공식 문서 - `https://ads.yandex.com/helpcenter/ko/dev/android/adaptive-inline-banner` - `https://ads.yandex.com/helpcenter/ko/dev/android/interstitial` ## 구현 계획 ### 1. ad unit id 주입 지점 확정 - 수정 대상: `app/build.gradle` - 계획: - `debug`/`release` 각각에 신규 광고 지면용 `buildConfigField`를 추가한다. - 계획 후보 키 - `YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID` - `YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID` - `YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID` - `YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID` - 이유: - 기존 `YANDEX_INLINE_BANNER_MYPAGE_AD_UNIT_ID`, `YANDEX_INTERSTITIAL_LIVE_ROOM_AD_UNIT_ID`와 동일한 관리 방식을 유지해야 하기 때문이다. ### 1-1. AD_UNIT_ID 분리 전략 - 권장안: - 이번 작업의 4개 지면은 모두 **서로 다른 AD_UNIT_ID**를 사용한다. - 근거: - 라이브 탭 배너, 라이브 상세 배너, 콘텐츠 상세 배너는 모두 같은 banner 포맷이지만 화면 맥락과 가시성, 스크롤 위치, 성과 측정 대상이 다르다. - 콘텐츠 상세 전면 광고는 포맷 자체가 interstitial이라 배너와는 반드시 분리해야 한다. - 지면별 AD_UNIT_ID를 분리하면 추후 리포트, fill rate, CTR, 운영 정책 조정, 특정 지면만 교체하는 작업이 쉬워진다. - 공용 ID가 가능한 경우: - 완전히 동일한 포맷이고, - 동일한 UX 목적을 가지며, - 운영/리포트도 합산으로 봐도 되는 경우에만 공용 사용을 검토할 수 있다. - 이번 요청에서의 판단: - 라이브 탭 배너 ↔ 라이브 상세 배너 ↔ 콘텐츠 상세 배너는 위치와 사용자 의도가 모두 달라 공용으로 묶지 않는 것이 낫다. - 따라서 이번 계획에서는 **페이지별·지면별 분리**를 기준으로 문서와 코드 반영을 진행한다. ### 2. 라이브 탭 배너 추가 - 수정 대상: - `app/src/main/res/layout/fragment_live.xml` - `app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt` - 위치: - `fragment_live.xml`의 `rv_latest_finished_live_channel` 다음, `ll_replay_live` 이전 - 계획: - 스크롤 섹션 사이에 `BannerAdView` 또는 배너 전용 컨테이너를 추가한다. - `LiveFragment`에 기존 `MyPageFragment.setupBottomInlineBanner()`와 같은 크기 계산/로드 로직을 화면 구조에 맞게 추가한다. - `maxAdHeightDp`는 90으로 제한한다. - `onDestroyView()`에서 배너 `destroy()`를 호출한다. ### 3. 라이브 상세 배너 추가 - 수정 대상: - `app/src/main/res/layout/fragment_live_room_detail.xml` - `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt` - 위치: - `ll_participate_wrapper` 아래, 크리에이터 프로필 `RelativeLayout` 위 - 계획: - bottom sheet의 세로 흐름을 유지하면서 배너 컨테이너를 삽입한다. - 참가자 영역이 숨겨지는 경우와 방장 여부에 따라 UI가 달라져도 광고 영역이 레이아웃을 깨지 않도록 표시 조건을 명확히 정한다. - 배너 크기 계산 시 실제 측정 너비와 90dp 상한을 사용한다. - fragment 종료 또는 dialog dismiss 시 배너 리소스를 해제한다. ### 4. 콘텐츠 상세 전면 광고 추가 - 수정 대상: - `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` - 진입 경로 근거: - `setupPlayArea()`에서 `binding.ivPlayOrPause.setOnClickListener(playClickAction)` - `binding.llPreview.setOnClickListener(playClickAction)` - 실제 재생은 `playClickAction` 내부의 `AudioContentPlayService` 시작으로 이어진다. - 계획: - `LiveRoomActivity`의 `InterstitialAdLoader` / `InterstitialAdLoadListener` / `InterstitialAdEventListener` 패턴을 재사용한다. - 무료 콘텐츠 재생 또는 미리듣기 클릭 시점에만 광고 노출을 시도한다. - 재생/일시정지 버튼은 서비스 브로드캐스트 상태를 단일 기준으로 삼아, 재생 중이면 무조건 `PAUSE`만 보내고 광고 로직은 타지 않도록 분리한다. - 광고가 없거나 실패하면 즉시 기존 `playClickAction`을 계속 진행한다. - 중복 노출 방지를 위한 1회성 상태를 Activity 생명주기에 맞춰 정의한다. - `onDestroy()`에서 loader, listener, ad 참조를 정리한다. ### 5. 콘텐츠 상세 배너 추가 - 수정 대상: - `app/src/main/res/layout/activity_audio_content_detail.xml` - `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` - 위치: - `ll_previous_next_content` 다음, `tv_scheduled_to_open`/`tv_theme`를 포함한 `RelativeLayout` 이전 - 계획: - 현재 스크롤 흐름을 유지하면서 중간 섹션으로 배너를 삽입한다. - `NestedScrollView` 내부 측정 너비 기준으로 adaptive inline banner를 로드한다. - `maxAdHeightDp`는 90으로 제한한다. - Activity 종료 시 배너 `destroy()`를 호출한다. ## 예상 수정 파일 - `docs/20260424_Yandex광고추가구현계획.md` - `app/build.gradle` - `app/src/main/res/layout/fragment_live.xml` - `app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt` - `app/src/main/res/layout/fragment_live_room_detail.xml` - `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt` - `app/src/main/res/layout/activity_audio_content_detail.xml` - `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` ## 검증 계획 - 공통 - `./gradlew :app:assembleDebug` - `./gradlew :app:testDebugUnitTest` - 수동 확인 - 라이브 탭 진입 후 최근 종료한 라이브와 라이브 다시 듣기 사이에 배너가 보이는지 확인한다. - 라이브 상세 bottom sheet를 열어 참여자 목록과 크리에이터 프로필 사이 배너 위치와 높이를 확인한다. - 콘텐츠 상세에서 무료 콘텐츠 재생과 미리듣기 각각에 대해 전면 광고 시도 후 기존 재생 흐름이 유지되는지 확인한다. - 콘텐츠 상세에서 이전화/다음화와 theme/open 예정 영역 사이 배너 위치와 높이를 확인한다. - 모든 배너가 90dp를 넘지 않는지 레이아웃 검사 또는 화면 캡처로 확인한다. ## 검증 기록 - 2026-04-24 - 무엇: Yandex 광고 추가 작업의 구현 계획 문서를 생성했다. - 왜: 저장소 규칙에 따라 구현 전에 `docs` 아래 계획 문서를 먼저 만들고, 그 문서를 기준으로 범위와 검증 기준을 고정해야 하기 때문이다. - 어떻게: - 생성 파일: `docs/20260424_Yandex광고추가구현계획.md` - 근거 파일: `app/build.gradle`, `SodaLiveApp.kt`, `MyPageFragment.kt`, `LiveRoomActivity.kt`, `LiveFragment.kt`, `LiveRoomDetailFragment.kt`, `AudioContentDetailActivity.kt`, 각 대응 XML 레이아웃 - 근거 문서: `https://ads.yandex.com/helpcenter/ko/dev/android/adaptive-inline-banner`, `https://ads.yandex.com/helpcenter/ko/dev/android/interstitial` - 결과: 광고 형식, 삽입 위치, 90dp 배너 높이 제한, 예상 수정 파일, 검증 계획을 구현 전에 확정했다. - 2026-04-24 - 무엇: AD_UNIT_ID 운영 전략을 기존 Yandex 광고와 같은 `BuildConfig` 방식으로 정리하고, 지면별 분리 여부 판단을 문서에 반영했다. - 왜: 구현 전에 ad unit 관리 방식을 고정해야 이후 코드 반영과 운영 기준이 흔들리지 않고, 지면별 성과 측정 단위도 명확해지기 때문이다. - 어떻게: - 기준 패턴: `MyPageFragment`의 inline banner id, `LiveRoomActivity`의 interstitial id - 판단 결과: 이번 작업의 4개 지면은 화면 맥락과 포맷이 달라 모두 별도 AD_UNIT_ID를 사용하는 방향으로 계획을 확정했다. - 결과: `app/build.gradle`의 `buildConfigField`를 지면별로 추가하는 방향이 계획 문서에 반영됐다. - 2026-04-24 - 무엇: 계획 문서 기준으로 라이브 탭, 라이브 상세, 콘텐츠 상세의 Yandex 광고 코드를 실제 반영했다. - 왜: 사용자 요청대로 3개 화면의 지정 위치에 배너/전면 광고를 추가하고, 기존 저장소의 Yandex 광고 패턴을 동일하게 확장해야 했기 때문이다. - 어떻게: - 수정 파일: `app/build.gradle`, `app/src/main/res/layout/fragment_live.xml`, `app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt`, `app/src/main/res/layout/fragment_live_room_detail.xml`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt`, `app/src/main/res/layout/activity_audio_content_detail.xml`, `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` - BuildConfig 반영: 신규 4개 지면용 `YANDEX_*` ad unit id 필드를 `debug`/`release`에 각각 추가했다. - 라이브 탭: `rv_latest_finished_live_channel`과 `ll_replay_live` 사이에 `BannerAdView`를 추가하고, `LiveFragment`에서 90dp 상한의 adaptive inline banner를 로드하도록 구현했다. - 라이브 상세: `ll_participate_wrapper`와 크리에이터 프로필 블록 사이에 `BannerAdView`를 추가하고, `LiveRoomDetailFragment`에서 90dp 상한의 adaptive inline banner를 로드하도록 구현했다. - 콘텐츠 상세: `ll_previous_next_content`와 theme/open 예정 블록 사이에 `BannerAdView`를 추가하고, `AudioContentDetailActivity`에서 90dp 상한의 adaptive inline banner를 로드하도록 구현했다. - 콘텐츠 상세 전면 광고: 무료 재생 또는 미리듣기 클릭 경로만 interstitial로 감싸고, 광고가 없거나 실패하면 기존 재생 액션을 즉시 이어가도록 구현했다. - 정리 코드: `LiveFragment.onDestroyView()`, `LiveRoomDetailFragment.onDestroyView()`, `AudioContentDetailActivity.onDestroy()`에서 배너/전면 광고 리소스를 정리했다. - 2026-04-24 - 무엇: 빌드, 테스트, 린트, 수동 확인 가능 여부를 점검했다. - 왜: 이번 변경은 `BuildConfig`, Kotlin, XML, 화면 생명주기를 함께 건드리므로 최소 컴파일·테스트와 실제 실행 가능 여부를 함께 확인해야 하기 때문이다. - 어떻게: - 진단 도구: `lsp_diagnostics` - 진단 결과: `.kt` LSP 서버 미설정으로 `No LSP server configured for extension: .kt` - 실행 명령: `./gradlew :app:assembleDebug` - 실행 결과: `BUILD SUCCESSFUL` - 실행 명령: `./gradlew :app:testDebugUnitTest` - 실행 결과: `BUILD SUCCESSFUL` - 실행 명령: `./gradlew :app:ktlintCheck` - 실행 결과: 실패 - 린트 실패 원인: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt:1:1 Package name must not contain underscore` - 린트 판단: 현재 저장소의 기존 패키지 경로 `audio_content` 때문에 발생한 규칙 위반으로, 이번 작업에서 새로 만든 오류는 확인되지 않았다. - 실행 명령: `adb devices` - 실행 결과: 연결 기기 없음 - 수동 확인 결과: ADB 연결 기기가 없어 앱 설치 및 실제 광고 노출 경로 수동 검증까지는 진행하지 못했다. - 비고: 신규 ad unit id는 placeholder 문자열로 넣었으므로, 실제 광고 서버 응답 검증은 실 ad unit id 교체 후 추가 확인이 필요하다. - 2026-04-24 - 무엇: Oracle 검토 의견을 반영해 배너 높이 상한과 interstitial 종료 경로 안전장치를 보강한 뒤 다시 빌드와 테스트를 확인했다. - 왜: 코드상 `maxAdHeightDp = 90`만으로 끝내지 않고 XML 레벨에서도 90dp 상한을 명시해 두는 편이 안전하고, Activity 종료 중 광고 콜백이 들어와도 재생 동작이 이어지지 않도록 방어해야 하기 때문이다. - 어떻게: - 수정 파일: `fragment_live.xml`, `fragment_live_room_detail.xml`, `activity_audio_content_detail.xml`, `AudioContentDetailActivity.kt` - 추가 반영: 각 `BannerAdView`에 `android:maxHeight="90dp"`를 명시했다. - 추가 반영: `AudioContentDetailActivity.continuePendingAudioContentPlayAction()`에서 `isFinishing || isDestroyed`일 때 재생 액션을 중단하도록 보강했다. - 실행 명령: `./gradlew :app:assembleDebug` - 실행 결과: `BUILD SUCCESSFUL` - 실행 명령: `./gradlew :app:testDebugUnitTest` - 실행 결과: `BUILD SUCCESSFUL` - 2026-04-24 - 무엇: 콘텐츠 상세의 재생/일시정지 버튼과 전면 광고 게이팅 로직을 사용자 의도에 맞게 분리했다. - 왜: 기존 구현은 pause 아이콘이 보여도 클릭 리스너가 재생 경로를 유지해, 무료 콘텐츠 또는 미리듣기에서 pause 클릭 시 전면 광고가 늦게 뜨는 문제가 있었기 때문이다. - 어떻게: - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` - 상태 기준: `AudioContentPlayService` 브로드캐스트의 `EXTRA_AUDIO_CONTENT_PLAYING` 값을 단일 기준으로 사용하도록 정리했다. - 버튼 동작: 재생 중이면 `PAUSE`만 보내고, 재생 시작 시점에만 무료/미리듣기 대상 여부를 판단해 interstitial을 시도하도록 분리했다. - 기대 효과: 무료 콘텐츠 또는 유료 콘텐츠 미리듣기에서 “재생 시작 시”에만 전면 광고가 걸리고, pause 클릭은 즉시 pause로 동작한다.