Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ android {
applicationId = "com.kuit.findu"
minSdk = 28
targetSdk = 35
versionCode = 14
versionName = "1.0.13"
versionCode = 15
versionName = "1.1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "GPT_KEY", properties["GPT_KEY"].toString())
Expand Down Expand Up @@ -53,7 +53,9 @@ android {
buildTypes {
release {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("release")
buildConfigField("String", "BASE_URL", properties["base.url.release"].toString())
buildConfigField("String", "ADMOB_BANNER_ID", properties["ADMOB_BANNER_ID_RELEASE"].toString())
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
Expand All @@ -64,7 +66,7 @@ android {
isDebuggable = true
versionNameSuffix = "-DEBUG"
buildConfigField("String", "BASE_URL", properties["base.url.dev"].toString())

buildConfigField("String", "ADMOB_BANNER_ID", properties["ADMOB_BANNER_ID_DEBUG"].toString())
}
}
buildFeatures {
Expand Down Expand Up @@ -169,4 +171,7 @@ dependencies {
implementation(libs.firebase.analytics.ktx)
implementation(libs.firebase.config.ktx)
implementation(libs.firebase.crashlytics)

// AdMob
implementation("com.google.android.gms:play-services-ads:23.1.0")
Comment on lines +174 to +176
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if gradle/libs.versions.toml exists and explore build structure
fd libs.versions.toml

# Also check the app/build.gradle.kts file to understand dependency usage
head -200 app/build.gradle.kts | grep -E "(implementation|libs\.|version)"

Repository: FindYou-Kuit/FindYou-Android

Length of output: 2875


🌐 Web query:

Google Play Services Ads SDK latest version 2025 Android

💡 Result:

The latest Google Play services Ads SDK artifact (com.google.android.gms:play-services-ads) released in 2025 is 24.9.0 (released 2025‑12‑11). [1][2]

Gradle:

implementation "com.google.android.gms:play-services-ads:24.9.0"

[2]


AdMob SDK 버전을 업데이트하고 버전 카탈로그 사용을 권장해요.

AdMob SDK 버전 23.1.0은 현재 많이 구버전이고, 최신 버전은 24.9.0입니다. 또한 다른 의존성들은 libs.* 버전 카탈로그를 사용하고 있는데, AdMob SDK만 하드코딩된 문자열로 되어 있어서 일관성 차원에서도 개선이 필요해요.

🔧 제안하는 수정

gradle/libs.versions.toml에 추가:

[versions]
playServicesAds = "24.9.0"

[libraries]
play-services-ads = { group = "com.google.android.gms", name = "play-services-ads", version.ref = "playServicesAds" }

그 후 app/build.gradle.kts에서:

     // AdMob
-    implementation("com.google.android.gms:play-services-ads:23.1.0")
+    implementation(libs.play.services.ads)
🤖 Prompt for AI Agents
In `@app/build.gradle.kts` around lines 174 - 176, Update the hardcoded AdMob
dependency to use the version catalog and bump to the latest SDK: add a
playServicesAds version "24.9.0" and a play-services-ads library entry in
gradle/libs.versions.toml, then replace the
implementation("com.google.android.gms:play-services-ads:23.1.0") line in
app/build.gradle.kts with the catalog reference (libs.play-services-ads) so the
project uses version cataloging and the updated 24.9.0 SDK.

}
4 changes: 4 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**

# ========== AdMob ==========
-keep class com.google.android.gms.ads.** { *; }
-dontwarn com.google.android.gms.ads.**

# ========== Jetpack Compose ==========
-keep class androidx.compose.** { *; }
-keep class androidx.compose.runtime.** { *; }
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
android:name="com.kakao.sdk.AppKey"
android:value="${KAKAO_NATIVE_APP_KEY_MANIFEST}" />

<!-- AdMob App ID -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-7675272869453438~5374193050" />

<!-- Kakao Login -->
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/kuit/findu/FindUApp.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.kuit.findu

import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import com.google.android.gms.ads.MobileAds
import com.kakao.sdk.common.KakaoSdk
import com.naver.maps.map.NaverMapSdk
import dagger.hilt.android.HiltAndroidApp
Expand All @@ -10,8 +12,14 @@ class FindUApp : Application() {
override fun onCreate() {
super.onCreate()

// 항상 라이트 모드 강제
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)

KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
NaverMapSdk.getInstance(this).client =
NaverMapSdk.NcpKeyClient(BuildConfig.NAVER_CLIENT_ID)

// AdMob 초기화
MobileAds.initialize(this)
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/com/kuit/findu/analytics/AnalyticsEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ data class AnalyticsEvent(
companion object {
const val SCREEN_VIEW = "screen_view" // TYPE
const val SCREEN_NAME = "screen_name" // EXTRA_KEY

// AdMob Events
const val AD_CLICK = "ad_click"
const val AD_IMPRESSION = "ad_impression"
const val AD_LOCATION = "ad_location"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.kuit.findu.presentation.ui.common

import android.os.Bundle
import android.util.Log
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.gms.ads.AdListener
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView
import com.google.android.gms.ads.LoadAdError
import com.google.firebase.analytics.FirebaseAnalytics
import com.kuit.findu.BuildConfig
import com.kuit.findu.analytics.AnalyticsEvent

private val AD_UNIT_ID = BuildConfig.ADMOB_BANNER_ID

@Composable
fun AdBanner(
modifier: Modifier = Modifier,
adLocation: String = "unknown"
) {
AndroidView(
modifier = modifier.fillMaxWidth(),
factory = { context ->
val analytics = FirebaseAnalytics.getInstance(context)

AdView(context).apply {
setAdSize(AdSize.BANNER)
adUnitId = AD_UNIT_ID
adListener = object : AdListener() {
override fun onAdClicked() {
analytics.logEvent(AnalyticsEvent.AD_CLICK, Bundle().apply {
putString(AnalyticsEvent.AD_LOCATION, adLocation)
})
}

override fun onAdImpression() {
analytics.logEvent(AnalyticsEvent.AD_IMPRESSION, Bundle().apply {
putString(AnalyticsEvent.AD_LOCATION, adLocation)
})
}

override fun onAdFailedToLoad(error: LoadAdError) {
Log.e("AdBanner", "광고 로드 실패: ${error.code} - ${error.message}")
}

override fun onAdLoaded() {
Log.d("AdBanner", "광고 로드 성공")
}
}
loadAd(AdRequest.Builder().build())
}
Comment on lines +3 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Jetpack Compose AndroidView lifecycle AdView destroy cleanup best practice

💡 Result:

For an AdMob AdView inside AndroidView, the cleanup best practice is:

  1. Create/load the AdView in factory (once) and update only cheap state in update (which can run many times). (developer.android.com)
  2. Call adView.destroy() from AndroidView(onRelease = …), because onRelease is specifically for “this View has exited the composition entirely and will not be reused again”. (developer.android.com)
  3. Tie pause()/resume() to the host lifecycle (e.g., via a LifecycleEventObserver), since Google’s reference shows resume() in onResume, pause() in onPause, and destroy() in onDestroy. (developers.google.com)
  4. If the AndroidView can be reused (lists / movable content), optionally implement onReset for “about to be reused in a different context” state-reset, and keep heavy cleanup for onRelease. (developer.android.com)

Compose example (recommended pattern)

`@Composable`
fun BannerAd(
  adUnitId: String,
  adSize: AdSize,
  modifier: Modifier = Modifier,
) {
  val lifecycleOwner = LocalLifecycleOwner.current

  AndroidView(
    modifier = modifier,
    factory = { context ->
      AdView(context).apply {
        setAdUnitId(adUnitId)
        setAdSize(adSize)
        loadAd(AdRequest.Builder().build())
      }
    },
    update = { adView ->
      // Optional: attach lifecycle-driven pause/resume
      val lifecycle = lifecycleOwner.lifecycle
      val observer = LifecycleEventObserver { _, event ->
        when (event) {
          Lifecycle.Event.ON_RESUME -> adView.resume()
          Lifecycle.Event.ON_PAUSE -> adView.pause()
          else -> Unit
        }
      }

      // Ensure we don't accumulate observers across recompositions
      adView.setTag(R.id.adview_lifecycle_observer, observer)
      lifecycle.addObserver(observer)
    },
    onReset = { adView ->
      // If you opt into reuse, reset transient state here (don’t destroy).
      adView.pause()
    },
    onRelease = { adView ->
      // Permanent removal: free resources here.
      adView.destroy()
    }
  )
}

Key point: use onRelease for destroy() (permanent removal), and don’t call destroy() from update (runs repeatedly) or onReset (reuse path). (developer.android.com)

Citations:


🌐 Web query:

Compose AndroidView factory disposal lifecycle management

💡 Result:

In AndroidView(...), the factory creates the View, and Compose manages that View instance across recompositions until it is either reused, temporarily detached, or permanently released. (developer.android.com)

What runs when

factory

  • Called only when a new View instance is needed (first time, or after the prior one was permanently released).
  • Not called on every recomposition. (developer.android.com)

update

  • Called after creation and on recompositions to push state into the existing View. (developer.android.com)

onReset (reuse signal)

  • Called when Compose intends to reuse the same View instance in a new context (e.g., movable content / ReusableContentHost).
  • If you don’t provide onReset, Compose treats the view as not reusable and will discard it when moved/removed. (developer.android.com)

Temporary detach can happen

  • Compose may remove the View from its parent but keep it around for possible reuse (deactivated, not yet released). (developer.android.com)

onRelease (permanent disposal)

  • Called when the View exits the composition entirely and will never be reused.
  • After onRelease, if the composable appears again later, Compose will call factory again to create a fresh View. (developer.android.com)

Practical pattern (cleanup goes in onRelease)

AndroidView(
  factory = { context ->
    MapView(context).apply { onCreate(null) }
  },
  update = { view ->
    // push state into the view
  },
  onReset = { view ->
    // prepare for reuse (clear listeners/transient state, cancel gestures, etc.)
  },
  onRelease = { view ->
    // free resources; this is the "disposed forever" callback
    view.onPause()
    view.onDestroy()
  }
)

(What you do in onRelease depends on the specific View API; the key is: treat onRelease as final disposal.) (developer.android.com)

If you need “is it currently attached/active?”

  • Use View.addOnAttachStateChangeListener, or access the host lifecycle via findViewTreeLifecycleOwner() (valid once attached). (developer.android.com)

Citations:


AdView 라이프사이클 정리가 없어 리소스 누수 위험이 있습니다.

Composable에서 AdView를 생성했는데 destroy 처리가 없어 리소스 누수 가능성이 있습니다. AndroidViewonRelease 콜백을 사용해서 정리해 주세요.

🛠️ 수정 제안
 import android.os.Bundle
 import android.util.Log
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
 
 private val AD_UNIT_ID = BuildConfig.ADMOB_BANNER_ID
 
@@ -20,6 +24,7 @@ fun AdBanner(
     modifier: Modifier = Modifier,
     adLocation: String = "unknown"
 ) {
+    val lifecycleOwner = LocalLifecycleOwner.current
     AndroidView(
         modifier = modifier.fillMaxWidth(),
         factory = { context ->
@@ -47,6 +52,27 @@ fun AdBanner(
                 loadAd(AdRequest.Builder().build())
             }
         }
+        update = { adView ->
+            val lifecycle = lifecycleOwner.lifecycle
+            val observer = LifecycleEventObserver { _, event ->
+                when (event) {
+                    Lifecycle.Event.ON_RESUME -> adView.resume()
+                    Lifecycle.Event.ON_PAUSE -> adView.pause()
+                    else -> Unit
+                }
+            }
+            adView.setTag(R.id.adview_lifecycle_observer, observer)
+            lifecycle.addObserver(observer)
+        }
+        onRelease = { adView ->
+            adView.destroy()
+        }
     )
 }
🤖 Prompt for AI Agents
In `@app/src/main/java/com/kuit/findu/presentation/ui/common/AdBanner.kt` around
lines 3 - 55, The AdView created in the AdBanner composable is never cleaned up;
update the AndroidView call inside AdBanner to supply an onRelease callback that
takes the created AdView and calls its destroy() (and optionally clears
adListener = null) to prevent resource leaks — locate the AndroidView factory
that constructs the AdView, add the onRelease parameter, and ensure you call
adView.destroy() (and null out adListener) in that callback.

}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.kuit.findu.R
import com.kuit.findu.presentation.ui.base.BaseVectorIcon
import com.kuit.findu.presentation.ui.common.AdBanner
import com.kuit.findu.presentation.util.WebViewUrl.CENTER_URL
import com.kuit.findu.presentation.util.WebViewUrl.PART_URL
import com.kuit.findu.ui.theme.FindUTheme
Expand Down Expand Up @@ -51,15 +52,9 @@ fun HomeWebLinkList(modifier: Modifier = Modifier) {
buttonText = "종합유기견보호센터 바로가기",
webUrl = CENTER_URL
)
Spacer(modifier = Modifier.height(60.dp))
Text(
text = "서비스 관련 정보 \n" +
"문의처는 어쩌고 저쩌고 \n" +
"전화번호 00-10-3-20442",
style = FindUTheme.typography.captionRegular12,
color = FindUTheme.colors.gray5
)

Spacer(modifier = Modifier.height(40.dp))
AdBanner(adLocation = "home_web_link")
Spacer(modifier = Modifier.height(20.dp))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.kuit.findu.presentation.ui.home.component.HomeReportedAnimalList
import com.kuit.findu.presentation.ui.home.component.HomeTopBar
import com.kuit.findu.presentation.ui.home.component.HomeWebLinkList
import com.kuit.findu.presentation.ui.home.viewmodel.HomeUiState
import com.kuit.findu.presentation.ui.common.AdBanner
import com.kuit.findu.ui.theme.FindUTheme
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.rememberPagerState
Expand Down Expand Up @@ -131,6 +132,12 @@ fun HomeScreen(
bannerList = bannerList,
pagerState = pagerState
)
Spacer(modifier = Modifier.height(15.dp))

AdBanner(
modifier = Modifier.padding(horizontal = 20.dp),
adLocation = "home_screen"
)
Spacer(modifier = Modifier.height(30.dp))
}
}
Expand Down
30 changes: 30 additions & 0 deletions app/src/main/java/com/kuit/findu/presentation/ui/my/MyFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ import com.kuit.findu.presentation.ui.my.dialog.MyProfileImageDialog
import com.kuit.findu.presentation.ui.my.dialog.MyWithdrawalDialog
import com.kuit.findu.presentation.ui.my.model.ProfileImageType
import com.kuit.findu.presentation.ui.my.viewmodel.MyViewModel
import com.google.android.gms.ads.AdListener
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.LoadAdError
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.remoteconfig.remoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings
import com.kuit.findu.analytics.AnalyticsEvent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -76,6 +82,30 @@ class MyFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)

observeViewModel()
setupAdView()
}

private fun setupAdView() {
val analytics = FirebaseAnalytics.getInstance(requireContext())

binding.adViewMy.adListener = object : AdListener() {
override fun onAdClicked() {
analytics.logEvent(AnalyticsEvent.AD_CLICK, android.os.Bundle().apply {
putString(AnalyticsEvent.AD_LOCATION, "my_page")
})
}

override fun onAdImpression() {
analytics.logEvent(AnalyticsEvent.AD_IMPRESSION, android.os.Bundle().apply {
putString(AnalyticsEvent.AD_LOCATION, "my_page")
})
}

override fun onAdFailedToLoad(error: LoadAdError) {
// 광고 로드 실패 시 처리
}
}
binding.adViewMy.loadAd(AdRequest.Builder().build())
}

private fun initListener() {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/layout/fragment_my.xml
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,13 @@

</ScrollView>

<com.google.android.gms.ads.AdView
android:id="@+id/ad_view_my"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:adSize="BANNER"
app:adUnitId="ca-app-pub-7675272869453438/1213424875"
app:layout_constraintBottom_toBottomOf="parent" />
Comment on lines +465 to +471
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

스크롤 영역과 광고가 겹칠 수 있습니다.

현재 ScrollView가 parent 하단까지 차지하고 AdView도 하단에 붙어 있어서, 콘텐츠가 광고 뒤로 가려질 수 있어요. ScrollView의 하단 제약을 AdView 상단으로 옮겨 주세요.

🛠️ 수정 제안
-    <ScrollView
+    <ScrollView
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/ad_view_my"
         app:layout_constraintTop_toBottomOf="@id/cl_my_top_container"
         app:layout_constraintVertical_bias="0.0"
         tools:layout_editor_absoluteX="72dp">
🤖 Prompt for AI Agents
In `@app/src/main/res/layout/fragment_my.xml` around lines 465 - 471, The AdView
with id ad_view_my is constrained to the parent bottom and can overlap the
ScrollView content; update the ScrollView's bottom constraint so its bottom is
anchored to the top of ad_view_my instead of the parent. Locate the ScrollView
element in the layout and change its bottom constraint (e.g.
layout_constraintBottom_toBottomOf="parent") to reference the AdView
(layout_constraintBottom_toTopOf="@id/ad_view_my") so the scrollable content
ends above the ad while keeping ad_view_my anchored to the parent's bottom.


</androidx.constraintlayout.widget.ConstraintLayout>

Loading