diff --git a/.github/workflows/develop_PR_builder.yml b/.github/workflows/develop_PR_builder.yml index 6cba1b30..7bc48e17 100644 --- a/.github/workflows/develop_PR_builder.yml +++ b/.github/workflows/develop_PR_builder.yml @@ -41,6 +41,7 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} POPHORY_BASE_URL: ${{ secrets.POPHORY_BASE_URL }} + ADMOB_APP_ID: ${{ secrets.ADMOB_APP_ID }} KEY_ALIAS: ${{ secrets.SENTRY_DSN }} KEY_PASSWORD: ${{ secrets.SENTRY_DSN }} STORE_PASSWORD: ${{ secrets.SENTRY_DSN }} @@ -48,6 +49,7 @@ jobs: echo sentryDsn=\"$SENTRY_DSN\" >> ./local.properties echo kakaoApiKey=$KAKAO_API_KEY >> ./local.properties echo pophoryBaseUrl=$POPHORY_BASE_URL >> ./local.properties + echo admobAppId=$ADMOB_APP_ID >> ./local.properties echo keyAlias=$KEY_ALIAS >> ./local.properties echo keyPassword=KEY_PASSWORD >> ./local.properties echo storePassword=$STORE_PASSWORD >> ./local.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 666370a9..3d0e2686 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,10 +66,12 @@ dependencies { // domain implementation(projects.domain.auth) implementation(projects.domain.share) + implementation(projects.domain.ad) // data implementation(projects.data.auth) implementation(projects.data.share) + implementation(projects.data.ad) // core implementation(projects.core.common) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 11ad9c14..e714103c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ android:value="true" /> + android:value="${admobAppId}" /> (AlbumEditState.Uninitialized) val albumEditState = _albumEditState.asStateFlow() @@ -50,4 +54,8 @@ class AlbumCoverEditViewModel @Inject constructor( fun updateCurrentPosition(currentDesignId: Long) { this.currentDesignId = currentDesignId + 1 } -} \ No newline at end of file + + suspend fun fetchAdConstant(adName: String): AdIdentifier? { + return fetchAdConstantUseCase(adName) + } +} diff --git a/app/src/main/java/com/teampophory/pophory/feature/home/HomeViewModel.kt b/app/src/main/java/com/teampophory/pophory/feature/home/HomeViewModel.kt index 971a3ceb..a926d984 100644 --- a/app/src/main/java/com/teampophory/pophory/feature/home/HomeViewModel.kt +++ b/app/src/main/java/com/teampophory/pophory/feature/home/HomeViewModel.kt @@ -2,7 +2,10 @@ package com.teampophory.pophory.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.teampophory.pophory.domain.ConfigureMeUseCase +import com.teampophory.pophory.BuildConfig +import com.teampophory.pophory.ad.usecase.GetAdConstantUseCase +import com.teampophory.pophory.ad.usecase.SetAdConstantUseCase +import com.teampophory.pophory.domain.usecase.ConfigureMeUseCase import com.teampophory.pophory.feature.home.store.model.AlbumItem import dagger.hilt.android.lifecycle.HiltViewModel import io.sentry.Sentry @@ -14,11 +17,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val configureMeUseCase: ConfigureMeUseCase + private val configureMeUseCase: ConfigureMeUseCase, + private val getAdConstantUseCase: GetAdConstantUseCase, + private val setAdConstantUseCase: SetAdConstantUseCase ) : ViewModel() { private val _homeState = MutableStateFlow(HomeViewState()) @@ -38,6 +44,14 @@ class HomeViewModel @Inject constructor( } Sentry.setUser(user) } + getAdConstantUseCase("android", BuildConfig.VERSION_NAME).onSuccess { + it.forEach { adConstant -> + Timber.d("adConstant: $adConstant") + setAdConstantUseCase(adConstant.name, adConstant.id) + } + }.onFailure { + Timber.e(it) + } } } @@ -54,4 +68,4 @@ class HomeViewModel @Inject constructor( _albumCountUpdateEvent.emit(Unit) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/teampophory/pophory/feature/home/store/StoreFragment.kt b/app/src/main/java/com/teampophory/pophory/feature/home/store/StoreFragment.kt index 995ecbd3..b7ef577e 100644 --- a/app/src/main/java/com/teampophory/pophory/feature/home/store/StoreFragment.kt +++ b/app/src/main/java/com/teampophory/pophory/feature/home/store/StoreFragment.kt @@ -104,21 +104,26 @@ class StoreFragment : Fragment() { private fun intiViews() { binding.ivEditButton.setOnClickListener { - if (viewModel.albums.value is StoreState.SuccessAlbums) { - val currentAlbumPosition = homeViewModel.homeState.value.currentAlbumPosition - val albumItem = viewModel.getCurrentAlbumItem(currentAlbumPosition) - val currentAlbumCoverId = albumItem?.albumCover ?: 1 - val intent = AlbumCoverEditActivity.newIntent( - context = requireContext(), - albumCoverId = currentAlbumCoverId, - albumId = albumItem?.id ?: 0 - ) - albumCoverChangeLauncher.launch(intent) - } + moveToAlbumCoverEditActivity() } binding.seekBarStore.setOnTouchListener { _, _ -> true } } + private fun moveToAlbumCoverEditActivity() { + if (viewModel.albums.value !is StoreState.SuccessAlbums) { + return + } + val currentAlbumPosition = homeViewModel.homeState.value.currentAlbumPosition + val albumItem = viewModel.getCurrentAlbumItem(currentAlbumPosition) + val currentAlbumCoverId = albumItem?.albumCover ?: 1 + val intent = AlbumCoverEditActivity.newIntent( + context = requireContext(), + albumCoverId = currentAlbumCoverId, + albumId = albumItem?.id ?: 0 + ) + albumCoverChangeLauncher.launch(intent) + } + private fun initHomeObserver() { homeViewModel.albumCountUpdateEvent.flowWithLifecycle(viewLifeCycle).onEach { viewModel.getAlbums() diff --git a/app/src/main/java/com/teampophory/pophory/util/dialog/TwoButtonCommonDialog.kt b/app/src/main/java/com/teampophory/pophory/util/dialog/TwoButtonCommonDialog.kt new file mode 100644 index 00000000..da78580b --- /dev/null +++ b/app/src/main/java/com/teampophory/pophory/util/dialog/TwoButtonCommonDialog.kt @@ -0,0 +1,114 @@ +package com.teampophory.pophory.util.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import com.teampophory.pophory.common.context.dialogWidthPercent +import com.teampophory.pophory.common.view.setOnSingleClickListener +import com.teampophory.pophory.common.view.viewBinding +import com.teampophory.pophory.databinding.DialogCommonTwoButtonBinding + +class TwoButtonCommonDialog : DialogFragment() { + + private val binding by viewBinding(DialogCommonTwoButtonBinding::bind) + + private var confirmButtonClickListener: (() -> Unit)? = null + private var dismissButtonClickListener: (() -> Unit)? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return DialogCommonTwoButtonBinding.inflate(inflater, container, false).root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViews() + initButtonListener() + } + + override fun onResume() { + super.onResume() + context?.dialogWidthPercent(dialog) + dialog?.window?.run { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + } + + fun setConfirmButtonClickListener(confirmButtonClickListener: () -> Unit) { + this.confirmButtonClickListener = confirmButtonClickListener + } + + fun setDismissButtonClickListener(dismissButtonClickListener: () -> Unit) { + this.dismissButtonClickListener = dismissButtonClickListener + } + + private fun initViews() { + val title = arguments?.getString(TITLE, "") + val description = arguments?.getString(DESCRIPTION, "") + val imageResId = arguments?.getInt(IMAGE_RES_ID) + val confirmButtonText = arguments?.getString(CONFIRM_BUTTON_TEXT, "") + val dismissButtonText = arguments?.getString(DISMISS_BUTTON_TEXT, "") + + with(binding) { + tvDialogTitle.text = title + tvDialogDescription.text = description + ivDialogIcon.setImageDrawable(imageResId?.let { + ivDialogIcon.isVisible = true + ContextCompat.getDrawable(requireContext(), it) + }) + tvConfirmButton.text = confirmButtonText + tvDismissButton.text = dismissButtonText + } + } + + private fun initButtonListener() { + binding.tvConfirmButton.setOnSingleClickListener { + confirmButtonClickListener?.invoke() + dismiss() + } + binding.tvDismissButton.setOnSingleClickListener { + dismissButtonClickListener?.invoke() + dismiss() + } + } + + companion object { + const val TAG = "TwoButtonCommonDialog" + + const val TITLE = "title" + const val DESCRIPTION = "description" + const val IMAGE_RES_ID = "imageResId" + const val CONFIRM_BUTTON_TEXT = "confirmButtonText" + const val DISMISS_BUTTON_TEXT = "dismissButtonText" + + + fun newInstance( + title: String, + description: String, + @DrawableRes + imageResId: Int, + confirmButtonText: String, + dismissButtonText: String + ): TwoButtonCommonDialog { + return TwoButtonCommonDialog().apply { + arguments = Bundle().apply { + putString(TITLE, title) + putString(DESCRIPTION, description) + putInt(IMAGE_RES_ID, imageResId) + putString(CONFIRM_BUTTON_TEXT, confirmButtonText) + putString(DISMISS_BUTTON_TEXT, dismissButtonText) + } + } + } + } +} diff --git a/app/src/main/res/drawable/bg_round_100dp_f5f5f5.xml b/app/src/main/res/drawable/bg_round_100dp_f5f5f5.xml new file mode 100644 index 00000000..fa031683 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_100dp_f5f5f5.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_album_cover.xml b/app/src/main/res/drawable/ic_album_cover.xml new file mode 100644 index 00000000..45d97d83 --- /dev/null +++ b/app/src/main/res/drawable/ic_album_cover.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_common_two_button.xml b/app/src/main/res/layout/dialog_common_two_button.xml new file mode 100644 index 00000000..a8f0aa08 --- /dev/null +++ b/app/src/main/res/layout/dialog_common_two_button.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40a580cf..78188864 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,13 @@ Pophory - 확인 + 확인속 + 취소 + 닫기 + 삭제 + 저장 + 계속하기 + 돌아가기 내서랍 @@ -52,7 +58,10 @@ 포릿 이야기 - + + 앨범을 수정할까요? + 앨범 커버를 수정하려면 광고 시청 하나 부탁드려요! + QR로 등록하기 이미지 로드에 실패 하였습니다. 이미지 다운로드에 실패하하였습니다. diff --git a/build-logic/convention/src/main/kotlin/com/teampophory/pophory/plugin/CommonConfigs.kt b/build-logic/convention/src/main/kotlin/com/teampophory/pophory/plugin/CommonConfigs.kt index fba50146..84535a62 100644 --- a/build-logic/convention/src/main/kotlin/com/teampophory/pophory/plugin/CommonConfigs.kt +++ b/build-logic/convention/src/main/kotlin/com/teampophory/pophory/plugin/CommonConfigs.kt @@ -28,6 +28,7 @@ internal fun Project.configureAndroidCommonPlugin() { manifestPlaceholders["sentryDsn"] = properties["sentryDsn"] as String manifestPlaceholders["kakaoApiKey"] = properties["kakaoApiKey"] as String manifestPlaceholders["pophoryBaseUrl"] = properties["pophoryBaseUrl"] as String + manifestPlaceholders["admobAppId"] = properties["admobAppId"] as String buildConfigField("String", "KAKAO_API_KEY", "\"${kakaoApiKey}\"") buildConfigField("String", "POPHORY_BASE_URL", "\"${pophoryBaseUrl}\"") diff --git a/core/common/src/main/java/com/teampophory/pophory/common/ad/AdName.kt b/core/common/src/main/java/com/teampophory/pophory/common/ad/AdName.kt new file mode 100644 index 00000000..67e81589 --- /dev/null +++ b/core/common/src/main/java/com/teampophory/pophory/common/ad/AdName.kt @@ -0,0 +1,5 @@ +package com.teampophory.pophory.common.ad + +enum class AdName(val adName: String) { + ALBUM_EDIT_COVER_REWARD_01("AlbumEditCover_reward_01") +} diff --git a/data/ad/.gitignore b/data/ad/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/ad/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/ad/build.gradle.kts b/data/ad/build.gradle.kts new file mode 100644 index 00000000..63d0b06e --- /dev/null +++ b/data/ad/build.gradle.kts @@ -0,0 +1,18 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + pophory("feature") + alias(libs.plugins.sentry) +} + +android { + namespace = "com.teampophory.pophory.data.ad" +} + +dependencies { + implementation(projects.domain.ad) + implementation(projects.core.common) + implementation(projects.core.network) + implementation(libs.security) + implementation(libs.bundles.retrofit) + implementation(libs.sentry) +} diff --git a/data/ad/consumer-rules.pro b/data/ad/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/data/ad/proguard-rules.pro b/data/ad/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/ad/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/ad/src/main/AndroidManifest.xml b/data/ad/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/data/ad/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/data/ad/src/main/java/com/teampophory/pophory/ad/model/AdConstantResponse.kt b/data/ad/src/main/java/com/teampophory/pophory/ad/model/AdConstantResponse.kt new file mode 100644 index 00000000..f5a683e1 --- /dev/null +++ b/data/ad/src/main/java/com/teampophory/pophory/ad/model/AdConstantResponse.kt @@ -0,0 +1,19 @@ +package com.teampophory.pophory.ad.model + + +import com.teampophory.pophory.ad.entity.AdIdentifier +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable +data class AdConstantResponse( + @SerialName("adId") + val adId: String? = null, + @SerialName("adName") + val adName: String? = null +) + + +fun AdConstantResponse.toAdIdentifier() = AdIdentifier( + id = adId.orEmpty(), + name = adName.orEmpty() +) diff --git a/data/ad/src/main/java/com/teampophory/pophory/ad/repository/DefaultAdRepository.kt b/data/ad/src/main/java/com/teampophory/pophory/ad/repository/DefaultAdRepository.kt new file mode 100644 index 00000000..0fa16307 --- /dev/null +++ b/data/ad/src/main/java/com/teampophory/pophory/ad/repository/DefaultAdRepository.kt @@ -0,0 +1,29 @@ +package com.teampophory.pophory.ad.repository + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.teampophory.pophory.ad.entity.AdIdentifier +import com.teampophory.pophory.ad.model.toAdIdentifier +import com.teampophory.pophory.ad.service.AdService +import javax.inject.Inject + +class DefaultAdRepository @Inject constructor( + private val adService: AdService, + private val sharedPreferences: SharedPreferences +) : AdRepository { + override suspend fun getAdConstant(os: String, version: String): Result> { + return runCatching { adService.getAdConstant(os, version).map { it.toAdIdentifier() } } + } + + override suspend fun setAdConstant(adName: String, adId: String) { + sharedPreferences.edit { + putString(adName, adId) + } + } + + override suspend fun fetchAdConstant(adName: String): AdIdentifier? { + return sharedPreferences.getString(adName, null)?.let { + AdIdentifier(adName, it) + } + } +} diff --git a/data/ad/src/main/java/com/teampophory/pophory/ad/service/AdService.kt b/data/ad/src/main/java/com/teampophory/pophory/ad/service/AdService.kt new file mode 100644 index 00000000..a257aaf2 --- /dev/null +++ b/data/ad/src/main/java/com/teampophory/pophory/ad/service/AdService.kt @@ -0,0 +1,14 @@ +package com.teampophory.pophory.ad.service + +import com.teampophory.pophory.ad.model.AdConstantResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface AdService { + @GET("/api/v2/ad") + suspend fun getAdConstant( + @Query("os") os: String, + @Query("version") version: String + ): List + +} diff --git a/domain/ad/.gitignore b/domain/ad/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/ad/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/ad/build.gradle.kts b/domain/ad/build.gradle.kts new file mode 100644 index 00000000..ed722350 --- /dev/null +++ b/domain/ad/build.gradle.kts @@ -0,0 +1,9 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + `java-library` + kotlin("jvm") +} + +dependencies { + implementation(libs.javax.inject) +} diff --git a/domain/ad/src/main/AndroidManifest.xml b/domain/ad/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/domain/ad/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/domain/ad/src/main/java/com/teampophory/pophory/ad/entity/AdIdentifier.kt b/domain/ad/src/main/java/com/teampophory/pophory/ad/entity/AdIdentifier.kt new file mode 100644 index 00000000..a65d8323 --- /dev/null +++ b/domain/ad/src/main/java/com/teampophory/pophory/ad/entity/AdIdentifier.kt @@ -0,0 +1,7 @@ +package com.teampophory.pophory.ad.entity + +data class AdIdentifier( + val id: String, + val name: String +) + diff --git a/domain/ad/src/main/java/com/teampophory/pophory/ad/repository/AdRepository.kt b/domain/ad/src/main/java/com/teampophory/pophory/ad/repository/AdRepository.kt new file mode 100644 index 00000000..d25acaf5 --- /dev/null +++ b/domain/ad/src/main/java/com/teampophory/pophory/ad/repository/AdRepository.kt @@ -0,0 +1,11 @@ +package com.teampophory.pophory.ad.repository + +import com.teampophory.pophory.ad.entity.AdIdentifier + +interface AdRepository { + suspend fun getAdConstant(os: String, version: String): Result> + + suspend fun setAdConstant(adName: String, adId: String) + + suspend fun fetchAdConstant(adName: String): AdIdentifier? +} diff --git a/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/FetchAdConstantUseCase.kt b/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/FetchAdConstantUseCase.kt new file mode 100644 index 00000000..ad9fd8c7 --- /dev/null +++ b/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/FetchAdConstantUseCase.kt @@ -0,0 +1,13 @@ +package com.teampophory.pophory.ad.usecase + +import com.teampophory.pophory.ad.entity.AdIdentifier +import com.teampophory.pophory.ad.repository.AdRepository +import javax.inject.Inject + +class FetchAdConstantUseCase @Inject constructor( + private val repository: AdRepository +){ + suspend operator fun invoke(adName: String): AdIdentifier? { + return repository.fetchAdConstant(adName) + } +} diff --git a/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/GetAdConstantUseCase.kt b/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/GetAdConstantUseCase.kt new file mode 100644 index 00000000..0c1edc93 --- /dev/null +++ b/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/GetAdConstantUseCase.kt @@ -0,0 +1,14 @@ +package com.teampophory.pophory.ad.usecase + +import com.teampophory.pophory.ad.entity.AdIdentifier +import com.teampophory.pophory.ad.repository.AdRepository +import javax.inject.Inject + + +class GetAdConstantUseCase @Inject constructor( + private val repository: AdRepository +) { + suspend operator fun invoke(os: String, version: String): Result> { + return repository.getAdConstant(os, version) + } +} diff --git a/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/SetAdConstantUseCase.kt b/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/SetAdConstantUseCase.kt new file mode 100644 index 00000000..b8d4d446 --- /dev/null +++ b/domain/ad/src/main/java/com/teampophory/pophory/ad/usecase/SetAdConstantUseCase.kt @@ -0,0 +1,12 @@ +package com.teampophory.pophory.ad.usecase + +import com.teampophory.pophory.ad.repository.AdRepository +import javax.inject.Inject + +class SetAdConstantUseCase @Inject constructor( + private val repository: AdRepository +) { + suspend operator fun invoke(adName: String, adId: String) { + repository.setAdConstant(adName, adId) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 4e9abb85..5d3e2a0f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,3 +32,5 @@ include(":feature:share") include(":domain:share") include(":data:share") include(":feature:onboarding") +include(":domain:ad") +include(":data:ad")