Skip to content

유산소·요가 종목 화이트보드 저장 수리 — reps 필터에서 기록 프로필 기준·시간·거리 계약 편입까지 (2026-08-30)

  • 기간: 2026-08-30 (1세션, 출시 전 전면 감사 발견 → 이슈 #926, 오너 지시 "해결좀 해줘")
  • 랜딩: PR #935(Phase 1~3, 9f767040) — 마이그레이션 20260830100000 Production db push 완료(포스트체크 통과·remote-schema EXIT 0), Vercel 배포 청크 고유 마커(20260830100000) 실측 — 종결(08-31 오너 지시로 실기기 확인 게이트 폐지 — 자동 검증으로 닫음, 이슈 [반영완료])
  • 설계서: 없음(단일 트랙 — 분석·Phase 계획은 이슈 #926 코멘트)
  • 정본: group_validated_board_exercises_v1(schema.sql 20260830100000 절) · groupStore.ts saveBoard/toBoard · group-props.md §4·§5
  • 도구: supabase migration list 원격 실측(번호 미선점 확인) · Production 배포 청크 재귀 크롤(고유 마커 = EXPECTED_LATEST_MIGRATION)
  • 게이트: groupBoardCardioRoundtrip.test.mjs 6건(#831 note·#837 %·#888 composite와 4부작) · pgTAP group_board_cardio_sets.test.sql 8건 · 마이그레이션 내 postcheck
  • 버그리포트: bug-040-20260830.md
  • 계약: 보드 세트에 duration_seconds?/distance_meters? 추가(상한 = 세션 저장 검증기와 동일), 컴포저 세트 필터 = planMode 세트 추가 게이트와 동일식, 프리필 드래프트에 기록 프로필(fields·measurementType·bwFactor) 동반

Phase 현황

Phase내용상태
Phase 1서버 계약: 보드 세트 duration_seconds/distance_meters 수용·에코 (20260830100000) + pgTAP✅ PR #935 · Production 적용
Phase 2클라 4층: 컴포저 필터·직렬화·재진입 재수화 / 스토어 왕복 / GbSeq 표시 / 프리필✅ PR #935 · 배포 청크 실측
Phase 3회귀 게이트: 왕복 테스트 6건(관통 1건 포함)✅ PR #935 동반
Phase 4BUG-040 + 작업 기록 + 등록 2곳✅ 이 문서

1. 배경

출시 전 전면 감사(2026-08-30)의 groups-social 레인이 발견했다: 시간/거리만 기록하는 종목(요가 ['duration']·베어크롤 ['distance'] — 20260821260000이 Production 카탈로그에 확정)이 든 화이트보드는 저장이 100% 실패한다. 재현 스크립트로 실제 payload가 {"name":"러닝","sets":[]}로 나가는 것까지 실측됐다. #831(v78 컴포저 배선, 08-26)에서 "시간·거리 전용 세트는 보드 세트 계약 밖 — 제외"로 명문화된 채 남은 미결이 출시 코드까지 그대로 잔존한 건이다.

2. 문제 제기

  • GroupComposer.save()Number(s.reps) > 0 세트만 직렬화 — reps 입력칸이 UI에 존재하지 않는 프로필이라 전 세트 탈락. 그런데 같은 편집기의 세트 추가 게이트(planMode)는 measureProfile.some(mHasValue)라 시간만 입력해도 "세트 설정 완료"가 정상 활성 — UI가 추가를 허용한 세트를 저장이 버리는 불일치가 병소.
  • 서버 검증기(20260829120000)의 세트 화이트리스트는 {type, load, reps, load_pct?, scheme?} — duration/distance는 보내도 조용히 탈락(Drops unknown keys)하고, sets < 1이면 22023으로 보드 전체 트랜잭션 중단. 22023은 클라에서 "입력값을 확인해 주세요."로만 표기돼 원인 미지목.
  • 코드 실측에서 잠복 구멍 2건 추가 확인: ① 보드 스냅샷이 fields/measurementType을 나르지 않아 수정 재진입 시 유산소 종목이 load/reps 입력으로 격하 ② 무게만 입력한 세트도 같은 필터에 조용히 소실(그 세트뿐이면 동일 22023). 인접으로 보드→세션 프리필도 기록 프로필을 싣지 않아 프리필 세션이 세션 저장의 카탈로그 프로필 대조 22023에 걸릴 구조.

3. 해결 방안

  • 원칙(감사 교훈 + #888 계보): 편집기가 쓸 수 있는 값은 스냅샷 계약이 나른다 — kind(#831)→pct_base(#837)→composite(#888)에 이은 왕복 소실 4번째를 같은 4부작 잠금으로 종결. 새 필드는 실경로 관통 테스트 1건으로 잠근다(출시 감사 #921 교훈).
  • 접근: 서버 먼저(새 키 전부 선택 — 구 payload 후방호환) → 클라 4층 → 게이트. 세트 필터는 자체 발명 없이 추가 게이트와 동일식으로 맞춰 불변식("추가를 허용한 세트는 버리지 않는다")을 코드 구조로 고정.

4. 적용한 내용

Phase 1 — 서버 계약(20260830100000): group_validated_board_exercises_v1에 세트 duration_seconds(>0..31,536,000)·distance_meters(>0..10,000,000) 수용·에코 — 상한은 세션 저장 검증기(validate_completed_workout_payload_v4)와 동일, >0은 클라 mHasValue와 같은 의미론(0 = 미입력). 함수·group_boards 코멘트 갱신 + postcheck(에코·미동반 키 부재·0 거부). pgTAP 8건(왕복 4·형태 1·거부 3).

Phase 2 — 클라 4층:

  • 컴포저: 필터를 exMeasures(e).some(mHasValue)로 교체 + durationSeconds/distanceMeters 직렬화 + 수정 재진입 프로필 재수화(catalog byId → 정규화 이름 — 스냅샷은 fields를 나르지 않는다).
  • 스토어: saveBoard snake 직렬화·toBoard camel 왕복.
  • 표시(GbSeq): 시간·거리 세트 5000m in 30:00·45:00 토큰(채워진 원자만, formatSetMeasurements와 같은 규칙) — 유산소 세트의 에코 load 0·reps 0은 미표기.
  • 프리필: 시간·거리 값 + 기록 프로필(fields·measurementType·bwFactor)을 드래프트로 운반 — 프로필 부재 시 makeFlowExercise가 load/reps로 격하되던 인접 구멍 동봉 수리(reps-only 맨몸 종목의 보드 프리필 세션도 같은 병).
  • 계약문 group-props.md §4·§5 갱신("reps>0 세트만" 명문 폐지).

작업 중 드러난 것: 랜딩 도중 origin/main이 4커밋 전진(#929·#930·#931·#934 — 같은 감사의 다른 레인 수리들)해 리베이스 후 재검증 — #927이 손댄 WorkoutRecord의 planMode 게이트가 동일식(456행)임을 재확인. BUG 번호도 038·039가 연속 선점돼 040으로 발번(같은 날 번호 경합 3번째 실전).

5. 적용 결과

항목
유산소·요가 종목 든 보드 저장성공률 0% (22023)저장·왕복 정상 (pgTAP 8건·왕복 테스트 6건)
재현 payloadsets:[]sets:[{…, duration_seconds: 1800, distance_meters: 5000}]
보드 세트 표시유산소 세트 무표기 불가(저장 자체 불가)5000m in 30:00x2 · 45:00x1
보드→세션 프리필duration/distance 항상 "" · 프로필 미동반보드 값 프리필 + 카탈로그 프로필 동반
수정 재진입유산소 종목에 무게·횟수 입력칸(격하)시간·거리 입력칸(재수화 실렌더 검증)
게이트duration/distance/cardio 문자열 0건react 6건 + pgTAP 8건 + postcheck 상주

사람 눈으로만 판단 가능한 잔여(실앱 표시 품질)는 사용 중 발견 시 재오픈으로 처리(08-31 게이트 폐지). 완료 멤버 기록 뷰(get_group_board_day_sessions_v1)의 세트 투영은 여전히 load/reps만 — 이번 범위 밖(별개 RPC), 멤버의 유산소 완료 기록 행은 값 없이 표기된다.

6. 이번 개선으로 향상된 것

  • 시간·거리 종목이 든 보드를 올릴 수 있다 — App Store 심사 시나리오(그룹 UGC)에서 유산소 보드가 죽는 경로 제거.
  • "컴포저가 추가를 허용한 세트는 직렬화가 버리지 않는다" 불변식이 코드(필터=게이트 동일식)와 테스트로 상주 — 무게만 입력한 세트의 조용한 소실도 함께 폐지.
  • 보드 스냅샷 왕복 4부작(note·%·composite·cardio) 완결 — 편집기 재사용 표면의 값 종류 전수가 계약에 편입.

남은 것

  • 완료 멤버 기록 뷰의 유산소 세트 투영(load/reps만) — 후속 판단 대상, 이슈 #926 코멘트에 명시.