Skip to content

Commit 6bfeb62

Browse files
authored
[Mod/1616] app service modification (#1618)
* [FEAT#1616] 현재 앱잼 기간인지 확인하는 기능 추가 * [FEAT#1616] 탭 서비스와 홈 서비스 구조 변경 및 구현 * [FEAT#1616] 앱 서비스 인메모리 캐싱 정책 추가 * [MOD#1616] 앱잼 기간일 때를 판단하여 각 탭의 딥링크와 메뉴 구조를 검증 및 반영하도록 수정 * [MOD#1616] 앱잼 기간일 때를 판단하여 각 마이페이지에 UI가 변경되어야하는 상황 반영 * [MOD#1616] 앱잼 기간일 때를 판단하여 각 솝트로그의 데이터와 UI가 변경되어야하는 상황 반영 * [FEAT#1616] 탭 서비스 구현 * [FEAT#1616] 홈 서비스 탭의 반응형 데이터 관찰 및 AppJam 모드 연동 구현 - `HomeRepository` 및 `DefaultHomeRepository`에 `observeTabAppService()`를 추가하여 `Flow`를 통한 서비스 리스트 관찰 기능 구현 - `MainViewModel`에서 `ObserveTabAppServiceUseCase`와 `isAppjamMode` 상태를 `combine`하여 데이터 변경 시 탭 상태가 자동 갱신되도록 개선 - `updateMainTabs` 함수가 `isAppjam` 상태를 파라미터로 직접 받도록 수정하여 로직의 명확성 및 데이터 일관성 확보 - 초기 데이터 요청 함수명을 `fetchTabAppServices`로 변경하여 관찰(Observe) 로직과 역할 분리
1 parent 8148e56 commit 6bfeb62

23 files changed

Lines changed: 309 additions & 134 deletions

File tree

core/localstorage/src/main/java/org/sopt/official/localstorage/source/UserStorage.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ interface UserStorage {
4343
val pushToken: Flow<String>
4444
val platform: Flow<String>
4545
val isSopletterOnboardingCompleted: Flow<Boolean>
46+
val isAppjamMode: Flow<Boolean>
4647

4748
suspend fun saveUserStatus(status: UserStatus)
4849
suspend fun savePushToken(token: String)
4950
suspend fun savePlatform(platform: String)
5051
suspend fun saveOnboardingCompleted(isOnboarded: Boolean)
52+
suspend fun saveIsAppjamMode(isAppjamMode: Boolean)
5153
suspend fun clearUser()
5254
}

core/localstorage/src/main/java/org/sopt/official/localstorage/sourceimpl/DefaultSoptStorage.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ class DefaultSoptStorage @Inject constructor(
6363
preferences[KEY_ONBOARDING_COMPLETED] ?: false
6464
}
6565

66+
override val isAppjamMode: Flow<Boolean> = dataStore.data.map { preferences ->
67+
preferences[KEY_IS_APPJAM_MODE] ?: false
68+
}
69+
6670
override suspend fun saveTokens(accessToken: String, refreshToken: String) {
6771
dataStore.edit { preferences ->
6872
preferences[KEY_ACCESS_TOKEN] = accessToken.encryptInReleaseMode(keyAlias = ACCESS_TOKEN_KEY_ALIAS)
@@ -121,6 +125,12 @@ class DefaultSoptStorage @Inject constructor(
121125
}
122126
}
123127

128+
override suspend fun saveIsAppjamMode(isAppjamMode: Boolean) {
129+
dataStore.edit { preferences ->
130+
preferences[KEY_IS_APPJAM_MODE] = isAppjamMode
131+
}
132+
}
133+
124134
override suspend fun clearUser() {
125135
dataStore.edit { preferences ->
126136
preferences.remove(KEY_USER_STATUS)
@@ -143,6 +153,7 @@ class DefaultSoptStorage @Inject constructor(
143153
private val KEY_PUSH_TOKEN = stringPreferencesKey("push_token")
144154
private val KEY_PLATFORM = stringPreferencesKey("platform")
145155
private val KEY_ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed")
156+
private val KEY_IS_APPJAM_MODE = booleanPreferencesKey("is_appjam_mode")
146157

147158

148159
private const val DEFAULT_VALUE = ""

data/home/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ android {
3232

3333
dependencies {
3434
implementation(projects.domain.home)
35+
implementation(projects.core.cache)
3536
implementation(projects.core.network)
3637
implementation(projects.core.common)
3738
implementation(platform(libs.okhttp.bom))

domain/home/src/main/java/org/sopt/official/domain/home/AppServiceManager.kt renamed to data/home/src/main/java/org/sopt/official/data/home/di/CacheModule.kt

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* MIT License
3-
* Copyright 2026 SOPT - Shout Our Passion Together
3+
* Copyright 2024-2026 SOPT - Shout Our Passion Together
44
*
55
* Permission is hereby granted, free of charge, to any person obtaining a copy
66
* of this software and associated documentation files (the "Software"), to deal
@@ -22,30 +22,29 @@
2222
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
* SOFTWARE.
2424
*/
25-
package org.sopt.official.domain.home
25+
package org.sopt.official.data.home.di
2626

27-
import kotlinx.coroutines.flow.MutableStateFlow
28-
import kotlinx.coroutines.flow.StateFlow
29-
import kotlinx.coroutines.flow.asStateFlow
30-
import org.sopt.official.domain.home.model.AppService
31-
import org.sopt.official.domain.home.usecase.GetAppServiceUseCase
32-
import javax.inject.Inject
27+
import dagger.Module
28+
import dagger.Provides
29+
import dagger.hilt.InstallIn
30+
import dagger.hilt.components.SingletonComponent
31+
import javax.inject.Named
3332
import javax.inject.Singleton
33+
import org.sopt.official.cache.InMemoryCache
34+
import org.sopt.official.domain.home.model.AppService
35+
import org.sopt.official.domain.home.model.HomeAppServiceInfo
3436

35-
/**
36-
* AppServiceManager: app-service 관리 클래스
37-
* */
38-
@Singleton
39-
class AppServiceManager @Inject constructor(
40-
private val getAppServiceUseCase: GetAppServiceUseCase
41-
) {
42-
private val _appServices = MutableStateFlow<List<AppService>?>(null)
43-
val appServices: StateFlow<List<AppService>?> = _appServices.asStateFlow()
37+
@Module
38+
@InstallIn(SingletonComponent::class)
39+
internal object CacheModule {
4440

45-
suspend fun fetchAppServices(forceUpdate: Boolean = false) {
46-
if (!forceUpdate && _appServices.value != null) return
41+
@Provides
42+
@Singleton
43+
@Named("homeAppService")
44+
fun provideHomeAppServiceCache(): InMemoryCache<HomeAppServiceInfo> = InMemoryCache()
4745

48-
getAppServiceUseCase()
49-
.onSuccess { _appServices.value = it }
50-
}
46+
@Provides
47+
@Singleton
48+
@Named("tabAppService")
49+
fun provideTabAppServiceCache(): InMemoryCache<List<AppService>> = InMemoryCache()
5150
}

data/home/src/main/java/org/sopt/official/data/home/mapper/HomeMapper.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package org.sopt.official.data.home.mapper
2626

2727
import org.sopt.official.data.home.remote.response.HomeAppServiceResponseDto
28+
import org.sopt.official.data.home.remote.response.HomeAppServicesResponseDto
2829
import org.sopt.official.data.home.remote.response.HomeDescriptionResponseDto
2930
import org.sopt.official.data.home.remote.response.HomeFloatingToastDto
3031
import org.sopt.official.data.home.remote.response.HomeLatestPostResponseDto
@@ -33,6 +34,7 @@ import org.sopt.official.data.home.remote.response.HomeReviewFormResponseDto
3334
import org.sopt.official.data.home.remote.response.RecentCalendarResponseDto
3435
import org.sopt.official.domain.home.model.AppService
3536
import org.sopt.official.domain.home.model.FloatingToast
37+
import org.sopt.official.domain.home.model.HomeAppServiceInfo
3638
import org.sopt.official.domain.home.model.LatestPost
3739
import org.sopt.official.domain.home.model.PopularPost
3840
import org.sopt.official.domain.home.model.RecentCalendar
@@ -57,6 +59,11 @@ internal fun HomeAppServiceResponseDto.toDomain(): AppService = AppService(
5759
deepLink = deepLink,
5860
)
5961

62+
internal fun HomeAppServicesResponseDto.toDomain(): HomeAppServiceInfo = HomeAppServiceInfo(
63+
isAppjamMode = isAppjamMode,
64+
appServices = appServices.map { it.toDomain() },
65+
)
66+
6067
internal fun HomeReviewFormResponseDto.toDomain(): ReviewForm = ReviewForm(
6168
title = this.title,
6269
subTitle = this.subTitle,

data/home/src/main/java/org/sopt/official/data/home/remote/api/HomeApi.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package org.sopt.official.data.home.remote.api
2626

2727
import org.sopt.official.data.home.remote.response.HomeAppServiceResponseDto
28+
import org.sopt.official.data.home.remote.response.HomeAppServicesResponseDto
2829
import org.sopt.official.data.home.remote.response.HomeDescriptionResponseDto
2930
import org.sopt.official.data.home.remote.response.HomeFloatingToastDto
3031
import org.sopt.official.data.home.remote.response.HomeLatestPostsResponseDto
@@ -38,7 +39,10 @@ internal interface HomeApi {
3839
suspend fun getHomeDescription(): HomeDescriptionResponseDto
3940

4041
@GET("home/app-service")
41-
suspend fun getHomeAppService(): List<HomeAppServiceResponseDto>
42+
suspend fun getHomeAppService(): HomeAppServicesResponseDto
43+
44+
@GET("home/tab-app-service")
45+
suspend fun getTabAppService(): List<HomeAppServiceResponseDto>
4246

4347
@GET("home/review-form")
4448
suspend fun getReviewForm(): HomeReviewFormResponseDto
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* MIT License
3+
* Copyright 2024-2026 SOPT - Shout Our Passion Together
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all
13+
* copies or substantial portions of the Software.
14+
*
15+
* https://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
* SOFTWARE.
24+
*/
25+
package org.sopt.official.data.home.remote.response
26+
27+
import kotlinx.serialization.SerialName
28+
import kotlinx.serialization.Serializable
29+
30+
@Serializable
31+
internal data class HomeAppServicesResponseDto(
32+
@SerialName("isAppjamMode")
33+
val isAppjamMode: Boolean,
34+
@SerialName("appServices")
35+
val appServices: List<HomeAppServiceResponseDto>,
36+
)

data/home/src/main/java/org/sopt/official/data/home/repository/DefaultHomeRepository.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
*/
2525
package org.sopt.official.data.home.repository
2626

27-
import org.sopt.official.common.coroutines.suspendRunCatching
2827
import javax.inject.Inject
28+
import javax.inject.Named
29+
import kotlinx.coroutines.flow.Flow
30+
import org.sopt.official.cache.InMemoryCache
31+
import org.sopt.official.common.coroutines.suspendRunCatching
2932
import org.sopt.official.data.home.mapper.toDomain
3033
import org.sopt.official.data.home.remote.api.CalendarApi
3134
import org.sopt.official.data.home.remote.api.HomeApi
3235
import org.sopt.official.domain.home.model.AppService
3336
import org.sopt.official.domain.home.model.FloatingToast
37+
import org.sopt.official.domain.home.model.HomeAppServiceInfo
3438
import org.sopt.official.domain.home.model.LatestPost
3539
import org.sopt.official.domain.home.model.PopularPost
3640
import org.sopt.official.domain.home.model.RecentCalendar
@@ -41,6 +45,8 @@ import org.sopt.official.domain.home.repository.HomeRepository
4145
internal class DefaultHomeRepository @Inject constructor(
4246
private val homeApi: HomeApi,
4347
private val calendarApi: CalendarApi,
48+
@param:Named("homeAppService") private val homeAppServiceCache: InMemoryCache<HomeAppServiceInfo>,
49+
@param:Named("tabAppService") private val tabAppServiceCache: InMemoryCache<List<AppService>>,
4450
) : HomeRepository {
4551

4652
override suspend fun getRecentCalendar(): Result<RecentCalendar> =
@@ -49,8 +55,18 @@ internal class DefaultHomeRepository @Inject constructor(
4955
override suspend fun getHomeDescription(): Result<UserDescription> =
5056
suspendRunCatching { homeApi.getHomeDescription().toDomain() }
5157

52-
override suspend fun getHomeAppService(): Result<List<AppService>> =
53-
suspendRunCatching { homeApi.getHomeAppService().map { it.toDomain() } }
58+
override suspend fun getHomeAppService(forceRefresh: Boolean): Result<HomeAppServiceInfo> =
59+
suspendRunCatching {
60+
if (forceRefresh) homeAppServiceCache.invalidate()
61+
homeAppServiceCache.getOrFetch { homeApi.getHomeAppService().toDomain() }
62+
}
63+
64+
override suspend fun getTabAppService(): Result<List<AppService>> =
65+
suspendRunCatching {
66+
tabAppServiceCache.getOrFetch { homeApi.getTabAppService().map { it.toDomain() } }
67+
}
68+
69+
override fun observeTabAppService(): Flow<List<AppService>?> = tabAppServiceCache.data
5470

5571
override suspend fun getHomeReviewForm(): Result<ReviewForm> =
5672
suspendRunCatching { homeApi.getReviewForm().toDomain() }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* MIT License
3+
* Copyright 2024-2026 SOPT - Shout Our Passion Together
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all
13+
* copies or substantial portions of the Software.
14+
*
15+
* https://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
* SOFTWARE.
24+
*/
25+
package org.sopt.official.domain.home.model
26+
27+
data class HomeAppServiceInfo(
28+
val isAppjamMode: Boolean,
29+
val appServices: List<AppService>,
30+
)

domain/home/src/main/java/org/sopt/official/domain/home/repository/HomeRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
*/
2525
package org.sopt.official.domain.home.repository
2626

27+
import kotlinx.coroutines.flow.Flow
2728
import org.sopt.official.domain.home.model.AppService
2829
import org.sopt.official.domain.home.model.FloatingToast
30+
import org.sopt.official.domain.home.model.HomeAppServiceInfo
2931
import org.sopt.official.domain.home.model.LatestPost
3032
import org.sopt.official.domain.home.model.PopularPost
3133
import org.sopt.official.domain.home.model.RecentCalendar
@@ -38,7 +40,11 @@ interface HomeRepository {
3840

3941
suspend fun getHomeDescription(): Result<UserDescription>
4042

41-
suspend fun getHomeAppService(): Result<List<AppService>>
43+
suspend fun getHomeAppService(forceRefresh: Boolean = false): Result<HomeAppServiceInfo>
44+
45+
suspend fun getTabAppService(): Result<List<AppService>>
46+
47+
fun observeTabAppService(): Flow<List<AppService>?>
4248

4349
suspend fun getHomeReviewForm(): Result<ReviewForm>
4450

0 commit comments

Comments
 (0)