diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8bd75f72..351241bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,6 +43,9 @@ android { buildConfigField("String", "BACKEND_BASE_URL", "\"$backendBaseUrl\"") val aiBaseUrl = localProperties.getProperty("AI_BASE_URL") buildConfigField("String", "AI_BASE_URL", "\"$aiBaseUrl\"") + + val admobAppId = localProperties.getProperty("ADMOB_APP_ID") ?: "ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" + manifestPlaceholders["ADMOB_APP_ID"] = admobAppId } buildTypes { @@ -162,6 +165,8 @@ dependencies { //Timber implementation("com.jakewharton.timber:timber:5.0.1") + + implementation("com.google.android.gms:play-services-ads:23.6.0") } ksp { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1a83c497..a3a97f36 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,10 @@ android:supportsRtl="true" android:theme="@style/Theme.Egobook" android:usesCleartextTraffic="true"> + + - \ No newline at end of file + diff --git a/app/src/main/java/com/egobook/app/MainActivity.kt b/app/src/main/java/com/egobook/app/MainActivity.kt index 82b0ac6e..30f81503 100644 --- a/app/src/main/java/com/egobook/app/MainActivity.kt +++ b/app/src/main/java/com/egobook/app/MainActivity.kt @@ -1,6 +1,7 @@ package com.egobook.app import android.os.Bundle +import android.util.Log import android.view.View import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity @@ -10,6 +11,12 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.egobook.app.databinding.ActivityMainBinding +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.rewarded.RewardedAd +import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback +import com.google.android.gms.ads.rewarded.ServerSideVerificationOptions import dagger.hilt.android.AndroidEntryPoint @@ -19,9 +26,17 @@ class MainActivity : AppCompatActivity(), BlurController, NotificationController ActivityMainBinding.inflate(layoutInflater) } + private var rewardedAd: RewardedAd? = null + private val adUnitId = "ca-app-pub-3940256099942544/5224354917" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + MobileAds.initialize(this) {} + + loadAd() + applyDefaultInsets(binding.main) applyDefaultInsets(binding.fcvNotificationDrawer) setContentView(binding.root) @@ -75,6 +90,42 @@ class MainActivity : AppCompatActivity(), BlurController, NotificationController } } + private fun loadAd() { + val adRequest = AdRequest.Builder().build() + RewardedAd.load(this, adUnitId, adRequest, object : RewardedAdLoadCallback() { + override fun onAdFailedToLoad(adError: LoadAdError) { + rewardedAd = null + Log.d("AdMob", "광고 로드 실패") + } + + override fun onAdLoaded(ad: RewardedAd) { + rewardedAd = ad + Log.d("AdMob", "광고 로드 성공!") + } + }) + } + + fun showAd(userId: String, onAdClosed: () -> Unit) { + if (rewardedAd != null) { + + val ssvOptions = ServerSideVerificationOptions.Builder() + .setUserId(userId) + + rewardedAd?.setServerSideVerificationOptions(ssvOptions.build()) + + rewardedAd?.show(this) { rewardItem -> + val rewardAmount = rewardItem.amount + val rewardType = rewardItem.type + Log.d("jang", "보상 지급! (서버로 콜백 날아감), $rewardType, rewardAmout: $rewardAmount") + } + onAdClosed() + rewardedAd = null + loadAd() + } else { + Log.d("jang", "아직 광고가 준비 안 됐어요. 잠시 후 다시 시도해주세요.") + } + } + override fun activateBlur(blurLevel: BlurLevel) { binding.blurView.apply { setBlurEnabled(true) @@ -93,4 +144,5 @@ class MainActivity : AppCompatActivity(), BlurController, NotificationController override fun closerDrawer() { binding.root.closeDrawer(binding.fcvNotificationDrawer) } + } diff --git a/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt b/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt index 6bc50469..cfbc77db 100644 --- a/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt +++ b/app/src/main/java/com/egobook/app/di/module/RepositoryModule.kt @@ -25,6 +25,7 @@ import com.egobook.app.ui.home.repository.NetworkHomeNotificationRepository import com.egobook.app.ui.home.repository.NetworkTendencyLevelService import com.egobook.app.ui.home.repository.NetworkUserRepository import com.egobook.app.ui.home.repository.UserActivityRepository +import com.egobook.app.ui.home.repository.UserAdRepository import com.egobook.app.ui.home.repository.UserPsychologyRepository import com.egobook.app.ui.home.repository.UserRepository import com.egobook.app.ui.home.repository.UserTendencyRepository @@ -84,6 +85,10 @@ abstract class RepositoryModule { @Singleton abstract fun bindActivityRecordRepository(impl: NetworkUserRepository): UserActivityRepository + @Binds + @Singleton + abstract fun bindAdRepository(impl: NetworkUserRepository): UserAdRepository + @Binds @Singleton abstract fun bindPsychologyRepository(impl: NetworkUserRepository): UserPsychologyRepository diff --git a/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt b/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt index feddde9d..be5b5c8b 100644 --- a/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt @@ -3,6 +3,8 @@ package com.egobook.app.ui.home import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.egobook.app.ui.home.repository.AdInfoDto +import com.egobook.app.ui.home.repository.UserAdRepository import com.egobook.app.ui.home.repository.UserPsychologyRepository import com.egobook.app.ui.home.repository.UserRepository import com.egobook.app.ui.home.user.Ink @@ -11,8 +13,10 @@ import com.egobook.app.ui.home.user.User import com.egobook.app.ui.shop.CustomItem import com.egobook.app.ui.shop.StoreRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -21,20 +25,26 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val userRepository: UserRepository, private val storeRepository: StoreRepository, + private val userAdRepository: UserAdRepository, private val psychologyRepository: UserPsychologyRepository ) : ViewModel() { + private val _uiState = MutableStateFlow(User(id=-1, Level(1), Ink(0))) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _adState = MutableStateFlow(AdInfoDto(0, 0, false, 0, "")) + val adState: StateFlow = _adState.asStateFlow() + + private val _equippedItems = MutableStateFlow>(emptyList()) + val equippedItems: StateFlow> = _equippedItems + + private val _dailyPhycologyReadState = MutableStateFlow(false) + val dailyPhycologyReadState = _dailyPhycologyReadState.asStateFlow() init { fetchUser() fetchEquipItems() fetchDailyPhycologyReadState() } - private val _uiState = MutableStateFlow(User(Level(1), Ink(0))) - val uiState: StateFlow = _uiState.asStateFlow() - private val _dailyPhycologyReadState = MutableStateFlow(false) - val dailyPhycologyReadState = _dailyPhycologyReadState.asStateFlow() - private val _equippedItems = MutableStateFlow>(emptyList()) - val equippedItems: StateFlow> = _equippedItems fun fetchEquipItems() { viewModelScope.launch { @@ -58,4 +68,19 @@ class HomeViewModel @Inject constructor( } } } + + fun loadCurrentAdInfo() { + viewModelScope.launch { + val adInfo = userAdRepository.loadAdInfo() + _adState.value = adInfo + } + } + + fun watchAd() { + viewModelScope.launch { + val message = userAdRepository.watchAd() + fetchUser() + Log.d("HomeViewModel", "Ad watched: $message") + } + } } diff --git a/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt b/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt index 4903f326..640493f6 100644 --- a/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt +++ b/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt @@ -5,6 +5,7 @@ import com.egobook.app.ui.home.user.Level import com.egobook.app.ui.home.user.User data class UserDto( + val userId: Int, val nickname: String, val level: Int, val ink: Int, @@ -13,5 +14,5 @@ data class UserDto( val isFirstAttendanceToday: Boolean, val attendanceRewardInk: Int ) { - fun toDomain(): User = User(Level(level), Ink(ink)) + fun toDomain(): User = User(id = userId, Level(level), Ink(ink)) } diff --git a/app/src/main/java/com/egobook/app/ui/home/repository/UserRepository.kt b/app/src/main/java/com/egobook/app/ui/home/repository/UserRepository.kt index f99ceb37..145705e1 100644 --- a/app/src/main/java/com/egobook/app/ui/home/repository/UserRepository.kt +++ b/app/src/main/java/com/egobook/app/ui/home/repository/UserRepository.kt @@ -5,7 +5,9 @@ import com.egobook.app.di.qualifier.BackendApi import com.egobook.app.ui.home.user.Tendency import com.egobook.app.ui.home.user.User import retrofit2.Retrofit +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import javax.inject.Inject import javax.inject.Singleton @@ -13,6 +15,12 @@ interface UserRepository { suspend fun load(): User } +interface UserAdRepository { + suspend fun loadAdInfo(): AdInfoDto + + suspend fun watchAd(): String +} + interface UserTendencyRepository { suspend fun loadTendencies(): List } @@ -32,6 +40,36 @@ interface NetworkTendencyLevelService { suspend fun loadTendencyLevels(): BaseResponse } +data class AdRequestDto( + val rewardType: String, + val targetId: Int?, + val adUnitId: String = "ca-app-pub-test/12345" +) + +data class AdResponseDto( + val code: String, + val message: String, + val status: Int +) + +interface NetworkAdService { + @GET("/ads/info") + suspend fun loadAdInfo(): BaseResponse + + @POST("/ads/testReward") + suspend fun watchAd( + @Body adRequest: AdRequestDto + ): AdResponseDto +} + +data class AdInfoDto( + val currentViewCount: Int, + val maxLimit: Int, + val isAvailable: Boolean, + val rewardPerAd: Int, + val message: String +) + data class PsychologyStateDto( val isBottleVisible: Boolean ) @@ -68,11 +106,13 @@ interface NetworkPsychologyService { @Singleton class NetworkUserRepository @Inject constructor( @BackendApi private val retrofit: Retrofit -) : UserRepository, UserTendencyRepository, UserActivityRepository, UserPsychologyRepository { +) : UserRepository, UserTendencyRepository, UserActivityRepository, UserPsychologyRepository, UserAdRepository { private val userService by lazy { retrofit.create(NetworkUserService::class.java) } private val tendencyLevelService by lazy { retrofit.create(NetworkTendencyLevelService::class.java) } private val activityRecordService by lazy { retrofit.create(NetworkActivityRecordService::class.java) } + private val networkAdService by lazy { retrofit.create(NetworkAdService::class.java) } + private val psychologyService by lazy { retrofit.create(NetworkPsychologyService::class.java) } override suspend fun load(): User { @@ -92,6 +132,20 @@ class NetworkUserRepository @Inject constructor( return activityRecordResponse.data.toDomain() } + override suspend fun loadAdInfo(): AdInfoDto { + val adInfoResponse: BaseResponse = networkAdService.loadAdInfo() + return adInfoResponse.data + } + + override suspend fun watchAd(): String { + val watchingAdResponse = networkAdService.watchAd( + AdRequestDto( + "INK", null + ) + ) + return watchingAdResponse.message + } + override suspend fun isReadDailyPsychology(): Boolean { val psychologyResponse: BaseResponse = psychologyService.isReadDailyPsychology() @@ -104,4 +158,5 @@ class NetworkUserRepository @Inject constructor( psychologyService.loadDailyPsychology() return psychologyResponse.data } + } diff --git a/app/src/main/java/com/egobook/app/ui/home/ui/AdDialog.kt b/app/src/main/java/com/egobook/app/ui/home/ui/AdDialog.kt index 4b7cb5fe..ab8d710f 100644 --- a/app/src/main/java/com/egobook/app/ui/home/ui/AdDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/home/ui/AdDialog.kt @@ -6,15 +6,24 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.egobook.app.MainActivity import com.egobook.app.databinding.DialogAdBinding import com.egobook.app.removeScreenBlur +import com.egobook.app.ui.home.HomeViewModel +import kotlinx.coroutines.launch class AdDialog() : DialogFragment() { private var _binding: DialogAdBinding? = null private val binding get() = checkNotNull(_binding) { "Fragment가 제거되었습니다." } - + private val viewModel: HomeViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,14 +36,52 @@ class AdDialog() : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val userId = arguments?.getString(ARG_USER_ID) ?: "사용자가 없습니다" + + viewModel.loadCurrentAdInfo() + + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.adState.collect { adState -> + if (adState.isAvailable) { + binding.btnWatch.isEnabled = true + } else { + binding.btnWatch.isEnabled = false + } + binding.btnWatch.text = "광고보기 ${adState.currentViewCount}/${adState.maxLimit}" + binding.tvAdReward.text = adState.rewardPerAd.toString() + } + } + binding.btnBack.setOnClickListener { removeScreenBlur() dismiss() } + + binding.btnWatch.setOnClickListener { + (activity as MainActivity).showAd(userId) { + viewModel.watchAd() + } + removeScreenBlur() + dismiss() + } } override fun onDestroyView() { super.onDestroyView() _binding = null } + + companion object { + private const val ARG_USER_ID = "arg_user_id" + + fun newInstance(userId: String): AdDialog { + val args = Bundle().apply { + putString(ARG_USER_ID, userId) + } + return AdDialog().apply { + arguments = args + } + } + } } diff --git a/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt b/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt index ccd3362e..74c85690 100644 --- a/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -42,7 +43,7 @@ class HomeFragment(): Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val viewModel: HomeViewModel by viewModels() + val viewModel: HomeViewModel by activityViewModels() lifecycleScope.launch { viewModel.uiState.collect { userState -> binding.tvLevel.text = "Lv ${userState.level.number}" @@ -102,8 +103,9 @@ class HomeFragment(): Fragment() { } binding.ivAd.setOnClickListener { + val currentUserId = viewModel.uiState.value.id applyScreenBlur(BlurLevel.BASE) - val dialog = AdDialog() + val dialog = AdDialog.newInstance(currentUserId.toString()) dialog.isCancelable = false dialog.show(parentFragmentManager, "ConfirmDialog") } diff --git a/app/src/main/java/com/egobook/app/ui/home/user/User.kt b/app/src/main/java/com/egobook/app/ui/home/user/User.kt index 6e23263d..470765c4 100644 --- a/app/src/main/java/com/egobook/app/ui/home/user/User.kt +++ b/app/src/main/java/com/egobook/app/ui/home/user/User.kt @@ -1,3 +1,3 @@ package com.egobook.app.ui.home.user -data class User(val level: Level, val ink: Ink) +data class User(val id: Int, val level: Level, val ink: Ink) diff --git a/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt b/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt index 0dedca7c..a8659935 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt @@ -51,6 +51,7 @@ class StoreFragment: Fragment() { insets } val viewModel: StoreViewModel by activityViewModels() + viewModel.loadInk() viewPager = binding.vp2StoreCollectionContainer viewPager.adapter = StoreCollectionAdapter(this) val tabLayout = binding.tlTabs diff --git a/app/src/main/res/layout/dialog_ad.xml b/app/src/main/res/layout/dialog_ad.xml index 015472be..03693423 100644 --- a/app/src/main/res/layout/dialog_ad.xml +++ b/app/src/main/res/layout/dialog_ad.xml @@ -63,13 +63,13 @@ android:src="@drawable/ink_icon" /> + android:layout_marginStart="4dp" /> @@ -82,7 +82,7 @@ + android:text="광고 보기 10/10" />