chore(ads): DARO 광고 제거를 반영한다

This commit is contained in:
2026-04-20 19:05:25 +09:00
parent 002d20dc0f
commit 40d8092880
15 changed files with 40 additions and 497 deletions

4
.gitignore vendored
View File

@@ -312,10 +312,6 @@ fabric.properties
!/gradle/wrapper/gradle-wrapper.jar !/gradle/wrapper/gradle-wrapper.jar
app/debug/ app/debug/
app/release/ app/release/
app/daro-key.txt
app/android-daro-key.txt
app/src/**/daro-key.txt
app/src/**/android-daro-key.txt
.junie/ .junie/
.kiro/ .kiro/

View File

@@ -1,21 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
buildscript {
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { stream ->
localProperties.load(stream)
}
}
ext.daroAppKey = project.findProperty('daroAppKey')
?: localProperties.getProperty('daroAppKey')
?: System.getenv('DARO_APP_KEY')
?: ''
}
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
@@ -27,7 +11,6 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint' id 'org.jlleitschuh.gradle.ktlint'
id 'com.google.firebase.crashlytics' id 'com.google.firebase.crashlytics'
id 'so.daro.a'
} }
android { android {
@@ -145,7 +128,6 @@ android {
} }
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17
} }
@@ -267,10 +249,6 @@ dependencies {
exclude group: "org.jetbrains.kotlin", module: "kotlin-android-extensions-runtime" exclude group: "org.jetbrains.kotlin", module: "kotlin-android-extensions-runtime"
} }
// Daro
implementation 'so.daro:daro-a:1.5.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
// ----- Test dependencies ----- // ----- Test dependencies -----
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.20.0' testImplementation 'org.mockito:mockito-core:5.20.0'

View File

@@ -245,55 +245,3 @@
-keep interface com.yalantis.ucrop** { *; } -keep interface com.yalantis.ucrop** { *; }
-dontwarn com.linecorp.linesdk.BR -dontwarn com.linecorp.linesdk.BR
# Daro Android SDK (Non-Reward)
# Pangle (ByteDance)
-keep class com.bytedance.sdk.** { *; }
# Amazon APS
-keep class com.amazon.** { *; }
-keep public class com.google.android.gms.ads.** { public *; }
-keep class com.iabtcf.** { *; }
# IronSource
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keep class com.ironsource.adapters.** { *; }
-dontwarn com.ironsource.**
-dontwarn com.ironsource.adapters.**
-keepclassmembers class com.ironsource.** { public *; }
-keep public class com.ironsource.**
-keep class com.ironsource.adapters.** { *; }
# IronSource - AppLovin integration
-keepclassmembers class com.applovin.sdk.AppLovinSdk { static *; }
-keep public interface com.applovin.sdk.** { *; }
-keep public interface com.applovin.adview.** { *; }
-keep public interface com.applovin.mediation.** { *; }
-keep public interface com.applovin.communicator.** { *; }
# IronSource - AndroidX
-keep class androidx.localbroadcastmanager.content.LocalBroadcastManager { *; }
-keep class androidx.recyclerview.widget.RecyclerView { *; }
-keep class androidx.recyclerview.widget.RecyclerView$OnScrollListener { *; }
# IronSource - Android
-keep class * extends android.app.Activity
# Retrofit
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# kotlinx.serialization
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
# Kotlin Coroutines
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

View File

@@ -11,11 +11,9 @@ import androidx.lifecycle.ProcessLifecycleOwner
import com.appsflyer.AppsFlyerLib import com.appsflyer.AppsFlyerLib
import com.appsflyer.deeplink.DeepLinkResult import com.appsflyer.deeplink.DeepLinkResult
import com.facebook.FacebookSdk import com.facebook.FacebookSdk
import droom.daro.a.Daro
import com.kakao.sdk.common.KakaoSdk import com.kakao.sdk.common.KakaoSdk
import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import droom.daro.SDKConfig
import kr.co.vividnext.sodalive.BuildConfig import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
@@ -54,8 +52,6 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
setupAppsFlyer() setupAppsFlyer()
setupNotifly() setupNotifly()
setupDaro()
} }
private fun isDebuggable(): Boolean { private fun isDebuggable(): Boolean {
@@ -141,16 +137,6 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
) )
} }
private fun setupDaro() {
Daro.init(
application = this,
sdkConfig = SDKConfig.Builder()
.setDebugMode(BuildConfig.DEBUG && isDebuggable())
.setAppMuted(false)
.build()
)
}
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
super.onStart(owner) super.onStart(owner)
isAppInForeground = true isAppInForeground = true

View File

@@ -71,15 +71,6 @@ import com.bumptech.glide.request.target.Target
import com.google.gson.Gson import com.google.gson.Gson
import com.orbitalsonic.waterwave.WaterWaveView import com.orbitalsonic.waterwave.WaterWaveView
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import droom.daro.core.adunit.DaroLightPopupAdUnit
import droom.daro.core.listener.DaroLightPopupAdListener
import droom.daro.core.listener.DaroLightPopupAdLoaderListener
import droom.daro.core.model.DaroAdDisplayFailError
import droom.daro.core.model.DaroAdInfo
import droom.daro.core.model.DaroAdLoadError
import droom.daro.core.model.DaroLightPopupAd
import droom.daro.core.model.DaroLightPopupAdOptions
import droom.daro.loader.DaroLightPopupAdLoader
import io.agora.rtc2.ClientRoleOptions import io.agora.rtc2.ClientRoleOptions
import io.agora.rtc2.IRtcEngineEventHandler import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtm.LinkStateEvent import io.agora.rtm.LinkStateEvent
@@ -176,11 +167,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private lateinit var roomInfoEditDialog: LiveRoomInfoEditDialog private lateinit var roomInfoEditDialog: LiveRoomInfoEditDialog
private lateinit var roomUserProfileDialog: LiveRoomUserProfileDialog private lateinit var roomUserProfileDialog: LiveRoomUserProfileDialog
private var daroLightPopupAdLoader: DaroLightPopupAdLoader? = null
private var daroLightPopupAd: DaroLightPopupAd? = null
private var hasRequestedDaroLightPopupEligibility = false
private var hasAttemptedDaroLightPopup = false
private var isSpeakerMute = false private var isSpeakerMute = false
private var isMicrophoneMute = false private var isMicrophoneMute = false
private var isSpeaker = false private var isSpeaker = false
@@ -468,8 +454,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
override fun onDestroy() { override fun onDestroy() {
// 액티비티 종료 전에 강제 음소거 상태를 원복한다. // 액티비티 종료 전에 강제 음소거 상태를 원복한다.
clearCapturePrivacyMuteState() clearCapturePrivacyMuteState()
clearDaroLightPopupLoader()
clearDaroLightPopupAd()
cropper.cleanup() cropper.cleanup()
hideKeyboard { hideKeyboard {
viewModel.quitRoom(roomId) { viewModel.quitRoom(roomId) {
@@ -1315,7 +1299,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
isCaptureRecordingAvailable = response.isCaptureRecordingAvailable isCaptureRecordingAvailable = response.isCaptureRecordingAvailable
syncRoomRoleState(response) syncRoomRoleState(response)
requestDaroLightPopupIfEligible()
syncCaptureSecurityPolicy() syncCaptureSecurityPolicy()
binding.tvChatFreezeSwitch.visibility = if (isHost) { binding.tvChatFreezeSwitch.visibility = if (isHost) {
View.VISIBLE View.VISIBLE
@@ -4293,111 +4276,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
} }
private fun requestDaroLightPopupIfEligible() {
if (hasRequestedDaroLightPopupEligibility || isHost) {
return
}
hasRequestedDaroLightPopupEligibility = true
if (DARO_LIGHT_POPUP_AD_UNIT_KEY.isBlank()) {
Logger.w("Daro light popup skipped because ad unit key is blank.")
return
}
viewModel.getRoomPriceForDaroLightPopup(roomId = roomId) { roomPrice ->
val resolvedRoomPrice = roomPrice ?: return@getRoomPriceForDaroLightPopup
if (
!shouldAttemptLiveRoomDaroLightPopup(
isHost = isHost,
roomPrice = resolvedRoomPrice,
hasAttemptedPopup = hasAttemptedDaroLightPopup
)
) {
return@getRoomPriceForDaroLightPopup
}
hasAttemptedDaroLightPopup = true
loadDaroLightPopup()
}
}
private fun loadDaroLightPopup() {
clearDaroLightPopupLoader()
clearDaroLightPopupAd()
val adUnit = DaroLightPopupAdUnit(
key = DARO_LIGHT_POPUP_AD_UNIT_KEY,
placement = DARO_LIGHT_POPUP_PLACEMENT,
options = DaroLightPopupAdOptions()
)
daroLightPopupAdLoader = DaroLightPopupAdLoader(
context = this,
adUnit = adUnit
).apply {
setListener(object : DaroLightPopupAdLoaderListener {
override fun onAdLoadSuccess(ad: DaroLightPopupAd, adInfo: DaroAdInfo) {
if (isFinishing || isDestroyed) {
ad.destroy()
clearDaroLightPopupLoader()
return
}
daroLightPopupAd = ad
ad.setListener(object : DaroLightPopupAdListener {
override fun onAdImpression(adInfo: DaroAdInfo) = Unit
override fun onAdClicked(adInfo: DaroAdInfo) = Unit
override fun onShown(adInfo: DaroAdInfo) = Unit
override fun onFailedToShow(
adInfo: DaroAdInfo,
error: DaroAdDisplayFailError
) {
Logger.w(
"Daro light popup failed to show. message=${error.message}"
)
clearDaroLightPopupLoader()
clearDaroLightPopupAd()
}
override fun onDismiss(adInfo: DaroAdInfo) {
clearDaroLightPopupLoader()
clearDaroLightPopupAd()
}
})
ad.show(this@LiveRoomActivity)
}
override fun onAdLoadFail(err: DaroAdLoadError) {
Logger.w(
"Daro light popup load failed. code=${err.code}, message=${err.message}"
)
clearDaroLightPopupLoader()
}
})
load()
}
}
private fun clearDaroLightPopupLoader() {
val adLoader = daroLightPopupAdLoader ?: return
daroLightPopupAdLoader = null
adLoader.destroy()
}
private fun clearDaroLightPopupAd() {
val ad = daroLightPopupAd ?: return
daroLightPopupAd = null
ad.destroy()
}
// endregion // endregion
companion object { companion object {
private const val DARO_LIGHT_POPUP_AD_UNIT_KEY = "59082e9e-de1b-4f5d-bbc3-8b4124d110d8"
private const val DARO_LIGHT_POPUP_PLACEMENT = "LiveRoomFreeListener"
private const val NO_CHATTING_TIME = 180L private const val NO_CHATTING_TIME = 180L
var isForeground: Boolean = false var isForeground: Boolean = false
} }

View File

@@ -1,9 +0,0 @@
package kr.co.vividnext.sodalive.live.room
internal fun shouldAttemptLiveRoomDaroLightPopup(
isHost: Boolean,
roomPrice: Int,
hasAttemptedPopup: Boolean
): Boolean {
return !isHost && roomPrice == 0 && !hasAttemptedPopup
}

View File

@@ -296,28 +296,6 @@ class LiveRoomViewModel(
) )
} }
fun getRoomPriceForDaroLightPopup(roomId: Long, onResult: (Int?) -> Unit) {
compositeDisposable.add(
repository.getRoomDetail(roomId, token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onResult(it.data.price)
} else {
Logger.w("Daro light popup room detail unavailable. roomId=$roomId")
onResult(null)
}
},
{ error ->
Logger.e(error.message ?: "Daro light popup room detail request failed.")
onResult(null)
}
)
)
}
fun isEqualToHostId(memberId: Int): Boolean { fun isEqualToHostId(memberId: Int): Boolean {
return memberId == roomInfoResponse.creatorId.toInt() return memberId == roomInfoResponse.creatorId.toInt()
} }

View File

@@ -15,15 +15,6 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.google.gson.Gson import com.google.gson.Gson
import com.orhanobut.logger.Logger
import droom.daro.core.adunit.DaroBannerAdUnit
import droom.daro.core.model.DaroAdInfo
import droom.daro.core.model.DaroAdLoadError
import droom.daro.core.model.DaroBannerSize
import droom.daro.core.model.DaroViewAd
import droom.daro.view.DaroAdViewListener
import droom.daro.view.DaroBannerAdView
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
@@ -66,9 +57,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
companion object { companion object {
private const val FUNCTION_BUTTON_SPAN_COUNT = 4 private const val FUNCTION_BUTTON_SPAN_COUNT = 4
private const val DARO_BANNER_AD_UNIT_KEY = "43df2529-31d8-45f8-a17d-1a760f5bc777"
private const val DARO_BANNER_PLACEMENT = "MyPage"
} }
private val viewModel: MyPageViewModel by inject() private val viewModel: MyPageViewModel by inject()
@@ -76,7 +64,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
private val functionButtonAdapter = FunctionButtonAdapter() private val functionButtonAdapter = FunctionButtonAdapter()
private var daroBannerAdView: DaroBannerAdView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -86,71 +73,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
bindData() bindData()
setupRecentContentSection() setupRecentContentSection()
setupLatestNotice() setupLatestNotice()
setupDaroBottomBanner()
}
override fun onDestroyView() {
daroBannerAdView?.destroy()
daroBannerAdView = null
binding.flDaroBannerContainer.removeAllViews()
super.onDestroyView()
}
private fun setupDaroBottomBanner() {
binding.flDaroBannerContainer.visibility = View.GONE
binding.flDaroBannerContainer.removeAllViews()
val adUnit = DaroBannerAdUnit(
key = DARO_BANNER_AD_UNIT_KEY,
placement = DARO_BANNER_PLACEMENT,
bannerSize = DaroBannerSize.Banner
)
val adView = DaroBannerAdView(
context = requireContext(),
adUnit = adUnit
).apply {
setListener(object : DaroAdViewListener {
override fun onAdImpression(adInfo: DaroAdInfo) = Unit
override fun onAdClicked(adInfo: DaroAdInfo) = Unit
override fun onAdLoadSuccess(ad: DaroViewAd, adInfo: DaroAdInfo) {
binding.flDaroBannerContainer.visibility = View.VISIBLE
}
override fun onAdLoadFail(err: DaroAdLoadError) {
handleDaroBannerLoadFail(err)
}
})
}
daroBannerAdView = adView
binding.flDaroBannerContainer.addView(adView)
adView.loadAd()
}
private fun handleDaroBannerLoadFail(err: DaroAdLoadError) {
binding.flDaroBannerContainer.visibility = View.GONE
Logger.w(
"Daro banner load failed. package=${BuildConfig.APPLICATION_ID}, placement=$DARO_BANNER_PLACEMENT"
)
Logger.w(
"Daro banner load failed. code=${err.code}, message=${err.message}"
)
if (err.message.contains("no fill", ignoreCase = true)) {
Logger.w(
"Daro no fill. Verify app-ads.txt, Live status, registered package=${BuildConfig.APPLICATION_ID}"
)
if (BuildConfig.DEBUG && BuildConfig.APPLICATION_ID.endsWith(".debug")) {
Logger.w(
"Debug package differs from release. Register ${BuildConfig.APPLICATION_ID} in Daro or test release package."
)
}
}
} }
private fun setupLatestNotice() { private fun setupLatestNotice() {

View File

@@ -347,14 +347,6 @@
android:paddingHorizontal="24dp" /> android:paddingHorizontal="24dp" />
</LinearLayout> </LinearLayout>
<!-- Daro bottom banner container -->
<FrameLayout
android:id="@+id/fl_daro_banner_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="32dp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,52 +0,0 @@
package kr.co.vividnext.sodalive.live.room
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class LiveRoomDaroLightPopupPolicyTest {
@Test
fun `비방장 무료 라이브에서 아직 시도하지 않았으면 라이트 팝업을 노출한다`() {
assertTrue(
shouldAttemptLiveRoomDaroLightPopup(
isHost = false,
roomPrice = 0,
hasAttemptedPopup = false
)
)
}
@Test
fun `방장이면 무료 라이브여도 라이트 팝업을 노출하지 않는다`() {
assertFalse(
shouldAttemptLiveRoomDaroLightPopup(
isHost = true,
roomPrice = 0,
hasAttemptedPopup = false
)
)
}
@Test
fun `유료 라이브면 비방장이어도 라이트 팝업을 노출하지 않는다`() {
assertFalse(
shouldAttemptLiveRoomDaroLightPopup(
isHost = false,
roomPrice = 100,
hasAttemptedPopup = false
)
)
}
@Test
fun `이미 시도한 액티비티 인스턴스에서는 다시 노출하지 않는다`() {
assertFalse(
shouldAttemptLiveRoomDaroLightPopup(
isHost = false,
roomPrice = 0,
hasAttemptedPopup = true
)
)
}
}

View File

@@ -12,7 +12,6 @@ buildscript {
dependencies { dependencies {
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'so.daro:daro-plugin:1.0.13'
} }
} }

View File

@@ -0,0 +1,40 @@
# 20260420 DARO 광고 제거
## 작업 체크리스트
- [x] DARO 광고 관련 실제 참조 범위를 확정한다.
QA: `build.gradle`, `app/build.gradle`, `app/proguard-rules.pro`, `SodaLiveApp.kt`, `MyPageFragment.kt`, `fragment_my.xml`, `LiveRoomActivity.kt`, `LiveRoomViewModel.kt`, `LiveRoomDaroLightPopupPolicy.kt`, `LiveRoomDaroLightPopupPolicyTest.kt`, `.gitignore`, 관련 `docs/*.md`의 DARO 참조 여부를 근거로 설명할 수 있어야 한다.
- [x] Gradle/설정 레벨의 DARO 의존성과 키 관련 설정을 제거한다.
QA: 루트/plugin/module 의존성, `daroAppKey` 읽기, `.gitignore`의 DARO 키 파일 예외, `app/proguard-rules.pro`의 DARO 전용 규칙이 제거되어야 한다.
- [x] 앱 초기화와 광고 노출 UI/로직에서 DARO 코드를 제거한다.
QA: `SodaLiveApp`의 SDK 초기화, `MyPageFragment` 배너, `fragment_my.xml` 배너 컨테이너, `LiveRoomActivity` 라이트 팝업 관련 코드가 제거되어야 한다.
- [x] DARO 제거 후 불필요해진 보조 코드와 테스트를 정리한다.
QA: `LiveRoomViewModel`의 DARO 전용 조회 메서드, `LiveRoomDaroLightPopupPolicy.kt`, `LiveRoomDaroLightPopupPolicyTest.kt`가 정리되어야 한다.
- [x] DARO 관련 작업 문서를 정리하고 검증 기록을 남긴다.
QA: DARO 기능 추가용 문서(`20260420_Daro광고기본세팅.md`, `20260420_마이페이지배너광고추가.md`, `20260420_무료라이브라이트팝업광고적용.md`)를 제거하고, 의존성 이력 문서(`20260420_BlurTransformation오류수정.md`)는 과거 원인 기록으로 유지한 채 이 문서 하단에 검증 결과를 누적 기록해야 한다.
## 범위 메모
- 요청 해석은 "저장소에 남아 있는 DARO 광고 관련 코드/설정/전용 문서 제거"로 한정한다.
- `local.properties``daroAppKey`는 로컬 비추적 환경 설정이므로 이번 저장소 변경 범위에서는 제외한다.
- `coreLibraryDesugaringEnabled`, `desugar_jdk_libs`, 외부 저장소 설정은 현재 근거만으로 DARO 전용이라고 단정할 수 없어 유지한다.
## 검증 계획
- `grep`으로 저장소 내 `Daro|daro|DARO` 참조를 재검색해 잔존 항목을 확인한다.
- `./gradlew :app:testDebugUnitTest`를 실행해 단위 테스트 회귀를 확인한다.
- `./gradlew :app:assembleDebug`를 실행해 앱 빌드 성공을 확인한다.
- 필요 시 `git diff`로 DARO 제거 범위가 요청 범위를 넘지 않았는지 수동 확인한다.
## 검증 기록
- 2026-04-20
- 무엇: DARO 광고 관련 Gradle 의존성/플러그인, 앱 초기화, 마이페이지 배너, 라이브룸 라이트 팝업, 전용 정책/테스트, 로컬 DARO 키 파일, 관련 기능 문서를 제거했다.
- 왜: 저장소에서 더 이상 DARO 광고 SDK와 그 진입 경로가 남지 않도록 요청 범위를 코드/설정/전용 문서 기준으로 정리해야 했기 때문이다.
- 어떻게:
- 수정 파일: `build.gradle`, `app/build.gradle`, `app/proguard-rules.pro`, `.gitignore`, `app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt`, `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/live/room/LiveRoomViewModel.kt`, `docs/20260420_DARO광고제거.md`
- 삭제 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDaroLightPopupPolicy.kt`, `app/src/test/java/kr/co/vividnext/sodalive/live/room/LiveRoomDaroLightPopupPolicyTest.kt`, `docs/20260420_Daro광고기본세팅.md`, `docs/20260420_마이페이지배너광고추가.md`, `docs/20260420_무료라이브라이트팝업광고적용.md`, `app/daro-key.txt`
- 유지 항목: `local.properties``daroAppKey`는 로컬 비추적 설정이라 저장소 변경 대상에서 제외했고, `coreLibraryDesugaringEnabled`, `desugar_jdk_libs`, 외부 저장소 설정은 DARO 전용 근거가 없어 유지했다.
- 실행 명령: `grep -R -nE "Daro|daro|DARO"`에 해당하는 저장소 재검색
- 결과: 코드/설정 대상(`*.kt`, `*.gradle`, `*.xml`, `*.pro`, `*.gitignore`)에서는 DARO 참조가 0건이었다. 남은 문자열은 `docs/20260420_DARO광고제거.md`와 과거 이력 문서 `docs/20260420_BlurTransformation오류수정.md`뿐이다.
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
- 결과: `BUILD SUCCESSFUL`. 단위 테스트와 debug 빌드가 모두 성공했고, Agora/Appsflyer 관련 기존 경고만 출력됐다.
- 실행 명령: `git status --short`
- 결과: DARO 제거와 직접 연결된 파일만 수정/삭제된 것을 수동 확인했다.
- 진단 도구: Kotlin(`.kt`)용 LSP 서버 미구성으로 `lsp_diagnostics` 실행 불가 확인

View File

@@ -1,28 +0,0 @@
# 20260420 Daro 광고 기본 세팅
## 작업 체크리스트
- [x] 공식 Daro Android 가이드를 기준으로 `Non-Reward` + Android View 전용 기본 세팅 범위를 확정한다.
QA: Compose 전용 의존성/예제는 제외되고, `daro-a`, plugin, key 설정, SDK 초기화, ProGuard 규칙만 반영되어야 한다.
- [x] `settings.gradle`, `build.gradle`, `app/build.gradle`에 Daro 저장소/플러그인/SDK 및 desugaring 설정을 추가한다.
QA: 공식 문서 버전(`daro-plugin:1.0.13`, `daro-a:1.5.3`)과 minSdk 23 대응 desugaring 설정이 반영되어야 한다.
- [x] `SodaLiveApp`에 Daro SDK 초기화를 추가한다.
QA: 광고 로드 이전 `Application.onCreate()`에서 `Daro.init(...)`가 호출되어야 한다.
- [x] `app/proguard-rules.pro`에 공식 Non-Reward ProGuard/R8 규칙을 반영한다.
QA: Daro 문서의 Non-Reward keep/dontwarn 규칙이 파일에 추가되어야 한다.
- [x] Daro 키 파일/앱 키의 비밀값 커밋 방지 경로를 보강한다.
QA: `app/daro-key.txt`, `app/android-daro-key.txt` 및 flavor/buildType 분기 키 파일이 git 추적 대상에서 제외되어야 한다.
- [x] 변경 사항을 빌드로 검증하고 결과를 기록한다.
QA: `:app:assembleDebug`, `:app:assembleRelease` 결과와 비밀값 의존 여부를 문서 하단 검증 기록에 남겨야 한다.
## 검증 기록
- 2026-04-20
- 무엇: Daro Android 기본 세팅(Non-Reward, Android View 전용)을 위해 저장소/플러그인/SDK/desugaring 설정, `SodaLiveApp` 초기화, Non-Reward ProGuard 규칙, 키 파일 ignore 경로를 반영했다.
- 왜: 공식 `get-started` 가이드 기준으로 Compose 의존성을 제외한 Android View 앱용 기본 연동과 난독화 설정이 필요했다.
- 어떻게:
- 수정 파일: `settings.gradle`, `build.gradle`, `app/build.gradle`, `.gitignore`, `app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt`, `app/proguard-rules.pro`
- 실행 명령: `./gradlew :app:assembleDebug`
- 결과: `DARO` 플러그인 적용과 의존성 해상도는 진행됐지만, 로컬 키 파일 `app/daro-key.txt`를 읽는 과정에서 `Tag mismatch!`가 발생해 `설정 파일이 유효하지 않습니다`로 실패했다.
- 실행 명령: `./gradlew :app:assembleRelease`
- 결과: release만 요청해도 플러그인 설정 단계에서 `debug` variant 키 검증이 먼저 수행됐고, 동일하게 `app/daro-key.txt``Tag mismatch!`로 실패했다. 따라서 현재 차단점은 ProGuard/R8 규칙이 아니라 로컬 Daro 키 파일/앱 키 정합성이다.
- 메모: 최신 문서는 `android-daro-key.txt`를 안내하지만, 실제 플러그인은 레거시 `app/daro-key.txt`도 읽고 있었다. 현재 로컬에 존재하는 키 파일과 `daroAppKey`가 서로 맞지 않거나 파일 자체가 유효하지 않아 빌드 검증이 차단됐다.
- 진단 도구: Kotlin(`.kt`)용 LSP 서버 미구성으로 `lsp_diagnostics` 실행 불가 확인

View File

@@ -1,44 +0,0 @@
# 20260420 마이페이지 배너 광고 추가
## 작업 체크리스트
- [x] Daro 공식 Android 배너 가이드와 현재 프로젝트의 광고 기본 세팅 상태를 확인한다.
QA: 배너 뷰 타입, 필수 값, 기존 SDK 초기화/플러그인 상태를 근거 파일로 설명할 수 있어야 한다.
- [x] `fragment_my.xml` 최하단에 배너 광고 영역을 추가한다.
QA: 기존 MyPage 콘텐츠 하단에 배너 광고 뷰가 배치되고, 화면 레이아웃을 깨지 않아야 한다.
- [x] `MyPageFragment.kt`에 배너 광고 로드/정리 로직과 임시 설정값 위치 안내를 추가한다.
QA: 사용자가 교체해야 하는 값이 코드에서 명확히 드러나고, Fragment 생명주기에 맞는 정리 처리가 있어야 한다.
- [x] 변경 사항을 진단하고 필요한 Gradle 검증을 수행한 뒤 결과를 기록한다.
QA: 변경 파일 진단 결과와 실행한 검증 명령/결과가 문서 하단에 누적 기록되어야 한다.
## 임시 설정값 위치
- `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`
- `DARO_BANNER_AD_UNIT_KEY`
- `DARO_BANNER_PLACEMENT`
- 위 두 상수를 실제 Daro 배너 값으로 교체하면 된다.
## 검증 기록
- 2026-04-20
- 무엇: `fragment_my.xml` 최하단에 Daro 배너 컨테이너를 추가하고, `MyPageFragment`에서 `DaroBannerAdView`를 생성해 로드/정리하도록 구현했다.
- 왜: 공식 Daro Android 배너 가이드가 XML 커스텀 태그 대신 컨테이너 뷰 + 코드 생성 방식의 `DaroBannerAdView` 사용을 요구했고, 요청 위치가 MyPage 최하단이었기 때문이다.
- 어떻게:
- 수정 파일: `app/src/main/res/layout/fragment_my.xml`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`, `docs/20260420_마이페이지배너광고추가.md`
- 임시 값 위치: `MyPageFragment.kt``DARO_BANNER_AD_UNIT_KEY`, `DARO_BANNER_PLACEMENT`
- 진단 도구: Kotlin/XML LSP 서버 미구성으로 `lsp_diagnostics`는 실행 불가였다.
- 실행 명령: `./gradlew :app:assembleDebug`
- 결과: 성공. Daro 플러그인 설정과 앱 debug 빌드가 모두 완료됐다.
- 실행 명령: `./gradlew :app:ktlintCheck`
- 결과: 실패. 이번 변경 파일이 아니라 기존 파일들(`NicknameUpdateViewModel.kt`, `ProfileUpdateActivity.kt`, `RecentContentDao.kt` 등)의 스타일 오류 때문에 `:app:ktlintMainSourceSetCheck`가 실패했다.
- 실행 명령: `adb devices`
- 결과: 연결된 기기가 없어 실제 화면 수동 확인은 이 환경에서 진행하지 못했다.
- 2026-04-20
- 무엇: MyPage Daro 배너의 `No fill` 원인을 추가 조사하고, 배너 로드 실패 시 진단 로그가 남도록 `MyPageFragment`를 보강했다.
- 왜: 공식 Daro 문서와 SDK 소스 기준으로 `No fill`은 코드 버그보다 `app-ads.txt`, 앱/광고 단위 Live 상태, 등록 패키지 불일치 같은 송출 조건 이슈 가능성이 더 높았고, 기존 코드는 실패를 조용히 숨기고 있었다.
- 어떻게:
- 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`, `docs/20260420_마이페이지배너광고추가.md`
- 조사 근거: Daro 공식 FAQ/배너 가이드, SDK `DaroAdLoader.kt``no fill` 처리 메시지, debug merged manifest의 패키지 `kr.co.vividnext.sodalive.debug`, release merged manifest의 패키지 `kr.co.vividnext.sodalive`
- 확인 사항: 저장소 내 `app-ads.txt` 파일은 없었고, debug 빌드는 `applicationIdSuffix '.debug'`로 인해 release 패키지와 다른 앱 ID를 사용한다.
- 적용 내용: `No fill` 발생 시 현재 `APPLICATION_ID`, placement, 에러 코드/메시지와 함께 `app-ads.txt`, Live 상태, 등록 패키지 확인이 필요하다는 로그를 남기고, debug 패키지(`.debug`)면 별도 등록 또는 release 패키지 테스트 필요 로그를 추가했다.
- 실행 명령: `./gradlew :app:assembleDebug`
- 결과: 성공. 진단 로그 추가 후에도 debug 빌드는 정상 완료됐다.
- 실행 명령: `./gradlew :app:ktlintCheck`
- 결과: 성공. `MyPageFragment.kt`의 로그 문자열 줄 길이를 정리한 뒤 lint를 통과했다.

View File

@@ -1,44 +0,0 @@
# 20260420 무료 라이브 라이트 팝업 광고 적용
## 작업 체크리스트
- [x] 관련 모듈과 기존 패턴, 무료 라이브 판별값, 광고 연동 조건을 조사한다.
QA: `LiveRoomActivity`, `GetRoomInfoResponse`, `GetRoomDetailResponse`, Daro 공식 Light Popup 가이드에서 조건 근거를 확인해야 한다.
- [x] 무료 라이브 라이트 팝업 노출 정책 테스트를 추가한다.
QA: 비방장 + 무료 + 미시도 조합만 허용되고, 나머지 조합은 모두 차단되어야 한다.
- [x] `LiveRoomActivity``LiveRoomViewModel`에 Light Popup 연동을 최소 변경으로 구현한다.
QA: 방장이 아니고 무료 라이브일 때만 Daro Light Popup 시도 경로가 존재해야 하며, 같은 액티비티 인스턴스에서 중복 시도되지 않아야 한다.
- [x] Ad Unit ID를 추후 입력할 수 있도록 단일 수정 지점을 만든다.
QA: 사용자가 수정해야 하는 상수 위치를 코드에서 즉시 확인할 수 있어야 한다.
- [x] 변경 사항을 진단하고 관련 Gradle 검증을 수행한 뒤 결과를 기록한다.
QA: 관련 단위 테스트와 `:app:assembleDebug` 결과를 문서 하단 검증 기록에 남겨야 한다.
## 구현 메모
- `LiveRoomActivity``GetRoomInfoResponse`에는 `creatorId`는 있지만 `price`/`isPaid`가 없어 무료 여부를 직접 판별할 수 없다.
- 무료 여부는 기존 `LiveRepository.getRoomDetail(roomId, token)` 경로의 `GetRoomDetailResponse.price`로 판별한다.
- 유료 라이브 입장 여부를 뜻하는 `GetRoomDetailResponse.isPaid`는 무료 판별값이 아니므로 광고 게이트에는 사용하지 않는다.
- Daro SDK는 이미 앱 전역에서 초기화되어 있으므로 `LiveRoomActivity`에는 Light Popup 로드/표시 로직만 추가한다.
- Ad Unit ID와 placement는 `LiveRoomActivity` companion object의 상수로 두어 사용자가 추후 값만 교체할 수 있게 한다.
## 검증 기록
- 2026-04-20
- 무엇: `LiveRoomActivity`에 무료 라이브 비방장 전용 Daro Light Popup을 추가하기 위한 조건, SDK 사용 방식, 최소 수정 범위를 조사했다.
- 왜: 현재 액티비티 응답에는 무료/유료 필드가 없어 별도 상세 응답 확인이 필요하고, Daro Light Popup은 공식 클래스/콜백명을 확인한 뒤 연결해야 하기 때문이다.
- 어떻게:
- 조사 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`, `app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt`
- 조사 문서: `https://guide.daro.so/ko/sdk-integration/android/ad-formats/lightpopup`
- 확인 결과: 비방장 조건은 `creatorId != SharedPreferenceManager.userId`, 무료 조건은 `GetRoomDetailResponse.price == 0`, SDK 타입은 `DaroLightPopupAdUnit`, `DaroLightPopupAdLoader`, `DaroLightPopupAdListener` 경로로 확정했다.
- 2026-04-20
- 무엇: `LiveRoomActivity`에 무료 라이브 비방장 전용 Daro Light Popup 시도 경로를 추가하고, Ad Unit ID 입력 위치를 companion object 상수로 고정했다.
- 왜: 현재 `GetRoomInfoResponse`만으로는 무료 여부를 알 수 없어 별도 상세 조회가 필요했고, 사용자가 나중에 광고 단위 ID만 안전하게 교체할 수 있어야 했기 때문이다.
- 어떻게:
- 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDaroLightPopupPolicy.kt`, `app/src/test/java/kr/co/vividnext/sodalive/live/room/LiveRoomDaroLightPopupPolicyTest.kt`, `docs/20260420_무료라이브라이트팝업광고적용.md`
- Ad Unit ID 위치: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt``DARO_LIGHT_POPUP_AD_UNIT_KEY`
- 실행 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.live.room.LiveRoomDaroLightPopupPolicyTest"`
- 결과: 성공. 비방장/무료/중복시도 조건 테스트가 `BUILD SUCCESSFUL`로 통과했다.
- 실행 명령: `./gradlew :app:assembleDebug`
- 결과: 성공. debug APK가 `BUILD SUCCESSFUL`로 생성됐다.
- 실행 명령: `./gradlew :app:ktlintCheck`
- 결과: 실패. 이번 변경과 무관한 기존 파일(`SodaLiveApp.kt`, 기존 `LiveRoomActivity.kt` 누적 포맷 위반) 때문에 `:app:ktlintMainSourceSetCheck`가 실패했다.
- 진단 도구: Kotlin(`.kt`)용 LSP 서버 미구성으로 변경 Kotlin 파일의 `lsp_diagnostics`는 실행 불가였고, Markdown 문서는 진단 이슈가 없었다.
- 실행 명령: `adb devices`, `adb devices -l`, `./gradlew :app:installDebug`
- 결과: 장치 연결 상태가 일시적으로 변동됐고 최종적으로 연결된 기기가 없어 `installDebug` 수동 QA를 완료하지 못했다. 따라서 실기기 광고 노출 검증은 [blocked] 상태다.