Skip to content

feat(afternote): 에디터에 임시저장 저장과 이어쓰기 프리필을 붙인다 (#808) - #1768

Closed
OnlyReadOneBook wants to merge 10 commits into
refactor/1804-editor-mvifrom
feat/808-draft-editor
Closed

OnlyReadOneBook wants to merge 10 commits into
refactor/1804-editor-mvifrom
feat/808-draft-editor

Conversation

@OnlyReadOneBook

@OnlyReadOneBook OnlyReadOneBook commented Sep 2, 2026 •

Copy link
Copy Markdown
Collaborator

철회 사유

임시저장 제공 여부와 진입·버튼·저장 후 흐름이 확정되지 않아 이 구현을 철회합니다. 기능 구현 완료로 처리하지 않습니다.

2026-09-27에 기존 PM 질문을 다시 확인했습니다. "애프터노트에 임시저장을 둘까요?"와 목록 위치·저장 후 목적지·이탈 시 저장 동작에 관한 질문에 PM 답글이 없습니다. #1792도 결정 전 구현 예약을 철회한 상태입니다.

현재 HEAD 670480bcdbf8ffd6a75ff2caf9bd8a2904c02538에서 추가한 Save.asDraft=true와 EditorFlowRoute.isDraft=true를 만드는 애프터노트 프로덕션 소비처는 없습니다. 새 임시저장 분기를 먼저 합치고 이후 화면을 연결하는 작업을 예약하지 않습니다. #2117과 영상 snapshot v6에 대한 이 PR의 후속 적응 예약도 제거합니다.

기존 발행 저장·수정 동작, 이미 병합된 서버 wire 필드와 조회 계약은 유지합니다. #2117의 기존 계약 이름 정리는 독립 변경으로 남깁니다. 향후 제품 결정이 나오면 당시 코드와 서버 계약을 다시 비교해 필요한 범위만 정의합니다.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

이 PR 은 feat/808-draft-resume 와 충돌 상태이고, 그래서 CI 가 돌지 않는다.

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

feat/808-draft-resume 를 병합해 충돌을 풀면 검증이 다시 돈다. 해소되면 이 라벨은 자동으로 떨어진다.

@OnlyReadOneBook

Copy link
Copy Markdown
Collaborator Author

Nav3 에서 다시 쓰는 배선을 떼어 #1791 로 분리했다

뗀 것 (→ #1791, blocked_by #1698)

파일 내용
AfternoteNavActions.kt navigateToEditorForResume(itemId, initialType) 선언
AppNavigationActions.kt 구현 13줄 — appState.navController.navigate(EditorFlowRoute(itemId, initialType, isDraft = true))
AfternoteNavGraph.kt 임시저장 목록의 이어쓰기 콜백 — 베이스(#1792)가 등록 블록을 지워 충돌났고 제거 쪽으로 해소했다

남은 것 — 277줄 중 약 264줄. AfternoteRoute.EditorFlowRoute.isDraft 인자, 에디터의 임시저장 저장·이어쓰기 프리필, 상세 응답 분기와 그 테스트.

목록 화면 자체가 #1792 로 넘어가 지금은 도달 경로가 없다 — 죽은 버튼이 남지 않는다.

베이스 갱신 — origin/feat/808-draft-resume (32b109d85) 를 머지했다. 스택 아래 PR #1763 도 같은 수술을 마친 상태다.

로컬 실측 (86b9691aa)

검증 결과
:app:compileDebugKotlin · compileDebugUnitTestKotlin ✅
:app:testDebugUnitTest · :feature:afternote:presentation:testDebugUnitTest ✅
:konsist:test ✅ (ProductionVisibilityKonsistTest 포함, 신규 위반 0)
ktlintCheck ✅

Refs #1791

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

프리필 자체는 정확합니다 — DraftDetail 의 「아직 없음」이 빈 입력칸이 되고, 프리필 타이밍을 손대지 않아 #1839 의 문제를 새로 만들지 않습니다. 다만 검증 건너뛰기가 서버가 완화하는 범위보다 넓고, 저장 버튼만 프로덕션에 살아 있어 그 결과를 볼 길이 이 PR 안에 없습니다.

Swagger 의 isDraft 설명은 완화 대상을 「credentials/playlist 등 필수 검증」으로 못 박습니다.

1. [blocking] ESTATE 에서 임시저장을 누르면 앱이 죽는다

AfternoteEditorViewModel.kt:439 → AfternoteEditorFormMapper.kt:274(생성)·:343(수정)

if (asDraft) null 이 validator 를 통째로 건너뜁니다. 그런데 AfternoteEditorValidator 의 첫 분기는 필수값이 아니라 ESTATE → UNIMPLEMENTED_TYPE 차단입니다. 그게 사라지면 buildCreateInput/buildUpdatePayload 의 error("Unimplemented type cannot be saved") 에 도달합니다.

buildSaveCommand 는 runCatching 으로 감싸여 있지 않고 viewModelScope.launch 안에서 그대로 던져지므로 Result.failure 가 아니라 크래시입니다. 그 error(...) 옆 주석 「Validator 에서 이미 차단되므로 여기 도달 시 호출자 버그」가 이 PR 이 만든 상황을 정확히 설명합니다.

2. [blocking] 발행 완료 애프터노트를 편집하다 「임시저장」을 누르면 발행분이 강등된다

AfternoteEditorViewModel.kt:570 의 updatePayload.copy(isDraft = asDraft) 는 asDraft 를 무조건 싣습니다. 임시저장 버튼은 상단바에 무조건 그려지므로 발행분 편집 화면에서도 눌립니다.

그러면 isDraft=true 로 PATCH 되어 홈 목록(draftOnly 미전송 = 발행분만)에서 사라지고, 임시저장 목록·이어쓰기 배선은 #1792·#1791 로 빠져 있어 되찾을 경로가 없습니다.

「임시저장으로 계속」은 원래 임시저장이던 것에만 뜻이 있습니다. route.isDraft 나 상세의 isDraft 로 발행분에는 null(유지)을 싣거나 버튼 자체를 감춰 주세요.

3. [blocking] 임시저장 버튼만 살아 있고 결과를 볼 길이 없다

AfternoteEditorScreen.kt:145, AfternoteEditorRoute.kt(onSaveSuccessNavigateHome)

저장 성공은 pendingSaveSuccessId → onSaveSuccessNavigateHome() 으로 홈에 가는데 홈은 발행분만 받습니다. 목록 화면(#1792)·이어쓰기 진입(#1791)이 둘 다 뒤로 빠졌으니, 이 PR 만 먼저 들어가면 사용자가 임시저장을 누르는 순간 그 애프터노트가 어디에서도 안 보입니다.

EditorFlowRoute(isDraft = true) 를 만드는 곳도 프로덕션에 0곳이라 getDraftDetail 은 죽은 코드입니다.

#1763 의 임시저장 목록 버튼은 콜백이 null 이라 안 그려집니다 — 「죽은 버튼은 안 남는다」던 그 처방입니다. 이 버튼에는 같은 보호가 없습니다. 버튼을 #1792 뒤로 미루거나 같은 방식으로 콜백을 nullable 로 막아 주세요.

4. [normal] 서버가 완화하지 않는 검증까지 건너뛴다

validator 가 막던 것 중 아래 둘은 임시저장에서도 서버가 요구합니다.

검증 근거
TITLE_REQUIRED Swagger AfternoteCreateRequest.required: [category, title]
LEAVE_MESSAGE_BODY_REQUIRED validator 주석이 「제목만 채운 블록은 서버가 400 으로 거절한다」고 이미 적음

결과는 「임시저장을 눌렀는데 400 이 나고 작성분이 통째로 날아감」입니다. validate() 를 「draft 에도 필요한 것 / 발행에만 필요한 것」으로 가르면 1번까지 함께 닫힙니다.

5. [nit] 새 버튼이 터치 타깃 테스트에서 단언되지 않는다

EditorTouchTargetTest.kt 가 onSaveDraftClick = {} 으로 컴파일만 맞췄습니다. 같은 테스트가 뒤로가기·등록은 role 과 콜백 호출까지 단언합니다. 「눌리면 임시저장 저장이 나간다」는 아무도 안 봅니다.

머지 순서 제안

에디터 ViewModel 을 지금 셋(#1768·#1838·#1839)이 같은 자리에서 만집니다.

  • #1838 ↔ 이 PR — AfternoteEditorViewModel.kt 에서 실제 충돌합니다. #1838 은 executeSaveCommand(command) 를 saveAfternoteUseCase(command) 로 바꾸고 그 아래 25줄을 지우는데, 이 PR 은 그 호출 바로 윗줄에 asDraft = asDraft, 를 넣고 지워지는 함수 바로 아래 buildSaveCommand 를 고칩니다.
  • #1839 ↔ 이 PR — loadExistingAfternoteForEdit 의 onSuccess 블록을 #1839 가 고치는데 이 PR 이 그 블록을 getDetail/getDraftDetail 분기로 다시 씁니다.

#1838 → #1839 → #1747 → #1763 → 이 PR 순이면 해소가 매번 마지막 한 곳으로 몰립니다. #1838 은 develop 직행 1커밋에 동작 변경 0이라 대기 비용이 없습니다.

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

Copy link
Copy Markdown
Collaborator Author

지적 5건 모두 반영했습니다 (f3633e129). 특히 1·4번을 함께 닫으라는 제안이 정확했습니다.

1·4. validator 를 「임시저장에도 필요한 것 / 발행에만 필요한 것」으로 갈랐습니다

validate(form, payload, asDraft) 로 바꾸고 ViewModel 의 if (asDraft) null 을 걷었습니다. 경계는 BE 실코드로 정했습니다 — AfternoteValidator.validateCreateRequest 가 validateLeaveMessage 를 카테고리 전략보다 먼저 부르고, title 은 AfternoteCreateRequest 의 @NotBlank 라 Bean Validation 단계에서 걸립니다. 즉 서버가 완화하는 것은 전략 안의 credentials·playlist 뿐입니다.

검증 임시저장 근거
UNIMPLEMENTED_TYPE(ESTATE) 막는다 서버 이전 문제. 새면 FormMapper 의 error(...) 로 크래시 — 지적하신 바로 그 경로입니다
TITLE_REQUIRED 막는다 AfternoteCreateRequest.title 이 @NotBlank
LEAVE_MESSAGE_BODY_REQUIRED 막는다 AfternoteValidationCommons.validateLeaveMessage 가 본문 빈 블록에 400. 이 함수는 isDraft 를 안 봅니다
ACCOUNT_CREDENTIALS_REQUIRED 건너뛴다 서버가 완화하는 바로 그 축

그 표를 AfternoteEditorValidator KDoc 에 그대로 넣었습니다. 회귀 테스트 4건을 붙였고, 그 중 하나는 같은 폼이 발행에선 막히고 임시저장에선 통과하는 것을 한 테스트에서 대조합니다.

2. 발행분 강등 — route.isDraft 로 막았습니다

val isDraftToSend = if (asDraft) true.takeIf { route.isDraft } else false

원래 임시저장이던 것에만 true 를 싣고, 발행분에는 키를 생략해 서버가 저장값을 유지하게 합니다. 등록 버튼은 어느 쪽이든 false 를 명시합니다. 「임시저장으로 계속」은 원래 임시저장이던 것에만 뜻이 있다는 지적 그대로입니다.

3. 결과를 볼 길 없는 버튼 — 콜백을 nullable 로 막았습니다

AfternoteEditorScreen(onSaveDraftClick: (() -> Unit)?) 로 바꾸고 null 이면 버튼을 아예 안 그립니다. AfternoteEditorRoute 는 배선이 들어올 때까지 null 을 넘깁니다.

#1763 이 임시저장 목록 버튼에 쓴 것과 같은 처방입니다 — 「죽은 버튼은 안 남는다」. 게이트는 상수 하나(DRAFT_ENTRY_WIRED)라 #1792·#1791 이 들어오면 그 줄만 지우면 됩니다. 지우는 조건을 KDoc 에 적어 뒀습니다.

말씀하신 대로 그동안 getDraftDetail 은 프로덕션에서 안 불립니다. 그건 이 PR 이 «기계만 세우고 배선은 뒤로» 인 형태라서이고, #1763 을 그 형태로 봐 주신 기준과 같습니다.

5. 터치 타깃 테스트 — 단언을 채웠습니다

EditorTouchTargetTest 가 이제 임시저장 버튼의 Role.Button 과 클릭 시 콜백이 나가는 것을 뒤로가기·등록과 같은 방식으로 단언합니다. 여기에 콜백이 null 일 때 버튼이 없다는 테스트를 하나 더 붙였습니다 — 3번의 처방이 조용히 뒤집히지 않게요.

검증

./gradlew :feature:afternote:presentation:testDebugUnitTest
→ exit 0 · 58클래스 219건 · 실패 0

머지 순서

제안하신 #1838 → #1839 → #1747 → #1763 → 이 PR 순에 동의합니다. #1838 이 executeSaveCommand 를 걷는 자리가 이 PR 이 고친 buildSaveCommand 바로 위·아래라, 그쪽이 먼저 들어오면 해소가 이 PR 한 곳으로 몰립니다.

@github-actions github-actions Bot removed the awaiting-author 변경요청 뒤 작성자 조치 없음 — 공은 작성자에게 있다 label Sep 3, 2026
OnlyReadOneBook added a commit that referenced this pull request Sep 3, 2026
리뷰 지적 2건 반영.

1. 홈 임시저장 진입점이 Role 도 48dp 도 없이 들어가던 것을 고쳤다. 글자 한 줄이라
   Box 로 타깃을 넓히고 role = Role.Button 을 얹었다. HomeDraftEntryVisibilityTest 가
   assertHasClickAction 까지만 보던 것을 assertAccessibleClickTargets 로 넓혀
   48dp·이름·Role 을 함께 잠갔다 — 두 처방을 각각 되돌려 실패하는 것을 확인했다.

2. 목록에서 고른 임시저장이 발행 상세 계약(getDetail)으로 열리는 문제는 라우트에
   isDraft 가 없어서인데 그 갈림은 #1791 몫이다. 그때까지 홈 진입점을 그리지 않도록
   DRAFT_RESUME_WIRED 로 막았다 — #1768 이 에디터의 임시저장 버튼에 쓴 것과 같은
   처방이다. #1791 이 들어오면 상수와 사용처를 함께 지운다.
@github-actions github-actions Bot added the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 3, 2026
@github-actions github-actions Bot removed the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 4, 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.

지난 지적 5건은 f3633e129 에서 전부 닫혔습니다. 코드로 대조했습니다.

먼저 확인해 둔 것들입니다. 이 PR 의 if (asDraft) true.takeIf { route.isDraft } else false 는 #1828 의 resolveUpdateIsDraft 와 진리표가 같습니다 — 발행분 + 임시저장이면 null 이 나가 키가 생략됩니다. 두 PR 이 어긋나지 않습니다. 와이어 계약은 base #1763 의 AfternoteDraftRequestTest 가 이미 잠그고 있고, 검증 경계는 신규 AfternoteEditorValidatorTest 4건이 실제로 잠급니다 — if (!asDraft) 블록을 위로 올리거나 추모 항목 early-return 을 안으로 넣으면 각각 빨개집니다. 마지막 테스트가 같은 폼을 발행에선 막고 임시저장에선 통과시키는 것을 한 자리에서 대조하는 것이 좋습니다.

EditorTouchTargetTest 신규 2건도 좋습니다. 콜백이 null 일 때 타깃이 0개임을 단언해서, 무조건 그리기로 되돌리면 즉시 빨개집니다. 죽은 버튼 금지가 타입만이 아니라 테스트로도 잠겼습니다.

새로 남은 것 셋입니다. blocking 은 없고 한 라운드면 닫힙니다.

1. [normal] 「임시저장」 버튼이 isSubmitEnabled 게이트를 안 받는다

AfternoteEditorScreen.kt:148-161 입니다. 바로 다음 줄(:162)의 등록 버튼 주석이 그 규칙을 적어 두었습니다 — 「저장이 나가 있는 동안·prefill 을 못 읽은 동안은 «등록» 을 흐리고 눌리지 않게 한다 (#705). 유휴 상태와 같은 모습이면 사용자는 응답 없는 화면으로 읽고 연타로 중복 저장을 시도한다.」

새 버튼에는 enabled 도 회색 처리도 없습니다. 저장 왕복 중 탭하면 AfternoteEditorViewModel.kt:437 의 if (editorState.isSaving) return 에 걸려 오류도 스낵바도 없이 통째로 삼켜집니다. prefill 실패 화면에서도 등록만 잠기고 이 버튼은 멀쩡히 눌립니다.

지금은 DRAFT_ENTRY_WIRED = false 라 잠들어 있지만, #1828 이 그 게이트를 지우면서 이 코드를 그대로 가져갑니다 — feat/1791-draft-resume-nav3 의 AfternoteEditorScreen.kt:145-157 에 enabled 없이 복사돼 있습니다. 저작지가 여기이니 여기서 고치는 편이 맞습니다.

2. [normal] 발행 완료분 편집 화면에서 이 버튼은 할 일이 없는데 검증만 느슨해진다

같은 자리이고 1번과 한 곳에서 닫힙니다.

route.isDraft == false && isEditing 인 화면에서 「임시저장」을 누르면 isDraftToSend 가 null 이라 발행 상태는 그대로입니다. 즉 결과가 「등록」과 같습니다. 그런데 validate(asDraft = true) 는 ACCOUNT_CREDENTIALS_REQUIRED 를 건너뜁니다. 발행된 소셜 노트의 ID 칸을 지우고 이 버튼을 누르면 FE 가 자기 게이트만 내리고 PATCH 를 보냅니다. 서버가 저장값 기준으로 막아 주면 400 이고, 요청의 isDraft 부재를 완화 신호로 읽는다면 빈 credentials 인 발행분이 만들어집니다. 어느 쪽이든 FE 가 게이트를 내릴 이유가 없는 자리입니다.

isEditing && !route.isDraft 일 때 버튼을 감추면 1·2 가 함께 닫힙니다. 신규 작성(itemId == null)은 임시저장이 의미가 있으니 남기면 됩니다.

3. [normal] 본문이 f3633e129·86b9691aa 이전 코드를 설명한다

본문은 머지 커밋 메시지로 남는 정본인데 네 군데가 현재 코드와 다릅니다.

본문 현재 코드
「누르면 화면 검증을 건너뜁니다」 추모 항목·제목·남기실 말씀 본문은 그대로 막습니다
「onSaveDraftClick 을 필수 인자로」 · 「탑바에 텍스트 액션 한 개가 느는 변경」 (() -> Unit)? 이고 DRAFT_ENTRY_WIRED = false 라 프로덕션에 안 그려집니다
「발행 노트를 「임시저장」으로 누르면 임시저장으로 되돌아갑니다」 route.isDraft 가 false 면 키를 생략합니다. 없는 위험을 리뷰어에게 묻고 있습니다
CI Test Plan reason 의 「app 의 navigation 을 건드려 … 이어쓰기 NavAction」 그 배선은 86b9691aa 로 #1791 에 떼어냈습니다. 이 PR 은 app 모듈을 한 줄도 안 만집니다

mode: selected 자체는 EditorFlowRoute 에 인자가 느는 것만으로도 유지할 만합니다. reason 이 대는 근거만 남은 사실로 다시 적어 주세요.

4. [nit] 수정 저장의 isDraftToSend 판정을 잠그는 테스트가 이 PR 에 없다

AfternoteEditorViewModel.kt:595 를 val isDraftToSend = asDraft 로 되돌려도 이 PR 의 신규 테스트가 전부 초록입니다. 「발행분 강등」은 제 지난 리뷰의 blocking 이던 축인데 회귀 잠금이 없습니다. #1828 의 ResolveUpdateIsDraftTest 가 결과적으로 덮지만 그 PR 이 들어오기 전까지는 무방비입니다. 그 동안 버튼이 안 그려져 실사용 위험이 없어 nit 으로 둡니다.

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

Copy link
Copy Markdown
Collaborator Author

두 지적을 반영했습니다. base(refactor/1804-editor-mvi)는 이미 이 브랜치의 조상이라 merge 로는 돌아오지 않았고(Already up to date), ce9a286 자체가 f6e9028 의 세 파일을 되돌린 것이어서 1b62f99 에서 TouchTargetAssertions.kt, TouchTargetAssertionsTest.kt, EditorServiceSelectionSheetTest.kt 를 base 트리로 복구했습니다. 세 파일 모두 git diff origin/refactor/1804-editor-mvi 가 0 줄이고 core:ui 테스트 75건(excluded target 케이스 포함)이 초록입니다.

4479567 에서 AfternoteEditorServerMediaDeleteSaveTest 에 route.isDraft = false 와 asDraft = true 조합의 PATCH payload isDraft 가 null 인지 잠그는 케이스를 추가했습니다. takeIf 를 지우면 그 테스트가 expected null but was true 로 빨개지는 것을 확인한 뒤 되돌렸고, feature:afternote:presentation 테스트 350건, ktlintCheck, app:compileDebugUnitTestKotlin 모두 통과합니다.

@github-actions github-actions Bot removed the awaiting-author 변경요청 뒤 작성자 조치 없음 — 공은 작성자에게 있다 label Sep 11, 2026
@OnlyReadOneBook
OnlyReadOneBook removed this pull request from stack #1973 September 19, 2026 00:39
@OnlyReadOneBook
OnlyReadOneBook added this pull request to stack #2110 September 19, 2026 00:39
@github-actions github-actions Bot added conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 labels Sep 20, 2026
@OnlyReadOneBook
OnlyReadOneBook removed this pull request from stack #2110 September 20, 2026 11:53
@OnlyReadOneBook
OnlyReadOneBook marked this pull request as draft September 20, 2026 11:53
@github-actions github-actions Bot removed conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 labels Sep 25, 2026
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.

feat(afternote): 임시저장 저장·발행 전환과 이어쓰기 프리필 코어

3 participants