Skip to content

refactor(afternote): 편집기 MVI와 소비 경계를 통합한다 (#1804) - #1972

Merged
OnlyReadOneBook merged 14 commits into
developfrom
refactor/1804-editor-mvi
Oct 6, 2026
Merged

OnlyReadOneBook merged 14 commits into
developfrom
refactor/1804-editor-mvi

Conversation

@OnlyReadOneBook

@OnlyReadOneBook OnlyReadOneBook commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1804
Refs #1968, #1732

편집기의 폼·미디어·수신자·저장·prefill 입력과 소비 호출을 onIntent로 모으고, 상태 변경을 MviViewModel의 순수 reducer로 옮깁니다. 기존 EditorFormMutations 규칙을 재사용하며 SavedState 직렬화·저장소 호출·계측은 reducer 밖에 둡니다.

직접 base는 #1971의 refactor/1902-memorial-photo입니다. 선행 순서는 #2164 → #2167 → #1837 → #1838 → #1971 → #1972입니다. 프로세스 복원 입력 보호 #1839는 이 최종 MVI 위에서 ApplyPrefill과 PrefillLoaded 계약을 확장합니다. 기존 mutable 상태와 공개 함수를 먼저 확장했다가 다시 MVI로 옮기던 중간 구현을 제거했습니다.

Nav3 공유 flow와 직접 화면 소비처를 Intent에 연결합니다. 오류 occurrence 소비, prefill 적용 전 skeleton, 부분 PATCH, 썸네일 재시도, 늦은 수신자 조회 결과 무시를 유지합니다. 서비스 시트와 이탈 경고 함수는 실제 프로덕션 호출 범위에 맞게 private로 줄이고 기존 화면 계약으로 테스트합니다. 접근성 검사는 Material3 스크림만 제외하고 실제 클릭 대상 검증을 유지합니다.

기존 나머지 8개 ViewModel은 #2021의 독립 후속 범위입니다. 이 PR은 현재 제공되는 에디터 경계만 다루며 미확정 임시저장 화면의 구현을 예약하지 않습니다.

검증

JVM 검증 리비전 ee6fa1f642e2ee29b20f67e6f9c56e2fdcea26ca에서 presentation 345개, domain 19개, data 98개 테스트가 통과했습니다(실패·오류·건너뜀 0). presentation ktlint, 실제 base 대비 전체 diff의 모듈 소유권 검사와 git diff --check를 통과했습니다.

현재 HEAD 44d6dc33e5edb585de68883450009211f53fbdca는 검증 리비전에서 수신자 복귀 KDoc만 최종 Intent 이름으로 정정했습니다. 프로덕션 코드와 테스트 코드는 동일하며 KDoc 링크 검사(끊김 0)와 ktlint를 통과했습니다.

🧪 CI Test Plan

{
  "androidTest": {
    "mode": "selected",
    "reason": "EditorFlowHost와 공유 ViewModel의 수신자 선택 결과 소비를 Intent로 바꾸므로 실제 앱 Nav3의 선택·해제·취소 복귀와 빈 목록/실패/재시도를 API 30에서 확인한다. 저장·프리필·복원·요청 경합과 화면 이탈 경고는 별도 JVM/Compose 회귀로 검증한다.",
    "tests": [
      {
        "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_receiverSelectUncheckingFormReceiverRemovesItFromEditorOnConfirm",
        "device": "api30"
      },
      {
        "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#afternoteSelectReceiver_emptyAndLoadFailureBlockConfirmUntilRetryLoadsList",
        "device": "api30"
      }
    ]
  }
}

@OnlyReadOneBook OnlyReadOneBook added the screenshot-baseline CI 표준 환경에서 PR screenshot baseline 생성·적용 요청 label Sep 8, 2026
@github-actions github-actions Bot added the android-test PR head에서 Android Managed Device 계측 테스트 실행 label Sep 8, 2026
@github-actions github-actions Bot added the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

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

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

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

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

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

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

@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.

MVI 전환 자체는 문제 없습니다. reduce 가 순수하고 IO·계측은 onIntent 쪽에 남았으며, dispatchForm 이 스냅샷 저장을 리듀서 밖으로 뺀 것도 맞습니다. toAfternoteEditorError 단위 테스트를 저장 동작 테스트로 옮긴 전환도 좋습니다. 다만 접근성 가드 하나가 설명 없이 사라져 그 부분만 요청합니다.

  • EditorServiceSelectionSheetTest 에서 assertAccessibleClickTargets() 가 빠졌다. 이 시트 전체의 48dp·이름·role·중첩 클릭 계약(#1167·#1179·#1669)을 보던 유일한 지점입니다. 남은 단언은 서비스 행 2개의 크기·role 뿐이라 검색 입력·빈 결과 안내·시트 컨테이너는 이제 아무도 보지 않습니다. PR 본문에도 이 제거가 없습니다.
  • 원인은 시트 진입점 교체입니다. 테스트를 EditorServiceSelectionSheetContent 대신 EditorServiceSelectionSheet(visible = true, ...) 로 바꾸면서 Material3 가 만드는 스크림 노드가 스캔에 들어옵니다. 로컬에서 되살려 돌려 보면 실패는 이것 하나입니다 — Role 누락: #0 touch=320.0.dp×470.0.dp name="Close sheet". 우리 화면 코드의 위반이 아니라 프레임워크 노드입니다.
  • 둘 중 하나로 되살려 주세요. (1) 렌더 seam 을 유지해 시트 본문을 직접 그리고 그 위에서 스윕을 돌리거나 — core:ui 의 MediaSelectSheetContent·MediaSelectSheetContentTest 가 저장소의 기존 패턴입니다 — (2) assertAccessibleClickTargets 에 제외 훅을 추가해 스크림만 빼고 스윕을 유지해 주세요. 되살릴 수 없다면 그 이유를 테스트 KDoc 과 PR 본문에 남겨 주세요.
  • 참고. seam 을 유지하면 프로덕션의 LocalInspectionMode 분기도 함께 사라집니다. 지금은 EditorServiceSelectionSheet 가 inspection 여부에 따라 다른 트리를 그려, screenshot 이 검증하는 경로와 실제 앱 경로가 갈립니다.

@github-actions github-actions Bot added the awaiting-author 변경요청 뒤 작성자 조치 없음 — 공은 작성자에게 있다 label Sep 11, 2026
@OnlyReadOneBook OnlyReadOneBook self-assigned this Sep 11, 2026
@github-actions github-actions Bot removed the awaiting-author 변경요청 뒤 작성자 조치 없음 — 공은 작성자에게 있다 label Sep 11, 2026
@OnlyReadOneBook

Copy link
Copy Markdown
Collaborator Author

지적한 접근성 스윕을 bce5e81 에서 되살렸습니다. 두 방법 중 (2) 로 갔습니다. assertAccessibleClickTargets 에 exclude 훅을 추가하고 시트 테스트는 compose ui 의 close_sheet 문자열(Material3 스크림의 contentDescription 출처)을 가진 노드만 뺍니다. 검색 입력·빈 결과 안내·행의 48dp·이름·role·중첩 클릭 계약은 그대로 스윕합니다. 제외한 노드 말고는 스윕이 유지된다는 회귀를 TouchTargetAssertionsTest 에 추가했고, 본문에도 적었습니다. core:ui 와 afternote presentation 의 해당 테스트·ktlint 를 로컬에서 통과했습니다.

@OnlyReadOneBook
OnlyReadOneBook removed this pull request from stack #1973 September 11, 2026 08:00
@OnlyReadOneBook
OnlyReadOneBook added this pull request to stack #1973 September 11, 2026 08:00
@OnlyReadOneBook

Copy link
Copy Markdown
Collaborator Author

음성 첨부 PR #1625 가 시안 근거 없음으로 닫혀(#1979) 이 브랜치를 그 커밋 없이 다시 쌓았습니다. 이전 tip bce5e81e6 의 diff 를 새 부모 위에 한 커밋으로 옮기고 음성 관련 심볼(MediaKind.AUDIO, MemorialAudio*, RecordSoundContract, 폼의 audioUrl, 관련 테스트)만 걷어냈습니다. 새 tip 은 f6e902831 입니다.

이 tip 에서 afternote domain / data / presentation 단위 테스트 19 / 100 / 338 건 통과(실패·오류·skip 0), 세 모듈 ktlintCheck, 앱 main·unitTest·androidTest Kotlin 컴파일을 확인했습니다. 다른 변경은 없습니다.

@OnlyReadOneBook
OnlyReadOneBook removed this pull request from stack #2121 September 25, 2026 08:53
@OnlyReadOneBook
OnlyReadOneBook added this pull request to stack #2165 September 25, 2026 08:53
@github-actions github-actions Bot removed the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Sep 26, 2026
@OnlyReadOneBook
OnlyReadOneBook removed this pull request from stack #2165 September 27, 2026 04:55
@OnlyReadOneBook
OnlyReadOneBook added this pull request to stack #2182 September 27, 2026 04:55

@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.

#1732 복원 가드를 #1839 로 넘기면서 남은 참조 없이 깔끔히 걷혔고, 나머지는 수신자 복귀 KDoc 을 최종 Intent 에 맞춘 정리입니다. 승인합니다.

@github-actions github-actions Bot added the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Oct 5, 2026
@github-actions github-actions Bot added conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 and removed sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 labels Oct 5, 2026
Base automatically changed from refactor/1902-memorial-photo to develop October 5, 2026 13:11
…or-mvi

# Conflicts:
#	feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/AfternoteEditorViewModel.kt
@github-actions github-actions Bot added sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 and removed conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 labels Oct 5, 2026
@OnlyReadOneBook
OnlyReadOneBook added this pull request to the merge queue Oct 6, 2026
Merged via the queue into develop with commit a705160 Oct 6, 2026
21 checks passed
@OnlyReadOneBook
OnlyReadOneBook deleted the refactor/1804-editor-mvi branch October 6, 2026 04:54
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

Development

Successfully merging this pull request may close these issues.

refactor(afternote): 편집기 ViewModel과 직접 소비 경계를 MVI로 전환한다

2 participants