진행중 세션 소실 — 감지 시스템·임시 보관·복원 팝업·서버 체크포인트 (이슈 #887)
- 기간: 2026-08-28 ~ 2026-08-29
- 랜딩: #898 (Phase 0) · #906 (Phase 1~3)
- 설계서: 이슈 #887 쓰레드(원인 판정서·실행 계획)
- 정본: workoutDraftCache.ts(보관함·검증 진단) · workoutDraftStore.ts(복원·플러시·체크포인트 배선) · workoutDraftCheckpoint.ts · 마이그레이션 20260829140000
- 도구: Production 실측(Management API·엣지 로그 타임라인 역산) + 실코드 재현 하네스(R1~R7)
- 게이트: workoutDraftArchive 12 · workoutDraftLifecycleFlush 4 · workoutDraftRestorePrompt 6 · workoutDraftCheckpoint 4 · pgTAP workout_draft_checkpoints_v1 13
- 버그리포트: BUG-037
- 계약: 봉투 파기 금지(격리 이동) · 부팅 복원 확인 팝업 · 체크포인트 operation_id 펜스
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0 | 감지 시스템(계측 8종·IDB 타임아웃·리포터 생존화) + 로컬 임시 보관함 + 복원 수리 | ✅ #898 |
| Phase 1 | 무조건 로컬 저장(hidden/pagehide 즉시 영속) | ✅ #906 |
| Phase 2 | 부팅 확인 팝업(이어서/삭제/닫기) + designContract 마커 | ✅ #906 |
| Phase 3 | 서버 임시 체크포인트(D5 — 20260829140000·스로틀 업로드·복원 폴백) | ✅ #906 + db push·Production 프로브 |
| Phase 4 | 게이트·검증(신규 26케이스 + pgTAP 13, 풀 레인 1861/1861) | ✅ |
1. 배경
08-28 저녁 오너의 진행중 운동이 두 차례 통째로 사라졌고(21:00 콜드 재부팅·~21:35), 재실행에도 돌아오지 않아 4분 만에 재입력했다(이슈 #887, BUG-037). 진행중 드래프트는 편집마다 IndexedDB 봉투로 자동저장되고 부팅 시 조용히 복원되는 구조였다.
2. 문제 제기
원인 조사(코드 6레인 전수 + Production 코드 그대로 재현 19케이스 + 반박 검증)의 구조적 결론: 소실 전 경로가 무계측(검증 실패·만료의 무통보 삭제, 복원 경합의 침묵 포기, 발행 게이트 드롭, IDB 행)이고 유저당 봉투 1슬롯이라 다음 운동 시작 ~0.95초에 이전 봉투가 영구 덮여 증거까지 인멸된다. 후보 3계열(S1 부팅 직후 선탭 봉쇄 / S2 실값 오염 검증 전멸 / S3 WKWebView 저장소 계층)이 모두 관측과 정합해 단일 확정이 불가능했다 — 판별 불능 자체가 07-30부터의 잠복 설계.
3. 해결 방안
- 원칙(오너 결정, 08-29): ① "경로 전부 무계측이 제일 문제 — 감지 시스템 먼저"(D 채택) ② "진행중 세션 임시 보관 시스템 구축" ③ D5(서버 체크포인트) 포함 Phase 0~4 중단 없이 ④ BRID(#879) 대기하지 않음(드래프트는 세션 전 산물).
- 접근: 원인 3계열을 모두 봉인하는 4층 — 파기 대신 격리(+계측) / 강제 종료 직전 즉시 저장 / 부팅 확인 팝업 / 서버 체크포인트.
4. 적용한 내용
Phase 0 (#898) — 봉투를 지우거나 덮어쓰는 전 지점(검증 실패·만료·덮어쓰기·저장/삭제/빈 초안 정리·낡은 편집 폐기)이 archive:user:<uid>:<ts> 보관함 이동으로 교체(최근 5세대·7일 TTL). 계측: draft_archive_invalid(첫 실패 지점 사유 토큰)·expired·overwritten·draft_restore_race_aborted·draft_restore_foreclosed(S1 직접 감지)·draft_publish_dropped_*(4게이트, 세션당 1회·펜스는 60초 지속 시)·draft_storage_wipe_detected(IDB/localStorage 쌍마커, lean 하드컷 준수 위해 workoutDraftStorageMarker.ts 분리). 복원 수리: requestId 증가를 실제 시작 시로 이동(침묵 포기 홀 제거) + 커밋 직전 선점 재확인(비행 중 새 시작 클로버 방지 — 조사에서 발견된 별개 잠복 결함) + settled 래치. IDB 전 연산 8초 타임아웃(행 → IdbTimeoutError). 리포터 큐 sessionStorage→localStorage + hidden/pagehide flush.
Phase 1~3 (#906) — hidden/pagehide에서 최신 스냅샷 즉시 영속(꼬리 유실 창 ~950ms 제거). 모바일 복원이 확인 팝업 발행(이어서 하기 / 삭제=재확인 1회·보관함 소프트 삭제 / 닫기=이어하기 바 유지). 서버 체크포인트: 유저당 1행 opaque jsonb(256KB)·펜스·TTL 3일 lazy 정리·RPC 3종(본인 한정), 클라 스로틀 업로드(60초·운동 교체 즉시)·봉투 정리 동펜스 서버 정리·복원 폴백 실체화(로컬 검증기·보관함 규율 그대로 통과).
작업 중 드러난 것 — ① Node 22의 tsx가 소스 내부 임포트와 테스트 임포트를 별개 리포터 인스턴스로 로드(로컬 Node 24는 단일) → 싱글턴 관찰 단언은 CI에서만 실패, 소스-앵커 관용구로 교체. ② 마이그레이션 번호 20260829100000이 같은 날 병행 트랙 3개(#879 100000·110000, #888 120000, #880 130000)에 차례로 선점 → 140000까지 두 번 재번호(번호 사다리 — 커밋 직전과 머지 직전 각각 재확인 필요). ③ 새 data-lg 마커는 designContract 등록 필수. ④ schema.sql append는 \n\n 구분자까지가 계약.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 소실 경로 계측 | 0종 (7일 이벤트 0건) | 계측 10종+ (파기 사유 토큰 포함) |
| 봉투 파기 시 데이터 | 즉시 소멸 | 보관함 5세대·7일 보존 |
| 강제 종료 직전 입력 | 최대 ~950ms 유실 | hidden 즉시 영속 |
| 재부팅 시 미저장 세션 인지 | 시작 탭 한정 바(구별 불능) | 전면 확인 팝업 100% |
| 기기 저장소 소실(S3) | 복구 불가 | 서버 체크포인트 복구(TTL 3일) |
| 재발 시 원인 판별 | 불능 | 1회 발생으로 S1/S2/S3 특정 |
| 게이트 | — | 신규 26케이스 + pgTAP 13 (풀 레인 1861/1861) |
Production 실측: 마이그레이션 장부 최신 20260829140000, 테이블·RLS·RPC 3종·직접 읽기 차단·authenticated 실행 가능 프로브 전부 확인. Vercel Production success + 청크 실측(mobileRoot-CEIdDgIn.js=부팅 팝업, HostFrames-PKe1vKp6.js=보관함·계측·체크포인트 마커).
미검증: 오너 실기기의 부팅 팝업·이어하기·서버 복구 실동작(확인 대기). 실기기 e2e는 하네스 제약(인증 부팅+IDB 시딩)으로 스토어 행동+SSR 마크업+배선 앵커 3층 대체.
6. 이번 개선으로 향상된 것
- 진행중 운동이 사라질 수 있는 원인 3계열이 각각 독립 층으로 봉인됐고, 소실이 다시 일어나면 텔레메트리 1회로 특정된다.
- "파기 대신 격리"가 드래프트 캐시의 기본 의미론이 됐다(사용자 삭제도 소프트 삭제).
- 기기 교체·재설치에도 진행중 운동이 따라오는 서버 층이 생겼다.
남은 것
- 오너 실기기 확인(부팅 팝업 3갈래·서버 복구) → BUG-037 상태 갱신.
- 임시 보관함 목록 UI(현재 복구 진입은 팝업 경유) — 별도 결정.
- 검증기 완화(규약 밖 값 1개 → 봉투 전멸 대신 엔트리 격리) — 별도 결정.