Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
548a686
[refactor] #89: 랜덤포즈 화면 재진입 시 불필요한 API 호출 방지
ikseong00 Feb 8, 2026
37ed3b6
[feat] #89: 랜덤 포즈 첫 방문 여부 확인을 위한 UserDataStore 추가
ikseong00 Feb 8, 2026
a067016
[feat] #89: 랜덤 포즈 첫 방문 시에만 튜토리얼 노출하도록 설정
ikseong00 Feb 8, 2026
1aa076d
[refactor] #78: 포즈 상세 화면에서 스크랩 적용 시 랜덤 포즈 화면에도 결과값 적용
ikseong00 Feb 8, 2026
863eb49
[feat] #89: 랜덤 포즈 로딩 실패 시 토스트 메시지 노출
ikseong00 Feb 8, 2026
953ce61
[refactor] #89: 랜덤 포즈 첫 방문 관련 로직 네이밍 및 로직 수정
ikseong00 Feb 8, 2026
e017a9f
[refactor] #89: 랜덤 포즈 첫 방문 여부 DataStore Key 이름 변경
ikseong00 Feb 8, 2026
0b8b19c
[fix] #89: 랜덤 포즈 튜토리얼 노출 조건 수정
ikseong00 Feb 8, 2026
54bc957
[refactor] #93: 랜덤 포즈 쿼리 파라미터 수정사항 적용
ikseong00 Feb 8, 2026
5be4009
[refactor] #89: 랜덤 포즈 추천 실패 시 예외 처리 방식 변경
ikseong00 Feb 8, 2026
3c4426f
[refactor] #89: 더 이상 추천할 포즈가 없을 때 불필요한 API 요청 방지
ikseong00 Feb 8, 2026
388e033
[refactor] #89: hasNextPost 기본값 수정
ikseong00 Feb 8, 2026
f35d94f
[feat] #89: API Exception 클래스 정의
ikseong00 Feb 8, 2026
463410f
[refactor] #89 랜덤 포즈 API 예외 처리 로직 수정
ikseong00 Feb 8, 2026
1084b20
[refactor] #89: DataStoreKey에서 HAS_VISITED_RANDOM_POSE 키 UserReposito…
ikseong00 Feb 8, 2026
54803f1
[refactor] #89: 포즈 스크랩 상태가 즉시 반영되도록 로직 수정
ikseong00 Feb 8, 2026
d3a2d20
[chore] #89: detekt 룰 적용
ikseong00 Feb 8, 2026
575b6f4
[chore] #89: detekt SwallowedException 비활성화
ikseong00 Feb 8, 2026
a853151
[refactor] #89: `markRandomPoseAsVisited` 함수명을 `setRandomPoseVisited`…
ikseong00 Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.neki.android.core.dataapi.datastore

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey

object DataStoreKey {
val ACCESS_TOKEN = stringPreferencesKey("access_token")
val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
val HAS_VISITED_RANDOM_POSE = booleanPreferencesKey("is_first_visit_random_pose")
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.neki.android.core.model.Pose
import com.neki.android.core.model.SortOrder
import kotlinx.coroutines.flow.Flow

const val NO_MORE_RANDOM_POSE = 400
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러코드에 대한 정의는 ApiException과 동일한 경로인 core:common에 있는게 어떨까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common에 저 변수를 선언해버리면,
400일 경우 랜덤포즈가 없다,는 내용이 공통적이어야할 것 같다고 생각됩니다.
한 곳으로 모으는 부분에 대해선 찬성이나,
음... common 혹은 network 모듈에 Exception 형태로 하는 건 어떨까요??

저 NO MORE RANDOM POSE 400은 해당 부분에서만 사용하니 냅두고 private 으로 변경한 다음에,
NoMoreRandomPoseException 으로 만들어서 뷰모델에서 참조하면 될 것 같습니다.

Copy link
Copy Markdown
Member

@Ojongseok Ojongseok Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkModule에 선언한다면 feature에서 core:data 의존성이 없어 viewmodel에서 접근이 안되지 않나요?

말씀하신대로 private으로 변경 혹은 제거한 후 아래 코멘트에서 말씀드린 것 처럼 발생 가능한 Exception 각각을 정의하고 throw하면 error is ClientApiException && error.code == NO_MORE_RANDOM_POSE ViewModel에서 현재처럼 NO_MORE_RANDOM_POSE에 접근할 필요가 없게 될 것 같습니다.


Comment thread
ikseong00 marked this conversation as resolved.
interface PoseRepository {

fun getPosesFlow(
Expand All @@ -22,14 +24,12 @@ interface PoseRepository {
suspend fun getSingleRandomPose(
headCount: PeopleCount,
excludeIds: Set<Long>,
maxRetry: Int,
): Result<Pose>

suspend fun getMultipleRandomPose(
headCount: PeopleCount,
excludeIds: Set<Long>,
poseSize: Int,
maxRetry: Int,
): Result<List<Pose>>

suspend fun updateScrap(poseId: Long, scrap: Boolean): Result<Unit>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.neki.android.core.dataapi.repository

import com.neki.android.core.model.UserInfo
import kotlinx.coroutines.flow.Flow

interface UserRepository {
val hasVisitedRandomPose: Flow<Boolean>
suspend fun markRandomPoseAsVisited()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저 같은 경우에는 온보딩 완료 플래그로 바꾸는 함수명을 setOnboardingCompleted()로 작성했는데 로컬 플래그를 설정하는 함수명을 조금 더 직관적인 setRandomPoseVisited()or markRandomPoseVisited()는 어떨까요?

#91 (comment) 해당 코멘트에서 말씀해주신 것 처럼 get플래그 : has + pp + 명사, set플래그에 대한 규칙도 정하면 좋을 것 같네요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setRandomPoseVisited() 이런 식으로 통일하겠습니다! a853151

suspend fun getUserInfo(): Result<UserInfo>
suspend fun updateUserInfo(nickname: String): Result<Unit>

suspend fun updateProfileImage(mediaId: Long?): Result<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ private val Context.authDataStore: DataStore<Preferences> by preferencesDataStor
private const val TOKEN_DATASTORE = "token_datastore"
private val Context.tokenDataStore: DataStore<Preferences> by preferencesDataStore(name = TOKEN_DATASTORE)

private const val USER_DATASTORE = "user_datastore"
private val Context.userDataStore: DataStore<Preferences> by preferencesDataStore(name = USER_DATASTORE)

@InstallIn(SingletonComponent::class)
@Module
internal object DataStoreModule {
Expand All @@ -30,4 +33,9 @@ internal object DataStoreModule {
@Singleton
@Provides
fun provideTokenDataStore(@ApplicationContext context: Context): DataStore<Preferences> = context.tokenDataStore

@UserDataStore
@Singleton
@Provides
fun provideUserDataStore(@ApplicationContext context: Context): DataStore<Preferences> = context.userDataStore
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ annotation class AuthDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TokenDataStore

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class UserDataStore
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ class PoseService @Inject constructor(
}

// 랜덤 포즈 조회
suspend fun getRandomPose(headCount: String): BasicResponse<PoseDetailResponse> {
suspend fun getRandomPose(headCount: String, excludeIds: String): BasicResponse<PoseDetailResponse> {
return client.get("/api/poses/random") {
parameter("headCount", headCount)
parameter("excludeIds", excludeIds)
}.body()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ package com.neki.android.core.data.repository.impl
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.neki.android.core.common.exception.RandomPoseRetryExhaustedException
import com.neki.android.core.data.paging.PosePagingSource
import com.neki.android.core.data.paging.ScrapPosePagingSource
import com.neki.android.core.data.remote.api.PoseService
import com.neki.android.core.data.util.runSuspendCatching
import com.neki.android.core.dataapi.repository.NO_MORE_RANDOM_POSE
import com.neki.android.core.dataapi.repository.PoseRepository
import com.neki.android.core.model.PeopleCount
import com.neki.android.core.model.Pose
import com.neki.android.core.model.SortOrder
import kotlinx.coroutines.flow.Flow
import retrofit2.HttpException
Comment thread
ikseong00 marked this conversation as resolved.
Outdated
import javax.inject.Inject

private const val PAGE_SIZE = 20
Expand Down Expand Up @@ -69,38 +70,40 @@ class PoseRepositoryImpl @Inject constructor(
override suspend fun getSingleRandomPose(
headCount: PeopleCount,
excludeIds: Set<Long>,
maxRetry: Int,
): Result<Pose> = runSuspendCatching {
repeat(maxRetry) {
val pose = poseService.getRandomPose(headCount = headCount.name).data.toModel()
if (pose.id !in excludeIds) {
return@runSuspendCatching pose
}
}
throw RandomPoseRetryExhaustedException("새로운 포즈를 찾지 못했어요")
val excludeIdsString = excludeIds.joinToString(",")
Comment thread
ikseong00 marked this conversation as resolved.
poseService.getRandomPose(
headCount = headCount.name,
excludeIds = excludeIdsString,
).data.toModel()
}

override suspend fun getMultipleRandomPose(
headCount: PeopleCount,
excludeIds: Set<Long>,
poseSize: Int,
maxRetry: Int,
): Result<List<Pose>> = runSuspendCatching {
val result = mutableListOf<Pose>()
val collectedIds = excludeIds.toMutableSet()
var retryCount = 0
repeat(poseSize) {
val excludeIdsString = collectedIds.joinToString(",")
val pose = try {
poseService.getRandomPose(
headCount = headCount.name,
excludeIds = excludeIdsString,
).data.toModel()
} catch (e: HttpException) {
// Http Error Code 이지만, 클라이언트에서 성공으로 취급
if (e.code() == NO_MORE_RANDOM_POSE) return@runSuspendCatching result
else throw e
}

while (result.size < poseSize && retryCount < maxRetry) {
val pose = poseService.getRandomPose(headCount = headCount.name).data.toModel()
if (pose.id !in collectedIds) {
result.add(pose)
collectedIds.add(pose.id)
} else {
retryCount++
}
}

result.ifEmpty { throw RandomPoseRetryExhaustedException("새로운 포즈를 찾지 못했어요") }
return@runSuspendCatching result
}

override suspend fun updateScrap(poseId: Long, scrap: Boolean): Result<Unit> = runSuspendCatching {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
package com.neki.android.core.data.repository.impl

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.neki.android.core.data.local.di.UserDataStore
import com.neki.android.core.data.remote.api.UserService
import com.neki.android.core.data.remote.model.request.UpdateProfileImageRequest
import com.neki.android.core.data.remote.model.request.UpdateUserInfoRequest
import com.neki.android.core.data.util.runSuspendCatching
import com.neki.android.core.dataapi.datastore.DataStoreKey
import com.neki.android.core.dataapi.repository.UserRepository
import com.neki.android.core.model.UserInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class UserRepositoryImpl @Inject constructor(
@UserDataStore private val dataStore: DataStore<Preferences>,
private val userService: UserService,
) : UserRepository {
override val hasVisitedRandomPose: Flow<Boolean> =
dataStore.data.map { preferences ->
preferences[DataStoreKey.HAS_VISITED_RANDOM_POSE] ?: false
}

override suspend fun markRandomPoseAsVisited() {
dataStore.edit { preferences ->
preferences[DataStoreKey.HAS_VISITED_RANDOM_POSE] = true
}
}

override suspend fun getUserInfo(): Result<UserInfo> = runSuspendCatching {
userService.getUserInfo().data.toModel()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.neki.android.feature.pose.impl.detail.PoseDetailViewModel
import com.neki.android.feature.pose.impl.main.PoseIntent
import com.neki.android.feature.pose.impl.main.PoseRoute
import com.neki.android.feature.pose.impl.main.PoseViewModel
import com.neki.android.feature.pose.impl.random.RandomPoseIntent
import com.neki.android.feature.pose.impl.random.RandomPoseRoute
import com.neki.android.feature.pose.impl.random.RandomPoseViewModel
import dagger.Module
Expand Down Expand Up @@ -68,12 +69,23 @@ private fun EntryProviderScope<NavKey>.poseEntry(navigator: Navigator) {
}

entry<PoseNavKey.RandomPose> { key ->
val resultBus = LocalResultEventBus.current
val viewModel = hiltViewModel<RandomPoseViewModel, RandomPoseViewModel.Factory>(
creationCallback = { factory ->
factory.create(key.peopleCount)
},
)

ResultEffect<PoseResult>(resultBus) { result ->
when (result) {
is PoseResult.ScrapChanged -> {
viewModel.store.onIntent(RandomPoseIntent.ScrapChanged(result.poseId, result.isScrapped))
}
}
}

RandomPoseRoute(
viewModel = hiltViewModel<RandomPoseViewModel, RandomPoseViewModel.Factory>(
creationCallback = { factory ->
factory.create(key.peopleCount)
},
),
viewModel = viewModel,
navigateBack = navigator::goBack,
navigateToPoseDetail = navigator::navigateToPoseDetail,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class RandomPoseUiState(
val currentIndex: Int = 0,
val poseList: ImmutableList<Pose> = persistentListOf(),
val committedScraps: Map<Long, Boolean> = emptyMap(),
val hasNewPose: Boolean = false,
Comment thread
ikseong00 marked this conversation as resolved.
Outdated
) {
val currentPose: Pose?
get() = poseList.getOrNull(currentIndex)
Expand All @@ -33,6 +34,7 @@ sealed interface RandomPoseIntent {
data object ClickScrapIcon : RandomPoseIntent
data object ClickLeftSwipe : RandomPoseIntent
data object ClickRightSwipe : RandomPoseIntent
data class ScrapChanged(val poseId: Long, val isScrapped: Boolean) : RandomPoseIntent
}

sealed interface RandomPoseEffect {
Expand Down
Loading