PISTE운동 스케줄 · 세트 기록
서론
운동을 다시 시작하면서 플랜은 마크다운 문서로 짜 두었는데, 헬스장에서 폰으로 그 문서를 열어 "오늘 뭐 하지, 몇 kg 들지"를 찾는 게 생각보다 번거로웠습니다. 지난번 무게는 메모 앱에, 체중은 또 다른 앱에 흩어져 있었고, "3세트 모두 12회를 채우면 다음엔 한 단계 올린다" 같은 규칙도 매번 머릿속으로 계산해야 했습니다.
PISTE는 FastAPI + React + Vite 기반의 개인용 운동 스케줄러입니다. 요일별 플랜에 맞춰 오늘 할 운동과 종목별 추천 무게를 보여 주고, 세트를 기록하면 다음 무게를 정해 줍니다. PC에서 Docker Compose로 띄워 두고 같은 와이파이의 휴대폰으로 헬스장에서 씁니다. 이름은 펜싱 경기대 피스트(piste)에서 따 왔고, 완료 · 빠짐 색도 펜싱 득점등의 초록 · 빨강을 그대로 썼습니다.
1. 왜 만들었나
- 헬스장에서 바로: 폰을 켜면 오늘 할 종목과 들어야 할 무게가 바로 보여야 했습니다. 세트 사이에는 휴식 시간도 재야 합니다.
- 무게 계산은 앱이: 이중 점진법(정해진 횟수 범위를 모든 세트가 채우면 증량)과 처음 하는 종목의 시작 무게 찾기를 규칙대로 계산해 주길 바랐습니다.
- 플랜은 자주 바뀐다: 일주일 사이에 루틴 구성을 여러 번 바꿨습니다. 그때마다 코드를 고치지 않고 화면에서 요일 · 루틴 · 종목 · 단계 기간을 바꿀 수 있어야 했습니다.
- 집 안에서만: 혼자 쓰는 기록이라 로그인 · 클라우드 없이 PC 한 대에 두고, 같은 와이파이에서만 엽니다.
2. 구조 및 아키텍처
백엔드는 FastAPI + SQLAlchemy 2 + Alembic, DB는 Docker에서는 PostgreSQL, 개발 · 테스트에서는 SQLite입니다. 프론트는 React + TypeScript + Vite + TanStack Query이고, 백엔드의 OpenAPI 스키마에서 타입을 자동으로 만들어 API 모양을 손으로 맞추지 않습니다. nginx가 빌드된 화면을 내보내고 /api만 백엔드로 넘깁니다.
MINI_Piste/
┣━━ 📂 backend/
┃ ┣━━ 📂 app/
┃ ┃ ┣━━ 📂 api/ # 라우터 (오늘 · 플랜 · 기록 · 측정 · 통계)
┃ ┃ ┣━━ 📂 services/ # 무게 추천 · 날짜별 계획 · 달력 집계 · 통계
┃ ┃ ┣━━ models.py # SQLAlchemy 모델
┃ ┃ ┣━━ schemas.py # Pydantic 입출력 (OpenAPI 의 원본)
┃ ┃ ┗━━ seed.py # 처음 설치용 기본 플랜 · 종목 · 루틴
┃ ┣━━ 📂 alembic/ # DB 마이그레이션
┃ ┗━━ 📂 tests/ # pytest
┣━━ 📂 frontend/
┃ ┣━━ 📂 e2e/ # Playwright (데스크톱 · 모바일)
┃ ┗━━ 📂 src/
┃ ┣━━ 📂 api/ # fetch 클라이언트 · Query 훅 · OpenAPI 타입 (자동 생성)
┃ ┣━━ 📂 pages/ # 홈 · 운동 · 운동 모드 · 달력 · 체중 · 분석 · 플랜 · 설정
┃ ┣━━ 📂 components/ # 운동 카드 · 선택 상자 · 날짜 선택기 · 그래프 · 일정 로드맵
┃ ┗━━ 📂 styles/ # 디자인 토큰 · 공통 부품 (화면별은 *.module.css)
┗━━ 📄 docker-compose.yml # db + api + web (nginx)
플랜과 기록은 이렇게 나뉩니다.
phases (단계: 이름 · 기간)
┗━━ phase_days (요일 7개: 근력 / 유산소 / 휴식, 루틴, 격주 루틴, 유산소 시간)
routines ── routine_items (종목 · 세트 · 횟수 범위 또는 시간 · 순서)
exercises ── exercise_muscles (근육 13부위, 주동 / 보조)
workouts (하루 1개) ── set_logs (무게 · 횟수 또는 시간 · 남은 개수 · 좌우)
daily_logs (체중 · 걸음 · 수면 · 못 함 표시) body_measurements (월말 둘레 · 체지방)
오늘 화면 하나가 그려지는 흐름은 대략 다음과 같습니다.
GET /api/today?date= ─▶ planner: 날짜가 속한 단계 → 요일 계획 → (격주면) 루틴 고르기
─▶ progression: 종목마다 가장 최근 세션의 세트로 다음 무게 계산
─▶ 그날 세션 · 일일 기록
React ─ TanStack Query ─▶ 세트 기록(POST) ─▶ 관련 화면(오늘 · 달력 · 홈 · 통계) 캐시만 다시 불러오기
3. 핵심 구현 디테일
① 다음 무게 추천 (이중 점진법)
종목마다 가장 최근에 한 세션의 세트를 보고 다음 무게를 정합니다.
| 지난 기록 | 다음 무게 |
|---|---|
| 기록 없음 | 추천 없음 → 무게 찾기부터 |
| 마지막이 무게 찾기 세트 | 찾은 무게로 시작 |
| 계획 세트 수 이상 · 모두 목표 상한 이상 | 증량 단위만큼 올림 (예: 레그프레스 +10kg) |
| 한 세트라도 하한 − 2 미만 | 한 단계 내림 (자세가 무너지는 신호) |
| 그 외 | 같은 무게 유지 |
한쪽씩 하는 종목(싱글 레그프레스 등)은 왼쪽 · 오른쪽을 따로 계산해 약한 쪽 기준으로 정합니다. 증량 단위 · 휴식 시간은 종목마다 다르게 둘 수 있습니다.
② 시작 무게 찾기
처음 하는 종목은 '가볍겠다' 싶은 무게로 12회를 하고 "몇 개 더 할 수 있었나"만 고르면 다음 세트 무게가 나옵니다. 5개 이상이면 +2단계, 4개면 +1단계, 2~3개면 확정, 12회를 못 채우면 −1단계입니다. 선택지 문구는 백엔드가 내려 주어 화면과 서버의 규칙이 어긋나지 않습니다.
③ 날짜 → 그날 계획
날짜가 속한 단계를 찾고, 요일로 그날 계획을 꺼냅니다. 단계 시작일부터 센 주차가 홀수이면 격주 루틴을 써서 전신 A / B가 주마다 자연스럽게 번갈아 나옵니다. 단계 기간이 서로 겹치면 날짜별 계획이 모호해지므로, 단계를 만들거나 기간을 바꿀 때 겹치면 서버가 거절하고 어느 단계와 겹치는지 알려 줍니다.
④ 연속 기록 · 출석률
계획된 운동일(근력 · 유산소)만 셉니다. 휴식일과 플랜 밖 날짜는 건너뛰고, 아직 안 한 오늘은 연속을 끊지 않습니다. 아프거나 일정이 생겨 '못 함'으로 표시한 날은 달력에 빨간 '빠짐' 대신 회색으로 남지만, 안 한 날이므로 연속은 끊고 출석률에도 들어갑니다. 출석률 옆에는 "3/4일"처럼 근거를 같이 보여 줍니다.
⑤ 화면 짜임새
- 타입 자동 생성:
npm run gen:api가 백엔드 OpenAPI →schema.ts를 만들고, 화면 코드는 거기서 이름만 붙여 씁니다. - CSS Modules + 디자인 토큰: 색 · 글자 · 모서리는 토큰으로, 화면별 모양은
*.module.css로 나눴습니다. 다크가 기본이고 라이트 · 기기 설정 따름을 고를 수 있습니다. - 브라우저 기본 UI 대신: select · 날짜 입력 · confirm · title 툴팁을 직접 만든 선택 상자 · 날짜 선택기 · 확인 창 · 말풍선으로 바꿨습니다. 휴대폰에서도 모양이 같고 키보드로도 조작됩니다.
- 글꼴 · 아이콘은 파일로: Pretendard(본문)와 Barlow Condensed(숫자, 스코어보드 느낌), 아이콘 스프라이트를 저장소 안에 두고 씁니다.
4. 화면으로 보는 기능
4.1 홈
- 오늘 운동: 종목별 추천 무게와 올린 폭(+10 등). 쉬는 날에는 다음 운동을 미리 보여 줍니다.
- 이번 주: 완료 · 연속 운동 · 출석률과 7일 상태 막대(완료 초록 · 못 함 회색).
- 체중: 7일 평균, 목표까지 진행 막대, 오늘 아침 체중을 바로 기록하는 칸, 아침 체중(점)과 7일 평균(선) 그래프.
- 전체 일정: 지금 단계의 진행과 다음 두 단계.
- 테마: 다크가 기본이고 라이트 · 기기 설정 따름을 설정에서 고릅니다 (서론 그림이 라이트 테마).
4.2 오늘 운동
- 종목 카드: 추천 무게와 이유("3세트 모두 12회 달성 → 110kg로 올림"), 지난번 기록.
- 세트 기록: 무게 · 횟수 · 남은 개수를 넣고 기록. 시간 종목(플랭크)은 초로 기록합니다.
- 유산소 · 메모 · 몸 상태: 같은 화면 아래에서 함께 기록하고, 운동을 못 한 날은 '못 함'으로 표시합니다.
4.3 운동 모드 · 휴식 타이머 (휴대폰)
- 한 종목씩 크게: 헬스장에서 한 손으로 누르기 좋게 글자 · 버튼을 키웠습니다. 위쪽 막대가 종목 진행입니다.
- 휴식 타이머: 세트를 기록하면 종목별 휴식 시간(큰 종목 90초, 작은 종목 60초)으로 자동 시작, −15초 · +30초 · 바로 시작. 끝나면 진동 · 소리로 알립니다.
- 자동 다음 종목 · 요약: 종목을 다 하면 다음 종목으로 넘어가고, 끝나면 세트 · 볼륨 · 신기록을 요약합니다.
4.4 달력
- 계획과 결과를 한 칸에: 근력은 파랑, 유산소는 청록, 완료한 날은 초록 바탕.
- 날짜 상세: 날짜를 누르면 그날 계획 · 기록한 세트 · 할 종목이 아래에 나옵니다.
4.5 체중 · 월말 측정
- 하루 값 대신 7일 평균: 체중은 하루하루 흔들리므로 달력 기준 7일 평균으로 추세를 봅니다.
- 월말 측정: 허리 · 가슴 · 엉덩이 · 허벅지 · 팔 둘레와 체지방, 측정일 7일 평균 체중을 함께 남깁니다.
4.6 분석
- 근육별 세트: 종목에 근육 13부위를 주동 / 보조로 달아 두고, 최근 7일 세트를 주동 1 · 보조 0.5로 셉니다. 세로선은 주간 목표입니다.
- 개인 기록: 종목별 최고 무게와 추정 1RM(Epley).
- 좌우 균형: 한쪽씩 하는 종목의 왼쪽 · 오른쪽 비교.
4.7 플랜 편집
- 전체 일정: 단계를 점과 선으로 잇는 목록. 단계 길이가 1주에서 3개월까지 달라 비율 막대 대신 목록으로 보여 줍니다.
- 단계: 새 단계(요일 계획 복사), 이름 · 기간 수정(직접 만든 날짜 선택기), 삭제.
- 요일 · 루틴 · 종목: 요일마다 근력 · 유산소 · 휴식과 루틴(격주 루틴)을 정하고, 루틴의 종목 · 순서 · 세트 · 횟수를 바꿉니다.
5. 한계 · 설치 · 사용 흐름
한계 (중요)
- 1인용 · 집 안 네트워크 전제입니다. 로그인이 없고 DB 비밀번호도 기본값이라 인터넷에 공개하는 용도가 아닙니다.
- 식단 · 칼로리 기록은 없습니다.
- 운동 모드의 화면 꺼짐 방지는 브라우저 정책상
localhost· HTTPS 에서만 동작합니다. 휴대폰에서http://<PC IP>로 열면 화면이 꺼질 수 있습니다. - 처음 들어가는 기본 플랜은 제 플랜 기준입니다. 플랜 화면에서 단계 · 요일 · 루틴을 바꿔 쓰면 됩니다.
사전 요구
| 항목 | 설명 |
|---|---|
| Docker | Docker Desktop (실행용) |
| Python 3.12 + uv, Node.js | 개발용 |
설치 · 실행
git clone https://github.com/Hyeonseok93/MINI_Piste.git
cd MINI_Piste
docker compose up -d --build
브라우저에서 http://localhost:8080 을 열고 이름을 입력하면 시작합니다. 휴대폰은 같은 와이파이에서 http://<PC의 IP>:8080. Windows에서는 start.cmd / stop.cmd 로 켜고 끌 수 있습니다.
저장 위치: Docker 볼륨 piste_pgdata (PostgreSQL). docker compose down -v 를 하면 기록이 모두 지워집니다.
사용 흐름
- 처음 열면 이름(과 키 · 시작 / 목표 체중)을 넣고 시작합니다.
- 플랜에서 단계 · 요일 계획 · 루틴을 내 일정에 맞게 고칩니다.
- 헬스장에서 휴대폰으로 운동 시작 → 추천 무게로 세트를 기록하고 휴식 타이머를 따라갑니다.
- 처음 하는 종목은 무게 찾기로 시작 무게를 정합니다.
- 아침마다 홈에서 체중을 기록하고, 월말에 둘레를 잽니다.
- 달력 · 분석에서 출석과 근육별 세트, 기록이 느는 것을 확인합니다.