On this page
JunkBox apps/workout-api 가이드
1. 애플리케이션 역할
apps/workout-api는 운동 도메인 앱임.
운동 세션 목록, 세션 생성/수정, 세트 기록, 다운로드 흐름을 담당함.
운동 계획 공개 API, 규칙 엔진 기반 계획 생성, 계획 저장 책임을 함께 담당함.
러닝 계획, 러닝 블록, 러닝 단계 저장, 러닝 계획 복사, 러닝 수행 완료 상태 관리를 담당함.
공통 로그인과 공통 코드/메타 조회는 hub-api와 공통 기반을 사용함.
2. 런타임 구성
FastAPI 애플리케이션 엔트리포인트는 junkbox_workout_api.main:app임.
기본 루트 /는 /workout-sessions로 리다이렉트됨.
상태 확인 경로는 /health임.
apps/workout-ui/dist를 /workout-ui 경로로 직접 마운트함.
libs/shared-ui 산출 자산은 /shared-ui 경로로 직접 마운트함.
workout 앱 전용 web manifest와 service worker를 직접 서빙함.
workout PWA icon과 favicon은 포털 메뉴의 APP_ICON + icon_val에서 해석한 shared-ui 앱 아이콘 세트를 사용함.
운영 자산의 표준 실행 흐름은 8003 백엔드 포트가 빌드된 프런트 산출물을 직접 서빙하는 구조임.
공통 shared-ui CSS/JS 링크는 템플릿 컨텍스트의 shared_ui_asset() 헬퍼가 파일 수정 시각 기준 버전 쿼리를 붙여 렌더링함.
공통 CSS 링크 선언은 shared-ui stylesheet 매크로를 통해 중앙화함.
공통 CSS는 hub-ui-base.css + hub-ui-mobile.css 조합으로 렌더링함.
2.1 로컬 개발 서버 연동
workout-api는 18003 포트 기준 백엔드로 실행할 수 있음.
apps/workout-ui는 15003 포트 기준 Vite 개발 서버로 실행할 수 있음.
workout 개발 모드에서 페이지 HTML은 18003이 렌더링하고, React 엔트리와 HMR 자산은 15003/workout-ui/* 경로에서 로드함.
Vite 개발 서버는 /, /workout-ui, /workout-ui/ 진입 시 /workout-sessions로 리다이렉트함.
workout 보호 화면 HTML은 경로별로 같은 workout.html 앱 셸을 렌더링하고, 실제 화면 선택은 apps/workout-ui React Router가 담당함.
로컬 비컨테이너 실행의 기본 Workout DB URL은 sqlite+aiosqlite:////sorc001/junkbox/db/workout.sqlite3임.
로컬 Docker compose 실행은 호스트 /sorc001/junkbox/db를 컨테이너 /app/db에 마운트하고, 컨테이너 내부 WORKOUT_DATABASE_URL=sqlite+aiosqlite:////app/db/workout.sqlite3를 사용함.
로컬 비컨테이너 실행과 Docker 실행은 같은 /sorc001/junkbox/db/workout.sqlite3 파일을 공유함.
2.2 운영 배포 및 로깅 기준
운영 컨테이너 기본 포트는 8003임.
운영 환경변수는 /sorc001/junkbox/env/.env.shared와 /sorc001/junkbox/env/.env.prod를 함께 참조하고, 컨테이너 내부 ENV_FILE=/app/env/.env.prod 기준으로 로딩함.
운영 로그 볼륨은 /logs001/junkbox/workout-api 호스트 경로를 컨테이너 내부 /app/logs에 연결함.
운영 DB 볼륨은 /sorc001/junkbox/db 호스트 경로를 컨테이너 내부 /app/db에 연결함.
운영 Workout DB 파일은 /sorc001/junkbox/db/workout.sqlite3이며 Docker 이미지 rebuild/recreate와 무관하게 호스트에 유지됨.
운영 컨테이너 내부 Workout DB 경로는 /app/db/workout.sqlite3임.
운영 WORKOUT_DATABASE_URL은 sqlite+aiosqlite:////app/db/workout.sqlite3임.
공통 애플리케이션 로그 파일 경로는 /app/logs/app.log임.
앱 import 시점에 shared-core의 setup_app_logging()을 호출하며, 설정값은 settings.modules.workout.logging을 사용함.
앱 import 시점에 shared-core의 create_async_engine(settings.modules.workout.database_url)를 호출하여 Workout 전용 SQLite engine을 초기화함.
파일 로그 sink는 rotation, retention, compression, enqueue 옵션을 공통 설정값 기준으로 적용함.
3. 라우팅 구조
3.1 인증 및 공통 페이지
GET /login
GET /error/403
/error/403은 하위 호환 진입점이며 별도 권한 오류 페이지를 렌더링하지 않고 hub-api 로그인 화면의 권한 없음 alert 흐름으로 연결함.
3.2 웹 화면
GET /workout-sessions
GET /workout-sessions/new
GET /workout-sessions/{session_id}/edit
GET /workout-plans
GET /running-plans
GET /running-plans/new
GET /running-plans/{plan_id}/edit
GET /running-run/{plan_id}
GET /workout.webmanifest
GET /workout-sw.js
3.3 운동 API
GET /api/workout/sessions
GET /api/workout/sessions/{session_id}
POST /api/workout/sessions
PUT /api/workout/sessions/{session_id}
DELETE /api/workout/sessions/{session_id}
GET /api/workout/plans/latest
GET /api/workout/plans/{plan_id}
GET /api/workout/plans/default-routine-mode
POST /api/workout/plans/generate
GET /api/workout/plans/{plan_id}/linked-session
GET /api/workout/meta/body-parts
GET /api/workout/meta/default-workout-date
GET /api/workout/meta/exercises
GET /api/workout/meta/routine-modes
GET /api/workout/meta/statuses
GET /api/workout/meta/exercises/{exercise_id}/latest-records
query: workout_date, current_session_id
현재 화면의 운동 세션을 제외하고, 해당 세션보다 이전인 운동 세션 중 선택 운동 종목이 포함된 직전 세션의 운동 기록 전체를 반환함.
GET /api/workout/exercises/progressive
query: limit, cursor, exercise_code, exercise_name, body_part_code, is_active
workout 앱 자체 운동 기준정보를 점진 목록 구조로 반환함.
GET /api/workout/exercises/{exercise_code}
POST /api/workout/exercises
PATCH /api/workout/exercises/{exercise_code}
DELETE /api/workout/exercises/{exercise_code}
GET /api/workout/records/date-range
GET /api/workout/records/download
GET /api/workout/running/plans
POST /api/workout/running/plans
GET /api/workout/running/plans/{plan_id}
PUT /api/workout/running/plans/{plan_id}
DELETE /api/workout/running/plans/{plan_id}
POST /api/workout/running/plans/{plan_id}/copy
현재 사용자의 기존 러닝 계획 전체를 신규 러닝 계획으로 복사함.
복사된 계획의 날짜는 서버 기준 금일이며, 수행 여부는 is_performed=false로 저장함.
러닝 계획명, 메모, 활성 블록, 활성 단계 패턴은 원본 값을 유지함.
POST /api/workout/running/plans/{plan_id}/performed
현재 사용자의 러닝 계획을 수행 완료 상태로 표시함.
러닝 세션 이력 테이블이나 별도 기록 payload를 생성하지 않음.
GET /api/workout/running/meta/steps
hub 공통코드 RUNNING_STEP 활성 코드를 sort_order_no 기준으로 반환함.
3.4 내부 API
workout 앱은 운동 계획 생성을 위한 internal workout plans API를 제공하지 않음.
운동 계획 생성은 workout-api 공개 API 안에서 규칙 엔진과 Workout SQLite 저장소를 사용해 완료됨.
hub-api 내부 master-data 조회와 sliding session 갱신에는 기존 HUB_API_INTERNAL_TOKEN_HEADER, HUB_API_INTERNAL_TOKEN 계약을 사용함.
4. 인증 및 권한 흐름
4.1 공통 로그인 진입
workout-api는 독자 로그인 화면을 유지하지 않음.
비로그인 사용자가 보호 페이지에 접근하면 hub-api의 공통 로그인 화면으로 이동함.
이때 required_module=WORKOUT과 현재 전체 URL 기반 redirect를 함께 전달함.
개발 모드에서도 로그인 화면은 hub-api 18001 기준 공통 진입점을 사용함.
개발 모드에서는 요청 헤더의 dev server origin을 사용하여 redirect=http://localhost:15003/...를 유지함.
4.2 페이지 접근 권한
보호 페이지는 WORKOUT 권한을 요구함.
JWT는 COOKIE_NAME 설정값 기준 쿠키 또는 Authorization: Bearer 헤더로 해석함.
현재 기본 쿠키 이름은 JUNKBOX_AUTH임.
allowed_modules에 과거 SYSTEM 값이 남아 있어도 공통 인증 계층은 HUB 기준으로 정규화하여 판단함.
WORKOUT 권한이 없으면 hub-api 공통 로그인 화면으로 이동하며 permission_denied=1을 전달함.
공통 로그인 화면은 권한 없음 alert를 한 번 표시하고 URL의 permission_denied 플래그를 제거함.
4.3 로그인 이후 복귀
공통 로그인 성공 시 redirect 값이 허용 origin이면 원래 요청한 workout URL로 복귀함.
복귀 후 WORKOUT 권한 재검증을 수행함.
15003 개발 서버 경유 요청도 허용 origin에 포함되므로 로그인 후 같은 프런트엔드 origin으로 복귀함.
4.4 슬라이딩 세션 갱신
workout-api는 FastAPI 앱 초기화 시 shared-core의 SlidingSessionMiddleware를 등록함.
운동 세션 편집 화면에서 페이지 이동 없이 저장 API만 반복 호출하는 경우에도 브라우저 쿠키 기반 요청은 슬라이딩 세션 갱신 대상임.
미들웨어는 HUB_INTERNAL_BASE_URL의 POST /api/internal/auth/refresh를 호출해 갱신 필요 여부를 확인함.
workout-api의 JSON API와 HTML 페이지 접근 검증은 일반 사용자 JWT에 대해 hub 내부 인증 API의 세션 유효성 검증을 사용함.
hub 내부 인증 API가 새 토큰을 반환하면 workout-api 응답은 JUNKBOX_AUTH HttpOnly 쿠키를 다시 설정함.
만료되었거나 유효하지 않거나 hub에서 revoke된 토큰은 미들웨어가 억지로 갱신하지 않으며, 표준 인증 실패 흐름에서 로그인 재진입 또는 JSON 401로 처리됨.
5. 데이터 구조
5.1 DB 기반 엔티티
Workout 도메인 데이터 저장소는 SQLite 파일 DB임.
Workout SQLite 파일은 /sorc001/junkbox/db/workout.sqlite3에 위치함.
Docker 컨테이너 내부 표준 경로는 /app/db/workout.sqlite3임.
WORKOUT_DATABASE_URL은 workout-api의 운동 도메인 DB 연결에만 사용함.
전역 DATABASE_URL은 PostgreSQL 잔여 영역에 사용될 수 있으며 Workout 도메인 테이블 저장에는 사용하지 않음.
운동 세션은 WorkoutSessionEntity를 사용함.
운동 기록은 WorkoutRecordEntity를 사용함.
운동 계획 엔티티는 WorkoutPlanEntity를 사용함.
운동 기준정보는 WorkoutExerciseEntity를 사용함.
러닝 계획은 RunningPlanEntity를 사용함.
러닝 블록은 RunningBlockEntity를 사용함.
러닝 단계는 RunningStepEntity를 사용함.
user_id는 Workout SQLite 내부 사용자 FK가 아니라 hub 사용자 식별자에 대한 느슨한 소유자 식별자임.
운동 세션, 운동 기록, 운동 계획은 모두 user_id를 필수 소유자 식별자로 저장함.
러닝 계획, 러닝 블록, 러닝 단계도 모두 user_id를 필수 소유자 식별자로 저장함.
공개 workout API는 인증된 현재 사용자 user_id를 서비스 계층으로 전달하며, 클라이언트가 임의 user id를 전달하는 구조를 사용하지 않음.
Workout SQLite는 workout-api를 단일 writer로 전제함.
SQLite 연결은 PRAGMA journal_mode=WAL, PRAGMA busy_timeout=5000, PRAGMA foreign_keys=ON을 적용함.
Workout SQLModel 엔티티는 PostgreSQL schema qualifier를 사용하지 않음.
Workout JSON 컬럼은 SQLAlchemy 공통 JSON 타입을 사용함.
5.1.1 SQLite 테이블
tb_workout_session_n
workout_session_id 정수 PK
workout_date
condition_score
session_memo_desc
exercise_order JSON
workout_plan_id
user_id
감사 컬럼과 soft delete용 is_active
tb_workout_record_n
workout_record_id 정수 PK
workout_session_id
exercise_id
set_order
weight_amount
rep_count
rpe_score
failure_rep_count
workout_status_code
user_id
감사 컬럼과 soft delete용 is_active
tb_workout_plan_n
workout_plan_id 정수 PK
plan_date
condition_score
routine_mode_code
user_id
감사 컬럼과 soft delete용 is_active
tb_workout_exercise_m
workout_exercise_id 정수 PK
exercise_code 고유 코드
exercise_name
exercise_desc
body_part_code
weight_step_kg
min_weight_kg
max_weight_kg
sort_order_no
감사 컬럼과 soft delete용 is_active
tb_running_plan_n
running_plan_id 정수 PK
running_date
running_name
running_desc
is_performed
user_id
감사 컬럼과 soft delete용 is_active
tb_running_block_n
running_block_id 정수 PK
running_plan_id
block_name
repeat_count
sort_order_no
user_id
감사 컬럼과 soft delete용 is_active
tb_running_step_n
running_step_id 정수 PK
running_block_id
activity_type_code
duration_seconds
target_speed_kmh
target_incline_percent
sort_order_no
user_id
감사 컬럼과 soft delete용 is_active
idx_workout_plan_date는 tb_workout_plan_n(plan_date)에 존재함.
idx_workout_exercise_body_part는 tb_workout_exercise_m(body_part_code)에 존재함.
idx_workout_exercise_sort는 tb_workout_exercise_m(sort_order_no, exercise_name)에 존재함.
idx_running_plan_user는 tb_running_plan_n(user_id, is_active, running_date, updated_at)에 존재함.
idx_running_block_plan_sort는 tb_running_block_n(running_plan_id, sort_order_no)에 존재함.
idx_running_step_block_sort는 tb_running_step_n(running_block_id, sort_order_no)에 존재함.
SQLite 테이블은 다른 SQLite 파일이나 PostgreSQL 테이블에 DB 레벨 FK를 걸지 않음.
러닝 기록 전용 테이블은 사용하지 않으며, tb_running_session_n이 존재하면 시작 시 제거됨.
5.2 운동 세션 저장 대상
세션 기본 정보
운동 그룹
세트 기록
직전 기록 가져오기 시 UI 확인용 조회 데이터는 직전 세션의 실제 기록 상태를 유지하고, 세션 반영 시에는 PLAN 상태 세트로 변환함.
5.3 운동 계획 저장 대상
운동 계획 기본 정보
계획 생성 시 세션과 PLAN 상태 레코드를 동시에 생성함.
tb_workout_plan_n은 계획 헤더 snapshot 역할만 담당하며 운동별 세트 내용은 연결된 tb_workout_session_n과 tb_workout_record_n에 저장함.
계획 상세 조회는 계획에 연결된 활성 세션과 활성 세트 레코드를 기준으로 루틴 카드 데이터를 구성함.
계획 상세 조회는 세트 상태가 PLAN에서 SUCCESS 또는 FAIL로 바뀐 뒤에도 같은 연결 세션의 활성 레코드를 기준으로 표시함.
사용자가 직접 운동 세션을 생성하거나 수정하면서 workout_plan_id를 전달하는 경우 해당 계획이 현재 사용자 소유인지 확인한 뒤 세션에 연결함.
다른 사용자의 운동 계획 ID는 현재 사용자의 운동 세션에 연결할 수 없음.
5.4 러닝 계획 저장 대상
러닝 계획은 근력운동 세션과 레코드 테이블을 확장하지 않고 별도 러닝 테이블에 저장함.
러닝 계획은 tb_running_plan_n에 날짜, 이름, 메모, 수행 여부를 저장함.
러닝 블록은 tb_running_block_n에 카드 이름, 반복 횟수, 정렬 순서를 저장함.
러닝 단계는 tb_running_step_n에 운동 유형 코드, 실행 시간, 목표 속도, 목표 경사도, 정렬 순서를 저장함.
러닝 단계 이름은 별도 입력값이나 저장 컬럼으로 관리하지 않음.
러닝 단계 표시명은 activity_type_code와 hub 공통코드 RUNNING_STEP.common_code_name을 기준으로 해석함.
반복 블록은 실행 시점에 timeline으로 펼쳐 사용하며, 저장소에는 반복 패턴만 저장함.
반복 블록의 내부 단계를 반복 횟수만큼 미리 복제해 저장하지 않음.
러닝 완료 여부는 계획 단위 is_performed로 관리함.
러닝 계획 복사는 기존 활성 계획을 현재 사용자 소유 기준으로 조회한 뒤 신규 tb_running_plan_n row와 신규 블록/단계 row를 생성함.
복사된 러닝 계획의 running_date는 서버 기준 금일이며 is_performed=false로 저장함.
러닝 계획 복사 시 원본 계획, 원본 블록, 원본 단계 row는 수정하지 않음.
러닝을 마지막 단계까지 완료하면 POST /api/workout/running/plans/{plan_id}/performed가 해당 계획을 수행 완료 상태로 갱신함.
러닝 수행 결과의 거리, 평균 속도, 평균 심박수, 체감 난이도, 메모는 현재 저장 계약에 포함하지 않음.
러닝 세션 이력 목록과 상세 조회 API는 제공하지 않음.
5.5 입력 규칙
운동 종목은 workout SQLite의 tb_workout_exercise_m 기준으로 조회함.
운동 종목 코드는 exercise_code를 사용하며 세션 레코드의 exercise_id와 같은 식별자 계약을 유지함.
신규 운동 기준정보 생성 요청에서 exercise_code가 비어 있으면 tb_workout_exercise_m.exercise_code 중 숫자 코드의 최대값에 1을 더해 자동 채번함.
운동 기준정보 수정 시 URL path의 exercise_code와 payload의 exercise_code가 달라지면 저장하지 않음.
운동 기준정보 삭제는 물리 삭제가 아니라 is_active=false soft delete로 처리함.
운동 부위는 hub 공통코드 BODY_PART를 기준으로 검증하고, 운동 기준정보의 body_part_code에 저장함.
운동 계획 루틴 모드는 공통코드 WORKOUT_ROUTINE_MODE를 기준으로 조회함.
운동 계획 루틴 모드 option은 활성 WORKOUT_ROUTINE_MODE 공통코드를 그대로 반환함.
GET /api/workout/plans/default-routine-mode는 현재 사용자의 가장 최근 실제 수행 record 1건을 기준으로 운동 계획 화면의 기본 루틴 모드를 계산함.
기본 루틴 모드 계산은 PLAN 상태 레코드를 제외하고, 세션과 레코드 양쪽에 현재 사용자 user_id와 활성 필터를 적용함.
최신 실제 수행 record의 운동 종목이 UPPER 부위이면 기본값은 LOWER, LOWER 부위이면 기본값은 UPPER, 기록이 없거나 부위를 판별할 수 없으면 UPPER임.
부위 필터는 hub 공통코드 BODY_PART와 운동 기준정보의 body_part_code 기준으로 동작함.
중량 선택 규칙은 운동 기준정보의 weight_step_kg를 step, min_weight_kg를 최소값, max_weight_kg를 최대값으로 사용함.
세트 상태는 계획, 성공, 실패 순환 배지 구조를 사용함.
상태가 성공이면 RPE 입력만 사용함.
상태가 실패면 실패 지점 입력만 사용함.
직전 기록 조회는 현재 로그인 사용자, 선택한 운동 종목, 현재 화면의 운동일과 세션 식별자를 기준으로 직전 운동 세션의 해당 운동 기록 전체를 반환함.
직전 기록 조회는 세션과 레코드 양쪽에 현재 사용자 user_id 필터를 적용함.
current_session_id가 전달되면 현재 세션은 조회 대상에서 제외됨.
workout_date와 current_session_id가 함께 전달되면 workout_date보다 이전 세션을 우선 대상으로 하며, 같은 운동일에서는 현재 세션보다 작은 세션 식별자만 직전 후보로 사용함.
workout_date만 전달되면 해당 운동일 이하의 세션 중 해당 운동이 포함된 가장 최근 세션을 후보로 사용함.
직전 후보 세션 안에 같은 운동 종목의 기록이 여러 묶음 또는 여러 세트로 존재하면 모두 반환함.
직전 후보가 없으면 workout_date=null, records=[] 응답을 반환하며 화면은 직전 기록 없음 모달 상태를 표시함.
세션 목록 카드의 운동 요약은 해당 세션에서 수행한 운동 종목을 중복 제거한 뒤 tb_workout_exercise_m.sort_order_no 오름차순 기준으로 정렬함.
운동 종목 정렬 순서는 세션 입력 순서나 기록 생성 순서가 아니라 workout 운동 기준정보의 정렬 계약을 따름.
러닝 단계 유형은 hub 공통코드 RUNNING_STEP을 기준으로 조회함.
러닝 단계 유형 option 정렬은 RUNNING_STEP.sort_order_no를 따름.
러닝 단계의 duration_seconds는 필수값이며 1초 이상이어야 함.
러닝 단계의 target_speed_kmh와 target_incline_percent는 선택값임.
러닝 계획 목록 정렬은 running_date desc, updated_at desc, created_at desc, running_plan_id desc 기준임.
5.6 SQLite 이관 및 파일 관리
Workout SQLite DB 생성과 데이터 이관은 애플리케이션 이미지 build/recreate와 분리된 운영 절차임.
PostgreSQL dump 기반 Workout 이관 스크립트는 scripts/migrate_workout_pg_dump_to_sqlite.py임.
이관 스크립트는 PostgreSQL dump의 workout.tb_workout_plan_n, workout.tb_workout_record_n, workout.tb_workout_session_n COPY ... FROM stdin 블록을 기준으로 세션·기록·계획 데이터를 적재함.
이관 스크립트는 legacy dump에 계획 AI 컬럼이 존재해도 신규 Workout SQLite 계획 테이블에는 계획 헤더 컬럼만 적재함.
운동 기준정보 테이블은 runtime bootstrap 계약과 같은 tb_workout_exercise_m 스키마를 사용함.
이관 스크립트 기본 입력 dump 경로는 프로젝트 루트 dbjbp1_20260719-080001.sql임.
이관 스크립트 기본 출력 DB 경로는 /sorc001/junkbox/db/workout.sqlite3임.
이관 스크립트는 \N을 NULL로, PostgreSQL boolean t/f를 SQLite 정수 1/0으로, JSONB 값을 SQLite JSON text로 변환함.
--force 옵션은 기존 SQLite DB 파일을 덮어쓰므로 운영 적용 전 백업을 전제로 사용함.
Workout SQLite 파일과 WAL sidecar 파일은 Git 추적 대상이 아님.
5.7 운동 기준정보 bootstrap
workout-api 시작 시 운동 기준정보 테이블이 없으면 tb_workout_exercise_m 스키마와 운동 기준정보 인덱스를 보장함.
초기 bootstrap은 hub 공통코드 EXERCISE 값을 workout 운동 기준정보 테이블로 적재할 수 있음.
bootstrap 시 EXERCISE.common_code는 exercise_code, common_code_name은 exercise_name, common_code_desc는 exercise_desc, parent_common_code는 body_part_code로 매핑함.
bootstrap 시 EXERCISE.attribute01은 weight_step_kg, attribute02는 min_weight_kg, attribute03은 max_weight_kg로 매핑함.
bootstrap은 이미 존재하는 exercise_code를 덮어쓰지 않음.
런타임 운동 선택, 세션 저장, 계획 생성, 운동 관리 화면은 bootstrap 이후 workout SQLite의 운동 기준정보를 기준으로 동작함.
workout-api 시작 시 계획 테이블은 usage_log_id, user_request_desc, plan_content, ai_advice_desc 없이 동작하는 현재 스키마를 보장함.
workout-api 시작 시 러닝 계획, 러닝 블록, 러닝 단계 테이블이 없으면 현재 러닝 스키마를 보장함.
workout-api 시작 시 러닝 계획 테이블의 running_date, is_performed 컬럼과 러닝 계획 인덱스를 현재 스키마 기준으로 보장함.
workout-api 시작 시 러닝 단계 테이블에 legacy step_name 컬럼이 있으면 제거함.
workout-api 시작 시 legacy 러닝 세션 테이블 tb_running_session_n이 있으면 제거함.
6. 운동 계획 생성 구조
6.1 공개 생성 API 책임
POST /api/workout/plans/generate는 현재 로그인 사용자 기준으로 규칙 엔진 기반 운동 계획을 생성함.
공개 API의 생성 요청은 컨디션 점수와 루틴 모드 코드를 포함함.
공개 API는 LLM provider, hub-api AI 오케스트레이션, SSE 스트림, AI 응답 파싱, JSON plan payload 저장을 사용하지 않음.
생성 성공 시 운동 계획 헤더, 연결 세션, PLAN 상태 세트 레코드를 한 트랜잭션으로 저장하고 저장된 계획 DTO를 반환함.
6.2 루틴별 운동 선택
규칙 엔진은 workout SQLite의 활성 운동 기준정보에서 운동명을 기준으로 대상 운동을 찾음.
상체 루틴은 렛 풀다운, 체스트 프레스, 숄더 프레스, 리어 델트 플라이, 펙 델트 플라이, 인클라인 바이셉스 컬 순서를 사용함.
하체 루틴은 옴니 레그 프레스, 라잉 레그 컬, 레그 익스텐션, 힙 어덕션 순서를 사용함.
루틴 모드 조회 API는 hub 공통코드 WORKOUT_ROUTINE_MODE의 활성 row를 그대로 반환함.
규칙 엔진의 명시 계산 대상은 UPPER, LOWER이며 알 수 없는 루틴 모드는 상체 루틴으로 정규화함.
대상 운동명이 workout 운동 기준정보에 없거나 비활성 상태이면 해당 운동은 생성 결과에서 제외됨.
6.3 기준 탑세트 산정
다음 계획 계산은 같은 사용자의 같은 운동에 대한 가장 최근 유효 운동 세션 하나를 먼저 찾음.
유효 후보는 활성 세션과 활성 기록, SUCCESS 상태, 중량, 반복수, RPE가 모두 있는 레코드임.
가장 최근 유효 세션 안에서 성공 세트 중 중량이 가장 큰 세트를 탑세트로 간주함.
중량이 같은 성공 세트가 여러 개이면 반복수가 가장 큰 세트를 탑세트로 간주함.
과거 최고 중량 또는 과거 최고 반복수는 다음 계획 계산 기준으로 사용하지 않음.
6.4 진행 규칙
유효 탑세트가 없으면 운동 기준정보의 min_weight_kg + weight_step_kg를 기본 탑세트 중량으로 사용하고 목표 반복수는 8회로 시작함.
기본 탑세트 중량은 max_weight_kg가 있으면 최대값을 넘지 않도록 제한함.
탑세트보다 한 단계 낮은 중량은 weight_step_kg만큼 낮춘 값이며, 낮출 수 없으면 min_weight_kg를 사용함.
직전 유효 탑세트 반복수가 8회 미만이면 같은 중량에서 목표 8회를 다시 계획함.
직전 유효 탑세트 RPE가 10 이상이면 중량과 실제 성공 반복수를 유지함.
직전 유효 탑세트 반복수가 8~11회이고 RPE가 9 이하이면 같은 중량에서 목표 반복수를 1회 올림.
직전 유효 탑세트 반복수가 12회 이상이고 RPE가 9 이하이면 중량을 한 단계 올리고 목표 반복수는 8회로 되돌림.
증량 결과는 max_weight_kg가 있으면 최대값을 넘지 않도록 제한함.
6.5 세트 구성과 저장
각 운동 계획은 기본 3세트로 저장함.
1세트는 탑세트보다 한 단계 낮은 중량과 8회 반복으로 저장함.
2세트는 계산된 탑세트 중량과 목표 반복수로 저장함.
3세트는 탑세트보다 한 단계 낮은 중량과 10회 반복으로 저장함.
생성된 운동 순서는 연결 세션의 exercise_order에도 저장함.
저장되는 운동 계획, 연결 세션, PLAN 상태 세트 레코드는 현재 로그인 사용자의 user_id를 동일하게 사용함.
6.6 사용자별 데이터 격리
운동 세션 목록, 세션 상세, 세션 생성/수정/삭제, 직전 기록 조회, 기록 다운로드, 날짜 범위 조회는 현재 로그인 사용자 기준으로 동작함.
운동 계획 최신 조회, 운동 계획 상세 조회, 운동 계획 생성, 연결 세션 조회는 현재 로그인 사용자 기준으로 동작함.
세션 상세와 직전 기록 조회는 세션 테이블과 레코드 테이블 양쪽에 사용자 필터를 적용함.
기록 다운로드는 세션 테이블과 레코드 테이블 양쪽에 사용자 필터를 적용하며 PLAN 상태 레코드는 다운로드 대상에서 제외함.
세션 생성/수정 시 전달된 workout_plan_id는 현재 사용자 소유 계획인지 검증한 뒤 연결함.
사용자별 데이터 격리 규칙은 화면 단이 아니라 API/service 계층에서 강제함.
7. 공통 master-data 소비 구조
공통 master-data의 Source of Truth는 hub-api임.
workout-api는 공통 코드와 메타를 로컬 파일에서 직접 읽지 않음.
workout-api는 shared-core.masterdata.MasterDataService를 통해 hub-api의 internal master-data API를 조회함.
내부 호출은 HUB_INTERNAL_BASE_URL, HUB_API_INTERNAL_TOKEN, HUB_API_INTERNAL_TOKEN_HEADER 설정을 사용함.
조회 결과는 shared-core의 TTL 캐시에 저장함.
캐시 TTL은 MASTERDATA_CODES_TTL_SECONDS, MASTERDATA_METAS_TTL_SECONDS 설정으로 제어함.
내부 API 호출 타임아웃은 MASTERDATA_HTTP_TIMEOUT_SECONDS 설정으로 제어함.
workout-api는 앱 favicon, apple touch icon, PWA manifest icon slug를 포털 메뉴의 APP_ICON + icon_val에서 우선 해석함.
포털 메뉴 조회 또는 매칭에 실패하면 workout 앱 아이콘 slug를 fallback으로 사용함.
workout-api는 운동 부위 BODY_PART, 운동 계획 루틴 모드 WORKOUT_ROUTINE_MODE, 운동 상태 WORKOUT_STATUS, 앱 아이콘 계약을 hub master-data에서 조회함.
workout-api는 러닝 단계 유형 RUNNING_STEP을 hub master-data에서 조회함.
운동 종목 EXERCISE 공통코드는 workout 운동 기준정보 bootstrap의 초기 source로만 사용하며, 일반 런타임의 운동 기준정보 조회와 저장은 workout SQLite tb_workout_exercise_m을 기준으로 함.
APP_PROFILE=local에서 HUB_API_INTERNAL_TOKEN이 비어 있으면 개발 편의용 local store fallback을 사용할 수 있음.
운영 workout 이미지에는 공통 master-data 로컬 원본 데이터가 포함되지 않으므로, 운영에서는 HUB_API_INTERNAL_TOKEN과 HUB_INTERNAL_BASE_URL 설정이 필수임.
캐시가 없고 내부 API 호출도 실패하면 공통 master-data 의존 기능은 오류 응답을 반환함.
8. 화면 구조
8.1 공통 구조
workout 화면은 모두 모바일 전용 360px 고정 폭 기준을 사용함.
데스크톱 반응형 확장을 기본 전제로 두지 않음.
공통 앱 셸, 공통 다크 톤, 공통 토스트 구조는 shared-ui를 사용함.
workout 전용 화면도 페이지 전용 CSS 없이 shared-ui 공통 클래스 조합으로 구성함.
서버 템플릿 문구는 템플릿 또는 라우터 컨텍스트에 한국어 문구로 직접 작성함.
공통코드 라벨과 메타 설정값처럼 데이터 자체가 마스터데이터인 값만 DB 조회 구조를 사용함.
보호 화면 서버 라우트는 인증과 권한 검증 후 공통 workout.html 앱 셸을 반환함.
화면 전환은 클라이언트 라우팅이 담당하고, 새로고침이나 직접 URL 접근 시에는 서버가 같은 앱 셸을 다시 내려줌.
보호 화면 앱 셸은 spa_navigation=true 컨텍스트를 사용하여 공통 앱 셸 링크 클릭을 React SPA navigation 이벤트로 전환할 수 있게 함.
서버 웹 라우트는 화면별 템플릿을 따로 렌더링하지 않고, 같은 앱 셸과 공통 page_props만 제공함.
8.2 세션 목록 화면
/workout-sessions는 카드형 목록 화면임.
상단에는 세션 개수와 페이지 크기 선택을 배치함.
페이지 크기 선택은 shared-ui 공통 segmented tab 계약을 사용함.
각 카드에는 운동 날짜와 요일, 컨디션, 세트 수, 운동 요약, 계획 연결 여부를 표시함.
운동 날짜는 YYYY-MM-DD (요일) 형식의 일반 날짜 텍스트로 표시하며 별도 뱃지나 장식 요소를 사용하지 않음.
하단 고정 액션바에서 다운로드, 새 세션 생성, 운동 계획 생성 진입을 제공함.
하단 고정 액션바의 계획 생성 버튼은 /workout-plans로 이동함.
/workout-plans는 앱 셸 상단 메뉴에는 독립 항목으로 노출하지 않고, /workout-sessions의 하위 사용자 흐름으로 유지함.
8.3 세션 편집 화면
/workout-sessions/new, /workout-sessions/{id}/edit는 공통 폼 구조를 사용함.
생성/수정 모드는 React Router path와 path parameter에서 결정함.
서버 템플릿은 생성/수정 화면별 bodyParts, defaultDate, sessionId를 주입하지 않으며, 화면은 필요한 메타와 기본 운동일을 API로 조회함.
상단에는 운동 날짜, 컨디션, 메모 입력을 배치함.
아래에는 운동 그룹 목록과 세트 입력 영역을 배치함.
신규 세션 저장은 생성 API 호출 뒤 전체 페이지 리로드 없이 같은 화면에서 edit 상태로 전환함.
숫자 입력은 텍스트 직접 입력보다 하단 고정 선택 시트 구조를 우선 사용함.
세트 입력 그리드의 중량 헤더는 한국어 문구 중량(kg)로 표기함.
세트 입력 그리드 헤더는 공통 스타일에서 대문자 변환을 강제하지 않으므로 kg 소문자를 유지함.
숫자 선택 시트와 직전 기록 모달은 열기 전 window.scrollY를 보존하고 닫기, 가져오기, 오류 처리 후 원래 위치로 복원함.
숫자 선택 시트의 숫자 옵션은 중앙 정렬함.
직전 기록 조회, 세트 추가, 그룹 순서 변경, 세트 삭제를 화면 안에서 처리함.
저장 성공 토스트는 한국어 문구 저장했습니다. 기준으로 표기함.
주요 액션은 하단 고정 액션바를 사용함.
하단 고정 액션바는 목록 아이콘 -> 연결 아이콘 -> 삭제 아이콘 -> 추가 -> 저장 순서로 배치함.
목록, 연결, 삭제는 최소 폭 아이콘 버튼을 사용하고, 추가, 저장은 남은 폭을 균등 분할함.
추가는 운동 추가 바텀시트를 여는 UI 액션이므로 고스트 버튼을 사용함.
삭제, 저장은 즉시 CUD를 일으키는 액션이므로 흰색 강조 버튼을 사용함.
하단 고정 액션바는 모바일 viewport 기준으로 고정되며, 라우트 전환 컨테이너의 containment나 animation 때문에 페이지 끝으로 밀리지 않아야 함.
연결 버튼은 workout_plan_id가 있는 세션에서만 노출함.
운동 그룹 카드 내부의 직전 기록, 추가, 삭제 보조 액션은 우측 정렬을 사용함.
직전 기록 버튼은 즉시 세트 반영이 아니라 모달 표시 후 가져오기를 누르고 확인창에서 승인해야 확정하는 구조를 사용함.
직전 기록 가져오기 확인창 문구는 화면 컴포넌트에 한국어 문구로 직접 작성함.
직전 기록 모달은 세트별 상태, 중량, 반복, 성공 시 RPE, 실패 시 FP를 보여주고, 상태 색상은 세트 입력 화면과 같은 규칙을 사용함.
운동 선택 바텀시트와 직전 기록 시트의 닫기 버튼은 고스트 버튼을 사용하며, 버튼 텍스트가 줄바꿈되지 않도록 최소 가로폭을 확보함.
운동 선택 바텀시트의 운동 부위 필터는 shared-ui 공통 segmented tab 계약을 사용함.
세트 입력 그리드는 헤더와 행이 동일한 컬럼 템플릿을 사용하며, 첫 번째 세트 번호 칼럼 폭을 가장 좁게 사용함.
8.4 다운로드 흐름
다운로드는 범위 선택 후 CSV 또는 XLSX 형식으로 수행함.
다운로드 진입 UI는 목록 화면 하단 액션과 바텀시트/모달 흐름을 사용함.
8.5 운동 계획 화면
/workout-plans는 단일 생성/조회 화면임.
기본 진입 시 현재 로그인 사용자의 가장 최근 운동 계획을 보여줌.
상단에는 컨디션 슬라이더와 운동 구분 모바일 선택 버튼을 배치함.
운동 구분 선택지는 GET /api/workout/meta/routine-modes 응답을 사용하며, 프런트엔드는 BottomOptionPicker 기반 하단 선택 시트로 표시함.
최근 계획을 불러오더라도 상단 입력 영역은 condition=100 기준으로 유지함.
생성 버튼은 POST /api/workout/plans/generate를 호출함.
결과 영역에는 규칙 엔진이 저장한 루틴 카드 목록을 보여줌.
루틴 카드의 kg 표기는 정수와 불필요한 trailing zero를 제거하고 실제 소수부가 있을 때만 소수점을 유지함.
하단 버튼으로 연결된 운동 기록 화면으로 이동할 수 있음.
연결 세션이 삭제된 경우 이동 대신 삭제된 운동 세션입니다. 토스트를 보여줌.
현재 /workout-plans의 실제 런타임은 apps/workout-ui 단일 SPA 엔트리와 React Router 구조임.
8.6 PWA 설치형 실행
workout 웹앱은 안드로이드 Chrome 기준 홈 화면 설치형 진입을 지원함.
GET /workout.webmanifest는 display=standalone, 앱 이름, 시작 경로, 테마 색상, 아이콘 정보를 제공함.
manifest의 icons 항목은 /shared-ui/app-icons/{icon_val}/icon-192.png, /shared-ui/app-icons/{icon_val}/icon-512.png 형식의 공통 앱 아이콘 자산을 사용함.
apple-touch-icon은 같은 앱 아이콘 slug의 icon-180.png를 사용함.
service worker는 설치 조건 충족과 standalone 진입 지원을 위한 최소 범위로 동작함.
오프라인 캐시 전략은 기본 구조에 포함하지 않음.
9. 프런트엔드 자산 구조
운동 세션 목록, 세션 편집, 운동 계획 화면은 apps/workout-ui의 단일 SPA 엔트리를 사용함.
대표 엔트리는 workout.tsx임.
서버 템플릿은 page_props JSON, React 마운트 루트, 단일 엔트리 스크립트만 제공함.
page_props는 사용자 같은 앱 공통 초기값 중심으로 사용하고, 화면별 식별자는 URL path/query에서 해석함.
서버 템플릿은 클라이언트 라우트별 초기 상세 데이터, 생성 기본일, 운동 메타를 주입하지 않음.
생성 기본일은 GET /api/workout/meta/default-workout-date가 제공하고, 운동 메타와 상세 데이터는 React 화면이 필요한 시점에 API로 조회함.
실제 목록 조회, 저장, 삭제, 직전 기록 조회, 운동 계획 생성은 화면별 TypeScript 코드가 API를 호출하여 수행함.
공통 토스트, 공통 SVG 아이콘, 공통 숫자 선택 시트는 libs/shared-ui/src/react 공통 계층을 사용함.
공통 탭, 필터, 보기 전환 UI는 libs/shared-ui/src/react의 segmented tab 공통 계층을 사용함.
React 화면 문구는 컴포넌트 코드에 한국어 문구로 직접 작성함.
운영 자산은 manifest 기준 해시 파일을 사용함.
공통 CSS/JS는 /shared-ui/...?... 버전 쿼리 링크를 사용하여 CDN이 이전 배포의 고정 경로 자산을 계속 재사용하지 않도록 함.
개발 자산은 Vite 개발 서버 기준 /workout-ui/src/entries/workout.tsx, /workout-ui/@vite/client, /workout-ui/@react-refresh 경로를 사용함.
10. 공통 의존 구조
10.1 공통 로그인
공통 로그인과 권한 복귀 흐름은 hub-api에 의존함.
개발 모드에서도 /api/auth, /api/hub, /api/portal, /login은 hub-api 18001로 프록시됨.
운영 모드에서 hub-api 로그인 화면 리다이렉트는 HUB_BASE_URL 설정값을 기준으로 생성함.
운영 HUB_BASE_URL이 비어 있거나 잘못되면 localhost:18001 기본값으로 fallback될 수 있으므로, 운영 .env.prod의 절대 URL 값이 중요함.
10.2 공통 마스터 데이터
운동 부위와 상태 규칙은 hub-api가 관리하는 공통코드에 의존함.
운동 종목 기준정보는 workout SQLite tb_workout_exercise_m에 저장하고 workout-api가 관리함.
hub 공통코드 EXERCISE는 workout 운동 기준정보 bootstrap의 초기 source로만 사용함.
운동 계획 루틴 모드는 hub-api가 관리하는 WORKOUT_ROUTINE_MODE 공통코드에 의존함.
화면 문구는 React 컴포넌트와 서버 템플릿에 한국어 문구로 직접 작성함.
10.3 공통 UI
앱 셸, 카드, 입력창, 상태 배지, 바텀시트, 토스트는 shared-ui에 의존함.
SPA navigation 이벤트, View Transition 기반 라우트 전환, route settling, skeleton, 안정 로딩, 터치 피드백은 shared-ui 공통 React 계층과 base CSS를 사용함.
모바일 고정 폭, 모바일 바텀 액션바, 모바일 시트는 shared-ui mobile CSS를 사용함.
10.4 AI 비의존
workout-api와 workout-ui는 운동 계획 생성에 libs/shared-ai를 사용하지 않음.
workout 앱은 운동 계획 생성용 LLM 호출, 프롬프트 메타, LangGraph 실행, SSE trace, usage log 연결, AI 조언 저장을 런타임 계약으로 사용하지 않음.
hub 앱의 공통 AI 기능, AI usage log, AI 라우팅 정책은 workout 앱 범위 밖의 공통 기능으로 유지됨.
10.5 규칙 엔진 의존
운동 계획 생성 공개 진입점과 실행 주체는 모두 workout-api임.
workout-api는 workout SQLite의 운동 기준정보, 운동 세션, 운동 기록, 운동 계획 테이블을 사용해 계획 생성과 저장을 완료함.
workout-ui는 생성 요청과 조회 결과만 소비하며, 운동별 중량·반복수·세트 수를 클라이언트에서 재계산하지 않음.
10.6 개발 모드 프록시 구조
/api/workout, /workout-sessions, /error/403, /shared-ui, /health는 workout-api 18003으로 프록시됨.
/error/403 프록시는 하위 호환 진입점 유지를 위한 것이며 별도 권한 오류 페이지를 의미하지 않음.
/workout-plans도 workout-api 18003으로 프록시됨.
/running-plans, /running-run, /workout-exercises도 workout-api 18003으로 프록시됨.
/workout.webmanifest, /workout-sw.js도 개발 모드에서 workout-api 18003으로 프록시됨.
PWA manifest와 service worker는 서버 렌더링 HTML의 동일 origin 링크에서 요청되므로, Vite 개발 서버 origin으로 접근하는 경우에도 해당 경로를 workout-api로 프록시해야 함.
공통 로그인과 hub 기능은 hub-api 18001로 프록시됨.
이 구조를 통해 workout 개발 시 hub-ui 프런트 개발 서버를 별도로 실행하지 않아도 공통 인증과 hub API를 사용할 수 있음.
11. 구현 제약
workout 화면도 페이지 전용 CSS를 추가하지 않음.
숫자 입력과 상태 입력은 현재 공통 모바일 패턴을 유지함.
workout-ui 빌드 산출물 경로를 하드코딩하지 않고 manifest 기준을 사용함.
Vite 개발 자산 경로와 React refresh preamble은 공통 템플릿 계층에서 처리하며, 개별 workout 화면 템플릿에 중복 선언하지 않음.
공통코드를 앱 내부 상수로 중복 정의하지 않음.
운동 계획 sets는 기본 3세트를 전제로 하며, 1세트와 3세트는 탑세트보다 한 단계 낮은 중량을 사용함.
운동 계획 생성 실패 시 AI 방식 fallback 또는 재시도 그래프를 사용하지 않음.
운영 이미지에는 apps/workout-ui/dist와 libs/shared-ui 자산을 포함하고, 공통 master-data 로컬 원본 데이터와 Workout SQLite DB 파일은 포함하지 않음.
12. 관련 문서
프로젝트 전체 구조는 docs/junkbox/common/guide-jb-core.md를 따름.
공통 인증 및 운영 허브 구조는 docs/junkbox/apps/guide-apps-hub-api.md를 따름.
workout 프런트 앱 구조는 docs/junkbox/apps/guide-apps-workout-ui.md를 따름.
공통 마스터 데이터 규칙은 docs/junkbox/common/guide-jb-common-master-data.md를 따름.
공통 UI 원칙은 docs/junkbox/common/guide-jb-common-ui.md를 따름.
공통 기반 구조는 docs/junkbox/libs/guide-lib-shared-core.md, docs/junkbox/libs/guide-lib-shared-ui.md를 따름.