Skip to content

빈 세션 종료 — 기록 0개인데 세션 완료 화면·빈 세션 저장으로 가던 종료 동선 제거 (2026-08-24)

  • 기간: 2026-08-24 (세션 1, 오너 보고 "기록한 종목이 하나도 없으면 그냥 세션 완료 뜨지 말고, 저장 안하고 종료하고 … 토스트 뜨고 종료되게 할 수 있어?")
  • 랜딩: PR #649(5bb1a9eb) — 마이그레이션·엣지 없음, Vercel 자동 배포
  • 설계서: 없음(단일 PR 소건 — 방안·결정은 보고 턴에서 오너 확정)
  • 정본: WorkoutFlow.tsx attemptFinish·mobileWorkoutExerciseRecorded / workoutWriteController.ts discardActiveWorkout(cause) / appController.tsx exitPlan({ notify })
  • 도구: 없음
  • 게이트: tests/react/workoutFlowEmptyFinish.test.mjs (기록 판정·필터 동치·cause 통과/차단·배선 계약)
  • 버그리포트: 없음(동작 개선 트랙)
  • 계약: workout-screen-props.mdonDiscard cause 인자 + "빈 세션 종료" 절 (이 문서와 같은 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.ts discardActiveWorkout: cause별 토스트 — 수정 모드 "수정을 취소하였습니다" 최우선 유지, 라이브 "운동 진행을 종료합니다", 기록 작성 "기록 작성을 취소했습니다", 무인자 기존 "삭제하였습니다".
  • appController.tsx exitPlan({ 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 채널 — 이후 폐기 사유별 문구가 필요하면 같은 틀로 확장.
  • 배선 계약 테스트가 게이트·토스트·계획 취소 알림 경로를 고정.

남은 것

  • 오너 실기기 확인(세 표면 토스트·즉시 종료 체감).
  • 데스크톱은 라이브 운동 플로우가 없어 스코프 밖(계획/기록 편집기는 별도 화면).