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.
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