Repository navigation
refactor(setting): 편집 화면 3개의 MVI 상태와 신호 계약 적용 (#2179) - #2180
Conversation
|
형제 충돌이 해소됐다 — 지금은 이 PR 과 같은 파일을 서로 다르게 고친 열린 PR 이 없다 (#1750). |
acc5f08 to
648d8df
Compare
| val error: DeliveryConditionError? = null, | ||
| val isSaving: Boolean = false, | ||
| ) | ||
| val pendingEvent: Unit? = null, |
There was a problem hiding this comment.
실을 값이 없는 신호를 Unit? 로 두셨는데, 이 경우는 core:ui 에 ObserveFlag 가 이미 정본으로 있습니다(#1802 확정, ObserveSignal.kt). val isSaved: Boolean = false + ObserveFlag(raised = uiState.isSaved, consumed = DeliveryConditionIntent.ConsumeSuccess, ...) 로 맞춰 주세요. 온보딩 LoginScreen·OnboardingProfileScreen 이 같은 모양입니다. Unit? 은 이 저장소에서 이 파일 하나만 쓰는 형태가 됩니다.
There was a problem hiding this comment.
반영했습니다(07ba9e7). pendingEvent: Unit? 를 isSaved: Boolean = false 로 바꾸고, 화면은 ObserveFlag(raised = uiState.isSaved, consumed = DeliveryConditionIntent.ConsumeSuccess, ...) 로 소비합니다. 저장 중복을 막는 onSave 의 조건도 isSaved 로 봅니다.
| * 대상 수신자 [SettingRoute.AfterDeliveryRoute] 는 assisted 로 받는다 — Nav3 entry 에는 Nav2 의 | ||
| * `savedStateHandle.toRoute<T>()` 자동 채움이 없다 (#1695). | ||
| */ | ||
| @HiltViewModel(assistedFactory = DeliveryConditionViewModel.Factory::class) |
There was a problem hiding this comment.
assisted 로 route 를 받는 이유(Nav3 entry 에는 savedStateHandle.toRoute<T>() 자동 채움이 없음, #1695)를 적은 KDoc 이 MVI 전환과 무관하게 지워졌습니다. 다음 사람이 SavedStateHandle 로 되돌리려 할 수 있는 자리라 근거 주석은 살려 주세요. ReceiverEditViewModel 도 같습니다.
| if (currentState.isSaving || currentState.pendingEvent != null) return | ||
| if (!email.isValidReceiverEmail()) { | ||
| _uiState.update { it.copy(errorMessage = UiText.Resource(R.string.setting_receiver_email_invalid)) } | ||
| dispatch(ReceiverEditReducerEvent.SaveFailed(UiText.Resource(R.string.setting_receiver_email_invalid))) |
There was a problem hiding this comment.
요청을 보내기도 전의 입력 검증 실패(이메일·전화번호 형식)를 SaveFailed 로 흘리고 있습니다. reducer 결과는 같지만, 이벤트 이름이 "저장 요청이 실패했다"로 읽혀 로그나 테스트에서 서버 실패와 구분되지 않습니다. ValidationFailed(message) 를 따로 두는 쪽을 제안합니다. isSaving 을 건드릴 이유도 없는 이벤트입니다.
There was a problem hiding this comment.
ReceiverEditReducerEvent.ValidationFailed(message) 를 따로 두고, 이메일·전화번호 형식 실패는 그쪽으로 보냅니다. reducer 는 errorMessage 만 바꾸고 isSaving 은 건드리지 않습니다. 서버 실패만 SaveFailed 로 남습니다(07ba9e7).
- 전달 조건 저장 완료 신호를 Unit? 대신 isSaved 플래그로 두고 core:ui ObserveFlag 로 소비한다. - 전달 조건·수신인 편집 ViewModel 의 assisted route 근거 KDoc(#1695)을 되살린다. - 수신인 편집의 이메일·전화번호 형식 검증 실패를 SaveFailed 가 아니라 ValidationFailed 로 보낸다.
수신자 수정의 pendingEvent 와 전달 조건의 isSaved 가 화면에서 한 번만 소비되는지는 ViewModel 단위 테스트로 보이지 않는다. 실제 설정 로컬 스택을 띄워 저장 성공은 정확히 한 칸 내려오고, 저장 실패와 잘못된 이메일·연락처는 화면을 닫지 않고 요청도 보내지 않는지 잰다. 성공 콜백을 두 번 부르게 바꾸면 성공 케이스 두 건이 실패하는 것을 로컬에서 확인했다.
Sadturtleman
left a comment
There was a problem hiding this comment.
세 편집 화면의 Intent·reduce 전환과 UiState 신호 소비가 기존 저장·실패·취소 동작을 그대로 유지하는 것을 확인했습니다. 승인합니다.
cd119fb to
86840b3
Compare
탭 순서(0=미활동, 1=수신자 요청)를 Screen(타입→인덱스)과 ViewModel(인덱스→타입)이 반대 방향으로 따로 알고 있어, 라디오 순서만 바꿔도 매핑이 조용히 어긋날 수 있었다. 매핑을 Screen 의 conditionTypes 한 곳으로 모으고 SelectConditionType 은 도메인 타입을 싣게 해 ViewModel 분기를 dispatch 한 줄로 줄인다.
86840b3 to
ebbbbea
Compare
두 화면은 성공 때 isSaving 을 풀고 완료 신호(pendingEvent, isSaved)로 재제출을 막았다. 신호는 ObserveSignal 이 화면을 닫는 호출 직후 바로 소비되므로, 퇴장 애니메이션 동안에는 버튼이 다시 켜지고 가드도 열려 같은 저장 요청이 한 번 더 나갈 수 있었다. 프로필 편집과 같이 성공 뒤 isSaving 을 유지해 잠금은 isSaving 하나가, 전달은 신호가 맡게 한다.
주석이 붙은 Saved 분기가 여러 줄로 잡혀 ktlint blank-line-between-when-conditions 가 실패했다.
성공 뒤에도 isSaving·isUpdating 을 true 로 두면 저장이 끝났는데 상태가 저장 중이라고 말하게 된다. 세 편집 화면에 되돌리지 않는 완료 사실(isSaved·isUpdated)을 두고, 진행 플래그는 성공 때 풀며, 재제출 가드와 버튼은 isSaveLocked·isUpdateLocked 로 둘을 함께 본다. 전달 조건의 화면 닫기 신호는 완료 사실과 이름이 겹치지 않게 shouldNavigateBack 으로 옮겼고, ReceiverRegisterContent 의 isLoading 은 실제 쓰임대로 isActionLocked 로 바꿨다.
shouldNavigateBack 은 ViewModel 이 뒤로 가기를 정하는 것처럼 읽혔다. 이동은 onSaveSuccess 를 받은 호스트가 정하므로, 신호를 콜백·소비 Intent 와 같은 사건(저장 성공)으로 isSaveSuccessPending 이라 부른다.
Saved 분기만 중괄호로 감싸 when-entry-bracing 이 모든 분기를 바꾸게 했다. 한 줄로도 ktlint 를 통과하므로 바뀐 내용(isSaved = true)만 diff 에 남긴다.
먼저 볼 파일:
ProfileEditContract.kt,ReceiverEditViewModel.kt,DeliveryConditionViewModel.kt,ProfileEditScreen.kt.Closes #2179
Refs #1502
프로필·수신자·전달 조건 편집의 상태와 성공 이벤트가 각각 MutableStateFlow와 Channel에 나뉘어 있었습니다. 세 ViewModel의 입력은 onIntent로, 상태 전이는 순수 reduce로 모으고 성공·실패 신호를 UiState에 보관해 화면이 ObserveSignal과 소비 Intent로 처리합니다. 저장 중 중복 제출과 신호 소비 전 재제출을 막고 실패 뒤 재시도 및 취소 전파를 유지합니다.
승인 뒤 추가 변경(ebbbbea):
DeliveryConditionIntent.SelectConditionType이 탭 인덱스(index: Int) 대신DeliveryConditionType을 받습니다. 탭 순서를 Screen과 ViewModel이 반대 방향으로 따로 알고 있던 매핑을 Screen의conditionTypes한 곳으로 모았습니다. 파일 5개, +9/−13이고 테스트 두 곳의 호출도 새 인자로 바꿨습니다.승인 뒤 추가 변경(35efc25..1ef0613): 수신자 수정과 전달 조건은 저장 성공 때
isSaving을 풀고 완료 신호(pendingEvent,isSaved)로 재제출을 막았습니다. 신호는 ObserveSignal이 화면을 닫는 호출 직후 소비되므로 퇴장 애니메이션 동안 버튼과 가드가 다시 열려 같은 저장 요청이 한 번 더 나갈 수 있었습니다. 프로필 편집은 성공 뒤에도isUpdating을 true로 두어 이 틈이 없었지만, 저장이 끝났는데 상태가 저장 중이라고 말하는 셈이었습니다. 세 화면에 되돌리지 않는 완료 사실(isSaved,isUpdated)을 두고 진행 플래그는 성공 때 풉니다. 재제출 가드와 버튼은 둘을 함께 보는isSaveLocked,isUpdateLocked를 씁니다. 전달 조건의 저장 성공 신호는 완료 사실과 이름이 겹치지 않게isSaveSuccessPending으로 옮겼습니다. 이동은onSaveSuccess를 받은 호스트가 정하므로 신호도 콜백·소비 Intent와 같은 사건 이름으로 둡니다.ReceiverRegisterContent의isLoading인자는 실제 쓰임대로isActionLocked로 바꿨습니다. 단위 테스트는 신호를 소비한 뒤에도 완료 사실이 남고 재저장이 요청을 보내지 않는지 확인합니다.#2173(
refactor/1695-setting-nav3)이 9월 27일 develop에 머지되어 base는 develop입니다. 수신자 ID의 assisted route와 #2123의 좁은 저장소 주입을 그대로 소비합니다. #2124의 저장 UseCase와 #2152의 사진 선택은 이 최종 상태 계약 뒤에 적용합니다. #1502의 나머지 세 화면은 이번 완료 범위에 포함하지 않습니다.검증: 설정 단위 테스트와 Konsist, 설정 ktlint, 앱 main/unit/androidTest 컴파일을 통과했습니다. 프로필 신호 유지·소비, 다른 신호의 늦은 소비, 저장 중·성공 후 중복 제출, 실패 재시도와 취소 전파를 신규 테스트로 확인했습니다. 기존 편집 저장과 화면 콜백 테스트도 새 Intent 계약으로 통과했습니다. Hilt 생성물은 변경 전 checkout의 잔존 산출물을 clean한 뒤 다시 검증했습니다. 계측 테스트
SettingEditorSaveSignalAndroidTest4건을 더해 저장 성공 신호가 스택을 한 칸만 내리는지, 저장 실패와 잘못된 이메일·연락처가 화면을 닫지 않고 요청도 보내지 않는지를 실제 설정 로컬 스택에서 확인합니다. 2861ce61에서 Unit Test, Lint, Ktlint, Screenshot, androidTest와 smoke CI가 통과했습니다.🧪 CI Test Plan
{ "androidTest": { "mode": "selected", "reason": "수신자 수정과 사후 전달 조건 화면의 저장 완료 신호를 UiState 로 옮겼습니다. 신호가 화면에서 한 번만 소비되어 스택을 정확히 한 칸 내리는지, 저장 실패와 입력 검증은 화면을 닫지 않는지를 실제 로컬 스택에서 확인하고, 두 화면의 진입 인자(receiverId) 경계가 그대로인지도 함께 돌립니다.", "tests": [ { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_receiverManageListRowNavigatesToEditWithExactReceiverId", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_deliveryConditionReceiverSelectionPreservesExactReceiverId", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#receiverEdit_saveSuccessPopsExactlyOneScreenToReceiverList", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#receiverEdit_saveFailureShowsMessageAndStaysOnEdit", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#receiverEdit_invalidEmailOrPhoneBlocksSaveWithoutRequest", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#deliveryCondition_saveSuccessPopsExactlyOneScreenToReceiverSelection", "device": "api34" } ] } }