유산소 운동에서 측정단위를 1개만 입력해도 되도록 허용 — 저장 차단 수리 + 5경로 e2e 보증 (2026-08-30)
- 기간: 2026-08-30 (1세션, 08-30 실 사용자 저장 차단 사고 원인 ① → 이슈 #936, 사고 추적 #942, 오너 go + e2e 범위 지시)
- 랜딩: PR #944(
0d4383b9) — 마이그레이션 20260830110000 Productiondb push완료, Vercel 배포 청크 마커(20260830110000inapp-Bj8RyM8w.js) 실측. 성근 재저장 성공 실측(08-31 07:08 KST, 사고 당시 차단된 운동 그대로 세션a4ff670b) + 오너 확인 08-31 — 트랙 종결(이슈 [반영완료] 닫음) - 설계서: 없음(단일 트랙 — 분석·Phase 계획은 이슈 #936 본문·코멘트)
- 정본:
recording.tsisCardioOnlyProfile(완료 요건 규칙 단일 소스) ·barbelicRepository.ts저장 직전 최종 검사 ·get_group_board_day_sessions_v1(schema.sql 20260830110000 절) - 도구: 사고 조사 세션의 성근 draft 백업 실물 재현 · supabase migration list 원격 실측(번호 미선점) · Production 배포 청크 재귀 크롤
- 게이트:
recordingProfileRoundTrip.test.mjs실경로 관통(시간·거리 단독) ·cardioMinOneField.e2e.mjs5경로(CI migration-smoketest:e2e-cardio) ·groupBoardCardioRoundtrip.test.mjs완료 인원 매핑 · 마이그레이션 postcheck - 버그리포트:
bug-041-20260830.md(수리 반영 갱신, 이 PR) - 계약: 유산소(거리·시간만) 프로필의 완료 세트 = 두 원자 중 하나 이상(값 있을 때만 범위 검사) — 클라 5층·서버가 전부 같은 규칙, 판정은
isCardioOnlyProfile한 곳. 보드 완료 인원 뷰 세트에duration_seconds?/distance_meters?(값 있을 때만 키)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0 | 실경로 관통 테스트 선작성(RED 재현) + 성근 draft 실물 판정 | ✅ PR #944 |
| Phase 1 | 저장 직전 최종 검사 수리(공용 규칙 재사용, 신규·수정 동시) | ✅ PR #944 · Production 반영 |
| Phase 2 | e2e 5경로 보증 + 보드 완료 인원 뷰 시간·거리 편입(20260830110000) + CI 편입 | ✅ PR #944 · 로컬 6/6·CI GREEN |
| Phase 3 | BUG-041 갱신 + 작업 기록 + 등록 2곳 | ✅ 이 문서 |
1. 배경
2026-08-30 실 사용자(acetuna·성근)의 완료 운동 저장이 7회 전부 실패했다(사고 추적 #942, BUG-041). 어썰트바이크 워밍업 세트를 시간 5:00만 기록(거리 비움)한 것이 트리거 — 화면의 세트 완료 판정은 이 입력을 허용하는데 저장이 거부되는 불일치. 오너가 규칙을 명시했다: 유산소는 시간만/거리만/조합 전부 허용, 무게운동은 무게+횟수 둘 다 필수 — 그리고 "유산소 입력 시 최소 1개 필드만 입력하면 나머지 미입력이어도 문제없이 기록됨"을 계획/기록/운동진행/화이트보드작성/화이트보드운동 전부에서 보증하는 e2e를 지시했다.
2. 문제 제기
- 오너가 원한 규칙은 이미 서버(20260821590000, 08-24 오너 지시)와 클라 4개 층(세트 완료 판정·조립·표시·읽기)에 구현돼 있었다 — 전부 공용 함수
isCardioOnlyProfile재사용. - 딱 한 층, 서버로 보내기 직전 앱이 자체로 한 번 더 하는 최종 검사(
validateCompletedWorkoutPayload)만 옛 규칙("프로필에 있는 원자는 값 필수")으로 남아 있었다 — PR #646(08-24)이 "4층 동시 완화"에서 이 5층째를 누락. 조립이 빈 원자를 생략한 payload를 이 검사가 거부해 서버 미도달(LG_WORKOUT_CONTRACT, http_status 없음). - #646 동반 테스트는 조립(매퍼 행)에서 멈춰 이 층을 태우지 않았고, e2e 저장 픽스처는 무게+횟수 최소 경로뿐 — 전날 #921과 동일한 "양끝만 잠그고 가운데 미태움" 패턴.
- 5경로 실측 중 추가 확인: #935(보드 유산소 저장 수리)가 보드 처방 스냅샷만 확장하고, 보드 완료 인원 뷰(
get_group_board_day_sessions_v1)의 세트 투영은{type, load, reps}로 남겨둠(그쪽 기록에 "범위 밖" 명시) — 보드에서 시작해 완료한 유산소 세트가 종목 기록 뷰에서 빈 토큰이 되는 잔여 구멍.
3. 해결 방안
- 원칙(오너 규칙 08-24·08-30): 유산소 = 최소 1원자, 무게운동 = 무게+횟수 전부. 완료 요건 판정은 공용 함수 하나로 수렴 — 이후 원자 추가(#932 cal) 시 그 함수만 넓히면 전 층 적용.
- 접근: ① 최종 검사가 자체 규칙을 발명하지 않고
isCardioOnlyProfile을 재사용(서버와 분기 구조 동일) ② 재발 게이트는 실경로 관통 + 실제 DB e2e — 단위·pgTAP이 구조적으로 못 잡던 "가운데 층"을 5경로 전부에서 끝까지 태운다 ③ 보드 완료 인원 뷰는 값이 있을 때만 키를 동반(구 payload 셰이프 불변, 구 클라 무영향).
4. 적용한 내용
Phase 0 — 재현: 시간 단독 유산소 세트를 createCompletedSession 실경로에 태우는 관통 테스트 선작성 → 사고와 동일 오류·동일 지점 RED. 성근 draft 백업 실물(종목 5개) 재현 → WOD 복합운동은 자유 기록(note)으로 적혀 있어 #932 결함과 무관 — 이번 수리만으로 draft 전체 저장 가능 판정.
Phase 1 — 수리: 최종 검사의 세트 검사에 cardioFlexible = isCardioOnlyProfile(...) 분기 — 유산소 프로필은 두 원자 다 비면 거부, 하나라도 있으면 값 있는 원자만 범위 검사. 신규·수정 저장이 같은 함수를 공유(호출 2곳)해 동시 해소. 관통 테스트 GREEN.
Phase 2 — e2e 5경로 + 완료 인원 뷰:
e2e/cardioMinOneField.e2e.mjs(service 모드 전용, crudRoundtrip 하네스 공유): ① 계획 시간 단독·거리 단독 저장→재로드 ② 완료 기록 시간 단독 저장(사고 재현 모양)→상세 왕복→거리 단독 수정 ③ 진행중 초안 서버 체크포인트 왕복 ④ 보드 저장·재로드 ⑤ 보드 계보(group-board:...) 완료가 완료 인원 뷰에 시간과 함께 서는 것까지.test:e2e-cardio로 CI migration-smoke 편입.- 마이그레이션 20260830110000:
get_group_board_day_sessions_v1세트 투영에 duration_seconds/distance_meters 조건 동반 + postcheck. 클라toDaySessionRow매핑 — 표시는 #935의 GbSeq가 그대로 소화(무변경). - 배포 매니페스트 EXPECTED_LATEST_MIGRATION 동기화(첫 CI verify가 불일치로 적발 — 게이트 정상 동작).
작업 중 드러난 것: 같은 날 다른 세션들이 같은 사고 계열을 병렬 수리 중(#935 보드 저장, #942 사고 추적·BUG-041/042 발번) — 겹침 확인 결과 원인 ①은 이 트랙에 위임돼 있었고, #935가 "범위 밖"으로 남긴 완료 인원 뷰를 이 트랙이 닫는 상보 구도로 정리. 로컬 검증용 Docker는 메모리에 기록된 AF_UNIX 소켓 크래시루프 우회(두 폴더 동시 rename)로 기동 — 절차를 확인하지 않고 먼저 띄웠다가 크래시를 재현한 뒤에야 적용(오너 지적).
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 시간만/거리만 기록한 유산소 세트의 완료 저장 | 100% 실패(서버 미도달 거부) — 성근 7회 실측 | 저장 성공 — 관통 테스트·e2e GREEN, Production 반영 |
| 인입된 시간-단독 세션의 수정 저장 | 같은 검사로 차단 | 같은 함수 수리로 동시 해소(e2e 수정 왕복 GREEN) |
| 성근 보존 운동(서버 체크포인트, 만료 09-02) | 재저장 불가 | draft 실물이 저장 경로 전체 통과 실측 — 재저장 가능 |
| 완료 요건 규칙의 위치 | 5층 중 4층만 공용 함수, 1층 자체 규칙 | 5층 전부 isCardioOnlyProfile 하나로 수렴 |
| 보드 완료 인원 종목 기록 뷰의 유산소 세트 | 빈 토큰(투영이 load/reps뿐) | 5:00 등 시간·거리 토큰(20260830110000+매핑) |
| 유산소 최소 1원자 게이트 | 없음(무게+횟수 픽스처뿐) | 관통 테스트 + 5경로 e2e CI 상주(로컬 6/6·CI migration-smoke GREEN) |
6. 이번 개선으로 향상된 것
- 유산소·요가류 종목은 계획/완료 기록(신규·수정)/운동 진행/화이트보드 작성/화이트보드 운동 어디서든 측정단위 1개만으로 기록된다 — 그리고 그 보증이 실제 DB를 타는 e2e로 상시 잠긴다.
- 완료 요건 규칙이 공용 함수 한 곳으로 수렴 — #932(칼로리 원자 신설)에서 cal을 추가할 때 이 함수 하나만 넓히면 전 층에 적용된다.
- "앱이 스스로 막는 저장"(LG_WORKOUT_CONTRACT + http_status 없음)이 재발하면 관통 테스트·e2e가 머지 전에 적발한다.
남은 것
성근 실기기 재저장 + 오너 실기기 확인→ 완료(08-31): 성근 재저장 Production 실측(세션a4ff670b) + 오너 확인, BUG-041 상태 해결로 갱신, 이슈 #936 [반영완료] 닫음.- 사고 나머지 원인도 별도 트랙에서 해소됨: 계획 24세트 상한 → #938 [반영완료] · 복합×유산소 계획 저장 → #947 [반영완료] (잔여 = cal 원자 #932·구형 scheme 소급 #953).