Skip to content

feat: v0.3 세션 빌더 - #4

Merged
cbcruk merged 1 commit into
mainfrom
claude/project-start-b3jyjo
Aug 16, 2026
Merged

cbcruk merged 1 commit into
mainfrom
claude/project-start-b3jyjo

Conversation

@cbcruk

@cbcruk cbcruk commented Aug 16, 2026

Copy link
Copy Markdown
Owner

경사 인터벌 프로그램을 짜면 총 상승고도·시간·부하를 누적 계산하고, VK 목표에 닿는 반복 횟수를 역산합니다. 테스트 52 → 77개.

방향이 반대다

계산기는 "목표 고도 → 얼마나 걸리나"를 풉니다. 세션은 트레드밀 앞에서 실제로 정하는 것 — 시간 — 이 입력이고 상승고도가 결과입니다.

양쪽이 공유하는 건 목표와 무관한 비율(경사각·세 방향 속도)뿐이라, geometry에서 computeRates()를 떼어내 둘 다 쓰게 했습니다. computeGeometry는 그 위에 목표 의존 부분만 얹는 형태로 줄었습니다.

같은 구간을 양방향으로 풀면 값이 일치해야 합니다 — 테스트로 고정했습니다.

25% · 5 km/h · 3분  →  세션: 61 m 상승
그 61 m를 계산기에 넣으면  →  180초, 같은 벨트거리·kcal·MET

구조는 한 겹만 중첩한다

세션 = 블록의 나열
블록 = 스텝의 나열 × 반복        # 워밍업 = 반복 1회짜리 블록
스텝 = { 경사, 속도, 시간 }

두 겹 이상은 실제 트레드밀 세션에서 쓸 일이 없고 UI만 복잡해집니다.

누적에서 고른 것들

  • averageMet은 시간가중입니다. 단순 평균은 1분짜리 40% 구간을 59분짜리 1% 구간과 같은 무게로 세서 세션 강도를 크게 부풀립니다. 그 케이스를 테스트로 박아뒀습니다.
  • averageVamMh는 총상승/총시간이라 쉬는 구간이 있으면 순간 VAM보다 낮게 나옵니다. 그게 맞는 값입니다.
  • 경고는 코드별로 합치고 sources[]에 유발 스텝을 남깁니다. 25% 스텝이 6개여도 GRADE_EXTRAPOLATED가 6번 뜨는 게 아니라 한 번 뜨고 "6/13 스텝"이 붙습니다.

VK 목표 역산

상승고도는 반복 횟수에 선형이라 이분탐색이 필요 없습니다.

n = ceil( (목표 − 나머지 블록이 쌓는 고도) / 한 바퀴 상승 )

올림 전 값(exactRepeat)도 같이 냅니다. 기본 세션에서 1,000 m는 "8바퀴 (정확히 7.98)"로 나오는데, 이게 보이면 8바퀴가 목표를 겨우 넘긴다는 걸 알 수 있습니다. 최소성(한 바퀴 빼면 목표 미달)도 테스트로 확인합니다.

플랜 직렬화

1*3@4.5@600|6*15@5@300;25@5@180
= 워밍업 3% 4.5km/h 10분, 그다음 6×(15% 5km/h 5분 + 25% 5km/h 3분)

base64 JSON이 더 짧고 쉽지만, 링크를 눈으로 읽고 손으로 고칠 수 있는 게 더 값집니다 — 세션은 남에게 보내는 물건입니다.

플랜은 통째로 받거나 통째로 버립니다. 스칼라 하나가 망가진 것과 달리, 반쯤 파싱된 운동 프로그램은 기본값보다 나쁩니다 — 사용자가 짜지 않은 세션을 자기 세션이라고 믿게 됩니다.

UI

계산기 / 세션 빌더 탭. 탭도 URL에 실립니다 — 세션을 짜서 보냈는데 상대가 계산기를 보면 곤란합니다.

편집 중에는 도메인 밖 값이 잠깐 스칠 수 있습니다(칸을 비우는 중). 코어는 그때 던지는 게 맞고, UI는 그걸 화면 붕괴 대신 한 줄로 보여줍니다.

검증

  • pnpm test 77개, pnpm typecheck, pnpm build 통과
  • 브라우저(Chromium), 콘솔 에러 0
    • 기본 세션 → 13스텝 전개, 누적 757 m / 0:58:00, 경고 집계 6/13
    • 역산 1,000 m → 8바퀴(7.98) → 적용 버튼 → 총 상승 1,002 m, URL이 8*로 갱신
    • 그 URL을 새 탭에서 열면 세션 탭·플랜·누적 그대로 복원
    • p=6*15@5@300;99@5@180 → 플랜 통째 거부, 이유 표시
    • 계산기 탭 회귀 없음(URL 왕복·링크 복사·모델 대조 그대로)

Generated by Claude Code

경사 인터벌 프로그램을 짜면 총 상승고도·시간·부하를 누적 계산하고,
VK 목표에 닿는 반복 횟수를 역산한다.

## 방향이 반대다

계산기는 "목표 고도 → 얼마나 걸리나"를 푼다. 세션은 트레드밀 앞에서 실제로
정하는 것 — 시간 — 이 입력이고 상승고도가 결과다. 양쪽이 공유하는 건 목표와
무관한 비율뿐이라 geometry에서 computeRates()를 떼어내 둘 다 쓰게 했다.
같은 구간을 양방향으로 풀면 값이 일치하는지 테스트로 고정했다.

## 구조는 한 겹만 중첩한다

세션 = 블록의 나열, 블록 = 스텝의 나열 × 반복. 워밍업은 반복 1회짜리 블록으로
표현된다. 두 겹 이상은 실제 세션에서 쓸 일이 없고 UI만 복잡해진다.

## 누적에서 고른 것들

- averageMet은 시간가중이다. 단순 평균은 1분 고강도 구간을 59분 저강도 구간과
  같은 무게로 세서 세션 강도를 부풀린다.
- averageVamMh는 총상승/총시간이라 쉬는 구간이 있으면 순간 VAM보다 낮다. 그게 맞다.
- 경고는 코드별로 합치고 sources[]에 유발 스텝을 남긴다. 25% 스텝이 6개여도
  GRADE_EXTRAPOLATED는 한 번 뜨고 "6/13 스텝"이 붙는다.

## 역산

상승고도는 반복 횟수에 선형이라 이분탐색이 필요 없다. 나머지 블록이 쌓는 고도를
빼고 한 바퀴 상승으로 나눈 뒤 올린다. 올림 전 값도 같이 낸다 — "7.98바퀴"가
보이면 8바퀴가 목표를 겨우 넘긴다는 뜻이다.

## 플랜 직렬화

`1*3@4.5@600|6*15@5@300;25@5@180`. base64 JSON이 더 짧지만 링크를 눈으로 읽고
손으로 고칠 수 있는 게 더 값지다 — 세션은 남에게 보내는 물건이다.

플랜은 통째로 받거나 통째로 버린다. 스칼라 하나가 망가진 것과 달리 반쯤 파싱된
운동 프로그램은 기본값보다 나쁘다. 사용자가 짜지 않은 세션을 자기 세션이라고
믿게 된다.

## UI

계산기 / 세션 빌더 탭. 탭도 URL에 실린다 — 세션을 짜서 보냈는데 상대가 계산기를
보면 곤란하다. 편집 중 도메인 밖 값이 스치면 코어는 던지는 게 맞고, UI는 그걸
화면 붕괴 대신 한 줄로 보여준다.

테스트 52 → 77개. 브라우저에서 전개·누적·역산·플랜 링크 왕복·거부 배너 확인,
계산기 탭 회귀 없음, 콘솔 에러 없음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2WbzDGnqbw7gxfQSqucbA
@cbcruk
cbcruk merged commit d13f801 into main Aug 16, 2026
1 check passed
@cbcruk
cbcruk deleted the claude/project-start-b3jyjo branch August 16, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants