Skip to content

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 push 540000·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.md get_pr_overview 절, 프론트 예산 src/react/services/appPerformanceBudgets.ts prOverview.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_standardsfront-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).
  • 프론트: maxStrengthStandards 16 → 288, 계약 문서 갱신. 화면 코드 무변경(기준표 행이 생기면 기존 경로가 그린다).
  • 결과 분포: SL 290종목 = 시간 지표 2(플랭크류, 대상 아님) + Production 12 + 매핑 201 + 신설 76(+ 중량 친업은 chin-ups 중량 지표 전용). 기준표 28 → 582행, 우리 종목 291개에 등급 기준 존재.

주요 결정과 그 근거

  • 맨몸 종목의 SL 중량 지표는 "중량 X" 형제가 있을 때만 싣는다(풀업·딥스 기존, 친업은 신설). 머슬업 중량은 형제가 없어 싣지 않음.
  • 신설 이름은 카탈로그 표기 관례(싱글레그·원암·시티드·리버스그립, name_en 하이픈)를 따른 제안 — 오너가 머지 전 바꿀 수 있게 PR 본문에 전표.
  • near-duplicate를 알면서도 신설한 것(D1 적용): 바벨 숄더프레스 ↔ 바벨 오버헤드 프레스, 트라이셉스 푸쉬다운 ↔ 케이블 푸쉬다운, 머신 카프 레이즈 ↔ 스탠딩 카프 레이즈 머신, 고블렛 스쿼트 ↔ 덤벨 고블렛 스쿼트. → 오너 D2(같은 날): "병합하는 게 좋아 보인다"20260821550000 4쌍 흡수(#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_standards28 → 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)
Productiondb 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, 이번에 전종목) — 오너 인지 사항.