Repository navigation
fix(core,setting): Nav3 목록의 조회 상태와 MVI 재시도를 연결한다 (#2045) - #2154
Conversation
|
열린 PR 중 이 PR 과 같은 파일을 서로 다르게 고친 것이 있다. 어느 쪽이 먼저 머지되든 나중 쪽은 base 를 병합해야 통과한다 (#1750).
머지 자체는 GitHub 이 막는다 — 한쪽이 들어가면 다른 쪽은 CONFLICTING 이 되어 merge queue 에 못 들어간다. 이 코멘트는 리컨사일러가 매 실행 현재 상태로 고쳐 쓴다. 해소되면 |
|
📸 CI 표준 환경에서 screenshot baseline 4개를 생성·검증해 |
실서버 확인 (2026-09-23)Android 16 에뮬레이터, dev 서버, QA 계정(수신자 2명)으로 확인했습니다. 첫 확인 (f715ae1)
첫 로딩, 정상 목록, 첫 조회 실패와 다시 시도는 관리 화면과 선택 화면 모두 기대대로였습니다. 마지막 칸이 문제였습니다. 수정 화면에 5초 넘게 머물러 목록 구독이 끊긴 뒤 오프라인으로 돌아오면, 새 구독의 첫 실패가 보이던 행을 지우고 전체 실패 화면을 띄웠습니다. 본문의 "5초를 넘겨 돌아와도 행을 그대로 둔다"와 달랐습니다. 수정 뒤 (466a5d7)
수신자 수정 화면이 온라인에서도 "수신자 정보를 불러오지 못했습니다" 를 띄우는 것은 이 PR 과 별개인 상세 DTO 결함이라 #2155 로 올렸습니다. 관계가 DAUGHTER 로 보이는 것은 QA 데이터가 영문으로 들어간 것입니다. |
466a5d7 to
d01c6d5
Compare
… into fix/2045-receiver-list-load-state
… fix/2045-receiver-list-load-state
… fix/2045-receiver-list-load-state
|
이 PR 은
|
…로 올린다 (#2045) 설정이 애프터노트 것을 베껴 만든 조회 실패 안내와 갱신 실패 배너를 core:ui 의 ReceiverListLoadFailure·ListRefreshErrorBanner 로 올리고 문구를 core_ui_ 리소스로 옮긴다. 설정 사본과 애프터노트 원본(수신자 선택의 private 안내, shared 배너), 그 setting_·afternote_ 문구를 지우고 두 기능이 core 것을 쓴다. 배너는 애프터노트 홈 목록도 쓰는 범용이라 receiver 가 아니라 loading 패키지에 둔다. 두 쪽 렌더는 그대로다.
# Conflicts: # core/data/src/main/java/com/afternote/core/data/repoimpl/UserReceiverRepositoryImpl.kt









먼저 볼 파일:
core/domain/.../ReceiverListState.kt,core/data/.../UserReceiverRepositoryImpl.kt,feature/setting/.../receiver/ReceiverListViewModel.kt,feature/setting/.../navigation/SettingNavHost.kt입니다.Closes #2045
Closes #1281
Refs #1695
Refs #1805
Refs #2135
Refs #2127
설정 수신자 목록에서 첫 조회 중이나 조회 실패가 "등록된 수신자가 없습니다"로 표시되던 문제를 고칩니다. 로딩·성공한 0건·최초 실패·기존 행을 보존한 갱신 실패를 구분하고 관리·선택 화면의 기존 상태 슬롯으로 표시합니다.
#2173의 최종 Nav3 entry와 MVI 계약을 소비합니다.
ObservationStarted·ObservationStopped의 5초 구독 유예를 유지하고, 조회 결과는 순수 reducer에서 처리합니다.RetryIntent는 실패에서 즉시 로딩으로 전이하여 연속 클릭이 요청을 중복시키지 않습니다. ViewModel의 별도 가변 목록 캐시와stateIn변환은 없습니다.코어 상태 Flow의 첫 로딩은 로그인 구간 식별을 전달합니다. 같은 세션으로 재구독하면 보이던 행을 유지하고, 다른 세션의 로딩을 받으면 이전 행을 버립니다. 401은 이전 목록 폐기를 결과에 명시하므로 재구독 첫 실패에서도 이전 행을 복원하지 않습니다. 기존 목록 전용 Flow의 방출 순서와 세션 격리는 유지합니다.
선행 관계와 전체 diff
실제 base는 #2173의
refactor/1695-setting-nav3입니다. #2130과 #2137의 독립 코어 base는 유지하며 두 PR을 먼저 머지해야 합니다. 해당 선행 변경은 Nav3 최종 트리에서 빠져 있어 현재 이 PR의 전체 26파일 diff에도 다음 8파일로 포함됩니다.UserReceiverRepositoryImpl.kt: 수정 뒤 목록 갱신과 세션별 캐시가 선행 범위이고, 결과 Flow·명시적 401 폐기는 이 PR 범위입니다.ReceiverRequestFailureTest.kt,ReceiverSessionBoundaryRaceTest.kt,ReceiverUpdateRefreshTest.kt,TestTokenSessionStore.kt,UserRepositoryImplTest.kt,TokenDataSource.kt,TokenDataSourceTest.kt: 선행 세션·목록 계약입니다.UserRepositoryImplTest는 #1429의 현재 좁은 생성자에 맞췄습니다.선행 PR 머지 후 최신 base에 반영해 이 중복을 없애고 남은 diff·HEAD·검증을 다시 확인합니다. 설정의 Nav2 그래프는 추가하지 않습니다. 목록 화면의 production 소비는 설정 모듈 안에만 있으므로 UI 상태·Intent·reducer 이벤트·화면의 visibility는 internal입니다.
검증
조회 실패 안내와 갱신 실패 배너는 사본을 두지 않고 core:ui 로 올렸습니다. 설정 쪽 새 블록이 애프터노트 수신자 선택의
SelectReceiverLoadFailed, 애프터노트 홈의ListRefreshErrorBanner와 레이아웃·토큰·문구 값까지 같아서,core.ui.receiver.ReceiverListLoadFailure와core.ui.loading.ListRefreshErrorBanner하나씩을 두고 설정과 애프터노트가 함께 씁니다. 문구는core_ui_리소스로 옮겼고 값은 그대로입니다. 스크린샷 렌더 127장을 변경 전후로 바이트 비교해 차이가 없었습니다.CI Test Plan
{ "androidTest": { "mode": "selected", "reason": "설정의 실제 Nav3 entry가 MVI UiState와 Retry Intent를 소비하도록 바뀌어, 관리 행 탭과 사후 전달 조건 선택의 receiverId 전달을 API 30·34에서 확인합니다. 기존 목록 Flow 소비자인 타임레터의 세션 비움도 같은 기기에서 검증합니다. 조회 상태·재구독·401·세션 격리는 코어 및 설정 단위 테스트가 검증합니다. 애프터노트 홈의 갱신 실패 배너와 작성 흐름 수신자 선택의 조회 실패 안내를 core:ui 공용 컴포저블로 옮겨, 두 화면의 계측 테스트도 같은 기기에서 다시 확인합니다.", "tests": [ { "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt", "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_receiverManageListRowNavigatesToEditWithExactReceiverId", "device": "api30" }, { "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": "api30" }, { "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/ReceiverSessionIsolationAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSessionIsolationAndroidTest#recipientList_receiverListEmptiesOnSessionChange_keepsScreenAliveWithoutPreviousReceiverIdentity", "device": "api30" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSessionIsolationAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSessionIsolationAndroidTest#recipientList_receiverListEmptiesOnSessionChange_keepsScreenAliveWithoutPreviousReceiverIdentity", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/AfternoteHomeRefreshBannerAndroidTest.kt", "selector": "com.afternote.afternote_fe.AfternoteHomeRefreshBannerAndroidTest#homeRefreshFailure_showsBannerOverKeptList_andFullErrorOnlyWhenListIsEmpty", "device": "api30" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/AfternoteHomeRefreshBannerAndroidTest.kt", "selector": "com.afternote.afternote_fe.AfternoteHomeRefreshBannerAndroidTest#homeRefreshFailure_showsBannerOverKeptList_andFullErrorOnlyWhenListIsEmpty", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteSelectReceiver_emptyAndLoadFailureBlockConfirmUntilRetryLoadsList", "device": "api30" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteSelectReceiver_emptyAndLoadFailureBlockConfirmUntilRetryLoadsList", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteEditorNavHost_receiverSelectOpensWithFormReceiverCheckedAndConfirmAddsOnlyTheChosen", "device": "api30" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteEditorNavHost_receiverSelectOpensWithFormReceiverCheckedAndConfirmAddsOnlyTheChosen", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteEditorNavHost_receiverSelectUncheckingFormReceiverRemovesItFromEditorOnConfirm", "device": "api30" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteEditorNavHost_receiverSelectUncheckingFormReceiverRemovesItFromEditorOnConfirm", "device": "api34" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteEditorNavHost_receiverSelectBackKeepsEditorReceiversUnchanged", "device": "api30" }, { "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSelectionResultAndroidTest.kt", "selector": "com.afternote.afternote_fe.ReceiverSelectionResultAndroidTest#afternoteEditorNavHost_receiverSelectBackKeepsEditorReceiversUnchanged", "device": "api34" } ] } }