-
Notifications
You must be signed in to change notification settings - Fork 1
[Refactor] 프로필 리팩토링 : 데이터, 도메인 레이어 #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- ProfileApi, ProfileRemoteDataSource 인터페이스 정의 - ProfileRemoteDataSourceImpl 구현 - 관련 DTO(Request/Response) 클래스 추가 - 모든 public 메서드에 대한 성공/실패 단위 테스트 케이스 작성
- ProfileRepository, ProfileLocalDataSource 인터페이스 정의 - ProfileRepository getProfile 구현 - 관련 모델 추가 - getProfile 성공 케이스에 대한 테스트 작성
- 모든 메서드에 대한 테스트 코드 작성(커버리지 100%)
Repository, Local/Remote DataSource Hilt 모듈
Walkthrough프로필 전반을 “레거시” 경로로 전환하면서, 네비게이션/화면/레포지토리/DI를 이중화했다. 동시에 “신규” 프로필 도메인/DTO/레포지토리 흐름을 도입해 로컬 캐시 + 원격 API(ProfileApi) 기반으로 재구성했다. 도메인 모델/에러/유스케이스와 테스트가 이에 맞게 추가·정리되었다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor VM as ViewModel/UseCase
participant Repo as ProfileRepositoryImpl (신규)
participant Local as ProfileLocalDataSource
participant Remote as ProfileRemoteDataSource (ProfileApi)
VM->>Repo: getProfile(): Flow<Result<Profile>>
activate Repo
Repo->>Local: getProfile(): Flow<Profile?>
Note over Repo,Local: 로컬에 값이 없으면 원격 조회
alt 로컬 값 존재
Local-->>Repo: Profile?
Repo-->>VM: Result.success(Profile)
else 로컬 값 없음
Repo->>Remote: getProfile()
Remote-->>Repo: ProfileResponse
Repo->>Repo: toProfile() 매핑
Repo->>Local: cacheProfile(Profile)
Local-->>Repo: (완료)
Repo-->>VM: Result.success(Profile)
end
deactivate Repo
sequenceDiagram
autonumber
actor UI as Profile Editor (신규)
participant Repo as ProfileRepositoryImpl
participant Remote as ProfileRemoteDataSource
participant Local as ProfileLocalDataSource
UI->>Repo: updateProfile(newProfile)
Repo->>Repo: newProfile.toUpdateProfileRequest()
Repo->>Remote: updateProfile(request)
Remote-->>Repo: Unit
Repo->>Local: cacheProfile(newProfile)
Local-->>Repo: (완료)
Repo-->>UI: Result.success(Unit)
%% 오류 시
Note over Repo,UI: 실패 시 Result.failure(UpdateProfileError ...)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt (1)
151-167: Dialog onDismiss에서 닫기 콜백이 아니라 열기 토글을 호출합니다.
onDismissRequest시에는 닫기 콜백(onDisMissExitDialog)을 호출해야 합니다. 현재 구현은 의도와 달라 오동작 가능성이 있습니다.- onDismissRequest = { - onRequestExitDialog() - }, + onDismissRequest = { + onDisMissExitDialog() + },core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt (1)
84-85: Kotlin 리터럴 문법 오류:.0→0.0. 컴파일이 불가합니다.부동소수점 리터럴은 선행 0이 필요합니다.
- val result = spotRepositoryImpl.fetchSpotList(.0, .0, mockk(relaxed = true)) + val result = spotRepositoryImpl.fetchSpotList(0.0, 0.0, mockk(relaxed = true))feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt (2)
295-318: 이미지 처리 로직의 리소스 관리 문제InputStream과 HTTP Response가 제대로 닫히지 않아 리소스 누수가 발생할 수 있습니다.
if (state.selectedPhotoUri.startsWith("content://")) { - val inputStream = context.contentResolver.openInputStream(imageUri) - byteArray = inputStream?.readBytes() - ?: throw IllegalArgumentException("이미지 읽기 실패") + byteArray = context.contentResolver.openInputStream(imageUri)?.use { + it.readBytes() + } ?: throw IllegalArgumentException("이미지 읽기 실패") mimeType = context.contentResolver.getType(imageUri) ?: "image/jpeg" } else if (state.selectedPhotoUri.startsWith("http://") || state.selectedPhotoUri.startsWith("https://")) { Timber.tag(TAG).d("원격 URL에서 이미지 가져오기 시작") val getRequest = Request.Builder().url(imageUri.toString()).build() - val getResponse = client.newCall(getRequest).execute() - - if (!getResponse.isSuccessful) { - Timber.tag(TAG).e("원격 이미지 가져오기 실패, code: %d", getResponse.code) - throw IllegalArgumentException("원격 이미지 가져오기 실패") - } - - byteArray = getResponse.body?.bytes() - ?: throw IllegalArgumentException("원격 이미지 읽기 실패") - mimeType = getResponse.header("Content-Type") ?: "image/jpeg" + client.newCall(getRequest).execute().use { getResponse -> + if (!getResponse.isSuccessful) { + Timber.tag(TAG).e("원격 이미지 가져오기 실패, code: %d", getResponse.code) + throw IllegalArgumentException("원격 이미지 가져오기 실패") + } + + byteArray = getResponse.body?.bytes() + ?: throw IllegalArgumentException("원격 이미지 읽기 실패") + mimeType = getResponse.header("Content-Type") ?: "image/jpeg" + }
173-174: 도메인 로직과 생년월일 검증 기준(1900년) 일치
ValidateBirthDateUseCase에서 최소 허용 연도를 1900년으로 설정(pastThreshold = LocalDate.of(1900, 1, 1))하므로, ViewModel에서도 하드코딩된 1940년 대신 1900년 기준을 사용해야 합니다.-if (year > currentYear || year <= 1940) return false +// 도메인 레이어의 ValidateBirthDateUseCase와 일관성 유지 +if (year > currentYear || year < 1900) return falsefeature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt (1)
40-56: UpdateProfileType 처리 흐름 통합 필요 — Legacy 상태흐름과 Result 혼재검증 결과: UpdateProfileType 기반 상태 흐름이 여전히 사용됩니다 (core/data/src/.../ProfileRepositoryLegacyImpl.kt, domain/src/.../ProfileRepositoryLegacy.kt, feature/profile/.../ProfileModViewModelLegacy.kt — updateProfile 후 updateProfileType emit, feature/profile/.../ProfileViewModelLegacy.kt — val updateProfileState = profileRepositoryLegacy.getProfileType(), feature/profile/.../ProfileScreenContainerLegacy.kt — collectLatest로 SUCCESS 처리).
조치(택1):
- Result 기반으로 통일: ViewModel이 updateProfile 결과를 처리해 UI 알림/사이드이펙트만 담당하도록 하고 repository의 updateProfileType/getProfileType/resetProfileType API 제거(관련 파일: domain/src/.../ProfileRepositoryLegacy.kt, core/data/src/.../ProfileRepositoryLegacyImpl.kt, feature/profile/.../ProfileModViewModelLegacy.kt, ProfileViewModelLegacy.kt, ProfileScreenContainerLegacy.kt).
- 또는 repository 중심 유지: updateProfile 내부에서 성공/실패 시 UpdateProfileType을 emit하도록 통합하고 ViewModel/UI는 상태흐름만 구독하도록 중복된 Result 처리 제거.
core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt (1)
129-131: 잘못된 expectedErrorClass 타입 파라미터
deleteVerifiedAreaErrorScenarios테스트에서 expectedErrorClass의 타입이KClass<ReplaceVerifiedArea>로 되어 있지만, 실제로는KClass<DeleteVerifiedAreaError>이어야 합니다.fun `인증 지역 삭제 API 실패 시 에러 객체를 반환한다`( errorCode: Int, - expectedErrorClass: KClass<ReplaceVerifiedArea> + expectedErrorClass: KClass<DeleteVerifiedAreaError> ) = runTest {
♻️ Duplicate comments (1)
app/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt (1)
15-15: Settings와의 프로필 네비게이션 정책 일치 여부 확인여기는
ProfileRouteLegacy.Graph로 이동하고, Settings 쪽은 leaf로 이동합니다. 의도된 차별화인지 확인하고, 아니라면 동일 정책으로 정리해 주세요.Also applies to: 38-39
🧹 Nitpick comments (56)
core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt (1)
3-7: 필드 명확성(네이밍) 제안
image는 URL 의미로 보입니다.imageUrl로의 리네임을 고려하면 가독성이 좋아집니다. 파급범위가 크면 유지하되 KDoc에 의미를 명시해 주세요.feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt (3)
50-60: 수동 문자열 자르기 대신 Compose Ellipsis 사용 권장문자 수 기준 수동 절단은 다국어/이모지에서 깨질 수 있습니다.
maxLines=1+overflow=TextOverflow.Ellipsis로 교체하세요.다음과 같이 정리 가능합니다:
- Text( - text = if (spot.name.length > 9) spot.name.take(8) + stringResource(R.string.ellipsis) else spot.name, + Text( + text = spot.name, color = AconTheme.color.White, style = AconTheme.typography.Title5, fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .padding(top = 20.dp) .padding(horizontal = 20.dp) )같은 변경을 아래 else 분기(라인 71-81)에도 적용해 주세요.
62-69: 플레이스홀더 이미지 a11y 보강
contentDescription = null대신 적절한 설명 문자열을 제공하면 스크린리더 접근성이 향상됩니다.- Image( + Image( painter = painterResource(R.drawable.ic_bg_no_store_profile), - contentDescription = null, + contentDescription = stringResource(R.string.no_store_image), contentScale = ContentScale.Crop, modifier = Modifier .fillMaxSize() .imageGradientLayer() )
29-33: FQCN 대신 import 사용 권장파라미터 타입을 완전수식명으로 표기하면 가독성이 떨어집니다. 파일 상단에 import 추가 후 간결하게 쓰세요.
-import com.acon.acon.core.model.model.profile.SavedSpotLegacy +import com.acon.acon.core.model.model.profile.SavedSpotLegacy ... -internal fun BookmarkItemLegacy( - spot: com.acon.acon.core.model.model.profile.SavedSpotLegacy, +internal fun BookmarkItemLegacy( + spot: SavedSpotLegacy,feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkSkeletonItemLegacy.kt (1)
15-19: 공개 API 축소: internal로 한정 권장UI 스켈레톤은 모듈 외부 노출 필요가 낮습니다.
internal로 범위를 줄여 공개 API 표면을 최소화하세요.-@Composable -fun BookmarkSkeletonItemLegacy( +@Composable +internal fun BookmarkSkeletonItemLegacy( skeletonHeight: Dp, modifier: Modifier = Modifier ) {core/data/src/main/kotlin/com/acon/core/data/dto/request/UpdateProfileRequestLegacy.kt (1)
8-13: @OptIn 위치 및 기본값 인코딩 정책 일관화
@OptIn은 클래스에 부착하는 형태가 더 일반적입니다(생성자 타겟보다 가독성↑).birthDate만EncodeDefault.NEVER인 반면profileImage는 누락되어 정책이 불일치합니다. 서버가 null 필드 생략을 기대한다면 동일 정책으로 맞추세요.-@Serializable -data class UpdateProfileRequestLegacy @OptIn(ExperimentalSerializationApi::class) constructor( - @SerialName("profileImage") val profileImage: String? = null, +@OptIn(ExperimentalSerializationApi::class) +@Serializable +data class UpdateProfileRequestLegacy( + @SerialName("profileImage") @EncodeDefault(EncodeDefault.Mode.NEVER) val profileImage: String? = null, @SerialName("nickname") val nickname: String, @SerialName("birthDate") @EncodeDefault(EncodeDefault.Mode.NEVER) val birthDate: String? = null )서버가 null 필드 생략을 요구하는지 API 스펙을 다시 확인해 주세요. 필요 시 두 필드 모두 동일 정책으로 유지해야 합니다.
core/navigation/src/main/java/com/acon/acon/core/navigation/route/ProfileRouteLegacy.kt (1)
5-18: sealed interface로 변경해 외부 모듈 임의 구현 방지ProfileRouteLegacy를
sealed interface로 선언해 외부에서 새로운 구현체 추가를 막습니다. Compose Navigation에서composable<ProfileRouteLegacy.ProfileLegacy>등으로 사용 중인 것도 확인되었습니다.-interface ProfileRouteLegacy { +sealed interface ProfileRouteLegacy {core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt (3)
8-10: nullable 리스트 대신 기본값 emptyList로 두는 것을 권장API가 null을 반환하지 않는 계약이라면, nullable보다 기본값을 두는 편이 NPE 방지와 사용성에 유리합니다. 서버가 null을 줄 가능성이 있다면 계약을 확인해 주세요.
적용 예시:
- data class SavedSpotsResponseLegacy( - @SerialName("savedSpotList") val savedSpotResponseLegacyList: List<SavedSpotResponseLegacy>? - ) + data class SavedSpotsResponseLegacy( + @SerialName("savedSpotList") val savedSpotResponseLegacyList: List<SavedSpotResponseLegacy> = emptyList() + )
19-23: spotId를 0L로 대체하면 충돌/의미 손실 위험null id를 0L로 치환하면 실제 id=0과 구분이 어려워집니다. null을 가진 항목은 매핑 단계에서 제외하거나, 상위 컨테이너에서 mapNotNull 처리하는 패턴을 고려해 주세요.
참고 예(컨테이너 -> 도메인 매핑 보강):
// 추가 함수(별도 파일/확장함수 권장) fun SavedSpotsResponseLegacy.toDomain(): List<SavedSpotLegacy> = savedSpotResponseLegacyList.orEmpty() .mapNotNull { it.toDomainOrNull() } // 반환 타입을 SavedSpotLegacy? 로 바꾸는 대안 fun SavedSpotResponseLegacy.toDomainOrNull(): SavedSpotLegacy? { val id = spotId ?: return null return SavedSpotLegacy( spotId = id, name = name.orEmpty(), image = image.orEmpty() ) }
19-19: 매핑 함수 명확성: toSavedSpot → toSavedSpotLegacy 또는 toDomain 권장도메인 타입이 SavedSpotLegacy인 만큼 함수명을 더 구체화하면 가독성이 좋아집니다.
app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt (1)
14-14: Profile 진입 지점 일관성 확보여기는
ProfileRouteLegacy.ProfileLegacy(leaf)로, SpotNavigation은ProfileRouteLegacy.Graph(graph root)로 이동합니다. 시작 지점이 달라지면 back stack 구성/동작이 달라질 수 있습니다. 의도가 아니라면 하나로 통일해 주세요.예: 그래프 루트로 통일
- navController.navigate(ProfileRouteLegacy.ProfileLegacy) { + navController.navigate(ProfileRouteLegacy.Graph) { popUpTo(SettingsRoute.Graph) { inclusive = true } }Also applies to: 34-38
domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt (1)
3-3: 확인: fetchSavedSpotList 반환 타입이 SavedSpotLegacy로 일관 적용됨domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt, core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt, feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/BookmarkViewModel.kt에서 Result<List> 사용을 확인했습니다 — 호출부 영향 없음. 권장: 레거시임을 명시하는 KDoc 또는 typealias 도입 고려.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (1)
104-114: 지역 변수 이름 섀도잉 제거(가독성 개선)동일 스코프 내 isDeepLink 이름 재사용이 있습니다. 혼동 방지를 위해 내부 변수명을 분리하세요.
- val isDeepLink = spotNavData.isFromDeepLink == true + val isDeepLinkNav = spotNavData.isFromDeepLink == true SpotDetailUiState.Success( - tags = spotNavData.tags.takeUnless { isDeepLink }, + tags = spotNavData.tags.takeUnless { isDeepLinkNav }, transportMode = spotNavData.transportMode, eta = spotNavData.eta, spotDetail = spotDetail, isAreaVerified = isAreaVerified, - isFromDeepLink = isDeepLink, + isFromDeepLink = isDeepLinkNav, navFromProfile = spotNavData.navFromProfile, )feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreen.kt (2)
108-114: 로딩 스켈레톤이 mock 데이터 크기에 종속 — 고정 카운트로 분리 권장mockSpotList 길이에 의존하면 추후 변경 시 UI 흔들립니다. 스켈레톤 개수를 상수로 두고 생성하세요.
- mockSpotList.chunked(2).forEach { rowItems -> + val skeletonCount = 6 + List(skeletonCount) { Unit }.chunked(2).forEach { rowItems -> Row( @@ - rowItems.forEach { spot -> + rowItems.forEach { BookmarkSkeletonItemLegacy( skeletonHeight = skeletonHeight, modifier = Modifier .weight(1f) .aspectRatio(160f / 231f) ) }
169-184: 내부 루프도 fastForEach로 통일(미세 최적화)chunked 후 외부는 fastForEach를 쓰지만 내부는 forEach입니다. 일관성 및 약간의 성능 향상을 위해 fastForEach로 변경을 제안합니다.
- rowItems.forEach { spot -> + rowItems.fastForEach { spot -> BookmarkItemLegacy( spot = spot, onClickSpotItem = { onSpotClick(spot.spotId) }, modifier = Modifier .weight(1f) .aspectRatio(160f / 231f) ) }core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt (1)
11-11: ProfileInfoCacheLegacy로 전환됨 — @singleton 바인딩 확인, 구 타입 없음
- 확인: core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt (lines 16–22)에서 providesProfileInfoCache가 @singleton으로 ProfileInfoCacheLegacy를 제공합니다.
- 권고: UserRepositoryImpl, SpotRepositoryImpl, ProfileRepositoryLegacyImpl 및 테스트들에 ProfileInfoCacheLegacy 사용처가 남아 있으므로, 전환 기간에 양쪽 캐시가 공존할 경우 세션 정리(clearSession) 시 두 캐시를 모두 비우는 전략을 적용하세요.
domain/src/main/java/com/acon/acon/domain/usecase/ValidateBirthDateUseCase.kt (2)
6-6: 생성자에서 불필요한 괄호를 제거하세요.빈 생성자에 괄호가 불필요합니다.
-class ValidateBirthDateUseCase() { +class ValidateBirthDateUseCase {
8-8: 하드코딩된 임계값을 설정으로 분리하는 것을 고려하세요.1900년 임계값이 하드코딩되어 있습니다. 향후 요구사항 변경 시 유연성을 위해 설정 파일이나 상수 클래스로 분리하는 것을 고려해보세요.
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/BookmarkViewModel.kt (2)
47-47: 성공 상태에서 List를 nullable로 둘 필요가 없습니다기본값이 emptyList()인데 타입을 List<…>?로 두면 사용처에서 불필요한 null 처리가 생깁니다. 비‑null List로 변경하는 것이 안전하고 일관됩니다.
아래와 같이 수정 제안드립니다:
-sealed interface BookmarkUiState { - data class Success(val savedSpotLegacies: List<com.acon.acon.core.model.model.profile.SavedSpotLegacy>? = emptyList()) : BookmarkUiState +sealed interface BookmarkUiState { + data class Success( + val savedSpotLegacies: List<com.acon.acon.core.model.model.profile.SavedSpotLegacy> = emptyList() + ) : BookmarkUiState추가로 가독성을 위해 타입을 import하여 FQCN 사용을 피하는 것도 권장합니다.
// 파일 상단 import 제안 import com.acon.acon.core.model.model.profile.SavedSpotLegacy
22-22: 하드코딩된 지연(delay) 제거 또는 디버그 게이트 필요의도적 스켈레톤 연출이 아니라면 800ms 지연은 사용자 체감 성능을 저하시킬 수 있습니다. 디버그 빌드에서만 동작하도록 게이트하거나 제거를 권장합니다.
- delay(800) + // if (BuildConfig.DEBUG) delay(800)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotResponse.kt (1)
12-19: 썸네일 공백/트리밍 처리 누락"공백 포함 URL"이 들어오면 그대로 Exist(url)로 전달됩니다. 앞뒤 공백을 제거해 저장/표시 이슈를 예방하세요.
- fun toSavedSpot() : SavedSpot { - val spotThumbnailStatus = when { - spotThumbnail.isNullOrBlank() -> SpotThumbnailStatus.Empty - else -> SpotThumbnailStatus.Exist(spotThumbnail) - } + fun toSavedSpot(): SavedSpot { + val normalized = spotThumbnail?.trim() + val spotThumbnailStatus = when { + normalized.isNullOrBlank() -> SpotThumbnailStatus.Empty + else -> SpotThumbnailStatus.Exist(normalized) + } return SavedSpot(spotId, spotName, spotThumbnailStatus) }core/model/src/main/java/com/acon/acon/core/model/model/profile/SpotThumbnailStatus.kt (1)
5-6: 불변식 보강: 빈 URL 금지Exist.url에 대한 간단한 불변식 체크를 추가하면 잘못된 상태 전파를 차단할 수 있습니다.
- data class Exist(val url: String) : SpotThumbnailStatus + data class Exist(val url: String) : SpotThumbnailStatus { + init { require(url.isNotBlank()) { "Spot thumbnail url must not be blank." } } + }domain/src/main/java/com/acon/acon/domain/error/profile/ValidateBirthDateError.kt (1)
8-12: 상태 없는 에러 타입은 object로 정의하여 할당/비교 비용 절감인스턴스 상태가 없으므로 object로 전환하면 불필요한 객체 생성을 줄이고 비교도 용이합니다.
- class InputIsFuture : ValidateBirthDateError() - class InputIsTooPast : ValidateBirthDateError() - class InvalidFormat : ValidateBirthDateError() { + object InputIsFuture : ValidateBirthDateError() + object InputIsTooPast : ValidateBirthDateError() + object InvalidFormat : ValidateBirthDateError() { override val code = UNSPECIFIED_SERVER_ERROR_CODE }domain/src/main/java/com/acon/acon/domain/usecase/ValidateNicknameUseCase.kt (1)
16-21: 공백만 입력 처리 및 트리밍 권장isEmpty 대신 isBlank 사용을 권장합니다. (스페이스만 있는 입력을 Empty와 동일하게 처리)
- nickname.isEmpty() -> Result.failure(ValidateNicknameError.EmptyInput()) + nickname.isBlank() -> Result.failure(ValidateNicknameError.EmptyInput())추가로, 서버에 전달하는 문자열은 trim() 적용을 고려해주세요.
- else -> profileRepository.validateNickname(nickname) + else -> profileRepository.validateNickname(nickname.trim())core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt (1)
19-23: @provides 반환 타입 명시 및 @iodispatcher 바인딩 검토(검증됨)
- providesProfileInfoCache에 명시적 반환 타입을 추가하세요.
- @iodispatcher는 core/common/src/main/java/com/acon/acon/core/common/DispatcherQualifiers.kt에 정의되어 있고, IO용 CoroutineScope 제공자는 app/src/main/java/com/acon/acon/di/CoroutineScopesModule.kt에 존재하므로 현재 주입은 유효합니다. 다만 관례적으로는 Dispatcher에 qualifier를 붙이고 Scope에는 별도 qualifier(@ApplicationScope 등)를 사용하는 것이 더 명확하므로 선택적 리팩터를 권장합니다.
- fun providesProfileInfoCache( - @IODispatcher scope: CoroutineScope, - profileRemoteDataSourceLegacy: ProfileRemoteDataSourceLegacy - ) = ProfileInfoCacheLegacy(scope, profileRemoteDataSourceLegacy) + fun providesProfileInfoCache( + @IODispatcher scope: CoroutineScope, + profileRemoteDataSourceLegacy: ProfileRemoteDataSourceLegacy + ): ProfileInfoCacheLegacy = ProfileInfoCacheLegacy(scope, profileRemoteDataSourceLegacy)core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt (3)
23-24: 쿼리 문자열을 경로에 하드코딩하지 말고 @query 파라미터로 분리하세요.테스트/유지보수성 및 인코딩 안전성 측면에서
?imageType=PROFILE을 경로에서 제거하고@Query("imageType")로 분리하는 편이 안전합니다.-@GET("/api/v1/images/presigned-url?imageType=PROFILE") -suspend fun getPreSignedUrl() : PreSignedUrlResponse +@GET("/api/v1/images/presigned-url") +suspend fun getPreSignedUrl( + @Query("imageType") imageType: String = "PROFILE" +): PreSignedUrlResponse
26-29: encoded=true 재확인 필요 (닉네임 특수문자/공백 인코딩 이슈).
encoded = true는 호출부가 직접 인코딩한 값을 전달한다는 뜻입니다. 서버가 raw 값을 기대한다면 기본값(false)로 두어 Retrofit이 안전하게 인코딩하도록 해야 합니다. 현재 계약을 재확인해 주세요. 문제가 없다면 주석으로 의도를 남겨주세요.-@Query("nickname", encoded = true) nickname: String +@Query("nickname") nickname: String
31-35: 쓰기 계열 API의 반환 타입 일관화 제안(Unit vs Response).동일 레벨의 호출인
updateProfile,saveSpot가 서로 다른 반환 타입을 사용 중입니다. 팀 컨벤션에 맞춰 일관화해 주세요(권장: 모두 Unit, 비-2xx는 예외로 처리).예시(모두 Unit로 통일):
@PATCH("/api/v1/members/me") suspend fun updateProfile( - @Body request: UpdateProfileRequestLegacy -): Response<Unit> + @Body request: UpdateProfileRequestLegacy +)Also applies to: 39-42
domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt (1)
7-12: 상태가 없는 서브타입은 object로 선언해 할당/비교 비용을 줄이세요.동일 의미의 인스턴스를 매번 생성할 필요가 없습니다.
object로 바꾸면 참조 동일성 비교와 메모리 절약에 유리합니다. 패턴이 프로젝트의 RootError 설계와 충돌하지 않는지 확인 바랍니다.- class UnsatisfiedCondition : ValidateNicknameErrorLegacy() { - override val code: Int = 40051 - } - class AlreadyUsedNickname : ValidateNicknameErrorLegacy() { - override val code: Int = 40901 - } + object UnsatisfiedCondition : ValidateNicknameErrorLegacy() { + override val code: Int = 40051 + } + object AlreadyUsedNickname : ValidateNicknameErrorLegacy() { + override val code: Int = 40901 + } @@ - return arrayOf( - UnsatisfiedCondition(), - AlreadyUsedNickname() - ) + return arrayOf( + UnsatisfiedCondition, + AlreadyUsedNickname + )Also applies to: 14-19
core/data/src/main/kotlin/com/acon/core/data/datasource/local/ProfileLocalDataSource.kt (2)
19-21: 식별자 섀도잉 방지: Flow 프로퍼티명을 명확히.파라미터
profile와 프로퍼티profile가 섀도잉되어 가독성이 떨어집니다. 프로퍼티명을profileFlow등으로 바꾸는 것을 권장합니다.- private val profile = _profile.asStateFlow() + private val profileFlow = _profile.asStateFlow() @@ - override fun getProfile(): Flow<Profile?> { - return profile - } + override fun getProfile(): Flow<Profile?> { + return profileFlow + }Also applies to: 26-28
10-13: suspend 시그니처와 구현 불일치: emit 사용 또는 suspend 제거 중 하나로 통일하세요.현재
suspend fun인데 내부에서 단순 대입(value =)만 합니다. 두 가지 중 하나로 맞추시길 권장합니다.옵션 A: suspend 유지 + emit 사용
override suspend fun cacheProfile(profile: Profile) { - _profile.value = profile + _profile.emit(profile) } @@ override suspend fun clearCache() { - _profile.value = null + _profile.emit(null) }옵션 B: suspend 제거(인터페이스/구현 모두)
-interface ProfileLocalDataSource { - - suspend fun cacheProfile(profile: Profile) +interface ProfileLocalDataSource { + fun cacheProfile(profile: Profile) fun getProfile() : Flow<Profile?> - suspend fun clearCache() + fun clearCache() } @@ - override suspend fun cacheProfile(profile: Profile) { + override fun cacheProfile(profile: Profile) { _profile.value = profile } @@ - override suspend fun clearCache() { + override fun clearCache() { _profile.value = null }Also applies to: 22-24, 30-32
core/data/src/test/java/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceTest.kt (1)
177-186: coVerifyOnce 헬퍼 시그니처 단순화 제안.
exactly=1을 강제하므로atLeast/atMost인자는 무의미합니다. 혼동을 줄이기 위해 제거해도 좋습니다.-private fun coVerifyOnce( - ordering: Ordering = Ordering.UNORDERED, - inverse: Boolean = false, - atLeast: Int = 1, - atMost: Int = Int.MAX_VALUE, - timeout: Long = 0, - verifyBlock: suspend MockKVerificationScope.() -> Unit -) { - coVerify(ordering, inverse, atLeast, atMost, 1, timeout, verifyBlock) -} +private fun coVerifyOnce( + ordering: Ordering = Ordering.UNORDERED, + inverse: Boolean = false, + timeout: Long = 0, + verifyBlock: suspend MockKVerificationScope.() -> Unit +) { + coVerify(ordering = ordering, inverse = inverse, exactly = 1, timeout = timeout, verifyBlock = verifyBlock) +}feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt (2)
273-279: 로케일 의존 소문자 변환을 Locale.ROOT로 고정하세요. (터키어 I 이슈 등)사용자 입력 정규화는 기본 로케일에 의존하지 않도록 처리하는 것이 안전합니다.
- val lowerCaseText = fieldValue.text.lowercase() + val lowerCaseText = fieldValue.text.lowercase(java.util.Locale.ROOT)추가: 상단 import가 필요합니다.
import java.util.Locale
99-101: 네이밍 일관성(nickname/birthday) 정리 제안.
nickNameFocusRequester→nicknameFocusRequester,birthDayFocusRequester→birthdayFocusRequester로 통일하면 가독성이 좋아집니다.- val nickNameFocusRequester = remember { FocusRequester() } - val birthDayFocusRequester = remember { FocusRequester() } + val nicknameFocusRequester = remember { FocusRequester() } + val birthdayFocusRequester = remember { FocusRequester() } @@ - focusRequester = nickNameFocusRequester, + focusRequester = nicknameFocusRequester, @@ - nickNameFocusRequester.requestFocus() + nicknameFocusRequester.requestFocus() @@ - focusRequester = birthDayFocusRequester, + focusRequester = birthdayFocusRequester, @@ - birthDayFocusRequester.requestFocus() + birthdayFocusRequester.requestFocus()Also applies to: 270-271, 283-284, 373-374, 384-385
core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt (1)
10-10: Fully-qualified 패키지 경로 사용 재검토 필요
companion object의Empty프로퍼티에서 fully-qualified 패키지 경로를 사용하고 있습니다. 동일한 파일 내의 클래스이므로 패키지 경로 없이 사용 가능합니다.- val Empty = com.acon.acon.core.model.model.profile.ProfileInfoLegacy("", "", null, emptyList()) + val Empty = ProfileInfoLegacy("", "", null, emptyList())feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt (1)
35-39: 불필요한 side effect 처리 로직 확인 필요
UpdateProfileImageLegacy처리 로직에서selectedPhotoId를 다시 사용하고 있는데, 이미LaunchedEffect에서 처리하고 있습니다. 또한selectedPhotoId ?: ""로 빈 문자열을 전달하는 것이 의도된 동작인지 확인이 필요합니다.현재 로직:
- Line 24-27에서
selectedPhotoId가 비어있지 않으면updateProfileImage호출- Line 35-39에서
UpdateProfileImageLegacyside effect 발생 시 다시updateProfileImage호출이는 중복 호출이 발생할 수 있습니다. Side effect가 언제 발생하는지 확인하여 불필요한 중복을 제거하는 것이 좋습니다.
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt (2)
161-162: TODO 주석 처리 필요저장한 장소가 없을 때의 처리에 대한 TODO 주석이 있습니다. 이미 Line 211-218에서 처리하고 있는 것으로 보입니다.
TODO를 제거하거나 추가 구현이 필요한지 확인이 필요합니다. 추가 구현이 필요하다면 이슈를 생성하여 추적하는 것이 좋겠습니다.
164-164: Empty 객체 비교 로직 개선 필요
ProfileInfoLegacy전체 객체를Empty싱글톤과 비교하고 있습니다. 이는 깨지기 쉬운 로직이며, 실제로 프로필 정보가 있는지 확인하는 더 명확한 방법을 사용하는 것이 좋습니다.- if (state.profileInfoLegacy != com.acon.acon.core.model.model.profile.ProfileInfoLegacy.Empty) { + if (state.profileInfoLegacy.nickname.isNotEmpty()) {또는
ProfileInfoLegacy에isEmpty()메서드를 추가하는 것도 고려해보세요:fun isEmpty(): Boolean = nickname.isEmpty() && image.isEmpty()domain/src/test/kotlin/com/acon/acon/domain/usecase/ValidateNicknameUseCaseTest.kt (3)
30-39: 테스트 메서드명을 영어로 변경 고려현재 테스트 메서드명이 한글로 작성되어 있습니다. 한글 사용이 팀 컨벤션이 아니라면, CI/CD 환경이나 다양한 도구와의 호환성을 위해 영어 사용을 고려해보세요.
- fun `입력이 없을 경우 입력 없음 예외 객체를 Result Wrapping하여 반환한다`() = runTest { + fun `should return EmptyInput error when input is empty`() = runTest {
54-69: 테스트 케이스 분리 고려하나의 테스트 메서드에서 여러 시나리오(한글, 대문자, 공백)를 검증하고 있습니다. 각각을 별도의 테스트로 분리하거나
@ParameterizedTest를 사용하면 실패 시 어떤 케이스가 실패했는지 더 명확하게 알 수 있습니다.@ParameterizedTest @ValueSource(strings = ["한글닉네임", "Capital", "very short"]) fun `should return InvalidFormat error when input contains invalid characters`(input: String) = runTest { // When val actualException = validateNicknameUseCase(input).exceptionOrNull() // Then assertIs<ValidateNicknameError.InvalidFormat>(actualException) }
71-85: 실제 Repository 호출 시나리오 추가 검증 필요유효한 닉네임에 대한 성공 케이스만 테스트하고 있습니다. Repository가 실패를 반환하는 경우(예: 이미 존재하는 닉네임)도 테스트하면 좋겠습니다.
추가 테스트 케이스 예시:
@Test fun `should return error when repository returns failure`() = runTest { // Given val sampleValidNickname = "validnickname" val expectedError = ValidateNicknameError.AlreadyExists() coEvery { profileRepository.validateNickname(sampleValidNickname) } returns Result.failure(expectedError) // When val actualResult = validateNicknameUseCase(sampleValidNickname) // Then assertEquals(expectedError, actualResult.exceptionOrNull()) }domain/src/main/java/com/acon/acon/domain/error/profile/UpdateProfileError.kt (1)
8-16: 프로필 도메인 에러에 고유 코드 부여 시 Constants, 에러 클래스, 테스트 모두 업데이트 필요
프로필 관련UpdateProfileError및Validate*Error클래스가 모두UNSPECIFIED_SERVER_ERROR_CODE(-1)을 사용하고 있으며,ProfileRepositoryTest도 이를 전제로 매핑되어 있습니다. 각 에러별로 고유 코드를 정의하려면Constants.kt에 신규 코드 추가 후 해당 에러 클래스 및 테스트 내 매핑을 함께 수정하세요.core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt (1)
123-132: 테스트 시나리오 개선 필요테스트 케이스에서
profileRemoteDataSource.getProfile()을 verify하고 있지만, collect 블록이 비어있어 실제로 데이터가 수집되는지 확인하지 않습니다.// When -profileRepository.getProfile().collect { } +val results = profileRepository.getProfile().toList() // Then coVerify(exactly = 1) { profileRemoteDataSource.getProfile() } +assertTrue(results.isNotEmpty(), "프로필 데이터가 수집되어야 합니다")domain/src/test/kotlin/com/acon/acon/domain/usecase/ValidateBirthDateUseCaseTest.kt (3)
27-27: 날짜 포맷 일관성 개선날짜 생성 시 공백 없이 작성되어 가독성이 떨어집니다.
-val sampleLocalDate = LocalDate.of(1899,12,31) +val sampleLocalDate = LocalDate.of(1899, 12, 31)
48-56: 테스트 구조 개선 필요Given/When/Then 주석이 있지만 When/Then 섹션에 주석이 누락되어 있습니다.
// Given val sampleLocalDate = LocalDate.now().plusDays(1) +// When val actualException = validateBirthDateUseCase(sampleLocalDate).exceptionOrNull() +// Then assertIs<ValidateBirthDateError.InputIsFuture>(actualException)
59-66: 테스트 구조 일관성 개선Given/When/Then 주석이 누락되어 있습니다.
// Given val sampleLocalDate = LocalDate.now() +// When val actualException = validateBirthDateUseCase(sampleLocalDate).exceptionOrNull() +// Then assertIsNot<ValidateBirthDateError.InputIsFuture>(actualException)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt (1)
34-35: 일관성 없는 코드 스타일다른 메서드들과 달리 한 줄로 작성되어 있습니다.
-suspend fun fetchSavedSpots() = profileAuthApiLegacy.fetchSavedSpots() -suspend fun saveSpot(saveSpotRequest: SaveSpotRequest) = profileAuthApiLegacy.saveSpot(saveSpotRequest) +suspend fun fetchSavedSpots(): SavedSpotsResponseLegacy { + return profileAuthApiLegacy.fetchSavedSpots() +} + +suspend fun saveSpot(saveSpotRequest: SaveSpotRequest): Response<Unit> { + return profileAuthApiLegacy.saveSpot(saveSpotRequest) +}feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt (1)
407-408: 복잡한 조건식 개선 필요
isEditButtonEnabledgetter의 조건식이 너무 복잡하여 가독성과 유지보수성이 떨어집니다.val isEditButtonEnabled: Boolean get() { - val isProfileImageChanged = when { - selectedPhotoUri.contains("basic_profile_image") && - fetchedPhotoUri.contains("basic_profile_image") -> false - - selectedPhotoUri.isNotEmpty() && selectedPhotoUri != fetchedPhotoUri -> true - else -> false - } - val isBirthValid = fetchedBirthday.isNotEmpty() && birthday.isEmpty() - val isContentValid = nicknameValidationStatus == NicknameValidationStatus.Valid && - (birthday.isEmpty() || birthdayValidationStatus == BirthdayValidationStatus.Valid) - - return (isProfileImageChanged && isContentValid) || isBirthValid || (isEdited && isContentValid) + val isProfileImageChanged = isProfileImageChanged() + val isBirthValid = isBirthDateCleared() + val isContentValid = isContentValid() + + return (isProfileImageChanged && isContentValid) || + isBirthValid || + (isEdited && isContentValid) } +private fun isProfileImageChanged(): Boolean { + return when { + selectedPhotoUri.contains("basic_profile_image") && + fetchedPhotoUri.contains("basic_profile_image") -> false + selectedPhotoUri.isNotEmpty() && selectedPhotoUri != fetchedPhotoUri -> true + else -> false + } +} + +private fun isBirthDateCleared(): Boolean { + return fetchedBirthday.isNotEmpty() && birthday.isEmpty() +} + +private fun isContentValid(): Boolean { + return nicknameValidationStatus == NicknameValidationStatus.Valid && + (birthday.isEmpty() || birthdayValidationStatus == BirthdayValidationStatus.Valid) +}core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (1)
104-109: 북마크 업데이트 시 에러 처리 개선 필요
addBookmark와deleteBookmark메서드에서fetchSavedSpots()실패 시 조용히 무시되고 있습니다. 사용자에게 북마크 작업은 성공했지만 로컬 캐시 업데이트는 실패했다는 것을 알려야 할 수 있습니다.override suspend fun addBookmark(spotId: Long): Result<Unit> { return runCatchingWith(AddBookmarkError()) { spotRemoteDataSource.addBookmark(AddBookmarkRequest(spotId)) profileRepositoryLegacy.fetchSavedSpots().onSuccess { fetched -> (profileInfoCacheLegacy.data.value.getOrNull() ?: return@onSuccess).let { profileInfo -> profileInfoCacheLegacy.updateData(profileInfo.copy(savedSpotLegacies = fetched)) } + }.onFailure { error -> + // 로그를 남기거나 모니터링에 보고 + // 북마크는 성공했지만 캐시 업데이트 실패 } } }Also applies to: 117-122
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
17-17: 레거시 명명 패턴이 일관되지 않음
ProfileRouteLegacy는 사용하면서ProfileScreenContainerLegacy와ProfileModScreenContainerLegacy를 import하고 있습니다. 그러나BookmarkScreenContainer는 Legacy 접미사가 없습니다. 북마크 화면도 레거시 플로우의 일부라면 일관성을 위해BookmarkScreenContainerLegacy로 명명해야 합니다.Also applies to: 22-23
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt (2)
21-21: 긴 패키지 경로 대신 import 문 사용 권장
com.acon.acon.core.model.model.profile.ProfileInfoLegacy.Empty를 전체 경로로 사용하고 있습니다. 가독성 향상을 위해 import 문을 추가하는 것이 좋습니다.+import com.acon.acon.core.model.model.profile.ProfileInfoLegacy override val container = - container<ProfileUiStateLegacy, ProfileUiSideEffectLegacy>(ProfileUiStateLegacy.Success(com.acon.acon.core.model.model.profile.ProfileInfoLegacy.Empty)) { + container<ProfileUiStateLegacy, ProfileUiSideEffectLegacy>(ProfileUiStateLegacy.Success(ProfileInfoLegacy.Empty)) {
38-42: 코루틴 실행 시 에러 처리 누락
resetUpdateProfileType()메서드에서 코루틴을 실행할 때 에러 처리가 없습니다. Repository 호출 중 예외가 발생하면 처리되지 않은 예외가 발생할 수 있습니다.fun resetUpdateProfileType() { viewModelScope.launch { - profileRepositoryLegacy.resetProfileType() + try { + profileRepositoryLegacy.resetProfileType() + } catch (e: Exception) { + // 로그 남기거나 적절한 에러 처리 + } } }domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt (1)
17-17:birthday파라미터 타입을 더 명확하게 정의 필요
updateProfile메서드의birthday파라미터가String?타입입니다. 날짜 형식에 대한 검증이나 도메인 모델 사용을 고려해보세요.
BirthDateStatus같은 도메인 타입을 파라미터로 받거나, 최소한 날짜 형식을 문서화하는 것이 좋습니다:-suspend fun updateProfile(fileName: String, nickname: String, birthday: String?, uri: String): Result<Unit> +/** + * @param birthday 생년월일 (형식: "YYYY.MM.DD", null인 경우 미지정) + */ +suspend fun updateProfile(fileName: String, nickname: String, birthday: String?, uri: String): Result<Unit>core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (2)
33-44: 로컬 캐시 실패 처리 필요
getProfileFromRemote()메서드에서 원격 데이터를 성공적으로 가져온 후profileLocalDataSource.cacheProfile(profile)을 호출하는데, 이 캐싱 작업이 실패할 경우에 대한 처리가 없습니다. 캐싱 실패가 전체 작업을 실패로 만들어서는 안 되므로, 캐싱 예외를 별도로 처리하는 것이 좋습니다.private fun getProfileFromRemote(): Flow<Result<Profile>> { return flow { emit(runCatchingWith { val profileResponse = profileRemoteDataSource.getProfile() val profile = profileResponse.toProfile() - profileLocalDataSource.cacheProfile(profile) + try { + profileLocalDataSource.cacheProfile(profile) + } catch (e: Exception) { + Timber.w(e, "Failed to cache profile locally") + } profile }) } }
62-66: 저장된 장소 목록 캐싱 고려
getSavedSpots()메서드는 매번 원격 데이터소스에서 직접 데이터를 가져옵니다. 프로필 데이터와 유사하게 저장된 장소 목록도 캐싱을 고려해보면 네트워크 요청을 줄이고 성능을 개선할 수 있을 것입니다.저장된 장소 목록에 대한 캐싱 로직을 구현하시겠습니까? 프로필과 유사한 방식으로 로컬 캐시를 활용할 수 있습니다.
core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt (1)
36-36: 테스트 클래스명과 실제 테스트 대상 불일치테스트 클래스명은
ProfileRepositoryImplTest이지만 실제로는ProfileRepositoryLegacyImpl을 테스트하고 있습니다. 테스트 대상을 명확히 하기 위해 클래스명을ProfileRepositoryLegacyImplTest로 변경하는 것이 좋습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (71)
app/src/main/java/com/acon/acon/navigation/AconNavigation.kt(2 hunks)app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt(5 hunks)app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt(2 hunks)app/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt(3 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCache.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCacheLegacy.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/local/ProfileLocalDataSource.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/di/LocalDataSourceModule.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/di/RemoteDataSourceModule.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt(4 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/request/UpdateProfileRequestLegacy.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponseLegacy.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotResponse.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryLegacyImpl.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt(5 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt(3 hunks)core/data/src/test/java/com/acon/core/data/datasource/local/ProfileLocalDataSourceTest.kt(1 hunks)core/data/src/test/java/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceTest.kt(1 hunks)core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt(7 hunks)core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt(1 hunks)core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt(3 hunks)core/data/src/test/java/com/acon/core/data/repository/UserRepositoryImplTest.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/BirthDateStatus.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/Profile.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileImageStatus.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpot.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/SpotThumbnailStatus.kt(1 hunks)core/navigation/src/main/java/com/acon/acon/core/navigation/route/ProfileRoute.kt(0 hunks)core/navigation/src/main/java/com/acon/acon/core/navigation/route/ProfileRouteLegacy.kt(1 hunks)domain/build.gradle.kts(1 hunks)domain/src/main/java/com/acon/acon/domain/error/Constants.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/error/profile/UpdateProfileError.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/error/profile/ValidateBirthDateError.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameError.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt(2 hunks)domain/src/main/java/com/acon/acon/domain/usecase/ValidateBirthDateUseCase.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/usecase/ValidateNicknameUseCase.kt(1 hunks)domain/src/test/kotlin/com/acon/acon/domain/usecase/ValidateBirthDateUseCaseTest.kt(1 hunks)domain/src/test/kotlin/com/acon/acon/domain/usecase/ValidateNicknameUseCaseTest.kt(1 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/MockSavedSpotList.kt(1 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/BookmarkViewModel.kt(2 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreen.kt(3 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModel.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt(1 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt(2 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkSkeletonItemLegacy.kt(1 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt(3 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt(11 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt(12 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt(2 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt(4 hunks)feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt(4 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt(3 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt(0 hunks)
💤 Files with no reviewable changes (4)
- feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt
- core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCache.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModel.kt
- core/navigation/src/main/java/com/acon/acon/core/navigation/route/ProfileRoute.kt
🧰 Additional context used
🧬 Code graph analysis (9)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreen.kt (2)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkSkeletonItemLegacy.kt (1)
BookmarkSkeletonItemLegacy(15-36)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt (1)
BookmarkItemLegacy(28-92)
core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt (1)
core/data/src/test/java/com/acon/core/data/TestUtils.kt (3)
createFakeRemoteError(18-22)assertValidErrorMapping(24-28)createErrorStream(12-16)
app/src/main/java/com/acon/acon/navigation/AconNavigation.kt (1)
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
profileNavigationLegacy(25-119)
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (2)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt (1)
ProfileScreenContainerLegacy(22-80)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt (1)
ProfileModScreenContainerLegacy(13-61)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt (1)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt (1)
ProfileModScreenLegacy(67-428)
core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt (1)
core/data/src/test/java/com/acon/core/data/TestUtils.kt (2)
createErrorStream(12-16)createFakeRemoteError(18-22)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt (1)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt (1)
ProfileScreenLegacy(50-336)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt (1)
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt (1)
BookmarkItemLegacy(28-92)
core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (1)
core/data/src/main/kotlin/com/acon/core/data/error/ErrorUtils.kt (2)
runCatchingWith(7-24)runCatchingWith(26-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
💻 Work Description
Profile Repository구현 및 테스트Profile Remote 데이터소스구현 및 테스트Profile Retrofit api인터페이스 (레트로핏 관련 어노테이션은 작성하지 않은 단계)Summary by CodeRabbit