diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index a22fe03d..c4f23723 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -155,6 +155,11 @@ 님의 클립 이주의 링크 이주의 추천 사이트 + 1분 설문조사 참여하고\n스타벅스 기프티콘 받기 + 토스터 사용 피드백을 남겨주시면\n추첨을 통해 기프티콘을 드려요! + 참여하기 + 일주일간 보지 않기 + 클립의 이름은 최대 15자까지 입력 가능해요 diff --git a/core/model/src/main/java/org/sopt/model/home/PopupInfo.kt b/core/model/src/main/java/org/sopt/model/home/PopupInfo.kt new file mode 100644 index 00000000..41dc9e5a --- /dev/null +++ b/core/model/src/main/java/org/sopt/model/home/PopupInfo.kt @@ -0,0 +1,9 @@ +package org.sopt.model.home + +data class PopupInfo( + val popupId: Int, + val popupImage: String, + val popupActiveStartDate: String, + val popupActiveEndDate: String, + val popupLinkUrl: String, +) diff --git a/core/model/src/main/java/org/sopt/model/home/PopupInvisible.kt b/core/model/src/main/java/org/sopt/model/home/PopupInvisible.kt new file mode 100644 index 00000000..66c43e34 --- /dev/null +++ b/core/model/src/main/java/org/sopt/model/home/PopupInvisible.kt @@ -0,0 +1,6 @@ +package org.sopt.model.home + +data class PopupInvisible( + val popupId: Int, + val popupHideUntil: String, +) diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/api/PopupService.kt b/data-remote/home/src/main/java/org/sopt/remote/home/api/PopupService.kt new file mode 100644 index 00000000..a3f23894 --- /dev/null +++ b/data-remote/home/src/main/java/org/sopt/remote/home/api/PopupService.kt @@ -0,0 +1,25 @@ +package org.sopt.remote.home.api + +import org.sopt.network.model.response.base.BaseResponse +import org.sopt.remote.home.request.RequestPopupInvisibleDto +import org.sopt.remote.home.response.ResponsePopupInfoDto +import org.sopt.remote.home.response.ResponsePopupInvisibleDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH + +interface PopupService { + companion object { + const val API = "api" + const val V2 = "v2" + const val POPUP = "popup" + } + + @PATCH("/$API/$V2/$POPUP") + suspend fun patchPopupInvisible( + @Body requestPopupInvisibleDto: RequestPopupInvisibleDto, + ): BaseResponse + + @GET("/$API/$V2/$POPUP") + suspend fun getPopupInfo(): BaseResponse +} diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/datasource/RemotePopupDatasourceImpl.kt b/data-remote/home/src/main/java/org/sopt/remote/home/datasource/RemotePopupDatasourceImpl.kt new file mode 100644 index 00000000..d6536f35 --- /dev/null +++ b/data-remote/home/src/main/java/org/sopt/remote/home/datasource/RemotePopupDatasourceImpl.kt @@ -0,0 +1,19 @@ +package org.sopt.remote.home.datasource + +import org.sopt.home.datasource.RemotePopupDataSource +import org.sopt.model.home.PopupInfo +import org.sopt.model.home.PopupInvisible +import org.sopt.remote.home.api.PopupService +import org.sopt.remote.home.request.RequestPopupInvisibleDto +import org.sopt.remote.home.response.toCoreModel +import javax.inject.Inject + +class RemotePopupDatasourceImpl @Inject constructor( + private val popupService: PopupService, +) : RemotePopupDataSource { + override suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): PopupInvisible = + popupService.patchPopupInvisible(RequestPopupInvisibleDto(popupId, hideDate)).data!!.toCoreModel() + + override suspend fun getPopupInfo(): List = + popupService.getPopupInfo().data!!.popupList.map { it.toCoreModel() } +} diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt index 741e3551..2ea85376 100644 --- a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt +++ b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt @@ -5,7 +5,9 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.sopt.home.datasource.RemoteHomeDataSource +import org.sopt.home.datasource.RemotePopupDataSource import org.sopt.remote.home.datasource.RemoteHomeDataSourceImpl +import org.sopt.remote.home.datasource.RemotePopupDatasourceImpl import javax.inject.Singleton @Module @@ -16,4 +18,10 @@ abstract class HomeDataSourceModule { abstract fun bindRemoteHomeDatasource( remoteHomeDataSourceImpl: RemoteHomeDataSourceImpl, ): RemoteHomeDataSource + + @Singleton + @Binds + abstract fun bindRemotePopupDataSource( + remotePopupDataSourceImpl: RemotePopupDatasourceImpl, + ): RemotePopupDataSource } diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt index b750cfaa..e7f289c7 100644 --- a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt +++ b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt @@ -6,8 +6,10 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.sopt.network.di.AuthLinkMindRetrofit import org.sopt.remote.home.api.HomeService +import org.sopt.remote.home.api.PopupService import retrofit2.Retrofit import javax.inject.Singleton + @Module @InstallIn(SingletonComponent::class) object HomeServiceModule { @@ -16,4 +18,9 @@ object HomeServiceModule { @Provides fun provideHomeService(@AuthLinkMindRetrofit retrofit: Retrofit): HomeService = retrofit.create(HomeService::class.java) + + @Singleton + @Provides + fun providePopupService(@AuthLinkMindRetrofit retrofit: Retrofit): PopupService = + retrofit.create(PopupService::class.java) } diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/request/RequestPopupInvisibleDto.kt b/data-remote/home/src/main/java/org/sopt/remote/home/request/RequestPopupInvisibleDto.kt new file mode 100644 index 00000000..a7e19640 --- /dev/null +++ b/data-remote/home/src/main/java/org/sopt/remote/home/request/RequestPopupInvisibleDto.kt @@ -0,0 +1,12 @@ +package org.sopt.remote.home.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPopupInvisibleDto( + @SerialName("popupId") + val popupId: Long, + @SerialName("hideDate") + val hideDate: Long, +) diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInfoDto.kt b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInfoDto.kt new file mode 100644 index 00000000..441edb2b --- /dev/null +++ b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInfoDto.kt @@ -0,0 +1,37 @@ +package org.sopt.remote.home.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.sopt.model.home.PopupInfo + +@Serializable +data class ResponsePopupInfoDto( + @SerialName("popupList") + val popupList: List, +) + +@Serializable +data class ResponsePopupInfo( + @SerialName("id") + val id: Int, + @SerialName("image") + val image: String, + @SerialName("activeStartDate") + val activeStartDate: String, + @SerialName("activeEndDate") + val activeEndDate: String, + @SerialName("linkUrl") + val linkUrl: String, +) + +internal fun ResponsePopupInfoDto.toCoreModel() = popupList.map { + it.toCoreModel() +} + +internal fun ResponsePopupInfo.toCoreModel() = PopupInfo( + popupId = id, + popupImage = image, + popupActiveStartDate = activeStartDate, + popupActiveEndDate = activeEndDate, + popupLinkUrl = linkUrl, +) diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInvisibleDto.kt b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInvisibleDto.kt new file mode 100644 index 00000000..a475497f --- /dev/null +++ b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInvisibleDto.kt @@ -0,0 +1,18 @@ +package org.sopt.remote.home.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.sopt.model.home.PopupInvisible + +@Serializable +data class ResponsePopupInvisibleDto( + @SerialName("popupId") + val popupId: Int, + @SerialName("hideUntil") + val hideUntil: String, +) + +internal fun ResponsePopupInvisibleDto.toCoreModel() = PopupInvisible( + popupId = popupId, + popupHideUntil = hideUntil, +) diff --git a/data/home/src/main/java/org/sopt/home/datasource/RemotePopupDataSource.kt b/data/home/src/main/java/org/sopt/home/datasource/RemotePopupDataSource.kt new file mode 100644 index 00000000..a6f76163 --- /dev/null +++ b/data/home/src/main/java/org/sopt/home/datasource/RemotePopupDataSource.kt @@ -0,0 +1,9 @@ +package org.sopt.home.datasource + +import org.sopt.model.home.PopupInfo +import org.sopt.model.home.PopupInvisible + +interface RemotePopupDataSource { + suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): PopupInvisible + suspend fun getPopupInfo(): List +} diff --git a/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt b/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt index 397f2b75..9137c2a7 100644 --- a/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt +++ b/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt @@ -6,6 +6,8 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.sopt.home.repository.HomeRepoImpl import org.sopt.home.repository.HomeRepository +import org.sopt.home.repository.PopupRepoImpl +import org.sopt.home.repository.PopupRepository import javax.inject.Singleton @Module @@ -16,4 +18,10 @@ abstract class RepositoryModule { abstract fun bindHomeRepository( homeRepoImpl: HomeRepoImpl, ): HomeRepository + + @Singleton + @Binds + abstract fun bindPopupRepository( + popupRepoImpl: PopupRepoImpl, + ): PopupRepository } diff --git a/data/home/src/main/java/org/sopt/home/repository/PopupRepoImpl.kt b/data/home/src/main/java/org/sopt/home/repository/PopupRepoImpl.kt new file mode 100644 index 00000000..ef1388af --- /dev/null +++ b/data/home/src/main/java/org/sopt/home/repository/PopupRepoImpl.kt @@ -0,0 +1,16 @@ +package org.sopt.home.repository + +import org.sopt.home.datasource.RemotePopupDataSource +import org.sopt.model.home.PopupInfo +import org.sopt.model.home.PopupInvisible +import javax.inject.Inject + +class PopupRepoImpl @Inject constructor( + private val remotePopupDataSource: RemotePopupDataSource, +) : PopupRepository { + override suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): Result = + runCatching { remotePopupDataSource.patchPopupInvisible(popupId, hideDate) } + + override suspend fun getPopupInfo(): Result> = + runCatching { remotePopupDataSource.getPopupInfo() } +} diff --git a/domain/home/src/main/java/org/sopt/home/repository/PopupRepository.kt b/domain/home/src/main/java/org/sopt/home/repository/PopupRepository.kt new file mode 100644 index 00000000..9b921c84 --- /dev/null +++ b/domain/home/src/main/java/org/sopt/home/repository/PopupRepository.kt @@ -0,0 +1,9 @@ +package org.sopt.home.repository + +import org.sopt.model.home.PopupInfo +import org.sopt.model.home.PopupInvisible + +interface PopupRepository { + suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): Result + suspend fun getPopupInfo(): Result> +} diff --git a/domain/home/src/main/java/org/sopt/home/usecase/GetPopupInfo.kt b/domain/home/src/main/java/org/sopt/home/usecase/GetPopupInfo.kt new file mode 100644 index 00000000..163db982 --- /dev/null +++ b/domain/home/src/main/java/org/sopt/home/usecase/GetPopupInfo.kt @@ -0,0 +1,11 @@ +package org.sopt.home.usecase + +import org.sopt.home.repository.PopupRepository +import org.sopt.model.home.PopupInfo +import javax.inject.Inject + +class GetPopupInfo @Inject constructor( + private val popupRepository: PopupRepository, +) { + suspend operator fun invoke(): Result> = popupRepository.getPopupInfo() +} diff --git a/domain/home/src/main/java/org/sopt/home/usecase/PatchPopupInvisible.kt b/domain/home/src/main/java/org/sopt/home/usecase/PatchPopupInvisible.kt new file mode 100644 index 00000000..65b069ec --- /dev/null +++ b/domain/home/src/main/java/org/sopt/home/usecase/PatchPopupInvisible.kt @@ -0,0 +1,12 @@ +package org.sopt.home.usecase + +import org.sopt.home.repository.PopupRepository +import org.sopt.model.home.PopupInvisible +import javax.inject.Inject + +class PatchPopupInvisible @Inject constructor( + private val popupRepository: PopupRepository, +) { + suspend operator fun invoke(popupId: Long, hideDate: Long): Result = + popupRepository.patchPopupInvisible(popupId, hideDate) +} diff --git a/feature/home/src/main/java/org/sopt/home/HomeContract.kt b/feature/home/src/main/java/org/sopt/home/HomeContract.kt index 797a17cf..03e4b057 100644 --- a/feature/home/src/main/java/org/sopt/home/HomeContract.kt +++ b/feature/home/src/main/java/org/sopt/home/HomeContract.kt @@ -1,6 +1,7 @@ package org.sopt.home import org.sopt.model.category.Category +import org.sopt.model.home.PopupInfo import org.sopt.model.home.RecommendLink import org.sopt.model.home.WeekBestLink @@ -14,6 +15,7 @@ data class HomeState( val url: String = "", val categoryId: Long? = 0, val categoryName: String? = "전체 클립", + val popupList: List = emptyList(), ) { fun calculateProgress(): Int { if (readToastNum > allToastNum) return 0 @@ -31,4 +33,5 @@ sealed interface HomeSideEffect { data object NavigateClipLink : HomeSideEffect data object NavigateWebView : HomeSideEffect data object ShowBottomSheet : HomeSideEffect + data object ShowPopupInfo : HomeSideEffect } diff --git a/feature/home/src/main/java/org/sopt/home/HomeFragment.kt b/feature/home/src/main/java/org/sopt/home/HomeFragment.kt index 70926045..fbc554ef 100644 --- a/feature/home/src/main/java/org/sopt/home/HomeFragment.kt +++ b/feature/home/src/main/java/org/sopt/home/HomeFragment.kt @@ -14,6 +14,7 @@ import org.sopt.home.adapter.HomeWeekLinkAdapter import org.sopt.home.adapter.HomeWeekRecommendLinkAdapter import org.sopt.home.adapter.ItemDecoration import org.sopt.home.databinding.FragmentHomeBinding +import org.sopt.model.home.PopupInfo import org.sopt.ui.base.BindingFragment import org.sopt.ui.nav.DeepLinkUtil import org.sopt.ui.view.onThrottleClick @@ -62,6 +63,7 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. is HomeSideEffect.NavigateClipLink -> navigateToDestination( "featureSaveLink://ClipLinkFragment/${viewModel.container.stateFlow.value.categoryId}/${viewModel.container.stateFlow.value.categoryName}", ) + is HomeSideEffect.ShowBottomSheet -> showHomeBottomSheet() is HomeSideEffect.NavigateWebView -> { val encodedURL = URLEncoder.encode(viewModel.container.stateFlow.value.url, StandardCharsets.UTF_8.toString()) @@ -69,6 +71,8 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. "featureSaveLink://webViewFragment/${0}/${false}/${false}/$encodedURL", ) } + + is HomeSideEffect.ShowPopupInfo -> showPopupInfo(viewModel.container.stateFlow.value.popupList) } } @@ -80,8 +84,10 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. getMainPageUserClip() getRecommendSite() getWeekBestLink() + getPopupListInfo() } } + private fun navigateToSetting() { binding.ivHomeSetting.onThrottleClick { viewModel.navigateSetting() @@ -154,4 +160,18 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. } } } + + private fun showPopupInfo(popupList: List) { + popupList.forEach { + if (viewModel.checkPopupDate(it.popupActiveStartDate, it.popupActiveEndDate) + ) { + val surveyDialog = SurveyDialogFragment.newInstance( + it.popupImage, + { viewModel.navigateWebview(it.popupLinkUrl) }, + { viewModel.patchPopupInvisible(it.popupId.toLong(), 7) }, + ) + surveyDialog.show(parentFragmentManager, this.tag) + } + } + } } diff --git a/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt b/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt index 9326aa7f..ee01ba25 100644 --- a/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt +++ b/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt @@ -15,9 +15,14 @@ import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import org.sopt.domain.category.category.usecase.PostAddCategoryTitleUseCase import org.sopt.home.usecase.GetMainPageUserClip +import org.sopt.home.usecase.GetPopupInfo import org.sopt.home.usecase.GetRecommendSite import org.sopt.home.usecase.GetWeekBestLink +import org.sopt.home.usecase.PatchPopupInvisible import org.sopt.model.category.Category +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale import javax.inject.Inject @HiltViewModel @@ -26,6 +31,8 @@ class HomeViewModel @Inject constructor( private val getRecommendSite: GetRecommendSite, private val getWeekBestLink: GetWeekBestLink, private val postAddCategoryTitle: PostAddCategoryTitleUseCase, + private val patchPopupInvisible: PatchPopupInvisible, + private val getPopupInfo: GetPopupInfo, ) : ContainerHost, ViewModel() { override val container: Container = container(HomeState()) @@ -86,6 +93,29 @@ class HomeViewModel @Inject constructor( } } + fun getPopupListInfo() = intent { + getPopupInfo.invoke().onSuccess { + postSideEffect(HomeSideEffect.ShowPopupInfo) + reduce { + state.copy(popupList = it) + } + }.onFailure { + Log.d("getPopupListInfo", "$it") + } + } + + fun patchPopupInvisible(popupId: Long, hideDate: Long) { + viewModelScope.launch { + patchPopupInvisible.invoke(popupId, hideDate) + .onSuccess { + Log.d("patchPopupInvisible", "$it") + } + .onFailure { + Log.d("patchPopupInvisible", "$it") + } + } + } + fun navigateSearch() = intent { postSideEffect(HomeSideEffect.NavigateSearch) } fun navigateSetting() = intent { postSideEffect(HomeSideEffect.NavigateSetting) } fun showBottomSheet() = intent { postSideEffect(HomeSideEffect.ShowBottomSheet) } @@ -106,4 +136,12 @@ class HomeViewModel @Inject constructor( reduce { state.copy(url = url) } postSideEffect(HomeSideEffect.NavigateWebView) } + + fun checkPopupDate(activeStartDate: String, activeEndDate: String): Boolean { + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val today = Calendar.getInstance().time + val startDate = dateFormat.parse(activeStartDate) + val endDate = dateFormat.parse(activeEndDate) + return today.after(startDate) && today.before(endDate) || today == startDate || today == endDate + } } diff --git a/feature/home/src/main/java/org/sopt/home/SurveyDialogFragment.kt b/feature/home/src/main/java/org/sopt/home/SurveyDialogFragment.kt new file mode 100644 index 00000000..f178776a --- /dev/null +++ b/feature/home/src/main/java/org/sopt/home/SurveyDialogFragment.kt @@ -0,0 +1,69 @@ +package org.sopt.home + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.View +import coil.load +import org.sopt.home.databinding.FragmentSurveyDialogBinding +import org.sopt.ui.base.BindingDialogFragment +import org.sopt.ui.view.onThrottleClick + +class SurveyDialogFragment : BindingDialogFragment( + { FragmentSurveyDialogBinding.inflate(it) }, +) { + private var imageUrl: String? = null + private var linkUrl: () -> Unit = {} + private var handleSkip: () -> Unit = {} + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + imageUrl = arguments?.getString("imageUrl") + setStyle(STYLE_NO_FRAME, android.R.style.Theme_Dialog) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setSurveyImage(imageUrl) + + binding.ivSurveyDialogClose.onThrottleClick { + dismiss() + } + + binding.btnSurveyDialog.onThrottleClick { + linkUrl.invoke() + dismiss() + } + + binding.btnSurveyDialogSkip.onThrottleClick { + handleSkip.invoke() + dismiss() + } + } + + override fun onStart() { + super.onStart() + val dialog = dialog + if (dialog != null) { + dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + } + + private fun setSurveyImage(text: String?) = + binding.ivSurveyDialog.load(text) + + companion object { + fun newInstance(imageUrl: String, onNavigateWebView: () -> Unit, onNegativeButtonClick: () -> Unit): SurveyDialogFragment { + val args = Bundle().apply { + putString("imageUrl", imageUrl) + } + return SurveyDialogFragment().apply { + arguments = args + linkUrl = onNavigateWebView + handleSkip = onNegativeButtonClick + } + // 얘는 bundle 안해도ㅗ되나? + } + } +} diff --git a/feature/home/src/main/res/layout/fragment_survey_dialog.xml b/feature/home/src/main/res/layout/fragment_survey_dialog.xml new file mode 100644 index 00000000..e152ce04 --- /dev/null +++ b/feature/home/src/main/res/layout/fragment_survey_dialog.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + +