Skip to main content

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-api18003 포트 기준 백엔드로 실행할 수 있음.
  • apps/workout-ui15003 포트 기준 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_URLsqlite+aiosqlite:////app/db/workout.sqlite3임.
  • 공통 애플리케이션 로그 파일 경로는 /app/logs/app.log임.
  • 앱 import 시점에 shared-coresetup_app_logging()을 호출하며, 설정값은 settings.modules.workout.logging을 사용함.
  • 앱 import 시점에 shared-corecreate_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-coreSlidingSessionMiddleware를 등록함.
  • 운동 세션 편집 화면에서 페이지 이동 없이 저장 API만 반복 호출하는 경우에도 브라우저 쿠키 기반 요청은 슬라이딩 세션 갱신 대상임.
  • 미들웨어는 HUB_INTERNAL_BASE_URLPOST /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_URLworkout-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_datetb_workout_plan_n(plan_date)에 존재함.
  • idx_workout_exercise_body_parttb_workout_exercise_m(body_part_code)에 존재함.
  • idx_workout_exercise_sorttb_workout_exercise_m(sort_order_no, exercise_name)에 존재함.
  • idx_running_plan_usertb_running_plan_n(user_id, is_active, running_date, updated_at)에 존재함.
  • idx_running_block_plan_sorttb_running_block_n(running_plan_id, sort_order_no)에 존재함.
  • idx_running_step_block_sorttb_running_step_n(running_block_id, sort_order_no)에 존재함.
  • SQLite 테이블은 다른 SQLite 파일이나 PostgreSQL 테이블에 DB 레벨 FK를 걸지 않음.
  • 러닝 기록 전용 테이블은 사용하지 않으며, tb_running_session_n이 존재하면 시작 시 제거됨.

5.2 운동 세션 저장 대상

  • 세션 기본 정보
    • 운동 날짜
    • 컨디션 점수
    • 메모
  • 운동 그룹
    • 운동 종목
    • 부위
    • 정렬 순서
  • 세트 기록
    • 중량
    • 반복
    • 상태
    • RPE 또는 실패 지점
  • 직전 기록 가져오기 시 UI 확인용 조회 데이터는 직전 세션의 실제 기록 상태를 유지하고, 세션 반영 시에는 PLAN 상태 세트로 변환함.

5.3 운동 계획 저장 대상

  • 운동 계획 기본 정보
    • 계획 날짜
    • 컨디션 점수
    • 루틴 모드 코드
  • 계획 생성 시 세션과 PLAN 상태 레코드를 동시에 생성함.
  • tb_workout_plan_n은 계획 헤더 snapshot 역할만 담당하며 운동별 세트 내용은 연결된 tb_workout_session_ntb_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_datecurrent_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_kmhtarget_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_codeexercise_code, common_code_nameexercise_name, common_code_descexercise_desc, parent_common_codebody_part_code로 매핑함.
  • bootstrap 시 EXERCISE.attribute01weight_step_kg, attribute02min_weight_kg, attribute03max_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_TOKENHUB_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.webmanifestdisplay=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, /loginhub-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, /healthworkout-api 18003으로 프록시됨.
  • /error/403 프록시는 하위 호환 진입점 유지를 위한 것이며 별도 권한 오류 페이지를 의미하지 않음.
  • /workout-plansworkout-api 18003으로 프록시됨.
  • /running-plans, /running-run, /workout-exercisesworkout-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/distlibs/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를 따름.