Skip to content

오피셜 운동종목 정비 — WodUp 대조 확장 · 분류 축 병합 · 기록 필드 정합 (2026-08-21)

  • 기간: 2026-08-21 (하루, 세션 3개 — ① WodUp 대조 수리·배치 1·분류 병합 Phase 0~3-1·Phase 4-0 준비 / ② 배치 2~3(헬스장 머신 커버리지) / ③ Phase 4-0 랜딩·4-1·4-2·전수 조사·결정 반영. 세션 ①이 한도로 끊겨 ③이 이어받았다)
  • 랜딩: PR #515 · #518 · #522 · #523 · #524 · #525 · #534 · #536 · #538 · #545 · #549 · #557, 마이그레이션 11건(20260821030000 · 040000 · 050000 · 050100 · 070000 · 100000 · 110000 · 150000 · 160000 · 240000 · 260000) 전부 Production 적용 + check:remote-schema EXIT=0 + 재프로브
  • 정식 카탈로그: 675종 → 713종, 분류 축 1개(exercise_category 8값), 기록 필드·요약형·체중 계수 전수 정합
  • 보고서: 전수 조사 결과 Claude artifact 610d2ae6

1. 배경

WodUp export 두 벌(강성근 + 오너 2022~2026, 종목 428키·12,600세트)을 Production 정식 카탈로그에 대조하는 일에서 출발했다. 목적은 단순했다 — 인입기가 연결 못 하는 종목이 무엇이고, 카탈로그에 무엇을 더 넣어야 하는가. 대조 결과 즉시 연결은 75%(9,505세트)였고 오연결 3건과 미연결 281키가 나왔다.

확장 배치를 랜딩하는 동안 오너의 질문 두 개가 트랙의 두 번째 축을 열었다. "body weight reps는 무슨 단위지?"와 "category랑 equipment의 차이가 뭐야?" — 둘 다 실측해 보니 카탈로그가 같은 뜻을 두 이름으로 들고 있었다. 확장·수리로 시작한 트랙이 분류 축 병합 캠페인(Phase 0~4)과 기록 필드 전수 조사까지 이어진 이유다.

2. 문제 제기

인입이 닿지 못하는 종목과, 잘못 닿은 종목

오연결 3건은 06-15 CSV 시대의 잔재였다 — WodUp Pull-Up 559세트가 오스트레일리안 풀업에, Snatch 274세트가 블록 스내치에, Row(카디오)가 체스트서포티드 로우 머신에 붙어 있었다. 미연결 281키 중 실사용 빈도가 높은 것들(머슬업 변형·하이 풀업·수영·걷기·계단·디피싯 스내치 계열 등)은 카탈로그에 없었고, 헬스장 머신 커버리지를 검토하자 공백보다 장비 오분류가 더 컸다 (레그컬 3종이 band/bodyweight로 적혀 머신 필터에서 안 보임 등).

분류 축이 둘이었다 — 하나는 살아 있고 하나는 휴면

equipment(자유 텍스트, 7값, 커스텀 폼·관리자·뷰매퍼가 실제로 소비)와 exercise_category(6값 enum, "제품 수준 버킷" 주석, 소비자 0)가 공존했다. 크로스탭을 열어보면 category는 equipment를 뭉갠 상위 버킷(cable→machine, band→bodyweight, kettlebell→other)이었고, 갈리는 7건은 오히려 equipment 쪽이 오기였다(해머 컬·리어델트 레이즈가 barbell, 워킹 런지가 barbell 등). 유저가 고를 수 있는 장비는 4종뿐인데 데이터 값은 그보다 많았고, 케이블·도구류(월볼·샌드백·슬레드)·밴드의 자리가 정해져 있지 않았다.

기록 필드의 뜻이 데이터와 어긋나 있었다

  • measurement_type='bodyweight_reps' 34행은 별도 단위가 아니었다 — 기록 필드는 {reps} 하나, 단위 표시도 "회", 프론트 전 경로가 reps와 동일 취급, 체중 반영은 bodyweight_factor가 전담. 남은 건 형제 종목끼리 갈리는 분류 얼룩(행잉 니레이즈 bw_reps ↔ 행잉 레그레이즈 reps)뿐이었다.
  • 맨몸 종목인데 체중 계수가 0인 행이 있었고(요가 흐름 등), 반대로 맨몸이 아닌데 계수가 붙은 행도 있었다(카디오·폼롤러·밴드 보조 동작 — 일부는 체중 볼륨을 조용히 부풀렸다).
  • 컷오버 뒤에도 RPC 출력 와이어 키는 'equipment'로 남아 DB·와이어·프론트가 한 이름이 아니었다.
  • 713행 전체에서 "이름은 홀드인데 reps" 같은 어색한 조합이 얼마나 있는지 아무도 세어 본 적이 없었다.

3. 해결 방안

원칙

  1. 오너 결정 위에서만 데이터를 바꾼다. 분류 8값·라벨·계수 규칙·요가/줄넘기/맨몸판 판정은 전부 오너가 확정한 뒤 반영했고, 보고는 형제 종목 대조표로 올려 결정 비용을 낮췄다.
  2. 분류 축은 하나. 살아 있는 equipment의 값 체계를 8값으로 수렴시킨 뒤 컬럼을 exercise_category로 개명하고, 휴면 컬럼은 드롭한다. 전환기 호환은 generated 미러로 잠깐만.
  3. 맨몸 의미론은 exercise_category='bodyweight' + bodyweight_factor가 전담. 별도 측정 단위(bodyweight_reps)를 두지 않는다. 불변식은 양방향 — 맨몸·reps ⇒ factor>0, 맨몸 아님 ⇒ factor 0.
  4. 마이그레이션 postcheck는 손댄 행·술어만 단언한다(전역 불변식은 pgTAP로). 새로 초기화한 DB에서만 참인 단언이 Production push를 죽인 전례를 따른다.
  5. 번호는 PR 오픈 시가 아니라 push 직전 장부 꼬리로 확정한다. 재조립을 스크립트로 만들어 재번호 비용을 분 단위로 유지한다.
  6. 검증은 verify 레인 패리티로npm run check + check:unused + build를 로컬에서 전부, 랜딩 후 Production 적용 → check:remote-schema → 일회용 raise-rollback 프로브로 실측.

접근

배치형 확장(#515→#518→#522→#523)으로 인입 공백부터 메우고, 분류 병합은 UI 선행 → 데이터 수렴 → 컷오버 → 정리 순의 Phase로 쪼갰다. 컷오버는 함수 11개를 재정의하되 출력 와이어 키는 건드리지 않아(계약·페이로드 예산 불변) 위험을 나눴고, 와이어 키 개명은 미러 드롭 뒤 마지막 조각으로 넘겼다. 캠페인이 끝난 뒤 713행 전수 조사를 돌려 "어색한 조합"을 한 번에 결정받고 데이터 마이그레이션 한 건으로 닫았다.

4. 적용한 내용

4-1. WodUp 대조 수리 + 카탈로그 확장 배치 1~3 (#515 · #518 · #522 · #523)

PR마이그레이션내용
#515030000오연결 3건 수리: Pull-Up(559세트)→풀업, Snatch(274세트)→스내치, Row→로잉머신
#518040000배치 1: 신설 20종(밴드 어시스트 바/링 머슬업·하이 풀업·수영·걷기·계단 오르기·디피싯 스내치 계열·파머스 홀드·중량 싯업/푸쉬업 등) + 별칭 보강 28건 + 교차 별칭 정리(수영 4영법의 Swim, 트레드밀의 Run) + WodUp 매핑 upsert 69건
#522050000배치 2(헬스장 머신 커버리지 검토): 신설 11종(아이소레터럴 3종·스탠딩 카프 레이즈 머신·스미스/덤벨 RDL·케이블 슈러그·랜드마인 프레스·일립티컬·스텝밀 등) + 별칭 5건 + 장비 오분류 수리 21행(레그컬→machine, 밴드 7종 machine→band, 불가리안·휴먼 플래그→bodyweight 등)
#523050100배치 3: 배치 2 제외분 5종 편입(스미스 굿모닝·벤트오버 로우, 케이블 RDL, 덤벨 굿모닝, 비하인드넥 랫풀다운)

4-2. 분류 축 병합 캠페인 Phase 0~3-1 (#524 · #525 · #534 · #536)

  • Phase 0 — 분류 설계(오너 확정): 8값 = barbell 바벨 / dumbbell 덤벨 / kettlebell 케틀벨 / machine 머신 / band 밴드 / bodyweight 맨몸 / cardio 유산소 / other 기타.
  • Phase 1 — UI 선행(#524): 커스텀 종목 폼 장비 탭 4→8종, 키↔라벨 맵 3곳, 관리자 목록 9종 (케이블은 컷오버까지 호환 유지), .equip-tabs 오버플로 수리.
  • Phase 2 — 데이터 수렴(#525, 070000): Production 713종 정합 감사 → 델타 106행 적용 (cable→machine 51 · category='cardio' 27 · 도구/회복류→other 27 · GHD→bodyweight 1). 최종 분포 bodyweight 243 / barbell 163 / machine 122 / dumbbell 90 / other 27 / cardio 27 / band 22 / kettlebell 19.
  • Phase 3 — 컷오버(#534, 100000): 휴면 exercise_category 드롭 → equipmentexercise_category로 개명 → equipmentgenerated 미러로 재생성(배포 지연 번들의 직접 select 호환) → 컬럼 참조 함수 11개 재정의(치환마다 발생 횟수를 단언하는 생성 스크립트). RPC 입력은 이중 키(exercise_category 우선), 출력 와이어 키는 'equipment' 유지. 프론트 독자 6곳 이중 읽기, 관리자 케이블 옵션 은퇴.
  • Phase 3-1 — 재은퇴 수리(#536, 110000): 컷오버의 함수 추출이 schema.sql 마지막 CREATE만 보고 후속 DROP을 놓쳐 은퇴 함수 materialize_wodup_unmatched_user_customs_v1을 부활시킨 것을 check:remote-schema 은퇴 거부 목록이 검출 → 재드롭.

4-3. Phase 4 — 정리 조각 (#538 · #545 · #549)

  • Phase 4-0 — bodyweight_repsreps 통일(#538, 150000): 34행 수렴 + 작성 경로 2곳 (커스텀 생성·관리자 단위 매핑)이 더는 bodyweight_reps를 만들지 않음 + pgTAP carve-out 단언 반전. enum 값·읽기 관용은 과거 관측 행과 배포 지연 클라이언트를 위해 유지.
  • Phase 4-1 — 미러 드롭 + 맨몸 불변식(#545, 160000): equipment generated 미러 드롭 (Production pg_depend 의존 0 + live 함수 216개 본문 스캔 컬럼 참조 0 확인 후). 맨몸 불변식 위반은 추정 5건이 아니라 7건(요가 5 + 불가리안 스플릿 스쿼트·GHD 글루트햄 레이즈) — 형제 관례로 계수 부여. pgTAP plan 14→16.
  • Phase 4-2 — RPC 출력 와이어 키 equipmentexercise_category(#549, 240000): 출력 키를 내던 함수 8개 재발행(get_exercise_catalog · build_home_dashboard_core_v2 · get_user_pr_exercise_summaries_snapshot_json · get_admin_health_report · get_admin_mapping_page · get_volume_overview_v1/v2/v3_core), get_exercise_catalog contract 4→5, 프론트 어댑터 allowedFields·검증 6곳·페이로드 타입 5종·계약·캐시·독자 폴백 10곳·출력형 픽스처 12파일 전환.

4-4. 기록 필드 전수 조사 + 결정 반영 (#557, 260000)

713행을 읽기 전용 프로브로 덤프해 규칙 검사(요약형 파생·track_pr·필드 결손·중복·누락·불변식)

  • 이름/가족 휴리스틱 + 형제 종목 대조표로 보고했다. 구조적 불일치 0건, 판단 필요 9건. 오너 결정을 데이터 마이그레이션 한 건으로 반영했다:
결정대상변경
워킹 런지는 맨몸(무게 ×)walking-lungebarbell·load+reps → bodyweight·reps
바벨 롤아웃은 맨몸barbell-rolloutbarbell → bodyweight, factor 0.5 → 0.75(앱휠·링 롤아웃과 동일)
줄넘기류는 reps만줄넘기·싱글언더·더블언더reps+duration → reps, 요약형 time → reps, factor 0
맨몸판은 무중량, 중량판은 장비를 이름에 붙인 별도 종목디클라인 싯업·불가리안 스플릿 스쿼트·GHD 글루트햄 레이즈load+reps → reps
요가 동작은 전부 맨몸·시간, factor 0태양경배 A/B·빈야사·고양이-소·로우 런지(Anjaneyasana)reps → duration, factor 0, PR 해제
베어크롤은 distance만bear-crawldistance+duration → distance
맨몸 아닌 행의 factor는 없음활성 시스템 행 25행(밴드 5·카디오 16·폼롤러 5·캡틴체어 니레이즈 1)factor → 0

pgTAP catalog_measurement_type_repair plan 16→17(맨몸 아닌 행 factor 0 단언), bodyweight_factor_profiles 예시 교체(로우 런지 → 런지) + 요가 로우 런지 duration·0 단언.

주요 결정과 그 근거

  • 8분류의 경계: 케이블→머신 흡수(유저 폼에 원래 없던 값). '도구(implement)' 버킷은 제안→기각 (통계·계산상 무의미, 기타로 수용 — "도구" 라벨 논쟁 자체가 개념 흐림의 방증). 밴드는 도구에 접는 안도 기각하고 분리 유지(기록 프로파일이 정반대: 밴드=reps-only vs 도구류=load 기록). 요가· 스트레칭·호흡=맨몸, 유산소는 유저 선택지에 포함.
  • bodyweight_reps 폐지: 단위도 계산도 reps와 같고 형제끼리 갈리는 얼룩만 남기고 있었다. 맨몸 의미론은 분류 + 계수가 이미 전담하고 있었으므로 표식을 걷어도 동작이 달라지지 않는다.
  • 컷오버에서 와이어 키를 유지한 이유와 나중에 바꾼 이유: 계약·페이로드 예산을 건드리지 않고 컬럼 병합만 먼저 안착시켰다. 미러 드롭까지 끝난 뒤 와이어 키를 바꿀 때는 카탈로그 계약만 4→5로 범프했다 — 카탈로그 어댑터는 허용 필드 집합을 엄격히 검사하지만 홈·PR·볼륨·관리자 어댑터는 추가 키를 허용하고 equipment가 nullable이라 구 번들이 장비 빈 값으로 조용히 동작한다(get_volume_overview (3,4) 선택자·pgTAP 핀 불변). RPC 입력의 이중 키는 그대로 두었다.
  • 맨몸 불변식을 양방향으로: 처음 목표는 "맨몸·reps ⇒ factor>0"이었지만 전수 조사가 반대쪽 (맨몸 아닌데 factor>0 — 줄넘기에서 체중 볼륨이 잡힘)을 드러냈고, 오너가 "맨몸 아니면 factor 없음"을 규칙으로 확정했다.
  • 계수는 형제 관례에서 읽는다: 불가리안 스플릿 스쿼트 1.0(스플릿 스쿼트·런지·피스톨 = 1.0), GHD 글루트햄 레이즈 0.5(노르딕 컬·백/힙 익스텐션 = 0.5), 바벨 롤아웃 0.75(앱휠·링 롤아웃 = 0.75).
  • 요가의 범위: 이름에 요가 정체성(산스크리트·경배·빈야사)이 있는 행만. 하이 런지·와이퍼 자세· 월슬라이드는 일반 맨몸 reps 동작으로 두었다.

작업 중 드러난 것

  • 은퇴 함수 부활(→ #536): schema.sql에서 함수를 추출할 때 drop function … <이름> 후속 존재를 반드시 확인해야 한다. 게이트(check:remote-schema 은퇴 거부 목록)가 Production 적용 직후 잡았다.
  • 번호가 세 번 밀렸다: Phase 4-2는 170000으로 조립했다가 #544(200000) 랜딩 → 210000, HQ 사다리 통지 → 220000, #548이 사다리와 달리 230000으로 랜딩 → 240000. 같은 저녁 병행 트랙 4개가 랜딩하면 번호는 push 직전 꼬리로만 확정된다. 조립 스크립트(함수 추출·치환 횟수 단언·프론트 27 치환· 픽스처 12파일) 덕에 매번 수 분에 재조립했다.
  • 로컬 check와 CI verify의 괴리: npm run check에는 check:unused 래칫과 build가 없다. CRLF 파일에서 줄 제거가 조용히 무시돼 고아 변수가 남았고 CI만 레드였다 → 이후 세 게이트를 전부 로컬에서 돌린 뒤 푸시.
  • pgTAP 예시 충돌: bodyweight_factor_profiles가 "로우 런지 = reps·1.0"을 예시로 박아 두어 요가 결정과 충돌했다. 카탈로그 행의 프로파일을 바꿀 때는 supabase/tests/database에서 그 슬러그를 grep할 것.
  • 전수 조사 휴리스틱의 오탐: 행 클린의 "행(hang)", 체스트서포티드의 "서포트", 로잉머신의 "로우", Plate-loaded 머신의 "Plate" — 이름 휴리스틱은 형제 대조 없이는 보고하지 말 것.
  • 미러 드롭 안전 근거를 두 겹으로: pg_depend는 뷰·인덱스·정책·제약만 추적한다. 함수 본문의 컬럼 참조는 schema.sql의 live 정의(마지막 CREATE, 후속 DROP 반영) 216개를 전수 스캔해 확인했다.

5. 적용 결과

항목결과
정식 카탈로그675 → 713종 (이 트랙 신설 36 = 배치 1 20 + 배치 2 11 + 배치 3 5; 나머지 2는 #520 탑 스내치·탑 클린), 별칭 보강 33건, WodUp 매핑 69건, 오연결 3건 수리
분류 축equipment + 휴면 exercise_category 2개 → exercise_category 1개, 8값 외 0행(bodyweight 243 / barbell 163 / machine 122 / dumbbell 90 / other 27 / cardio 27 / band 22 / kettlebell 19 → 결정 반영 후 bodyweight 245 · barbell 162)
장비 오분류 수리배치 2의 21행 + 수렴 델타 106행 + 결정 반영 2행
bodyweight_reps34행 → 0행 (작성 경로 2곳 봉쇄)
맨몸 불변식위반 7 → 0 (맨몸·reps ⇒ factor>0), 맨몸 아닌 행 factor 25 → 0 (양방향 pgTAP 단언)
기록 필드 정합요약형 파생 불일치 0 · track_pr 불일치 0 · 결손/중복/누락 0 (713행 전수)
와이어RPC 출력 키 'equipment''exercise_category'(함수 8개), 카탈로그 contract 5, 프론트 독자 폴백 10곳 제거, types/supabase.ts 미러 필드 제거
pgTAPcatalog_measurement_type_repair plan 14 → 17, bodyweight_factor_profiles 10 → 11
검증마이그레이션 11건 전부 Production 적용 + check:remote-schema EXIT=0 + raise-rollback 프로브(행 상태·함수 본문 토큰·실제 RPC 페이로드 키)

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

분류가 한 이름으로 통한다

DB 컬럼·RPC 와이어 키·프론트 독자·커스텀 폼 선택지가 전부 exercise_category 8값이다. 유저는 8종 중에서 고르고, 관리자·뷰매퍼·검색이 같은 값을 읽는다.

맨몸 볼륨 계산이 규칙이 됐다

맨몸·reps 종목은 반드시 체중 계수를 갖고, 맨몸이 아닌 종목은 갖지 않는다. 요가 흐름이 볼륨 0으로 새거나 줄넘기가 체중 볼륨으로 부풀던 두 방향의 누수가 닫혔고, pgTAP가 양방향을 지킨다.

기록 필드가 종목의 실체를 말한다

713행 전수에서 요약형·PR 플래그·필드 원자가 서로 맞고, "어색한 조합"은 오너 결정으로 정리됐다 (줄넘기=횟수, 요가=시간, 맨몸판=무중량, 베어크롤=거리). 중량판은 장비를 이름에 붙인 별도 종목이라는 작명 규칙도 이때 확정됐다.

인입이 닿는 종목이 늘었다

WodUp 실사용 키의 연결 공백 중 빈도 높은 것들이 정식 종목·별칭·매핑으로 들어갔고, 06-15 잔재 오연결 3건이 바로잡혔다.

카탈로그 변경의 작업 방식이 정착했다

읽기 전용 프로브로 실측 → 형제 대조표로 보고 → 결정 → 슬러그 기준 멱등 마이그레이션 + 손댄 행만 postcheck → 전역 불변식은 pgTAP → Production 적용 후 재프로브. 이 순서가 배치 1부터 결정 반영까지 같은 형태로 반복됐다.

남은 것

  • 캡틴체어 니레이즈 — 결정대로 machine 유지·factor 0으로 뒀지만 형제 캡틴체어 레그레이즈는 bodyweight라 분류 갈림이 남아 있다.
  • 하이 런지 · 와이퍼 자세 · 월슬라이드 — 요가/홀드가 아닌 일반 맨몸 reps로 두었다. 요가로 본다면 같은 규칙(duration·factor 0)으로 추가 반영.
  • 통계 재계산 확인 — 요약형·계수 변경은 카탈로그에만 적용됐다. 과거 세션의 볼륨/PR 집계가 새 규칙으로 재계산되는지(통계 리프레시 큐 연동)는 검증하지 않았다 — 통계 중앙화 트랙 소관.
  • 구 번들 새로고침 — 카탈로그 계약 4→5로 이미 열려 있던 탭은 카탈로그 로드에서 시끄럽게 실패 하고 새로고침으로 해소된다(#541·#545와 같은 정책).
  • 위생 — 인덱스 exercises_equipment_idx가 개명된 컬럼 위에 옛 이름으로 남아 있음, RPC 입력의 equipment 폴백 키 존속, bodyweight_reps enum 값·읽기 관용 존속(과거 관측 행 보호).
  • WodUp 대조 잔여 — #515 오연결 3건을 탔던 과거 인입분 재포인팅(재인입 트랙이 새 export를 올리면 무의미), 커스텀 유지 판정분(체조 드릴 한글 커스텀 · Banded Strict Muscle Up 바/링 불명 · Box Clean Pull)은 의도적 미매핑.