Skip to content

Commit 7ca3054

Browse files
authored
[Feat/#1613] sp3 onboarding api (#1614)
* [FEAT#1613] 솝레터 온보딩 및 토픽 관련 데이터 레이어 구현 및 로컬 스토리지 확장 - 솝레터 온보딩 관련 API 및 데이터 레이어 구현 (`SopletterOnboardingService`, `DataSource`, `Repository`) - `SopletterOnboardingResponseDto` 및 `SopletterOnboardingModel` 추가를 통한 데이터-도메인 모델 매핑 지원 - `UserStorage` 및 `DefaultSoptStorage`에 온보딩 완료 여부(`isOnboardingCompleted`) 관리를 위한 DataStore 로직 추가 - 솝레터 토픽 및 메시지 조회를 위한 `SopletterTopicService` 정의 - 솝레터 온보딩 관련 DI 모듈(`SopletterOnboardingRepositoryModule` 등) 구현 - `SopletterDataSourceImpl` 클래스 접근 제한자 수정 (`internal` -> `public`) - `feature:sopletter` 모듈 내 `core:navigation` 의존성 추가 * [FEAT#1613] 홈 화면 내 솝레터(Sopletter) 진입점 추가 및 네비게이션 연동 - `MainScreen`에 `sopletterGraph`를 추가하고 솝레터 화면으로 이동하기 위한 네비게이션 로직 구현 - `HomeRoute`에서 '솝트 더 재밌게 즐기기' 섹션의 주석을 해제하고, 서비스 리스트 중 '솝레터' 항목만 필터링하여 노출하도록 수정 - `HomeNavigator` 및 `HomeRoute` 파라미터에 `navigateToSopletter` 콜백 추가 - `feature:main` 모듈의 `build.gradle.kts`에 `feature:sopletter` 프로젝트 의존성 추가 * [REFACTOR#1613] isSopletterOnboardingCompleted 으로 코리 수정 * [REFACTOR#1613] 코리 수정 * [MOD#1613] 홈 앱 서비스 클릭 핸들러 단순화 및 미사용 코드 제거 - `HomeEnjoySoptServicesBlock` 및 `AppServiceItem`의 클릭 콜백 타입을 `(String?, String) -> Unit`에서 `() -> Unit`으로 변경 - `HomeRoute`에서 상세 라우팅 로직이 포함된 `onAppServiceClick` 구현부를 주석 처리 - `HomeRoute` 내 미사용 임포트(`rememberCoroutineScope`, `launch`, `HomeUrl`) 및 변수(`scope`) 제거 * [MOD#1613] 의존성 및 이중 패딩 문제 수정
1 parent 99aef4f commit 7ca3054

32 files changed

Lines changed: 517 additions & 41 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import org.sopt.official.model.UserStatus
3232
* @param userStatus 유저 상태
3333
* @param pushToken 푸시 토큰
3434
* @param platform 인증 플랫폼
35+
* @param isSopletterOnboardingCompleted 솝레터 온보딩 완료 여부
3536
* @param saveUserStatus 유저 상태 저장
3637
* @param savePushToken 푸시 토큰 저장
3738
* @param savePlatform 인증 플랫폼 저장
@@ -41,9 +42,11 @@ interface UserStorage {
4142
val userStatus: Flow<UserStatus>
4243
val pushToken: Flow<String>
4344
val platform: Flow<String>
45+
val isSopletterOnboardingCompleted: Flow<Boolean>
4446

4547
suspend fun saveUserStatus(status: UserStatus)
4648
suspend fun savePushToken(token: String)
4749
suspend fun savePlatform(platform: String)
50+
suspend fun saveOnboardingCompleted(isOnboarded: Boolean)
4851
suspend fun clearUser()
4952
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ package org.sopt.official.localstorage.sourceimpl
2626

2727
import androidx.datastore.core.DataStore
2828
import androidx.datastore.preferences.core.Preferences
29+
import androidx.datastore.preferences.core.booleanPreferencesKey
2930
import androidx.datastore.preferences.core.edit
3031
import androidx.datastore.preferences.core.stringPreferencesKey
3132
import kotlinx.coroutines.flow.Flow
@@ -58,6 +59,10 @@ class DefaultSoptStorage @Inject constructor(
5859
preferences[KEY_PLAYGROUND_TOKEN]?.decryptInReleaseMode(keyAlias = PLAYGROUND_TOKEN_KEY_ALIAS) ?: DEFAULT_VALUE
5960
}
6061

62+
override val isSopletterOnboardingCompleted: Flow<Boolean> = dataStore.data.map { preferences ->
63+
preferences[KEY_ONBOARDING_COMPLETED] ?: false
64+
}
65+
6166
override suspend fun saveTokens(accessToken: String, refreshToken: String) {
6267
dataStore.edit { preferences ->
6368
preferences[KEY_ACCESS_TOKEN] = accessToken.encryptInReleaseMode(keyAlias = ACCESS_TOKEN_KEY_ALIAS)
@@ -110,6 +115,12 @@ class DefaultSoptStorage @Inject constructor(
110115
}
111116
}
112117

118+
override suspend fun saveOnboardingCompleted(isOnboarded: Boolean) {
119+
dataStore.edit { preferences ->
120+
preferences[KEY_ONBOARDING_COMPLETED] = isOnboarded
121+
}
122+
}
123+
113124
override suspend fun clearUser() {
114125
dataStore.edit { preferences ->
115126
preferences.remove(KEY_USER_STATUS)
@@ -131,6 +142,7 @@ class DefaultSoptStorage @Inject constructor(
131142
private val KEY_USER_STATUS = stringPreferencesKey("user_status")
132143
private val KEY_PUSH_TOKEN = stringPreferencesKey("push_token")
133144
private val KEY_PLATFORM = stringPreferencesKey("platform")
145+
private val KEY_ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed")
134146

135147

136148
private const val DEFAULT_VALUE = ""

data/sopletter/src/main/java/org/sopt/official/data/sopletter/datasourceimpl/SopletterDataSourceImpl.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ import org.sopt.official.data.sopletter.dto.response.SopletterMessageDetailRespo
3030
import org.sopt.official.data.sopletter.dto.response.SopletterReportFormResponseDto
3131
import org.sopt.official.data.sopletter.service.SopletterService
3232
import javax.inject.Inject
33-
34-
internal class SopletterDataSourceImpl @Inject constructor(
33+
class SopletterDataSourceImpl @Inject constructor(
3534
private val sopletterService: SopletterService,
3635
) : SopletterDataSource {
3736
override suspend fun getDefaultMessages(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.sopt.official.data.sopletter.onboarding.datasource
2+
3+
import org.sopt.official.data.sopletter.onboarding.dto.response.SopletterOnboardingResponseDto
4+
5+
interface SopletterOnboardingDataSource {
6+
suspend fun getOnboarding(): SopletterOnboardingResponseDto
7+
suspend fun completeOnboarding(): SopletterOnboardingResponseDto
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.sopt.official.data.sopletter.onboarding.datasourceimpl
2+
3+
import org.sopt.official.data.sopletter.onboarding.datasource.SopletterOnboardingDataSource
4+
import org.sopt.official.data.sopletter.onboarding.dto.response.SopletterOnboardingResponseDto
5+
import org.sopt.official.data.sopletter.onboarding.service.SopletterOnboardingService
6+
import javax.inject.Inject
7+
8+
class SopletterOnboardingDataSourceImpl @Inject constructor(
9+
private val sopletterOnboardingService: SopletterOnboardingService
10+
): SopletterOnboardingDataSource {
11+
override suspend fun getOnboarding(): SopletterOnboardingResponseDto =
12+
sopletterOnboardingService.getOnboarding()
13+
14+
override suspend fun completeOnboarding(): SopletterOnboardingResponseDto =
15+
sopletterOnboardingService.completeOnboarding()
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.sopt.official.data.sopletter.onboarding.di
2+
3+
import dagger.Binds
4+
import dagger.Module
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import org.sopt.official.data.sopletter.onboarding.datasource.SopletterOnboardingDataSource
8+
import org.sopt.official.data.sopletter.onboarding.datasourceimpl.SopletterOnboardingDataSourceImpl
9+
import javax.inject.Singleton
10+
11+
@Module
12+
@InstallIn(SingletonComponent::class)
13+
interface SopletterOnboardingDataSourceModule {
14+
@Binds
15+
@Singleton
16+
fun bindSopletterOnboardingDataSource(
17+
impl: SopletterOnboardingDataSourceImpl
18+
) : SopletterOnboardingDataSource
19+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* MIT License
3+
* Copyright 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.sopletter.onboarding.di
26+
27+
import dagger.Binds
28+
import dagger.Module
29+
import dagger.hilt.InstallIn
30+
import dagger.hilt.components.SingletonComponent
31+
import org.sopt.official.data.sopletter.onboarding.repositoryimpl.SopletterOnboardingRepositoryImpl
32+
import org.sopt.official.domain.sopletter.onboarding.repository.SopletterOnboardingRepository
33+
import javax.inject.Singleton
34+
35+
@Module
36+
@InstallIn(SingletonComponent::class)
37+
internal interface SopletterOnboardingRepositoryModule {
38+
39+
@Binds
40+
@Singleton
41+
fun bindSopletterOnboardingRepository(
42+
impl: SopletterOnboardingRepositoryImpl,
43+
): SopletterOnboardingRepository
44+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* MIT License
3+
* Copyright 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.sopletter.onboarding.di
26+
27+
import dagger.Module
28+
import dagger.Provides
29+
import dagger.hilt.InstallIn
30+
import dagger.hilt.components.SingletonComponent
31+
import org.sopt.official.common.di.AppRetrofit
32+
import org.sopt.official.data.sopletter.onboarding.service.SopletterOnboardingService
33+
import retrofit2.Retrofit
34+
import retrofit2.create
35+
import javax.inject.Singleton
36+
37+
@Module
38+
@InstallIn(SingletonComponent::class)
39+
internal object SopletterOnboardingServiceModule {
40+
41+
@Provides
42+
@Singleton
43+
internal fun provideSopletterOnboardingService(
44+
@AppRetrofit(true) retrofit: Retrofit,
45+
): SopletterOnboardingService = retrofit.create()
46+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.sopt.official.data.sopletter.onboarding.dto.response
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import org.sopt.official.domain.sopletter.onboarding.model.SopletterOnboardingModel
6+
7+
@Serializable
8+
data class SopletterOnboardingResponseDto(
9+
@SerialName("nickname")
10+
val nickname: String,
11+
@SerialName("isOnboarded")
12+
val isOnboarded: Boolean // 온보딩 진행 여부
13+
) {
14+
fun toDomain() = SopletterOnboardingModel(
15+
nickname = nickname,
16+
isOnboarded = isOnboarded
17+
)
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.sopt.official.data.sopletter.onboarding.repositoryimpl
2+
3+
import org.sopt.official.common.coroutines.suspendRunCatching
4+
import org.sopt.official.data.sopletter.onboarding.service.SopletterOnboardingService
5+
import org.sopt.official.domain.sopletter.onboarding.model.SopletterOnboardingModel
6+
import org.sopt.official.domain.sopletter.onboarding.repository.SopletterOnboardingRepository
7+
import javax.inject.Inject
8+
9+
internal class SopletterOnboardingRepositoryImpl @Inject constructor(
10+
private val sopletterOnboardingService: SopletterOnboardingService,
11+
) : SopletterOnboardingRepository {
12+
13+
override suspend fun getOnboarding(): Result<SopletterOnboardingModel> = suspendRunCatching {
14+
sopletterOnboardingService.getOnboarding().toDomain()
15+
}
16+
17+
override suspend fun completeOnboarding(): Result<SopletterOnboardingModel> = suspendRunCatching {
18+
sopletterOnboardingService.completeOnboarding().toDomain()
19+
}
20+
}

0 commit comments

Comments
 (0)