빈 세션 종료 — 기록 0개인데 세션 완료 화면·빈 세션 저장으로 가던 종료 동선 제거 (2026-08-24)
- 기간: 2026-08-24 (세션 1, 오너 보고 "기록한 종목이 하나도 없으면 그냥 세션 완료 뜨지 말고, 저장 안하고 종료하고 … 토스트 뜨고 종료되게 할 수 있어?")
- 랜딩: PR #649(
5bb1a9eb) — 마이그레이션·엣지 없음, Vercel 자동 배포 - 설계서: 없음(단일 PR 소건 — 방안·결정은 보고 턴에서 오너 확정)
- 정본:
WorkoutFlow.tsxattemptFinish·mobileWorkoutExerciseRecorded/workoutWriteController.tsdiscardActiveWorkout(cause)/appController.tsxexitPlan({ notify }) - 도구: 없음
- 게이트:
tests/react/workoutFlowEmptyFinish.test.mjs(기록 판정·필터 동치·cause 통과/차단·배선 계약) - 버그리포트: 없음(동작 개선 트랙)
- 계약:
workout-screen-props.md—onDiscardcause 인자 + "빈 세션 종료" 절 (이 문서와 같은 PR)
1. 배경
운동 진행·지난 기록 작성·계획 작성은 2026-07-31에 WorkoutFlow 한 문법으로 통일됐고, 종료 버튼("운동 종료"/"작성 완료")은 미완료 종목 확인 다이얼로그를 거쳐 세션 완료(WORKOUT COMPLETE) 화면 → 저장으로 이어졌다. 이 경로에 "기록이 하나도 없다"는 분기가 없어, 실수로 운동을 시작만 하고 끝내도 요약 화면("총 0kg 들었어요 · 빈 세션 — 수고하셨습니다! 💪")을 지나 빈 세션이 실제로 저장됐다.
2. 문제 제기
기록 0개 세션이 완료 화면을 지나 그대로 저장된다
라이브에서 완료 세트 0개로 "운동 종료"를 누르면 미완료 확인 뒤 완료 화면 진입·저장(조기 종료 필터 mobileWorkoutFinishEarlyExercises가 "전부 미기록이면 목록 유지" 규칙이라 빈 세션이 통과). 기록 작성도 종목 0개로 "작성 완료"가 눌리면 같은 경로였다.
빈 종료·취소가 무음이거나 문구가 어긋난다
계획 작성 취소는 토스트 없이 조용히 닫혔고, 기록 작성의 "작성 취소"(종목 0개)는 지운 것이 없는데 "삭제하였습니다"가 떴다.
3. 해결 방안
원칙 (오너 결정, 2026-08-24)
- D1 "기록한 종목이 하나도 없으면 세션 완료 뜨지 말고, 저장 안 하고 종료 + 표면별 토스트" — 채택(운동 진행을 종료합니다 / 기록 작성을 취소했습니다 / 계획 작성을 취소했습니다).
- D2 "유저가 입력한 데이터가 뭐라도 있으면 한번 다시 물어보고, 아예 없는 경우면 바로 종료. 아예 없는 경우는 세션 완료/요약화면 없이" — 채택(A안: 종목 0개=무확인 즉시 종료, 입력만 있고 기록 0개=확인 1회).
접근
| 대안 | 판정 |
|---|---|
화면 게이트(attemptFinish) + discard 커맨드에 cause 전달, 토스트는 컨트롤러가 선택 | 채택 — 기존 discard가 드래프트 삭제·탭 복귀·토스트를 이미 소유, 문구만 표면별 분기 |
| 조건 불문 즉시 종료(B안) | 기각 — 입력해 둔 세트가 확인 없이 날아감 |
| 완료 화면에서 빈 세션 저장만 차단 | 기각 — 무의미한 요약 화면("총 0kg")을 여전히 지나감 |
4. 적용한 내용
PR #649 (5bb1a9eb)
WorkoutFlow.tsx: 종료 버튼·마지막 종목 "운동 종료하기" 공통attemptFinish게이트 — 종목 0개 =discardEmptySession()즉시 폐기, 라이브에서 기록 0개(완료 세트·완료 플래그·작성된 자유 기록 전무) =endAsk:"empty"확인 카드("기록한 세트가 없어요") 1회 뒤 폐기. 기록 판정은 조기 종료 필터에서 추출한mobileWorkoutExerciseRecorded재사용(필터 의미 동치, 테스트 고정).activeWorkoutCommands.ts:discard(options?: { cause })— 아는 cause(empty-live-finish/empty-record-finish)만 어댑터 통과(이벤트 객체 유입 차단).workoutWriteController.tsdiscardActiveWorkout: cause별 토스트 — 수정 모드 "수정을 취소하였습니다" 최우선 유지, 라이브 "운동 진행을 종료합니다", 기록 작성 "기록 작성을 취소했습니다", 무인자 기존 "삭제하였습니다".appController.tsxexitPlan({ notify })+mobileApp.tsx취소 경로 배선: 계획 취소·"저장하지 않고 나가기"에 "계획 작성을 취소했습니다" — 탭 전환 이탈은 종전대로 무음.
주요 결정과 그 근거
- backfill은 종목이 있으면 기존 경로 그대로 — 직렬화가 세트 done을 강제하므로 적은 내용 전부가 기록이다(빈 판정은 종목 0개뿐).
- 지난 세션 수정 모드의 빈 종료는 "수정 취소" — 종목을 다 지우고 "작성 완료"를 눌러도 기존 세션을 빈 저장으로 덮지 않는다.
- 신규 데이터 마커 0 — 빈 확인 카드의 확정 버튼은 기등록
workout.discardSession.confirm재사용.
작업 중 드러난 것
onClick={onDiscard}처럼 이벤트가 첫 인자로 물리는 기존 호출부가 있어, cause는 커맨드 층에서 화이트리스트로 걸렀다.- 로컬
check:lint는 primary 체크아웃에서도eslint-plugin-react-hooks결손으로 실행 불가(내 변경과 무관, CI 레인에서 통과 확인). node_modules 재설치 후보.
5. 적용 결과
| 항목 | 전 → 후 |
|---|---|
| 라이브 종목 0개 "운동 종료" | 다이얼로그→완료 화면→빈 세션 저장 → 즉시 종료 + "운동 진행을 종료합니다" |
| 라이브 입력만 있고 기록 0개 | 완료 화면→빈 세션 저장 → 확인 1회 뒤 저장 없이 종료 + 같은 토스트 |
| 기록 작성 종목 0개("작성 완료"/"작성 취소") | 빈 세션 저장 / "삭제하였습니다" → 즉시 종료 + "기록 작성을 취소했습니다" |
| 계획 작성 취소 | 무음 → "계획 작성을 취소했습니다" |
| 검증 | tests/react 1,608개 통과 · tsc --noEmit 클린 · CI verify 3m26s pass. 오너 실기기 확인 대기(토스트 문구·동선 체감) |
6. 이번 개선으로 향상된 것
빈 세션이 데이터로 남지 않는다
실수 시작·빈 작성이 달력/피드/통계에 "빈 세션"으로 쌓이던 경로가 닫혔다.
종료가 상황에 맞는 한 마디로 끝난다
표면마다 무엇이 일어났는지(종료/취소) 토스트가 말해 주고, 무의미한 요약 화면을 지나지 않는다.
구조적으로 남는 것
mobileWorkoutExerciseRecorded= "기록한 종목" 판정 단일 정의(조기 종료 필터와 공유).- discard cause 채널 — 이후 폐기 사유별 문구가 필요하면 같은 틀로 확장.
- 배선 계약 테스트가 게이트·토스트·계획 취소 알림 경로를 고정.
남은 것
- 오너 실기기 확인(세 표면 토스트·즉시 종료 체감).
- 데스크톱은 라이브 운동 플로우가 없어 스코프 밖(계획/기록 편집기는 별도 화면).