Feat/#5 mypage screen구현#40
Hidden character warning
Conversation
Walkthrough회원 탈퇴 API 및 화면/뷰모델/네비게이션 추가, 관심사 편집 기능 추가, MyPage/Main 네비게이션 확장, Accept/Reject 모달에 이미지 커스터마이징(painter/painterTint)·nullable 파라미터 도입, 에러 아이콘 리소스 및 study/isAlone 필드 추가. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant UI as CancelMemberShipScreen
participant VM as CancelMemberShipViewModel
participant Repo as UserRepository
participant DS as UserDataSource
participant Svc as UserService
participant API as Backend API
User->>UI: 회원 탈퇴 버튼 클릭
UI->>VM: leaveSpot()
VM->>VM: viewModelScope.launch
VM->>Repo: leaveSpot()
Repo->>DS: leaveSpot()
DS->>Svc: leaveSpot()
Svc->>API: DELETE /api/members/me
API-->>Svc: 응답(성공/실패)
Svc-->>DS: NullResultResponse
DS-->>Repo: NullResultResponse
Repo-->>VM: Result<String> (code 기반)
VM->>VM: leaveStatus 업데이트
VM-->>UI: 상태 변경 전달 (collect)
UI->>User: 성공/실패 다이얼로그 표시 및 후속 네비게이션
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In
`@core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/modal/RejectModal.kt`:
- Around line 80-86: The image tinting currently applies
ColorFilter.tint(painterTint) even when painterTint is Color.Unspecified, which
renders the image transparent; update the Image usage (the Image composable
where painter = painter and colorFilter = ColorFilter.tint(painterTint)) to only
apply a colorFilter when painterTint != Color.Unspecified (or make painterTint
default to null) so that ColorFilter.tint is not used for Color.Unspecified
values.
In `@core/model/src/main/java/com/umcspot/spot/model/Global.kt`:
- Around line 66-67: StudyTheme.from currently uses StudyTheme.entries.first {
it.name == value } which will throw NoSuchElementException for unknown API
values; update it to mirror SocialLoginType.from by returning a safe default
(e.g., KOREAN or another appropriate StudyTheme constant) when no match is
found: replace the .first { ... } call with .firstOrNull { it.name == value } ?:
<DEFAULT_THEME>. Also check call sites such as UserMapper.kt where
UserPreferredCategoryResponseDto.categories are mapped to use the safe-from
behavior to prevent runtime crashes on malformed API data.
In
`@domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.kt`:
- Line 6: The mapping of API category strings to enums in
UserPreferredCategoryResult currently uses categories.map { StudyTheme.from(it)
} which will throw NoSuchElementException for unknown values; change the
conversion to a safe form by either updating StudyTheme.from to return a
safe/default value or replace the mapping with a resilient transformation (e.g.,
categories.mapNotNull { runCatching { StudyTheme.from(it) }.getOrNull() } or a
similar filter/validation) so unexpected category strings are ignored or mapped
to a default without throwing.
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/CancelMemberShipScreen.kt`:
- Around line 163-179: The success dialog uses RejectDialog with an unnecessary
cancel label; update the component props to remove or hide the negative action
instead of showing "취소": in the RejectDialog invocation replace noButtonText =
"취소" with either noButtonText = null (or an empty string) and/or remove the
negative button prop if the component supports a flag (e.g., showNoButton =
false); keep the positive handlers (onClick/onDismiss) that clear
showSuccessDialog and call successCancelMemberShip so the OK flow remains
unchanged.
- Around line 150-161: The RejectDialog's confirm handler leaves showFailDialog
true via onClick = moveToParticipatingStudy, causing the dialog to reappear;
update the handler so that when the user clicks the confirm button it first sets
showFailDialog = false and then calls moveToParticipatingStudy (or call a small
wrapper function that does both), ensuring the dialog is dismissed before
navigation; locate the RejectDialog usage and modify the onClick binding
accordingly.
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/CancelMemberShipViewModel.kt`:
- Around line 17-31: The current CancelMemberShipViewModel keeps _leaveStatus as
a plain String and never updates it on failure, so the UI can't react; change
state to a sealed class (e.g., LeaveState with Idle/Loading/Success/Error) and
update the ViewModel: set Loading before calling userRepository.leaveSpot(), set
Success on onSuccess and set Error (including the exception message) in
onFailure; replace usages of leaveStatus and _leaveStatus to emit the
sealed-state values so the UI can show failure feedback.
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/navigation/CancelMemberShipNavigation.kt`:
- Around line 30-31: Change the CancelMemberShip route to implement the base
Route interface instead of MainTabRoute: update the declaration of the data
object CancelMemberShip to implement Route (retain `@Serializable`) rather than
MainTabRoute; also audit other MyPage sub-screens like EditInterest and
PreferCategory and make the same change so only MainNavTab entries implement
MainTabRoute while sub-screens implement Route.
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/editInterestStudy/EditInterestStudyScreen.kt`:
- Around line 128-134: The code sets showDialog = true immediately after calling
viewmodel.updatePreferCategories(draftThemes) so the success dialog appears even
on API failure; expose an update result StateFlow in the ViewModel (e.g.,
private MutableStateFlow<Boolean> _updateSuccess and public val updateSuccess),
update it based on the API response inside updatePreferCategories, then in
EditInterestStudyScreen collect viewmodel.updateSuccess with
collectAsStateWithLifecycle and trigger showDialog only when updateSuccess
becomes true (use a LaunchedEffect or equivalent observer on updateSuccess to
set showDialog = true).
🧹 Nitpick comments (9)
core/designsystem/src/main/res/drawable/error.xml (1)
1-9: 하드코딩된 색상 대신 테마 색상 리소스 사용을 권장합니다.
#F34343색상이 직접 하드코딩되어 있습니다. 다크 모드 지원이나 일관된 색상 관리를 위해 테마 속성(?attr/colorError) 또는 색상 리소스(@color/error_color)를 사용하는 것이 좋습니다.♻️ 제안된 수정
<path android:pathData="M16.5,2.75C24.094,2.75 30.25,8.907 30.25,16.5C30.25,24.093 24.094,30.25 16.5,30.25C8.906,30.25 2.75,24.093 2.75,16.5C2.75,8.907 8.906,2.75 16.5,2.75ZM16.5,5.042C10.182,5.042 5.042,10.182 5.042,16.5C5.042,22.818 10.182,27.958 16.5,27.958C22.818,27.958 27.958,22.818 27.958,16.5C27.958,10.182 22.818,5.042 16.5,5.042ZM16.499,19.94C16.863,19.94 17.212,20.085 17.47,20.343C17.728,20.6 17.872,20.95 17.872,21.314C17.872,21.678 17.728,22.028 17.47,22.285C17.212,22.543 16.863,22.688 16.499,22.688C16.134,22.688 15.785,22.543 15.527,22.285C15.27,22.028 15.125,21.678 15.125,21.314C15.125,20.95 15.27,20.6 15.527,20.343C15.785,20.085 16.134,19.94 16.499,19.94ZM16.492,9.625C16.741,9.625 16.982,9.715 17.17,9.879C17.358,10.042 17.481,10.269 17.515,10.516L17.524,10.655L17.53,16.845C17.53,17.107 17.431,17.358 17.253,17.549C17.075,17.741 16.83,17.857 16.57,17.875C16.309,17.893 16.051,17.811 15.848,17.646C15.645,17.482 15.513,17.246 15.477,16.987L15.467,16.847L15.462,10.658C15.462,10.522 15.488,10.388 15.54,10.263C15.592,10.137 15.668,10.023 15.763,9.928C15.859,9.832 15.973,9.756 16.098,9.704C16.223,9.652 16.356,9.625 16.492,9.625Z" - android:fillColor="#F34343"/> + android:fillColor="@color/error_red"/>
colors.xml에 색상 리소스 추가:<color name="error_red">#F34343</color>domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt (1)
21-21: 도메인 반환 타입에 HTTP 코드 문자열 노출은 재검토 권장.도메인 레이어라면
Result<Unit>또는 전용 결과 타입(예: sealed class)로 의미를 캡슐화하는 편이 유지보수에 유리합니다.feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt (2)
38-40: 사용되지 않는 import 확인 필요
CancelMemberShipScreen이 import되었지만 이 파일에서 직접 사용되지 않는 것으로 보입니다.CancelMemberShip과EditInterest는 navigation route 매칭에 사용되지만,CancelMemberShipScreen은 navigation graph에서 사용될 것으로 예상됩니다.♻️ 불필요한 import 제거 제안
-import com.umcspot.spot.mypage.cancelMemberShip.CancelMemberShipScreen import com.umcspot.spot.mypage.cancelMemberShip.navigation.CancelMemberShip import com.umcspot.spot.mypage.editInterestStudy.navigation.EditInterest
156-156: 람다 단순화 가능
{ finishApp() }대신finishApp을 직접 전달할 수 있습니다.♻️ 제안
- finishApp = { finishApp() } + finishApp = finishAppfeature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/CancelMemberShipScreen.kt (1)
52-57: 상태 코드를 상수로 분리하고 재시도 시 상태 초기화를 고려하세요.
"MEMBER4006","MEMBER200"같은 하드코딩된 문자열은 상수로 분리하는 것이 유지보수에 유리합니다. 또한 사용자가 재시도할 경우 동일한 상태 코드가 다시 발생하면LaunchedEffect가 트리거되지 않을 수 있습니다.♻️ 상수 분리 제안
private object LeaveStatusCode { const val FAILURE_HOSTING_STUDY = "MEMBER4006" const val SUCCESS = "MEMBER200" }feature/mypage/src/main/java/com/umcspot/spot/mypage/editInterestStudy/EditInterestStudyViewModel.kt (2)
7-7: 사용되지 않는 import 제거
com.umcspot.spot.study.model.Study는 이 파일에서 사용되지 않습니다.♻️ 수정 제안
-import com.umcspot.spot.study.model.Study
35-45: API 실패 시 사용자 피드백 부재
updatePreferCategories실패 시 로그만 기록하고 UI에 알리지 않습니다. 사용자는 저장이 성공한 것으로 오해할 수 있습니다. 에러 상태를 노출하거나 Result를 반환하여 UI에서 실패를 처리하는 것이 좋습니다.♻️ 에러 상태 추가 제안
private val _updateError = MutableStateFlow<String?>(null) val updateError: StateFlow<String?> = _updateError.asStateFlow() fun updatePreferCategories(categories: List<StudyTheme>) { viewModelScope.launch { _updateError.value = null userRepository.setUserTheme(categories) .onSuccess { _preferCategories.value = categories } .onFailure { _updateError.value = it.message Log.e("EditInterestStudyViewModel", "updatePreferCategories: ${it.message}") } } }feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt (1)
129-131:onEditInterestLocationClick이 빈 람다로 구현됨
onEditInterestLocationClick = { }이 빈 람다로 설정되어 있습니다. 관심 지역 편집 기능이 아직 구현되지 않은 것으로 보입니다.TODO 주석을 추가하거나 이 기능 구현을 추적하는 이슈를 생성하시겠습니까?
feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/navigation/CancelMemberShipNavigation.kt (1)
12-14: 네이밍 일관성 문제
navigateToCancelMembership함수명은 "Membership"을 한 단어로 사용하지만, 데이터 객체CancelMemberShip과 그래프 함수cancelMemberShipGraph는 "MemberShip"으로 대문자 S를 사용합니다. 일관성을 위해 통일하는 것을 권장합니다."Membership"이 표준 영어 철자이므로, 다른 식별자들을
CancelMembership으로 변경하는 것이 좋습니다.♻️ 네이밍 통일 제안
-fun NavController.navigateToCancelMembership(navOptions: NavOptions? = null) { - navigate(CancelMemberShip, navOptions) +fun NavController.navigateToCancelMembership(navOptions: NavOptions? = null) { + navigate(CancelMembership, navOptions) } -fun NavGraphBuilder.cancelMemberShipGraph( +fun NavGraphBuilder.cancelMembershipGraph( contentPadding : PaddingValues, successCancelMemberShip: () -> Unit, moveToParticipatingStudy: () -> Unit, ) { - composable<CancelMemberShip> { + composable<CancelMembership> { ... } } `@Serializable` -data object CancelMemberShip : MainTabRoute +data object CancelMembership : MainTabRoute
| Image( | ||
| painter = painterResource(R.drawable.emoji_sad), | ||
| painter = painter, | ||
| contentDescription = null, | ||
| modifier = Modifier | ||
| .size(screenWidthDp(33.dp)) | ||
| .size(screenWidthDp(33.dp)), | ||
| colorFilter = ColorFilter.tint(painterTint) | ||
| ) |
There was a problem hiding this comment.
Color.Unspecified 사용 시 이미지가 보이지 않는 버그
painterTint의 기본값이 Color.Unspecified일 때 ColorFilter.tint()를 적용하면 이미지가 투명하게 렌더링되어 보이지 않습니다. Color.Unspecified는 내부적으로 0x00000000 값을 가지므로, tint 필터가 이미지를 사실상 숨기게 됩니다.
🐛 colorFilter 조건부 적용 제안
Image(
painter = painter,
contentDescription = null,
modifier = Modifier
.size(screenWidthDp(33.dp)),
- colorFilter = ColorFilter.tint(painterTint)
+ colorFilter = if (painterTint != Color.Unspecified) ColorFilter.tint(painterTint) else null
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Image( | |
| painter = painterResource(R.drawable.emoji_sad), | |
| painter = painter, | |
| contentDescription = null, | |
| modifier = Modifier | |
| .size(screenWidthDp(33.dp)) | |
| .size(screenWidthDp(33.dp)), | |
| colorFilter = ColorFilter.tint(painterTint) | |
| ) | |
| Image( | |
| painter = painter, | |
| contentDescription = null, | |
| modifier = Modifier | |
| .size(screenWidthDp(33.dp)), | |
| colorFilter = if (painterTint != Color.Unspecified) ColorFilter.tint(painterTint) else null | |
| ) |
🤖 Prompt for AI Agents
In
`@core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/modal/RejectModal.kt`
around lines 80 - 86, The image tinting currently applies
ColorFilter.tint(painterTint) even when painterTint is Color.Unspecified, which
renders the image transparent; update the Image usage (the Image composable
where painter = painter and colorFilter = ColorFilter.tint(painterTint)) to only
apply a colorFilter when painterTint != Color.Unspecified (or make painterTint
default to null) so that ColorFilter.tint is not used for Color.Unspecified
values.
| fun from(value: String): StudyTheme = | ||
| StudyTheme.entries.first { it.name == value } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat core/model/src/main/java/com/umcspot/spot/model/Global.kt | head -120Repository: SPOTeam/Android-V2
Length of output: 2292
🏁 Script executed:
# Search for all calls to StudyTheme.from()
rg -n "StudyTheme\.from" --type=kt -C3Repository: SPOTeam/Android-V2
Length of output: 88
🏁 Script executed:
# Check SocialLoginType.from() for comparison
rg -n "SocialLoginType\.from" --type=kt -A5Repository: SPOTeam/Android-V2
Length of output: 88
🏁 Script executed:
rg -n "StudyTheme\.from" -C3Repository: SPOTeam/Android-V2
Length of output: 693
🏁 Script executed:
# Also search for any try-catch blocks around StudyTheme.from
rg -n "StudyTheme\.from" -C5Repository: SPOTeam/Android-V2
Length of output: 903
🏁 Script executed:
# Search in all kotlin files
fd "\.kt$" | xargs grep -n "StudyTheme.from" -C3Repository: SPOTeam/Android-V2
Length of output: 693
🏁 Script executed:
# Search for all StudyTheme.from occurrences (ensure we found them all)
rg "StudyTheme\.from"Repository: SPOTeam/Android-V2
Length of output: 189
🏁 Script executed:
# Look at UserPreferredCategoryResponseDto definition to understand data source
fd "UserPreferredCategoryResponseDto" | head -5Repository: SPOTeam/Android-V2
Length of output: 156
🏁 Script executed:
# Find and examine the DTO definition
rg -n "UserPreferredCategoryResponseDto" --type=kotlin | head -10Repository: SPOTeam/Android-V2
Length of output: 1440
🏁 Script executed:
# Search in all files for UserPreferredCategoryResponseDto
fd "\." | xargs grep -l "UserPreferredCategoryResponseDto" 2>/dev/null | head -5Repository: SPOTeam/Android-V2
Length of output: 450
🏁 Script executed:
cat data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredCategoryResponseDto.ktRepository: SPOTeam/Android-V2
Length of output: 446
🏁 Script executed:
# Check the mapping function context
cat data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt | head -70Repository: SPOTeam/Android-V2
Length of output: 2282
🏁 Script executed:
# Check if there's any error handling at the service/repository level
rg -n "getUserPreferredCategory|toDomain" data/user/src/main/java/com/umcspot/spot/user/service/ -A5 -B5Repository: SPOTeam/Android-V2
Length of output: 1178
API 응답 매핑 시 런타임 크래시 위험
StudyTheme.from()이 .first { }를 사용하므로 일치하는 항목이 없으면 NoSuchElementException을 발생시킵니다. 현재 UserMapper.kt:56에서 API 응답 데이터(UserPreferredCategoryResponseDto.categories)를 직접 변환할 때 이 함수를 호출하고 있는데, API가 예상치 못한 값을 반환하거나 데이터가 손상된 경우 앱이 크래시됩니다.
같은 파일의 SocialLoginType.from() (lines 51-56)은 .firstOrNull { } ?: KAKAO로 안전하게 기본값을 반환하는 패턴을 사용하고 있어 일관성이 없습니다.
🔧 안전한 기본값 반환 제안
companion object {
- fun from(value: String): StudyTheme =
- StudyTheme.entries.first { it.name == value }
+ fun from(value: String): StudyTheme =
+ StudyTheme.entries.firstOrNull { it.name == value } ?: OTHER
}🤖 Prompt for AI Agents
In `@core/model/src/main/java/com/umcspot/spot/model/Global.kt` around lines 66 -
67, StudyTheme.from currently uses StudyTheme.entries.first { it.name == value }
which will throw NoSuchElementException for unknown API values; update it to
mirror SocialLoginType.from by returning a safe default (e.g., KOREAN or another
appropriate StudyTheme constant) when no match is found: replace the .first {
... } call with .firstOrNull { it.name == value } ?: <DEFAULT_THEME>. Also check
call sites such as UserMapper.kt where
UserPreferredCategoryResponseDto.categories are mapped to use the safe-from
behavior to prevent runtime crashes on malformed API data.
|
|
||
| data class UserPreferredCategoryResult( | ||
| val categories : List<StudyTheme?>, | ||
| val categories : List<StudyTheme>, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "UserPreferredCategoryResult" -e ktRepository: SPOTeam/Android-V2
Length of output: 146
🏁 Script executed:
git ls-files | grep -i "user" | grep -i "\.kt$" | head -20Repository: SPOTeam/Android-V2
Length of output: 1679
🏁 Script executed:
rg "UserPreferredCategoryResult" -A 10 -B 2Repository: SPOTeam/Android-V2
Length of output: 8229
🏁 Script executed:
rg "categories.*List.*StudyTheme" -A 5 -B 5Repository: SPOTeam/Android-V2
Length of output: 6306
🏁 Script executed:
cat domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.ktRepository: SPOTeam/Android-V2
Length of output: 245
🏁 Script executed:
cat data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredCategoryResponseDto.ktRepository: SPOTeam/Android-V2
Length of output: 446
🏁 Script executed:
rg "fun from|class StudyTheme" --type kt -A 10 -B 2Repository: SPOTeam/Android-V2
Length of output: 88
🏁 Script executed:
rg "StudyTheme" -A 15 | head -100Repository: SPOTeam/Android-V2
Length of output: 11614
🏁 Script executed:
fd "StudyTheme" -e ktRepository: SPOTeam/Android-V2
Length of output: 44
🏁 Script executed:
find . -name "StudyTheme.kt" -o -name "StudyTheme.java"Repository: SPOTeam/Android-V2
Length of output: 44
🏁 Script executed:
rg "class StudyTheme|enum class StudyTheme" -A 20Repository: SPOTeam/Android-V2
Length of output: 2000
StudyTheme.from() 변환 실패 시 예외 처리 필요.
현재 매핑에서 categories.map { StudyTheme.from(it) }는 API 응답의 문자열이 enum 값과 일치하지 않으면 NoSuchElementException을 발생시킵니다. 예상되지 않은 카테고리 값에 대한 필터링, 유효성 검사 또는 안전한 변환 로직을 추가하세요.
categories = categories.mapNotNull {
runCatching { StudyTheme.from(it) }.getOrNull()
}또는 StudyTheme.from()에서 안전한 기본값 처리를 구현하세요.
🤖 Prompt for AI Agents
In
`@domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.kt`
at line 6, The mapping of API category strings to enums in
UserPreferredCategoryResult currently uses categories.map { StudyTheme.from(it)
} which will throw NoSuchElementException for unknown values; change the
conversion to a safe form by either updating StudyTheme.from to return a
safe/default value or replace the mapping with a resilient transformation (e.g.,
categories.mapNotNull { runCatching { StudyTheme.from(it) }.getOrNull() } or a
similar filter/validation) so unexpected category strings are ignored or mapped
to a default without throwing.
feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/CancelMemberShipScreen.kt
Show resolved
Hide resolved
feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/CancelMemberShipScreen.kt
Show resolved
Hide resolved
| private val _leaveStatus = MutableStateFlow("") | ||
| val leaveStatus : StateFlow<String> = _leaveStatus | ||
|
|
||
| fun leaveSpot() { | ||
| Log.d("CancelMemberShipVM", "leaveSpot CALLED") | ||
| viewModelScope.launch { | ||
| userRepository.leaveSpot() | ||
| .onSuccess { status -> | ||
| Log.d("CancelMemberShipVM", "leaveSpot success status=$status") | ||
| _leaveStatus.value = status | ||
| } | ||
| .onFailure { e -> | ||
| Log.e("CancelMemberShipVM", "leaveSpot failed", e) | ||
| } | ||
| } |
There was a problem hiding this comment.
실패 시 UI 상태 업데이트 누락
leaveSpot() 실패 시 _leaveStatus가 초기값 ""로 유지되어 UI에서 에러 상태를 감지할 수 없습니다. 사용자에게 실패 피드백을 제공하지 못합니다.
sealed class를 사용한 상태 관리를 권장합니다:
🔧 에러 상태 처리 추가 제안
+sealed class LeaveState {
+ object Idle : LeaveState()
+ object Loading : LeaveState()
+ data class Success(val status: String) : LeaveState()
+ data class Error(val message: String) : LeaveState()
+}
+
`@HiltViewModel`
class CancelMemberShipViewModel `@Inject` constructor(
private val userRepository: UserRepository
) : ViewModel() {
- private val _leaveStatus = MutableStateFlow("")
- val leaveStatus : StateFlow<String> = _leaveStatus
+ private val _leaveState = MutableStateFlow<LeaveState>(LeaveState.Idle)
+ val leaveState: StateFlow<LeaveState> = _leaveState
fun leaveSpot() {
Log.d("CancelMemberShipVM", "leaveSpot CALLED")
+ _leaveState.value = LeaveState.Loading
viewModelScope.launch {
userRepository.leaveSpot()
.onSuccess { status ->
Log.d("CancelMemberShipVM", "leaveSpot success status=$status")
- _leaveStatus.value = status
+ _leaveState.value = LeaveState.Success(status)
}
.onFailure { e ->
Log.e("CancelMemberShipVM", "leaveSpot failed", e)
+ _leaveState.value = LeaveState.Error(e.message ?: "탈퇴 처리 중 오류가 발생했습니다")
}
}
}
}🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/cancelMemberShip/CancelMemberShipViewModel.kt`
around lines 17 - 31, The current CancelMemberShipViewModel keeps _leaveStatus
as a plain String and never updates it on failure, so the UI can't react; change
state to a sealed class (e.g., LeaveState with Idle/Loading/Success/Error) and
update the ViewModel: set Loading before calling userRepository.leaveSpot(), set
Success on onSuccess and set Error (including the exception message) in
onFailure; replace usages of leaveStatus and _leaveStatus to emit the
sealed-state values so the UI can show failure feedback.
.../main/java/com/umcspot/spot/mypage/cancelMemberShip/navigation/CancelMemberShipNavigation.kt
Outdated
Show resolved
Hide resolved
| onClick = { | ||
| viewmodel.updatePreferCategories(draftThemes) | ||
| showDialog = true | ||
| }, | ||
| shape = SpotShapes.Soft, | ||
| state = TextButtonState.B500State, | ||
| ) |
There was a problem hiding this comment.
API 응답 확인 없이 성공 다이얼로그 표시
viewmodel.updatePreferCategories(draftThemes) 호출 직후 showDialog = true로 설정하지만, API 호출이 실패해도 성공 다이얼로그가 표시됩니다. ViewModel에서 업데이트 성공 상태를 노출하고, 성공 시에만 다이얼로그를 표시해야 합니다.
🔧 수정 방향 제안
ViewModel에서 업데이트 결과를 StateFlow로 노출하고:
// In ViewModel
private val _updateSuccess = MutableStateFlow(false)
val updateSuccess: StateFlow<Boolean> = _updateSuccess.asStateFlow()Screen에서 해당 상태를 관찰:
val updateSuccess by viewmodel.updateSuccess.collectAsStateWithLifecycle()
LaunchedEffect(updateSuccess) {
if (updateSuccess) {
showDialog = true
}
}🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/editInterestStudy/EditInterestStudyScreen.kt`
around lines 128 - 134, The code sets showDialog = true immediately after
calling viewmodel.updatePreferCategories(draftThemes) so the success dialog
appears even on API failure; expose an update result StateFlow in the ViewModel
(e.g., private MutableStateFlow<Boolean> _updateSuccess and public val
updateSuccess), update it based on the API response inside
updatePreferCategories, then in EditInterestStudyScreen collect
viewmodel.updateSuccess with collectAsStateWithLifecycle and trigger showDialog
only when updateSuccess becomes true (use a LaunchedEffect or equivalent
observer on updateSuccess to set showDialog = true).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt`:
- Around line 132-137: The onLogoutClick handler currently only navigates to
Landing and misses the actual logout/data-clear steps; before calling
navigator.navController.navigateToLanding(clearStackNavOptions) invoke your
app's logout/session-clear routine (for example call AuthViewModel.logout(),
SessionManager.clearSession(), or UserRepository.clearCachedData) to remove
tokens, user session, and cached data, then navigate; if logout is implemented
elsewhere, call that existing method (e.g., authViewModel.logout or
sessionManager.clear) from onLogoutClick to ensure cleanup happens prior to
navigation.
🧹 Nitpick comments (1)
feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt (1)
38-40: 사용되지 않는 import가 있습니다.
CancelMemberShipScreen(line 38)은 import되었지만 이 파일에서 사용되지 않습니다.CancelMemberShip과EditInterest만 route 매핑에서 사용됩니다.♻️ 불필요한 import 제거
-import com.umcspot.spot.mypage.cancelMemberShip.CancelMemberShipScreen import com.umcspot.spot.mypage.cancelMemberShip.navigation.CancelMemberShip import com.umcspot.spot.mypage.editInterestStudy.navigation.EditInterest
| onLogoutClick = { | ||
| // 1) 로그아웃 처리(데이터 삭제) 트리거 | ||
|
|
||
| // 2) Landing으로 이동하면서 스택 클리어 | ||
| navigator.navController.navigateToLanding(clearStackNavOptions) | ||
| } |
There was a problem hiding this comment.
로그아웃 로직이 누락되었습니다.
주석에 "로그아웃 처리(데이터 삭제) 트리거"가 언급되어 있지만, 실제 구현이 없습니다. Landing 페이지로 이동하기 전에 사용자 세션, 토큰, 캐시된 데이터 등을 정리해야 합니다. 그렇지 않으면 다음 사용자가 이전 세션 데이터에 접근할 수 있는 보안 문제가 발생할 수 있습니다.
로그아웃 처리가 다른 곳(예: ViewModel, Repository)에서 이루어지는지 확인해주세요.
🤖 Prompt for AI Agents
In `@feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt` around lines
132 - 137, The onLogoutClick handler currently only navigates to Landing and
misses the actual logout/data-clear steps; before calling
navigator.navController.navigateToLanding(clearStackNavOptions) invoke your
app's logout/session-clear routine (for example call AuthViewModel.logout(),
SessionManager.clearSession(), or UserRepository.clearCachedData) to remove
tokens, user session, and cached data, then navigate; if logout is implemented
elsewhere, call that existing method (e.g., authViewModel.logout or
sessionManager.clear) from onLogoutClick to ensure cleanup happens prior to
navigation.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
`@data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt`:
- Around line 48-52: The isAlone property in StudyResponseDto is non-nullable
without a default, which will break deserialization if the server omits the
field; add a safe default (e.g., isAlone: Boolean = false) to the
StudyResponseDto data class so missing or older-server payloads deserialize
safely, keeping the `@SerialName`("isAlone") annotation intact.
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyScreen.kt`:
- Around line 289-293: In ParticipatingStudyScreen, the HorizontalDivider is
always rendered which causes an isolated divider when isOwner == false and
isAlone == true; update the composable to render HorizontalDivider conditionally
(e.g., only when isOwner is true or isAlone is false) so the divider appears
only when there is an item above it; locate the HorizontalDivider call in
ParticipatingStudyScreen and wrap it with the appropriate if-check using the
existing isOwner and isAlone boolean flags.
- Around line 140-141: The empty lambda handlers onLeaveClick and onDeleteClick
in ParticipatingStudyScreen mean the UI actions do nothing; add corresponding
methods to ParticipatingStudyViewModel (e.g., leaveStudy(studyId: String) and
deleteStudy(studyId: String) or similarly named methods that perform the
API/repository calls and update state) and wire those methods into the screen by
replacing the empty lambdas with calls that pass the current study id to
viewModel.leaveStudy(...) and viewModel.deleteStudy(...); ensure the ViewModel
exposes success/error handling (LiveData/StateFlow) so the UI can react to the
result and update the list or show errors accordingly.
🧹 Nitpick comments (2)
domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt (1)
44-60: 더미 데이터의 isAlone 일관성 개선 권장
Line 57에서isAlone이currentMembers와 불일치할 수 있습니다. 프리뷰/테스트에서 의미가 어긋나 UI가 혼동될 수 있어요.♻️ 제안 수정 (isAlone이면 currentMembers=1)
- val max = 8 + (index % 5) // 8~12 - val mem = (3 + (index % 6)).coerceAtMost(max) + val max = 8 + (index % 5) // 8~12 + val isAlone = index % 2 == 1 + val mem = if (isAlone) 1 else (3 + (index % 6)).coerceAtMost(max) return StudyResult( id = index.toLong(), name = title, description = "주 2회 진행, 코드리뷰", maxMembers = max, currentMembers = mem, likeCount = 10 + index * 2, isLiked = index%2 == 0, - isOwner = index%2 == 1, - isAlone = index%2 == 1, + isOwner = isAlone, + isAlone = isAlone, hitCount = 150 + index * 20, profileImageUrl = ImageRef.Name("ic_study_default") )feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyScreen.kt (1)
233-234: 코드 포맷팅 일관성.
onDelete:() -> Unit에서 콜론 뒤에 공백이 누락되었습니다. 다른 파라미터들과 일관성을 유지하기 위해 수정이 필요합니다.♻️ 수정 제안
- onDelete:() -> Unit + onDelete: () -> Unit
| @SerialName("isAlone") | ||
| val isAlone: Boolean, | ||
|
|
||
| @SerialName("hitCount") | ||
| val hitCount: Int = 0, |
There was a problem hiding this comment.
역호환을 위해 기본값 추가 필요
Line 48-49의 isAlone가 non-null인데 기본값이 없어, 서버가 필드를 누락하면 역직렬화 실패 가능성이 있습니다. 배포 순서가 엇갈릴 경우 앱 크래시로 이어질 수 있어요.
🛠 제안 수정
`@SerialName`("isAlone")
- val isAlone: Boolean,
+ val isAlone: Boolean = false,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @SerialName("isAlone") | |
| val isAlone: Boolean, | |
| @SerialName("hitCount") | |
| val hitCount: Int = 0, | |
| `@SerialName`("isAlone") | |
| val isAlone: Boolean = false, | |
| `@SerialName`("hitCount") | |
| val hitCount: Int = 0, |
🤖 Prompt for AI Agents
In
`@data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt`
around lines 48 - 52, The isAlone property in StudyResponseDto is non-nullable
without a default, which will break deserialization if the server omits the
field; add a safe default (e.g., isAlone: Boolean = false) to the
StudyResponseDto data class so missing or older-server payloads deserialize
safely, keeping the `@SerialName`("isAlone") annotation intact.
| onLeaveClick = {}, | ||
| onDeleteClick = {} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for the file and understand the project structure
find . -name "ParticipatingStudyScreen.kt" -o -name "*ParticipatingStudyViewModel*" | head -20Repository: SPOTeam/Android-V2
Length of output: 258
🏁 Script executed:
# Look for the ParticipatingStudyScreen.kt file content around lines 140-141
fd "ParticipatingStudyScreen.kt" --exec wc -l {} \;Repository: SPOTeam/Android-V2
Length of output: 162
🏁 Script executed:
# Search for ViewModel classes related to ParticipatingStudy
fd ".*ViewModel.*" -e kt | grep -i participatingRepository: SPOTeam/Android-V2
Length of output: 159
🏁 Script executed:
# Read the ParticipatingStudyScreen.kt around lines 140-141 to see context
sed -n '130,160p' ./feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyScreen.ktRepository: SPOTeam/Android-V2
Length of output: 1065
🏁 Script executed:
# Read the ParticipatingStudyViewModel.kt to check for leave/delete methods
cat -n ./feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyViewModel.ktRepository: SPOTeam/Android-V2
Length of output: 3652
🏁 Script executed:
# Search for onLeaveClick and onDeleteClick usage in the file
rg -n "onLeaveClick|onDeleteClick" ./feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyScreen.kt -A 2 -B 2Repository: SPOTeam/Android-V2
Length of output: 930
빈 람다 핸들러로 인해 'Leave Study' 기능이 완전히 구현되지 않았습니다.
onLeaveClick과 onDeleteClick이 빈 람다 {}로 설정되어 있고, ViewModel에 이를 처리할 메서드가 없습니다. 사용자가 메뉴에서 "스터디 나가기" 또는 "스터디 삭제하기"를 클릭하면 핸들러가 호출되지만(lines 203-204) 아무 작업도 수행되지 않습니다.
ParticipatingStudyViewModel에 스터디를 나가거나 삭제하는 메서드를 추가하고, 이를 호출하도록 ParticipatingScreen의 핸들러를 구현해야 합니다.
🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyScreen.kt`
around lines 140 - 141, The empty lambda handlers onLeaveClick and onDeleteClick
in ParticipatingStudyScreen mean the UI actions do nothing; add corresponding
methods to ParticipatingStudyViewModel (e.g., leaveStudy(studyId: String) and
deleteStudy(studyId: String) or similarly named methods that perform the
API/repository calls and update state) and wire those methods into the screen by
replacing the empty lambdas with calls that pass the current study id to
viewModel.leaveStudy(...) and viewModel.deleteStudy(...); ensure the ViewModel
exposes success/error handling (LiveData/StateFlow) so the UI can react to the
result and update the list or show errors accordingly.
| HorizontalDivider( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| thickness = 1.dp, | ||
| color = SpotTheme.colors.gray200 | ||
| ) |
There was a problem hiding this comment.
isOwner=false 및 isAlone=true일 때 불필요한 Divider가 표시됩니다.
현재 HorizontalDivider가 항상 렌더링되지만, isOwner=false이고 isAlone=true인 경우 메뉴 상단에 아무것도 없이 Divider만 표시되어 UI가 어색해집니다.
Divider는 위에 표시할 항목이 있을 때만 렌더링되어야 합니다.
🐛 수정 제안
- HorizontalDivider(
- modifier = Modifier.fillMaxWidth(),
- thickness = 1.dp,
- color = SpotTheme.colors.gray200
- )
+ if (isOwner || !isAlone) {
+ HorizontalDivider(
+ modifier = Modifier.fillMaxWidth(),
+ thickness = 1.dp,
+ color = SpotTheme.colors.gray200
+ )
+ }🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/participating/ParticipatingStudyScreen.kt`
around lines 289 - 293, In ParticipatingStudyScreen, the HorizontalDivider is
always rendered which causes an isolated divider when isOwner == false and
isAlone == true; update the composable to render HorizontalDivider conditionally
(e.g., only when isOwner is true or isAlone is false) so the divider appears
only when there is an item above it; locate the HorizontalDivider call in
ParticipatingStudyScreen and wrap it with the appropriate if-check using the
existing isOwner and isAlone boolean flags.
Related issue 🛠
Work Description 📝
Screenshot 📸
Uncompleted Tasks 😅
PR Point 📌
트러블 슈팅 💥
Summary by CodeRabbit
새로운 기능
UI 개선
✏️ Tip: You can customize this high-level summary in your review settings.