Strength Level 전종목 근력 등급 — 프론트 스쿼트 랭킹 공백에서 290종목 기준표·76종목 신설까지 (2026-08-24)
- 기간: 2026-08-24 (세션 1개 — 오너 보고 "프론트 스쿼트는 종목 랭킹이 안 뜨는데 왜 그럴까" → 원인 조사 → 287종목 대조 → 오너 지시 "확실한 애들은 우리꺼랑 매핑해서 넣어주고, 나머지는 전부 신규 종목으로 추가해서 Strength Level 전종목이 랭킹이 뜨게" → 집행)
- 랜딩: PR #631(
981aa64f, 540000) + 후속 병합 PR #635(D2,20260821550000_strengthlevel_merge_near_duplicates.sql) — 엣지 0건, 웹은 Vercel 자동 배포(프론트 예산 상수 1건). Production 적용 완료(db push540000·550000, remote-schema missing 0, Vercel 번들 manifest 확인) - 설계서: 없음(조사 → 오너 즉시 지시 → 집행; 대조표는 세션 scratchpad
sl-catalog-match.md, 본 문서 5절에 요약) - 정본: 종목 대응
scripts/strength-standards/bindings.json(매핑) ·new-exercises.json(신설) ·bound-in-production.json(08-07 검증분), 앵커 원본strengthlevel-source.json, RPC 계약docs/data/app-screen-rpc-contract.mdget_pr_overview절, 프론트 예산src/react/services/appPerformanceBudgets.tsprOverview.maxStrengthStandards - 도구:
scripts/strength-standards/parse-strengthlevel-pages.mjs(페이지 캐시 → 앵커 JSON, 캐시는 gitignore),generate-catalog-migration.mjs(입력 4개 → 마이그레이션 데이터 블록,--write/--check) - 게이트:
tests/react/strengthLevelFullCatalog.test.mjs(블록 = 생성기 출력 · SL 전종목 바인딩 완전성·유일성 · 신규 slug/이름 무충돌 · 컷라인 = 프론트 곡선 · SQL 상한 = 프론트 예산), 마이그레이션 pre/postcheck(템플릿 존재·행 수·컷라인·고정점·prosrc) - 버그리포트: 없음(데이터 부재 — 코드 결함 아님)
- 계약:
get_pr_overview.strength_standards범위·상한 절
1. 배경
종목 상세(나의 기록)의 등급 배지·"전체급 기준 상위 N%"·등급 사다리는 get_pr_overview.strength_standards[]에서 그 종목의 exercise_id와 일치하는 행을 찾을 때만 그려진다(buildStrengthStandardRating이 행이 없으면 null, 화면은 조용히 생략). Production 기준표는 PR #221(07-15, "핵심 8종목")과 #374(08-07, 여성 + 4종목)의 화이트리스트를 거친 12종목 28행뿐이었다. Strength Level 원본은 2026-07-18에 287종목 전부를 받아 캐시해 두었지만(scripts/.cache/strength-standards/, gitignore) 시드 생성기 CORE_STRENGTH_STANDARD_SLUGS가 나머지 279종목을 버렸다 — 프론트 스쿼트도 그중 하나.
2. 문제 제기
프론트 스쿼트 등급이 비어 있었다 — 기준표 행 부재, 코드 결함 아님
exercise_strength_standards에 front-squat 행이 없음 → d.rank/d.cutlines null → RecordsDetail.tsx:238 배지·백분위·사다리 전부 비표시. 원본 캐시에는 남 55/77/105/137/172 · 여 30/45/62/83/105가 온전히 있었다.
기준표를 전종목으로 늘리면 get_pr_overview가 조용히 잘랐을 것
기준표 subquery가 order by exercise_id limit 16. 성별당 14행이라 보이지 않던 상한이, 전종목(성별당 291행)에서는 exercise_id 순서 앞 16행만 남기고 나머지를 에러 없이 떨궜을 것이다.
7월 캐시의 값은 오늘 사이트와 달랐다
7월 캐시: 스쿼트 남 64/93/130/173/219, 딥스 남 중량 −8/18/50/86/125. 2026-08-24 재수집: 스쿼트 66/95/131/173/219, 딥스 3/24/49/78/109 = Production 12종목의 08-07 검증값과 정확히 일치. 캐시를 그대로 썼다면 새 종목만 오래된 스냅샷이 됐을 것.
베이스라인 seed가 identity 시퀀스를 뒤처지게 두었다
exercise_strength_standards 행이 id 명시 insert로 시드돼 시퀀스는 1. 새 행 insert가 duplicate key (id)=(1)로 죽었다(로컬 리플레이에서 발견, Production도 같은 상태).
3. 해결 방안
원칙 (오너 지시 D1, 2026-08-24)
- D1 "너가 판정한 47건 말고 확실한 애들은 우리꺼랑 매핑해서 넣어주고, 나머지애들 전부는 신규 종목으로 추가해서 Strength Level 전종목이 랭킹이 뜨게" → 채택. "확실함"의 정의를 사람 판단이 아니라 기계 채널로 고정했다: 이름 정확 일치 / 토큰 집합 일치 / 바벨 접두·머신 접미 변형 / SL 공식 별칭(SL 페이지가 "같은 운동"이라 선언한 이름), SL 카테고리와 우리 카테고리가 맞을 때만, (우리 종목, 지표)당 SL 한 행만. 그 밖의 모든 것 = 신설. 1차 보고의 "내가 판정한 47건" 중 SL 별칭으로 기계 일치가 된 17건은 매핑, 나머지는 신설로 갔다.
접근
| 대안 | 판단 |
|---|---|
| 7월 캐시로 시드 | 기각 — 값 드리프트(2절). 290종목 재수집(1.2초 간격, 실패 0) |
기준표 전량을 get_pr_overview에 싣기(상한만 올림) | 기각 — 상한 291행 × 성별 = 231KB를 매 호출마다 |
| 유저 관련 종목만(exercise_summaries ≤128 ∪ pr_summary 멤버 15) + 상한 288 | 채택 — 일반 40행 32KB, 최악 288행 231KB(예산 500KB 안) |
| 신설 종목 프로파일을 JSON에 하드코딩 | 기각 — Production 값과 드리프트 |
like 템플릿 행에서 적용 시점에 복사(#551 batch2 방식) | 채택 — id/brid는 slug 파생, default synonym은 트리거 |
| 기존 28행 앵커를 오늘 값으로 갱신 | 불필요 — 값이 동일(2절), on conflict do nothing으로 무접촉 |
4. 적용한 내용
Phase 1 — 조사·대조 (같은 세션, PR 없음)
원인 2층(행 부재 + 상한 16) 확인, 7월 캐시 287종목 × 카탈로그 708종목 자동 대조(193 일치 / 94 미확정 → SL 별칭 채널로 210/77).
Phase 2 — 재수집·매핑·신설·RPC (#631, 540000)
scripts/strength-standards/parse-strengthlevel-pages.mjs: 페이지 HTML → 앵커 JSON. 8월 페이지는 메타가data-exercise-*속성으로 옮겨가 있어(7월은 본문 JSON) 두 형식 모두 읽고, 부위·별칭은 7월 캐시를 fallback으로 합친다.generate-catalog-migration.mjs: 입력 4개 → (1) 신규 종목 76행 insert(템플릿 복사, sort_order는 대표 구간 밖) + synonym 별칭, (2) 기준표 554행 insert(앵커 +deriveStrengthStandardCutlinesFromAnchors컷라인, exercise_id는 slug로 적용 시점 해석, 시퀀스 재동기화), (3) 생성 postcheck(행 수·synonym·컷라인 9칸). 마이그레이션 본문은 precheck(템플릿 53종 존재),get_pr_overview재발행(기준표 범위·상한 288), postcheck(고아 0·남성 스쿼트 레전드 290 불변·프론트 스쿼트 바인딩·prosrc).- 프론트:
maxStrengthStandards16 → 288, 계약 문서 갱신. 화면 코드 무변경(기준표 행이 생기면 기존 경로가 그린다). - 결과 분포: SL 290종목 = 시간 지표 2(플랭크류, 대상 아님) + Production 12 + 매핑 201 + 신설 76(+ 중량 친업은 chin-ups 중량 지표 전용). 기준표 28 → 582행, 우리 종목 291개에 등급 기준 존재.
주요 결정과 그 근거
- 맨몸 종목의 SL 중량 지표는 "중량 X" 형제가 있을 때만 싣는다(풀업·딥스 기존, 친업은 신설). 머슬업 중량은 형제가 없어 싣지 않음.
- 신설 이름은 카탈로그 표기 관례(싱글레그·원암·시티드·리버스그립, name_en 하이픈)를 따른 제안 — 오너가 머지 전 바꿀 수 있게 PR 본문에 전표.
- near-duplicate를 알면서도 신설한 것(D1 적용): 바벨 숄더프레스 ↔ 바벨 오버헤드 프레스, 트라이셉스 푸쉬다운 ↔ 케이블 푸쉬다운, 머신 카프 레이즈 ↔ 스탠딩 카프 레이즈 머신, 고블렛 스쿼트 ↔ 덤벨 고블렛 스쿼트. → 오너 D2(같은 날): "병합하는 게 좋아 보인다" →
202608215500004쌍 흡수(#568 방향: 기준표 재지정·별칭 이관·동적 FK 재지정·흡수 행 삭제). 바벨 숄더프레스만은 기준표를 재지정하지 않고 삭제 — 바벨 오버헤드 프레스가 이미 08-07 검증된 SL military-press 기준을 들고 있어 재지정하면 같은 종목·지표에 기준이 두 벌이 된다. 입력 정본은 병합 후 상태로 갱신(bindings 3쌍 재지정 + shoulder-press 명시적 skip, new-exercises 76→72)하고, 랜딩된 540000 데이터 블록은 역사로 동결(계약 테스트가 SHA-256으로 고정).
작업 중 드러난 것
- Bash 툴이 명령 문자열의
\b를 백스페이스(0x08)로 바꾼다 — 정규식이 든 스크립트는 Write 툴로 쓸 것(heredoc·python 인라인 모두 걸림). - 7월 캐시와 사이트 현재 값의 드리프트(2절) — 기준표 재수집은 항상 시드 직전에.
- 베이스라인 id 명시 seed → identity 시퀀스 1(2절). 같은 패턴의 다른 테이블도 새 insert 전에
setval확인. - SL 페이지 구조 변경(메타 위치) — 파서는 두 형식을 읽지만 다음 재수집 때 또 바뀔 수 있다;
--check가 블록 드리프트를 잡는다. - 기존 생성기
generate-strength-standard-cutlines.mjs는 baseline의 28행만 파싱(EXPECTED_STANDARD_ROWS = 28)하며 410000 블록과만 묶여 있다 — 손대지 않았고 새 행은 새 생성기가 다룬다.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 등급 기준이 있는 정규 종목 | 14 → 291 (Strength Level 288종목 → 우리 291종목; 풀업·딥스·친업은 reps/중량 2종목) |
exercise_strength_standards 행 | 28 → 582 → 580(D2 병합 후: shoulder-press 2행 삭제, 3쌍 재지정) — 격리 스택 리플레이 EXIT 0 |
| 정규 종목 수 | 708 → 784 → 780(D2 병합 후) |
| 프론트 스쿼트 | 남 54/77/105/137/172 → 레전드 225 · 여 31/45/63/83/104 → 레전드 135, 프론트 스쿼트 행에 바인딩 |
get_pr_overview 기준표 | 상한 16(전역) → 288(유저 PR 종목 ∪ pr_summary 멤버) — 일반 40행 32KB, 상한 231KB |
| 기존 12종목 컷라인 | 바이트 동일(postcheck 고정점 남성 스쿼트 레전드 290) |
| 게이트 | pgTAP 51파일 798 PASS · npm run check 0 · npm test 1816/1816 · check:unused 0 · build 0 · CI scope·verify·migration-smoke 전부 success(run 32652812174) |
| Production | db push 540000 EXIT 0 · remote-schema missing 0/failures 0 · 프로브: 종목 784·기준표 582행·SL 288종목→우리 291종목·null 컷라인 0·고아 0·프론트 스쿼트 남 레전드 225/여 135·스쿼트 290 불변·RPC 범위 적용 · Vercel 번들 barbelicRepository-DsGfPV_3.js에 manifest 540000·maxStrengthStandards 288 |
| 오너 실기기 확인 | 미확인(적용 뒤 프론트 스쿼트 상세에서 배지·백분위 확인 요청) |
6. 이번 개선으로 향상된 것
Strength Level이 아는 종목이면 등급이 뜬다
288종목 전부가 우리 종목 하나에 1:1로 붙어 있어 "왜 이 종목만 등급이 없지"가 사라진다. 시간 지표 종목(플랭크류)만 예외로 남는다.
기준표 반입이 절차가 됐다
재수집(파서) → 입력 JSON → 생성기 → 마이그레이션 블록 → --check와 계약 테스트가 드리프트를 잡는다. 다음 재수집은 같은 경로로 source_scraped_at만 바뀐다.
구조적으로 남는 것
get_pr_overview기준표는 유저 관련 종목으로 한정 — 전종목이 늘어도 페이로드가 따라 늘지 않는다.- "확실한 매핑"의 기계 정의(채널 4종 + 카테고리 일치 + 종목·지표 유일성)가
bindings.json에 남아 다음 종목 추가의 기준이 된다.
남은 것
- 신설 72종목(병합 후) 이름 확인(오너 판단; near-duplicate 4쌍은 D2로 병합 완료).
- 머슬업 SL 중량 지표(중량 머슬업 종목 없음)·플랭크류(시간 지표) — 기준표 모델 밖.
- Strength Level 데이터 라이선스(06-22 메모 "개인 실험용" → 이미 12종목이 Production, 이번에 전종목) — 오너 인지 사항.