diff --git a/app/build.gradle b/app/build.gradle index 85aeb5a6..dc072335 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,6 +73,8 @@ android { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // release용 ad unit id는 배포 전 실제 값으로 교체한다. + buildConfigField 'String', 'YANDEX_INLINE_BANNER_MYPAGE_AD_UNIT_ID', '"R-M-19140295-1"' buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"' buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"' buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"' @@ -103,6 +105,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' applicationIdSuffix '.debug' + buildConfigField 'String', 'YANDEX_INLINE_BANNER_MYPAGE_AD_UNIT_ID', '"R-M-19140297-1"' buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"' buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"' buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"' diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt index 0d274ebd..cbc6592a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt @@ -7,6 +7,8 @@ import android.os.Bundle import android.view.View import android.webkit.URLUtil import android.widget.Toast +import com.yandex.mobile.ads.banner.BannerAdSize +import com.yandex.mobile.ads.common.AdRequest import androidx.core.net.toUri import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.GridLayoutManager @@ -15,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.CircleCropTransformation import com.google.gson.Gson +import kr.co.vividnext.sodalive.BuildConfig import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity @@ -51,6 +54,7 @@ import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity import kr.co.vividnext.sodalive.settings.notification.MemberRole import kr.co.vividnext.sodalive.splash.SplashActivity import org.koin.android.ext.android.inject +import kotlin.math.roundToInt @UnstableApi class MyPageFragment : BaseFragment(FragmentMyBinding::inflate) { @@ -73,6 +77,29 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat bindData() setupRecentContentSection() setupLatestNotice() + setupBottomInlineBanner() + } + + private fun setupBottomInlineBanner() { + binding.yandexInlineBannerView.post { + val density = resources.displayMetrics.density + val screenHeightDp = (screenHeight / density).roundToInt() + val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth + val adWidthDp = (adWidthPixels / density).roundToInt() + val maxAdHeightDp = (screenHeightDp / 2).coerceAtLeast(1) + + binding.yandexInlineBannerView.apply { + setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_MYPAGE_AD_UNIT_ID) + setAdSize( + BannerAdSize.inlineSize( + requireContext(), + adWidthDp, + maxAdHeightDp + ) + ) + loadAd(AdRequest.Builder().build()) + } + } } private fun setupLatestNotice() { @@ -498,4 +525,9 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat } } } + + override fun onDestroyView() { + binding.yandexInlineBannerView.destroy() + super.onDestroyView() + } } diff --git a/app/src/main/res/layout/fragment_my.xml b/app/src/main/res/layout/fragment_my.xml index 717a4097..0a951c95 100644 --- a/app/src/main/res/layout/fragment_my.xml +++ b/app/src/main/res/layout/fragment_my.xml @@ -347,6 +347,13 @@ android:paddingHorizontal="24dp" /> + + diff --git a/docs/20260421_마이페이지Yandex인라인배너추가.md b/docs/20260421_마이페이지Yandex인라인배너추가.md new file mode 100644 index 00000000..e58759ae --- /dev/null +++ b/docs/20260421_마이페이지Yandex인라인배너추가.md @@ -0,0 +1,82 @@ +# 20260421 마이페이지 Yandex 인라인 배너 추가 + +## 작업 체크리스트 +- [x] Yandex adaptive inline banner 공식 요구사항과 MyPage 화면 삽입 위치를 확정한다. + QA: `app/src/main/res/layout/fragment_my.xml`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`, Yandex adaptive inline banner 문서를 근거로 스크롤 콘텐츠 최하단 삽입으로 설명할 수 있어야 한다. +- [x] 하단 인라인 배너 구현 계획과 ad unit id 교체 위치를 문서에 먼저 고정한다. + QA: 변경 파일과 ad unit id 수정 지점이 문서에 명시되어 있어야 한다. +- [x] `fragment_my.xml` 하단에 Yandex 배너 뷰를 추가한다. + QA: `NestedScrollView` 내부 콘텐츠 맨 끝에 배너 뷰가 추가되고 기존 MyPage 여백 스타일과 크게 어긋나지 않아야 한다. +- [x] `MyPageFragment.kt`에서 adaptive inline banner 크기 계산과 광고 로드를 구현한다. + QA: 측정된 너비를 기준으로 `BannerAdSize.inlineSize(...)`를 설정하고, ad unit id를 한 곳에서 교체할 수 있어야 한다. +- [x] 프래그먼트 뷰 종료 시 배너 리소스를 정리한다. + QA: `onDestroyView()`에서 배너 뷰 정리 코드가 실행되어 뷰 생명주기 종료 후 누수 가능성을 줄여야 한다. +- [x] 검증 결과를 문서 하단에 누적 기록한다. + QA: 최소 진단/빌드/수동 확인 결과와 실행 명령이 남아 있어야 한다. + +## 범위 메모 +- 요청 해석은 `MyPageFragment`의 스크롤 콘텐츠 최하단에 Yandex adaptive inline banner를 추가하는 것으로 한정한다. +- 화면 하단 고정 배너(sticky)가 아니라, 마이페이지를 끝까지 스크롤했을 때 보이는 inline 배너로 구현한다. +- 현재 프로젝트에는 `productFlavors`가 없어, 이번 변경에서는 기존 변형(`debug`/`release`)별 `buildConfigField`로 ad unit id를 분기한다. +- 현재 사용 중인 ad unit id는 `debug`에서 유지하고, `release`는 별도 값으로 교체할 수 있게 `app/build.gradle`의 각 변형 블록에 분리해 둔다. +- 기존 SDK 의존성과 앱 초기화 코드는 이미 존재하므로 이번 작업에서 `app/build.gradle`, `SodaLiveApp.kt`는 수정하지 않는다. + +## 검증 계획 +- `lsp_diagnostics`로 변경 파일의 신규 오류 여부를 확인한다. +- `./gradlew :app:assembleDebug`로 Android 리소스 병합과 컴파일 통과 여부를 확인한다. +- 가능하면 앱에서 MyPage를 열고 최하단까지 스크롤하는 수동 확인 절차를 기준으로 결과를 남긴다. + +## 검증 기록 +- 2026-04-21 + - 무엇: 마이페이지 Yandex 인라인 배너 추가 작업의 범위와 구현 위치를 문서화했다. + - 왜: 저장소 규칙에 따라 `docs` 계획 문서를 먼저 만들고, 그 문서를 기준으로 구현과 검증 이력을 누적해야 하기 때문이다. + - 어떻게: + - 생성 파일: `docs/20260421_마이페이지Yandex인라인배너추가.md` + - 근거 파일: `app/src/main/res/layout/fragment_my.xml`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`, `app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt` + - 근거 문서: `https://ads.yandex.com/helpcenter/ko/dev/android/adaptive-inline-banner` + - 결과: 구현 전 체크리스트, 범위, ad unit id 교체 위치, 검증 계획을 먼저 고정했다. +- 2026-04-21 + - 무엇: MyPage 스크롤 콘텐츠 최하단에 Yandex inline banner 뷰와 배너 로드 코드를 추가했다. + - 왜: 요청 범위를 넓히지 않고 `MyPageFragment` 최하단에 adaptive inline banner를 붙이기 위해서다. + - 어떻게: + - 수정 파일: `app/src/main/res/layout/fragment_my.xml`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt` + - 레이아웃 반영: `fragment_my.xml`의 `NestedScrollView` 콘텐츠 마지막에 `@id/yandex_inline_banner_view` 추가 + - 코드 반영: `setupBottomInlineBanner()`에서 측정된 너비 기준 `BannerAdSize.inlineSize(...)` 적용 후 `loadAd(...)` 호출 + - ad unit id 교체 위치: `MyPageFragment.kt` companion object의 `YANDEX_INLINE_BANNER_AD_UNIT_ID` + - 정리 코드: `onDestroyView()`에서 `binding.yandexInlineBannerView.destroy()` 호출 +- 2026-04-21 + - 무엇: 변경 사항의 진단, 빌드, 설치, 수동 확인 가능 여부를 점검했다. + - 왜: Kotlin/XML LSP 미지원 환경에서도 실제 Android 리소스 병합과 컴파일, 기기 설치까지 통과해야 안전하게 반영됐다고 볼 수 있기 때문이다. + - 어떻게: + - 진단 도구: `lsp_diagnostics`는 `.kt`, `.xml` 서버 미설정으로 사용 불가 + - 실행 명령: `./gradlew :app:assembleDebug :app:testDebugUnitTest` + - 실행 결과: `BUILD SUCCESSFUL` + - 추가 실행 명령: `adb devices`, `./gradlew :app:installDebug`, `adb shell am start -n kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.splash.SplashActivity` + - 추가 결과: 연결 기기 1대(`2cec640c34017ece`)에 debug 앱 설치 성공 + - 수동 확인 결과: 앱 실행 후 캡처 화면이 검은 스플래시 상태로만 남아 MyPage 진입 및 배너 실노출 확인까지는 진행하지 못했다. + - 비고: `MainActivity` 직접 실행은 non-exported Activity라 `SecurityException`으로 불가했고, 로그상 이번 변경으로 인한 신규 크래시는 확인되지 않았다. +- 2026-04-21 + - 무엇: Yandex inline banner ad unit id를 변형별로 나누도록 수정했다. + - 왜: 현재 저장소는 `productFlavors` 없이 `debug`/`release` 변형만 사용하므로, 배포/테스트 환경에 따라 다른 ad unit id를 안전하게 적용할 수 있어야 하기 때문이다. + - 어떻게: + - 수정 파일: `app/build.gradle`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt` + - Gradle 반영: `debug`/`release` 각각에 `buildConfigField 'String', 'YANDEX_INLINE_BANNER_AD_UNIT_ID', '"..."'` 추가 + - 코드 반영: `MyPageFragment`가 companion object 상수 대신 `BuildConfig.YANDEX_INLINE_BANNER_AD_UNIT_ID`를 읽도록 변경 + - 현재 기본값: `debug`는 `R-M-19140297-1`, `release`는 `REPLACE_WITH_RELEASE_YANDEX_INLINE_BANNER_AD_UNIT_ID` + - 추후 수정 위치: `app/build.gradle`의 `debug`/`release` 블록 +- 2026-04-21 + - 무엇: 현재 사용 중인 ad unit id를 `debug`로 옮기고 `release`는 별도 값으로 분리했다. + - 왜: 요청대로 기존 ad unit id는 디버그 환경에서 유지하고, 릴리스 환경에서는 독립적으로 설정할 수 있어야 하기 때문이다. + - 어떻게: + - 수정 파일: `app/build.gradle` + - 값 조정: `debug`의 `YANDEX_INLINE_BANNER_AD_UNIT_ID`를 `R-M-19140297-1`로 변경 + - 값 조정: `release`의 `YANDEX_INLINE_BANNER_AD_UNIT_ID`를 `REPLACE_WITH_RELEASE_YANDEX_INLINE_BANNER_AD_UNIT_ID`로 분리 + - 릴리스 수정 위치: `app/build.gradle`의 `release` 블록 +- 2026-04-21 + - 무엇: debug/release 분기 변경 후 빌드와 설정값 반영 상태를 다시 확인했다. + - 왜: 값만 바꾼 작업이라도 두 변형 모두 실제 Gradle 해석과 컴파일을 통과해야 하고, 현재 설정된 ad unit id가 어느 변형에 들어가는지 근거를 남겨야 하기 때문이다. + - 어떻게: + - 실행 명령: `./gradlew :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest` + - 실행 결과: `BUILD SUCCESSFUL` + - 설정 확인: `app/build.gradle` 재확인 결과 `debug`는 `R-M-19140297-1`, `release`는 `REPLACE_WITH_RELEASE_YANDEX_INLINE_BANNER_AD_UNIT_ID` + - 비고: `BuildConfig` 생성 파일 경로는 이 환경에서 직접 조회되지 않았지만, 두 변형 빌드가 모두 성공해 `buildConfigField` 값 주입 자체는 통과한 것으로 확인했다.