Skip to content

fix(core,setting): Nav3 목록의 조회 상태와 MVI 재시도를 연결한다 (#2045) - #2154

Merged
OnlyReadOneBook merged 6 commits into
developfrom
fix/2045-receiver-list-load-state
Sep 28, 2026
Merged

OnlyReadOneBook merged 6 commits into
developfrom
fix/2045-receiver-list-load-state

Conversation

@OnlyReadOneBook

@OnlyReadOneBook OnlyReadOneBook commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

먼저 볼 파일: 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에서 처리합니다. Retry Intent는 실패에서 즉시 로딩으로 전이하여 연속 클릭이 요청을 중복시키지 않습니다. 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:data 93개, core:datastore 11개, setting 102개, Konsist 54개 통과.
  • 새 결과 계약 11건, 최종 MVI ViewModel 11건, 기존 lifecycle 관측 4건, 상태별 Compose 8건이 포함됩니다. 재구독 첫 일반 실패의 행 유지, 새 세션 로딩의 행 폐기, 재구독 첫 401, 연속 Retry와 검색·선택 유지 경계를 검증합니다.
  • core:domain 12개, 앱 main·unit·androidTest 및 setting screenshot 소스 Kotlin 컴파일, 관련 ktlint 통과.
  • 세션 비교·401 폐기·Retry 즉시 로딩을 제거한 변이에서 관련 회귀 5건이 실패했고, 원복 후 11건이 통과했습니다.
  • 상태별 Preview 4개와 기존 Actions 표준 baseline 4장은 동일한 화면을 보존합니다. 현재 HEAD의 Screenshot CI 결과로 다시 확인합니다. 이번 변경에서 에뮬레이터·실서버 QA를 다시 수행하지 않았습니다.

조회 실패 안내와 갱신 실패 배너는 사본을 두지 않고 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"
      }
    ]
  }
}

@github-actions github-actions Bot added the android-test PR head에서 Android Managed Device 계측 테스트 실행 label Sep 23, 2026
@OnlyReadOneBook
OnlyReadOneBook added this pull request to stack #2131 September 23, 2026 08:01
@github-actions github-actions Bot added the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Sep 23, 2026
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

열린 PR 중 이 PR 과 같은 파일을 서로 다르게 고친 것이 있다. 어느 쪽이 먼저 머지되든 나중 쪽은 base 를 병합해야 통과한다 (#1750).

머지 자체는 GitHub 이 막는다 — 한쪽이 들어가면 다른 쪽은 CONFLICTING 이 되어 merge queue 에 못 들어간다.
이 라벨은 그 전에 순서를 정하라는 신호다. 먼저 갈 쪽을 정하고, 나중 쪽은 그 머지 뒤에 base 를 병합한다.
상대 PR 에도 같은 안내가 달린다.

이 코멘트는 리컨사일러가 매 실행 현재 상태로 고쳐 쓴다. 해소되면 sibling-conflict 라벨은 자동으로 떨어진다.

@OnlyReadOneBook OnlyReadOneBook added the screenshot-baseline CI 표준 환경에서 PR screenshot baseline 생성·적용 요청 label Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📸 CI 표준 환경에서 screenshot baseline 4개를 생성·검증해 f715ae1 로 커밋했습니다.
실행 로그·생성 artifact: https://github.com/Afternote/Afternote-FE/actions/runs/35834914286
이미지 내용은 이 PR의 PNG diff에서 눈으로 최종 확인해 주세요. 필수 검사도 새 SHA에 다시 요청했습니다.

@github-actions github-actions Bot removed the screenshot-baseline CI 표준 환경에서 PR screenshot baseline 생성·적용 요청 label Sep 23, 2026
@OnlyReadOneBook

Copy link
Copy Markdown
Collaborator Author

실서버 확인 (2026-09-23)

Android 16 에뮬레이터, dev 서버, QA 계정(수신자 2명)으로 확인했습니다.

첫 확인 (f715ae1)

develop: 서버가 2명을 주는데 0건 안내 첫 조회 실패 다시 시도 성공 선택 화면 첫 조회 실패 수정 화면에서 5초 넘게 있다가 오프라인 복귀
develop 거짓 0건 첫 조회 실패 다시 시도 성공 선택 화면 첫 조회 실패 재구독 실패로 행 사라짐

첫 로딩, 정상 목록, 첫 조회 실패와 다시 시도는 관리 화면과 선택 화면 모두 기대대로였습니다. 마지막 칸이 문제였습니다. 수정 화면에 5초 넘게 머물러 목록 구독이 끊긴 뒤 오프라인으로 돌아오면, 새 구독의 첫 실패가 보이던 행을 지우고 전체 실패 화면을 띄웠습니다. 본문의 "5초를 넘겨 돌아와도 행을 그대로 둔다"와 달랐습니다.

수정 뒤 (466a5d7)

수정 화면에서 9초 뒤 오프라인 복귀 온라인에서 다시 시도 목록을 나갔다가 오프라인 재진입 선택 화면을 백그라운드 뒤 오프라인 복귀
행 유지와 갱신 실패 배너 다시 시도로 최신 목록 새 진입은 전체 실패 선택 화면 행과 선택 유지
  • 수정 화면에서 돌아올 때 목록 조회가 실제로 나가 UnknownHostException 으로 실패했고, 행 2개와 갱신 실패 배너가 남았습니다. 화면 위 뒤로 버튼과 시스템 back 으로 각각 재현했습니다.
  • 네트워크를 켠 뒤 배너의 다시 시도를 누르면 조회가 200 으로 오고 배너가 사라집니다. 누르기 전에는 요청이 나가지 않습니다.
  • 목록을 완전히 나갔다가 오프라인으로 다시 들어오면 새 화면이라 전체 실패 화면입니다. 기대한 대로입니다.
  • 선택 화면은 앱을 10초 백그라운드로 보냈다 돌아와도 행과 선택이 남습니다. 선택 화면에는 배너가 없는 설계라, 이 경우 갱신 실패는 화면에 표시되지 않습니다.

수신자 수정 화면이 온라인에서도 "수신자 정보를 불러오지 못했습니다" 를 띄우는 것은 이 PR 과 별개인 상세 DTO 결함이라 #2155 로 올렸습니다. 관계가 DAUGHTER 로 보이는 것은 QA 데이터가 영문으로 들어간 것입니다.

@github-actions github-actions Bot added the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

이 PR 은 fix/2135-receiver-session-cache 와 충돌 상태이고, 그래서 CI 가 돌지 않는다.

pull_request 워크플로는 base 와 head 를 합친 가상 머지 커밋(refs/pull/<N>/merge)에서
실행되는데, 충돌이면 GitHub 이 그 ref 를 만들지 못해 워크플로가 트리거조차 되지 않는다.
체크가 실패하는 것이 아니라 아예 생성되지 않으므로, 새 커밋을 올려도 no checks reported
인 채 최신 커밋이 한 번도 검증되지 않은 상태로 남는다 (#1028).

fix/2135-receiver-session-cache 를 병합해 충돌을 풀면 검증이 다시 돈다. 해소되면 이 라벨은 자동으로 떨어진다.

@OnlyReadOneBook
OnlyReadOneBook changed the base branch from fix/2135-receiver-session-cache to refactor/1695-setting-nav3 September 27, 2026 05:11
@github-actions github-actions Bot removed the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 27, 2026
@OnlyReadOneBook OnlyReadOneBook changed the title fix(core,setting): 수신자 목록의 로딩·실패를 빈 목록과 구분하고 재시도를 잇는다 (#2045) fix(core,setting): Nav3 목록의 조회 상태와 MVI 재시도를 연결한다 (#2045) Sep 27, 2026
Base automatically changed from refactor/1695-setting-nav3 to develop September 27, 2026 11:01
@github-actions github-actions Bot added the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 27, 2026
…로 올린다 (#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
@github-actions github-actions Bot removed the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 28, 2026
@OnlyReadOneBook
OnlyReadOneBook added this pull request to the merge queue Sep 28, 2026
Merged via the queue into develop with commit 012f7eb Sep 28, 2026
22 checks passed
@OnlyReadOneBook
OnlyReadOneBook deleted the fix/2045-receiver-list-load-state branch September 28, 2026 08:03
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 계측 테스트 실행 sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다

Projects

None yet

2 participants