기록 입력 구조 재설계 — 복합종목의 "무게 없는 세트 실행 에러"에서 종목별 필수 입력 논리식까지 (2026-08-31)
- 기간: 2026-08-30 ~ 2026-08-31 (1 세션, 오너 보고 "복합종목에서 무게 미입력 시 세트 설정은 통과되는데 실행에서 에러가 나는 문제" → 같은 날 "좀 근본적인 구조 문제로 접근해야겠다 — ① 복합종목은 측정단위가 다른 어떤 종목이어도 섞을 수 있게 ② 각 종목마다 반드시 입력해야 하는 논리식")
- 랜딩: PR #956(묶음 ① Phase 1-1~1-3·2,
9d71ecc9) · #958(묶음 ② Phase 3~4,b3319986) · #959(묶음 ③ Phase 1-4·2-3,2995bd4c) — 마이그레이션20260830170000·20260831100000·20260831110000Production 적용, Vercel 배포 success. 선행 수리 #929(이슈 #927, BUG-038) - 설계서: 없음 — Phase 계획은 이슈 #947 쓰레드에 게시(예상 효과 절 포함), 묶음별 완료 보고도 같은 쓰레드
- 정본:
exercises.required_inputs(jsonb 논리식 트리, CHECKrequired_inputs_valid_v1+ BEFORE 트리거 기본값) · 평가 함수required_inputs_satisfied_v1· 클라 미러src/react/types/recordingRequirement.ts· 복합 의미론src/react/ui/shared/compositeSemantics.ts(parts 값 모델·표현 가능성 규칙) · 카탈로그 RPC v7 - 도구: 없음(레포 안 코드·마이그레이션만)
- 게이트: pgTAP
exercise_required_inputs_v1(26)·record_input_logic_consumption_v1(12)·보드 parts postcheck, e2e CASE-021(순수 컴플렉스 무회귀)·CASE-022(이종 복합 실브라우저 왕복), 클라 스위트 2,161(관통 테스트 다수), ts-boundary 게이트 allowlist(트리 데이터 키any오탐 격리) - 버그리포트:
bug-report/bug-038-20260830.md(선행 #927 게이트 불일치) - 계약:
docs/data/app-screen-rpc-contract.md(en·ko) 카탈로그 v7 단락,docs/data/rpc-catalog.mdv7
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1-1 (서버 정본) | required_inputs 컬럼·문법/평가/기본값 함수·백필·트리거·CHECK (20260830170000) | ✅ PR #956 (9d71ecc9) |
| Phase 1-2 (클라 정본) | 평가기 미러 + mProfileSatisfied 논리식 소비(부재 시 종전 휴리스틱과 동치) | ✅ PR #956 |
| Phase 1-3 (데스크톱 단일화) | dkpMissingRecordingFields 미러 폐지 → 공유 평가기 | ✅ PR #956 |
| Phase 1-4 (관리자·반출) | 카탈로그 RPC v7 required_inputs 반출·클라 반입·게이트 카탈로그 해석 + 관리자 표시·편집 | ✅ PR #959 (2995bd4c) |
| Phase 2-1·2-2 (게이트 통일) | 모바일·데스크톱 계획 "하나만 채우면 저장" 폐기 → 평가기 단일 기준 | ✅ PR #956 |
| Phase 2-3 (서버 소비) | 완료 저장·보드 검증기가 required_inputs_satisfied_v1 소비(기본 트리 판정 불변) | ✅ PR #959 |
| Phase 2-4 (게이트 회귀) | 계획 관대 케이스 폐기 테스트 교체 | ✅ PR #956 |
| Phase 3 (이종 복합 모델·편집기) | 세트 parts[](동작별 원자 묶음)·동작 프로필 보존·동작별 입력기·D2 무게 채움·표현 가능성 규칙 | ✅ PR #958 (b3319986) |
| Phase 4 (저장 관통·보드·e2e) | 완료 저장 동작별 행 전개·보드 검증기 parts(20260831100000)·CASE-021/022 | ✅ PR #958 |
| 구형 데이터 소급 | 저장된 구형 복합 세트의 parts 변환 | ⬜ 별도 이슈 #953 (오너 지시) |
1. 배경
출시 직전 실사용에서 오너가 보고한 결함이 발단이다: 복합종목(예: 풀업+딥스)의 세트를 무게 없이 설정하면 통과되는데, 실행(완료) 단계에서 에러가 났다. 원인 수리(#927 → PR #929, 게이트 단일화)와 e2e(CASE-021)로 그 결함은 닫았지만, 파고들수록 구조 문제가 드러났다: ① 복합종목은 동작별 반복수(scheme) 하나만 표현할 수 있어 시간·거리 동작(핸드스탠드 홀드 등)을 섞을 수 없고, 섞으면 횟수로 둔갑하거나 저장이 실패했다(#942 ③). ② "세트에 무엇을 반드시 입력해야 하는가"의 규칙이 화면 4곳·서버 2곳에 제각각 하드코딩되어(계획은 관대, 라이브는 엄격, 유산소는 별도 분기) 판정이 갈라졌다. 오너가 구조 전환을 지시해 이 트랙이 됐다.
2. 문제 제기
복합종목은 "동작별 횟수 배열"만 표현할 수 있었다
세트의 동작별 값이 scheme(정수 배열) 하나라, 무게×횟수가 아닌 동작이 끼면 표현 자체가 불가능했다. 빌더가 구성 종목의 기록 프로필(fields)을 버려서, 맨몸 풀업에도 무게칸이 생기는 마찰(#927 계보)과 유산소 동작이 낀 복합의 계획 저장 실패(#942 ③, 서버 카탈로그 대조 22023)가 이 병소에서 나왔다.
필수 입력 규칙이 화면·서버에 6벌로 흩어져 있었다
"무게×횟수는 둘 다, 유산소는 셋 중 하나"(오너 결정)가 어떤 표면에는 있고 어떤 표면에는 없었다 — 계획 작성은 "하나만 채우면 저장"으로 관대해, 무게 없는 세트가 계획을 통과한 뒤 실행에서 죽었다. 커스텀 규칙(예: "이 종목은 무게 또는 횟수 하나만")을 만들 방법도 없었다.
3. 해결 방안
원칙 (오너 결정, 2026-08-30·31)
- D1 논리식은 중첩 가능한 트리 —
{"all":[...]}/{"any":[...]}, 원자 load·reps·distance·duration(칼로리는 별도 트랙). - D2 복합의 무게는 동작별 분리, 측정 단위가 같으면 앞 동작 값을 디폴트로 채움("일단 해보자").
- D3 복합 축은 A→B 순서 유지.
- D4 구형 데이터 소급은 별도 이슈로 분리(#953) — 읽기 정규화(scheme→reps 해석)가 소급 전까지 상주.
접근
- 기본값 동치 원칙: 명시 논리식이 없는 종목(전 행 백필)은 종전 휴리스틱(유산소-단독=any, 그 외=all)과 판정이 정확히 같은 기본 트리를 쓴다 — 전 사용자 무영향으로 구조만 교체. 대안(일괄 신규 규칙 강제)은 기존 데이터·습관 파손 위험으로 기각.
- 표현 가능성 규칙: 전 동작이 횟수-단독이고 무게가 세트 공유로 표현되면 종전 형(scheme)으로 직렬화 — 순수 바벨 컴플렉스의 %·lb 공유 무게 편집기와 구 payload 형이 그대로 산다. 그 표현력을 넘는 세트만
parts[]. 대안(전량 parts 컷오버)은 보드·구버전·통계 폴백의 동시 파손 위험으로 기각. - CI는 묶음당 1회(오너 지시 08-31): phase 소단위 PR·CI 폐지, 브랜치 누적 후 묶음 PR 1번 — 이 트랙에서 처음 적용(묶음 ① 1회 + ② 4회(적발 3층 수리 포함) + ③ 2회).
4. 적용한 내용
묶음 ① — 논리식 정본화 + 게이트 통일 (#956, 20260830170000)
exercises.required_inputs 신설(백필=기본 트리, NOT NULL·CHECK·BEFORE 트리거가 null→기본값, 프로필만 바뀌어 트리가 무효화되면 재유도), 서버 함수 3종(required_inputs_valid/satisfied/default_required_inputs_v1), 클라 1:1 미러(recordingRequirement.ts), mProfileSatisfied가 논리식을 소비, 데스크톱 미러 구현 폐지, 계획 게이트 관대 분기 폐기. pgTAP plan(26).
묶음 ② — 이종 복합 확장 (#958, 20260831100000)
세트 parts[](동작별 {load?, reps?, durationSeconds?, distanceMeters?}) 값 모델·읽기 정규화(구형 scheme 흡수)·직렬화 표현 가능성 규칙, 빌더·플로우의 동작 프로필 보존(카탈로그 재수화 폴백), 모바일·데스크톱 동작별 입력기(공유 무게 컴플렉스는 기존 편집기 무회귀), 완료 저장의 동작별 행 전개(각 행 자기 recording_fields·원자만 — 시간 동작 행에 횟수 없음, #942 ③ 구조 종식), 보드 검증기 parts 수용·에코, e2e CASE-021·022.
묶음 ③ — 반출·편집·서버 소비 (#959, 20260831110000)
카탈로그 RPC v6→v7(required_inputs 동반), 클라 반입(어댑터 화이트리스트·캐시 키 v5·normalizeExercise가 camel 단일 키 확정), 게이트 해석 순서 통일(종목 자체→카탈로그 행→기본 트리), 관리자 상세 "필수 입력" 표시(무게 + 횟수/(거리 또는 시간))와 JSON 편집 폼(문법·원자⊆기록필드 검증, 빈 값=기본값 재유도, 쓰기 5층 관통), 완료 저장·보드 검증기의 논리식 소비(제공 원자=값이 실려 온 원자, 값 유효성은 기존 블록 — 기본 트리 판정 불변; reps·load 블록 presence 조건화로 커스텀 트리의 선택 원자 허용; 보드는 카탈로그 실존 id만 판정·애드혹 관대·immutable→stable).
주요 결정과 그 근거
- 스냅샷 프로필 행(이종 복합의 동작별 행 포함)은 그 프로필의 기본 트리로 판정 — 트리의 원자는 카탈로그 프로필에 CHECK로 봉인돼 있어 다른 프로필에 적용할 수 없다.
- 제공 원자는 presence 기준(rep_failure의 reps 0 포함) — 값 유효성은 기존 원자별 블록이 담당하므로 이중 판정 없이 기본 트리 동치가 성립한다.
- 관리자 편집은 JSON 원문 + 클라 검증 + 서버 CHECK 봉인 — 전용 트리 빌더 UI는 사용 빈도 대비 과설계로 보류.
작업 중 드러난 것
- PostgreSQL
text[] || '문자열'함정: 배열끼리 연결로 해석해 문자열을 배열 리터럴로 캐스팅(22P02) — 검증기가 즉사했다. CI 1차 적발,array_append로 교체. 배열에 원소를 붙일 땐 항상array_append. - CASE-022가 신설 과정에서 실결함 3층 적발: 드래프트 정규화의 scheme 강제 크래시 → 쓰기 프로젝션·계약 화이트리스트의 parts 유실 → 플로우 payload 직렬화의 parts 유실. "새 필드는 실경로 관통 테스트 1건"(#921 교훈) 재실증 — parts는 5층을 지나며 어느 층이 빠져도 조용히 유실된다.
- 스키마 계약 파서 잠복 결함(tests/support/schemaSql.mjs): 역할 목록 grant(
to a, b;)를 단일 역할 패턴이 못 닫아 이후 아무 마이그레이션 추가 시 69KB 오포획 — 근본 수리. - 마이그레이션 번호 선점 3회(같은 날 병렬 트랙 4개) — 푸시 직전 원격
schema_migrations실측이 유일한 안전선. - 테스트의 카탈로그 버전 분기 정규식(
[23456])이 v7을 몰라 legacy 분기로 오폭 — 버전 상승 시 분기 매트릭스도 함께.
5. 적용 결과
| 항목 | 전 → 후 |
|---|---|
| 복합종목 구성 | 무게×횟수·횟수 동작만 → 측정단위 무관 임의 조합(풀업+핸드스탠드 홀드 = "10회 + 30초") |
| 이종 복합 저장 | 횟수로 둔갑·계획 저장 22023 실패(#942 ③) → 동작별 행에 자기 원자만 기입, 왕복 유지(CASE-022 실브라우저 검증) |
| 필수 입력 규칙 | 화면 4곳·서버 2곳 각자 하드코딩 → 논리식 트리 1벌(서버 함수=클라 미러, 기본 트리에서 판정 종전 동치) |
| 계획 작성 게이트 | "하나만 채우면 저장"(무게 없는 세트가 실행에서 사망) → 라이브·계획·보드 동일 논리식 |
| 종목별 커스텀 규칙 | 불가능 → 관리자가 편집(예: {"any":["load","reps"]}) — 카탈로그 v7로 전 클라 게이트·서버 검증에 즉시 반영 |
| 순수 바벨 컴플렉스 | — |
| 검증 | 클라 스위트 2,152→2,161, pgTAP +38케이스, e2e 19→21케이스, Production check:remote-schema missing 0 |
| 확인 | 오너 실기기 확인 완료(2026-08-31) — 이슈 [반영완료] 닫힘. 구형 저장 데이터 소급 미실시(#953) — 소급 전까지 구형 세트는 읽기 정규화로 표시만 정상 |
6. 이번 개선으로 향상된 것
사용자: 어떤 조합의 복합종목도 만들고 저장하고 다시 열 수 있다
유저 A가 풀업+핸드스탠드 홀드를 한 세트로 묶으면 편집기가 동작별로 횟수·초를 따로 묻고, 저장하면 일지에도 "10회 + 30초"로 남는다. 종전엔 이 조합 자체가 만들어지지 않거나 홀드가 횟수로 둔갑했다.
사용자: "설정은 되는데 실행에서 죽는" 부류의 결함이 구조적으로 차단
계획·라이브·보드·서버가 같은 논리식 하나로 판정하므로, 한 표면만 관대해서 생기는 지연 실패가 원리적으로 사라졌다.
구조적으로 남는 것
required_inputs논리식 계약(서버 함수 3종 + 클라 미러 + CHECK·트리거) — 새 표면은 이 평가기만 소비하면 된다.- parts 값 모델과 표현 가능성 규칙(compositeSemantics) — 구형 형 불변을 코드로 보증.
- CASE-021·022 e2e + pgTAP 2파일 — 회귀 시 CI가 잡는다.
- CI 묶음-1회 운영 방식의 첫 실증(적발 4건 전부 묶음 PR 안에서 수리).
남은 것
- 구형 데이터 소급: 이슈 #953 — 저장된 구형 복합 세트를 parts로 변환(그 전까지 읽기 정규화 상주).
- 칼로리 원자: 오너가 별도 트랙에서 제작 중이라 이번 논리식 원자에서 제외.