Skip to content

refactor(setting): 편집 화면 3개의 MVI 상태와 신호 계약 적용 (#2179) - #2180

Merged
1hyok merged 12 commits into
developfrom
refactor/2179-setting-editors-mvi
Oct 4, 2026
Merged

1hyok merged 12 commits into
developfrom
refactor/2179-setting-editors-mvi

Conversation

@1hyok

@1hyok 1hyok commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

먼저 볼 파일: 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한 뒤 다시 검증했습니다. 계측 테스트 SettingEditorSaveSignalAndroidTest 4건을 더해 저장 성공 신호가 스택을 한 칸만 내리는지, 저장 실패와 잘못된 이메일·연락처가 화면을 닫지 않고 요청도 보내지 않는지를 실제 설정 로컬 스택에서 확인합니다. 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"
      }
    ]
  }
}

@github-actions github-actions Bot added the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

형제 충돌이 해소됐다 — 지금은 이 PR 과 같은 파일을 서로 다르게 고친 열린 PR 이 없다 (#1750).
다시 생기면 이 코멘트를 현재 상태로 고쳐 쓴다.

@1hyok
1hyok added this pull request to stack #2181 September 27, 2026 04:44
@github-actions github-actions Bot removed the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Sep 27, 2026
Base automatically changed from refactor/1695-setting-nav3 to develop September 27, 2026 11:01
@1hyok
1hyok force-pushed the refactor/2179-setting-editors-mvi branch from acc5f08 to 648d8df Compare September 27, 2026 11:01
val error: DeliveryConditionError? = null,
val isSaving: Boolean = false,
)
val pendingEvent: Unit? = null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

실을 값이 없는 신호를 Unit? 로 두셨는데, 이 경우는 core:ui 에 ObserveFlag 가 이미 정본으로 있습니다(#1802 확정, ObserveSignal.kt). val isSaved: Boolean = false + ObserveFlag(raised = uiState.isSaved, consumed = DeliveryConditionIntent.ConsumeSuccess, ...) 로 맞춰 주세요. 온보딩 LoginScreen·OnboardingProfileScreen 이 같은 모양입니다. Unit? 은 이 저장소에서 이 파일 하나만 쓰는 형태가 됩니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

반영했습니다(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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

assisted 로 route 를 받는 이유(Nav3 entry 에는 savedStateHandle.toRoute<T>() 자동 채움이 없음, #1695)를 적은 KDoc 이 MVI 전환과 무관하게 지워졌습니다. 다음 사람이 SavedStateHandle 로 되돌리려 할 수 있는 자리라 근거 주석은 살려 주세요. ReceiverEditViewModel 도 같습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

두 ViewModel 모두 assisted route 근거 KDoc(#1695)을 원래 문구 그대로 되살렸습니다(07ba9e7).

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요청을 보내기도 전의 입력 검증 실패(이메일·전화번호 형식)를 SaveFailed 로 흘리고 있습니다. reducer 결과는 같지만, 이벤트 이름이 "저장 요청이 실패했다"로 읽혀 로그나 테스트에서 서버 실패와 구분되지 않습니다. ValidationFailed(message) 를 따로 두는 쪽을 제안합니다. isSaving 을 건드릴 이유도 없는 이벤트입니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ReceiverEditReducerEvent.ValidationFailed(message) 를 따로 두고, 이메일·전화번호 형식 실패는 그쪽으로 보냅니다. reducer 는 errorMessage 만 바꾸고 isSaving 은 건드리지 않습니다. 서버 실패만 SaveFailed 로 남습니다(07ba9e7).

- 전달 조건 저장 완료 신호를 Unit? 대신 isSaved 플래그로 두고 core:ui ObserveFlag 로 소비한다.
- 전달 조건·수신인 편집 ViewModel 의 assisted route 근거 KDoc(#1695)을 되살린다.
- 수신인 편집의 이메일·전화번호 형식 검증 실패를 SaveFailed 가 아니라 ValidationFailed 로 보낸다.
수신자 수정의 pendingEvent 와 전달 조건의 isSaved 가 화면에서 한 번만 소비되는지는
ViewModel 단위 테스트로 보이지 않는다. 실제 설정 로컬 스택을 띄워 저장 성공은 정확히 한 칸
내려오고, 저장 실패와 잘못된 이메일·연락처는 화면을 닫지 않고 요청도 보내지 않는지 잰다.
성공 콜백을 두 번 부르게 바꾸면 성공 케이스 두 건이 실패하는 것을 로컬에서 확인했다.
@github-actions github-actions Bot added the android-test PR head에서 Android Managed Device 계측 테스트 실행 label Sep 30, 2026
@1hyok
1hyok removed this pull request from stack #2181 October 1, 2026 11:22
@1hyok
1hyok added this pull request to stack #2228 October 1, 2026 11:22

@Sadturtleman Sadturtleman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

세 편집 화면의 Intent·reduce 전환과 UiState 신호 소비가 기존 저장·실패·취소 동작을 그대로 유지하는 것을 확인했습니다. 승인합니다.

@1hyok
1hyok force-pushed the refactor/2179-setting-editors-mvi branch from cd119fb to 86840b3 Compare October 4, 2026 04:09
탭 순서(0=미활동, 1=수신자 요청)를 Screen(타입→인덱스)과 ViewModel(인덱스→타입)이
반대 방향으로 따로 알고 있어, 라디오 순서만 바꿔도 매핑이 조용히 어긋날 수 있었다.
매핑을 Screen 의 conditionTypes 한 곳으로 모으고 SelectConditionType 은 도메인 타입을
싣게 해 ViewModel 분기를 dispatch 한 줄로 줄인다.
@1hyok
1hyok force-pushed the refactor/2179-setting-editors-mvi branch from 86840b3 to ebbbbea Compare October 4, 2026 04:10
1hyok added 5 commits October 4, 2026 08:10
두 화면은 성공 때 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 에 남긴다.
@1hyok
1hyok added this pull request to the merge queue Oct 4, 2026
Merged via the queue into develop with commit 074e0c8 Oct 4, 2026
22 checks passed
@1hyok
1hyok deleted the refactor/2179-setting-editors-mvi branch October 4, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android-test PR head에서 Android Managed Device 계측 테스트 실행

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(setting): 편집 화면 3개의 MVI·UiState 신호를 선행 적용

3 participants