JunkBox apps/workout-ui 가이드
1. 애플리케이션 역할
apps/workout-ui는 workout 운영 화면 전용 프런트엔드 앱임.
workout-api가 제공하는 서버 렌더링 앱 셸 위에 React SPA를 마운트하는 역할을 담당함.
- 운동 세션 목록, 세션 편집, 운동 계획, 운동 관리, 러닝 계획, 러닝 실행 화면 라우트를 단일 React 엔트리에서 소유함.
- 공통 스타일과 공통 React 유틸은
libs/shared-ui를 소비함.
2. 런타임 구성
- 번들러는 Vite 6 기반 React + TypeScript 구조를 사용함.
- 운영 산출물은
apps/workout-ui/dist에 생성됨.
workout-api는 이 디렉터리를 /workout-ui 경로로 직접 마운트함.
- 개발 서버 기본 포트는
15003임.
- 루트 진입
/, /workout-ui, /workout-ui/는 /workout-sessions로 리다이렉트됨.
- workout 앱은
workout-api가 제공하는 manifest와 service worker, shared-ui 앱 아이콘 자산과 함께 설치형 PWA 진입을 지원함.
3. 엔트리 구조
src/entries/workout.tsx
- 단일 엔트리는
BrowserRouter와 WorkoutApp을 마운트함.
- 화면 전환은 React Router가 담당하며, 서버 앱 셸의 상단 네비게이션 클릭은
shared-ui SPA navigation 이벤트를 통해 React Router로 전달됨.
WorkoutApp은 /workout-sessions, /workout-sessions/new, /workout-sessions/:sessionId/edit, /workout-plans, /workout-exercises, /workout-exercises/new, /workout-exercises/:exerciseCode/edit, /running-plans, /running-plans/new, /running-plans/:planId/edit, /running-run/:planId 라우트를 소유함.
WorkoutApp은 junkbox:spa-navigate 이벤트를 수신해 앱 셸 네비게이션을 React Router navigation으로 연결함.
WorkoutApp은 라우트 본문을 SpaRouteTransition으로 감싸고, 라우트 변경 후 useSpaRouteSettling을 호출함.
4. 페이지 구조
- 단일 엔트리는 서버 템플릿이 주입한
workout-ui-root, workout-ui-page-props를 기준으로 마운트함.
workout-ui-page-props는 앱 공통 초기값 중심으로 사용하고, 화면별 식별자와 모드는 URL path/query를 기준으로 해석함.
- 페이지 구현은
src/pages/*.tsx에 위치함.
- 운동 도메인 응답 계약과 화면 props 계약은
src/types/models.ts 기준으로 관리함.
- 브라우저 전역 타입과
import.meta.env 타입은 src/global.d.ts 기준으로 관리함.
5. 공통 의존 구조
- 공통 React 유틸은
@shared-ui/* alias를 통해 libs/shared-ui/src/react를 참조함.
- 공통 SVG 아이콘, 토스트, HTTP 요청, 페이지 props 파서, 숫자 선택 시트를 직접 복제하지 않음.
- 공통 CSS는 서버 템플릿이
shared-ui stylesheet 매크로를 호출해 hub-ui-base.css + hub-ui-mobile.css 조합으로 로드함.
- workout 모바일 화면은 Tabulator를 사용하지 않으며, 서버 템플릿은 Tabulator CSS를 로드하지 않음.
- SPA 전환, skeleton, 터치 피드백, 로딩 안정화 같은 화면 크기 무관 공통 장치는
shared-ui base CSS와 React 계층에서 소비함.
- 모바일 고정 폭, 바텀 액션바, 바텀시트 같은 모바일 레이아웃 종속 규칙만
shared-ui mobile CSS에서 소비함.
6. 개발 서버 프록시 구조
/api/workout, /workout-sessions, /workout-plans, /workout-exercises, /running-plans, /running-run, /error/403, /shared-ui, /health, /workout.webmanifest, /workout-sw.js는 workout-api로 프록시됨.
/error/403 프록시는 하위 호환 진입점 유지를 위한 것이며 별도 권한 오류 페이지를 의미하지 않음.
/workout.webmanifest와 /workout-sw.js는 workout-api가 제공하는 PWA 자산이며, Vite 개발 서버 origin에서 화면을 열 때도 동일 origin 요청이 404로 떨어지지 않도록 프록시 대상에 포함함.
/api/auth, /api/hub, /api/portal, /login은 인증 발급과 hub 공통 API를 위해 hub-api로 프록시됨.
- 개발 모드 엔트리 자산 경로는
/workout-ui/src/entries/workout.tsx 기준임.
- Vite dev client 경로는
/workout-ui/@vite/client임.
- React refresh runtime 경로는
/workout-ui/@react-refresh임.
7. 로그인 복귀 규칙
- workout 보호 페이지는 비로그인 시
hub-api 공통 로그인으로 리다이렉트됨.
- 개발 모드에서
workout-api는 요청 헤더의 dev server origin을 사용하여 redirect=http://localhost:15003/...를 유지함.
hub-api 로그인 성공 후 required_module=WORKOUT과 허용 origin 검증을 통과하면 15003 개발 서버 경로로 복귀함.
WORKOUT 권한이 없으면 hub-api 로그인 화면으로 돌아가며 permission_denied=1을 통해 권한 없음 alert를 한 번 표시함.
8. 구현 원칙
- workout 전용 화면만 포함하고 hub 화면 엔트리와 타입은 포함하지 않음.
- 화면 이동은
window.location.href가 아니라 React Router navigation을 사용함. 단, 파일 다운로드와 로그인/로그아웃처럼 문서 이동이 필요한 흐름은 예외임.
- 라우트 전환은
shared-ui의 SpaRouteTransition과 View Transition 기반 SPA navigation 옵션을 사용해 이전 화면 스냅샷이 빠지고 다음 화면 스냅샷이 들어오는 흐름으로 처리함.
- 라우트 변경 직후에는
shared-ui의 useSpaRouteSettling을 사용해 스크롤 위치를 초기화하고 주요 오브젝트가 가볍게 안착하는 흐름을 적용함.
- 최상위 화면 데이터 로딩은 텍스트 로딩 박스보다
shared-ui의 SkeletonBlock을 우선 사용함.
- 빠르게 끝나는 요청의 skeleton 깜빡임은
shared-ui의 useStableLoading으로 방지하고, 화면별 임의 타이머를 만들지 않음.
- 터치 피드백과 카드/패널 안착 애니메이션은
shared-ui 공통 base CSS에 위임하고, 모바일 바텀 액션바 같은 모바일 전용 레이아웃 효과만 모바일 CSS에 둠.
- SPA 효과를 위해 화면별 CSS를 만들지 않으며, 새 앱에 같은 체계를 적용할 때도
shared-ui의 SPA 공통 틀을 먼저 사용함.
- 페이지 전용 CSS 파일을 추가하지 않음.
- 공통 시각 규칙은
shared-ui 클래스 조합으로만 표현함.
- 공통 유틸 또는 아이콘이 필요하면 먼저
libs/shared-ui/src/react 확장을 우선함.
- 모바일 화면은
360px 고정 폭 기준을 전제로 마크업과 공통 클래스를 조합함.
- 사용자 노출 텍스트는 화면 컴포넌트에 한국어 문구로 직접 작성함.
- 공통코드 라벨과 메타 설정값처럼 데이터 자체가 마스터데이터인 값만 DB 조회 구조를 사용함.
9. 화면 동작 구조
9.1 운동 기록 목록
/workout-sessions는 카드형 목록 화면임.
- 카드 선택과 새 세션 생성은 클라이언트 라우팅으로 이동하며 앱 shell을 다시 로드하지 않음.
- 목록 조회 로딩 중에는
SkeletonBlock 목록 variant를 표시함.
- 상단 제목 외 장식성 설명 카피를 기본 전제로 두지 않음.
- 총 세션 수 문구는 컴포넌트 코드에 한국어 문구로 직접 작성함.
- 페이지 크기 선택은
shared-ui의 SegmentedTabs를 사용하며 10개, 20개, 50개 옵션을 같은 탭 표준으로 표시함.
- 세션 카드의 운동 날짜는
YYYY-MM-DD (화)처럼 날짜 옆에 요일을 같은 텍스트 위계로 붙여 표시함.
- 세션 카드의 요일 표시는 별도 뱃지, 색상, 수식 요소를 사용하지 않음.
- 세션 카드의 운동 종목 뱃지는 API가 반환한 순서를 그대로 사용하며, 백엔드는 workout 운동 기준정보의
sort_order_no 오름차순 기준으로 정렬한 운동 요약을 제공함.
- 하단 고정 액션바에서 다운로드, 새 세션 생성, 운동 계획 생성 진입을 제공함.
- 하단 고정 액션바의 버튼 순서는
다운로드 -> 새 세션 -> 계획 생성임.
다운로드와 새 세션은 고스트 버튼을 사용하고, 계획 생성은 흰색 강조 버튼을 사용함.
- 운동 계획 화면은 상단 앱 셸 메뉴에 독립 메뉴로 노출하지 않으며, 운동 기록 화면의
계획 생성 버튼에서 /workout-plans로 진입함.
9.2 세션 상세
/workout-sessions/new, /workout-sessions/{session_id}/edit는 같은 React 페이지를 사용함.
- 생성/수정 모드는 서버 props가 아니라 React Router path와 path parameter 기준으로 결정함.
- 생성 화면의 기본 운동일은
GET /api/workout/meta/default-workout-date로 조회함.
- 폼 초기 메타와 상세 조회 로딩 중에는
SkeletonBlock form variant를 표시함.
- 상단에는 운동 날짜, 컨디션, 메모를 배치하고, 하단에는 운동 그룹 카드와 세트 입력 그리드를 배치함.
- 세트 입력은
jb-workout-set-grid 계열 공통 클래스를 사용하여 헤더와 행을 같은 컬럼 템플릿으로 맞춤.
- 세트 입력 그리드의 중량 헤더는 화면 컴포넌트의 한국어 문구
중량(kg)로 표기함.
- 세트 입력 그리드 헤더는 공통 스타일에서 대문자 변환을 강제하지 않으며, 단위
kg는 소문자를 유지함.
- 세트 상태는
계획, 성공, 실패를 순환하며, 성공 시 RPE, 실패 시 FP 계열 입력만 활성화함.
- 운동 그룹 카드의 보조 액션은 우측 정렬로 배치하며,
직전 기록, 추가, 삭제 순서를 사용함.
- 운동 그룹 카드의
추가 버튼은 해당 기구에 새 세트 행을 추가하며, 새 행의 상태는 계획으로 생성함.
- 새 세트 행의 중량과 반복수는 같은 화면 안에서 같은 기구의 가장 최근
성공 세트가 있으면 그 값을 우선 사용하고, 없으면 직전 기록 조회 API가 제공하는 같은 기구의 가장 최근 성공 세트 값을 사용함.
- 같은 화면과 직전 기록 조회 결과에 사용할 수 있는
성공 세트가 없으면 새 세트 행은 기구별 최소 중량과 기본 반복수 1회를 사용함.
- 직전 기록 버튼은 모달을 먼저 열고, 사용자가
가져오기를 누른 뒤 확인창에서 승인할 때만 현재 그룹 세트를 PLAN 기준으로 덮어씀.
- 직전 기록 가져오기 확인창 문구는 화면 컴포넌트에 한국어 문구로 직접 작성함.
- 직전 기록 가져오기 확인창에서 취소하면 직전 기록 모달은 유지되고 현재 세트 입력값은 변경되지 않음.
- 직전 기록 조회는 현재 화면의 운동일과 저장된 세션 식별자를 API query로 함께 전달함.
- 저장된 세션 화면에서 직전 기록을 조회하면 현재 세션은 제외되고, 현재 세션보다 이전인 운동 세션 중 같은 운동 종목이 포함된 가장 최근 세션의 기록 전체가 표시됨.
- 신규 세션 화면에서 직전 기록을 조회하면 현재 화면의 운동일 이하에서 같은 운동 종목이 포함된 가장 최근 세션의 기록 전체가 표시됨.
- 직전 후보 세션 안에 같은 운동 종목 기록이 여러 묶음 또는 여러 세트로 존재하면 모달은 해당 기록을 모두 보여줌.
- 직전 후보가 없으면 같은 모달 안에서 직전 기록 없음 상태를 표시하고
가져오기 버튼을 비활성화함.
- 직전 기록 모달은 직전 세트의 상태, 중량, 반복, 그리고 상태에 따라
RPE 또는 FP만 한 줄 카드로 보여줌.
- 직전 기록 모달의 상태 칩은 세션 입력 화면과 같은 색 규칙을 사용함.
- 운동 선택 바텀시트와 직전 기록 시트의 닫기 버튼은 고스트 버튼을 사용하며, 버튼 텍스트가 줄바꿈되지 않도록 최소 가로폭을 확보함.
- 운동 선택 바텀시트의 운동 부위 필터는
shared-ui의 SegmentedTabs를 사용하며, 부위 항목 수가 늘어날 수 있으므로 가로 스크롤 가능한 탭 구조를 사용함.
- 중량 숫자는 정수면 소수점 없이 표시하고, 소수점이 있을 때만 소수부를 유지함.
- 숫자 선택 시트는
shared-ui의 BottomNumberPicker를 사용하며, 실제 하단 선택 시트 외형과 스크롤 동작은 공통 BottomOptionPicker 계약을 재사용하고 숫자 옵션은 중앙 정렬함.
- 숫자 선택 시트와 직전 기록 모달은 호출 시점의 문서 스크롤 위치를 보존하고 닫기, 가져오기, 오류 처리 후 같은 위치로 복원함.
- 하단 고정 액션바는
목록, 연결, 삭제, 추가, 저장 흐름을 사용하며, 추가와 저장이 남은 폭을 균등 분할함.
- 하단 고정 액션바는 모바일 viewport 하단에 고정되며, SPA route transition stage 안에서도 페이지 스크롤 끝으로 밀리지 않는 구조를 사용함.
9.3 운동 계획
/workout-plans는 최근 계획 조회와 새 계획 생성 화면을 겸함.
/workout-plans는 독립 상단 메뉴 항목으로 노출하지 않고 운동 기록 메뉴의 하위 흐름으로 취급함.
/workout-plans 경로에서 앱 셸의 활성 메뉴는 운동 기록임.
- 특정 계획 조회는
planId query string을 React Router search params로 해석함.
- 계획 조회 로딩 중에는
SkeletonBlock detail variant를 표시함.
- 컨디션 슬라이더와 운동 구분 선택 버튼을 상단에 두고, 결과 영역에 규칙 엔진이 생성한 루틴 카드를 보여줌.
- 운동 구분 선택은
jb-select 외형의 버튼과 shared-ui의 BottomOptionPicker 기반 모바일 하단 선택 시트를 사용함.
- 운동 구분 선택지는
GET /api/workout/meta/routine-modes 응답을 option으로 사용함.
- 운동 구분 기본값은
GET /api/workout/plans/default-routine-mode 응답을 사용함.
- 기본값 API가 반환하는 루틴 모드가 option 목록에 없거나 메타 조회가 실패하면 화면은
UPPER를 fallback으로 사용함.
jb-select 외형 선택 버튼은 공통 select chevron 배경만 사용하고 별도 텍스트 화살표를 추가하지 않음.
- 운동 계획 화면은 네이티브
<select>를 사용하지 않음.
- 계획 생성 요청에는
condition_score, routine_mode_code를 포함함.
- 루틴 카드의 kg 표기는 정수와 불필요한 trailing zero를 제거하고 실제 소수부가 있을 때만 소수점을 유지함.
- 생성 버튼은
POST /api/workout/plans/generate를 호출하고, 성공 응답의 저장된 계획 DTO로 화면을 갱신함.
- 생성 로딩 중에는 버튼과 화면 상태로 요청 진행 상태를 표시함.
- 실패 토스트는 생성 API 오류 메시지를 기준으로 노출함.
- 연결된 세션 이동 버튼은 linked session 존재 여부를 확인한 뒤 동작함.
9.4 운동 관리
/workout-exercises는 workout 앱 자체 운동 기준정보를 관리하는 모바일 카드형 목록 화면임.
- 운동 관리 화면은 PC 기준 Tabulator 화면을 사용하지 않음.
- 목록 조회는
GET /api/workout/exercises/progressive를 호출하며, 카드 선택 시 /workout-exercises/{exerciseCode}/edit로 이동함.
- 목록 카드는 운동명을 주요 제목으로 표시하고, 코드값, 운동 부위, 중량 단위, 최소 중량, 최대 중량, 정렬 순서, 사용 여부를 하단 뱃지 묶음으로 표시함.
- 목록 카드에는 운동 설명을 표시하지 않음.
- 하단 고정 액션바에는 새 운동 등록 버튼을 배치함.
/workout-exercises/new와 /workout-exercises/{exerciseCode}/edit는 같은 폼 화면을 재사용함.
- 신규 등록 화면의 운동 코드는 직접 입력하지 않으며, 코드 입력칸은 disabled 상태로 표시하고 저장 시 API가 자동 채번함.
- 수정 화면의 운동 코드는 읽기 전용으로 표시하며, 코드 변경 저장은 허용하지 않음.
- 운동 코드와 사용 여부 체크박스는 같은 입력 행에 배치함.
- 운동 설명 입력은
jb-input 기반 textarea 스타일을 사용함.
- 운동 부위 선택은
jb-select 외형의 버튼과 shared-ui의 BottomOptionPicker 기반 모바일 하단 선택 시트를 사용함.
- 운동 부위 선택지는
GET /api/workout/meta/body-parts 응답을 사용하며, 선택 안 함을 빈 문자열 option으로 함께 제공함.
jb-select 외형 선택 버튼은 공통 select chevron 배경만 사용하고 별도 텍스트 화살표를 추가하지 않음.
- 운동 관리 등록/수정 화면은 네이티브
<select>를 사용하지 않음.
- 저장은 생성 시
POST /api/workout/exercises, 수정 시 PATCH /api/workout/exercises/{exerciseCode}를 호출함.
- 생성 성공 후 화면은 API 응답의
exercise_code를 기준으로 수정 라우트로 전환함.
- 삭제는
DELETE /api/workout/exercises/{exerciseCode}를 호출하며, API는 운동 기준정보를 soft delete 처리함.
- 화면 문구와 토스트 문구는 React 컴포넌트에 한국어 문구로 직접 작성함.
9.5 러닝 계획 목록
/running-plans는 근력운동 기록과 분리된 러닝 계획 목록 화면임.
- 상단 앱 셸 메뉴의 러닝 진입은
운동 관리 우측에 배치됨.
- 목록 카드는 러닝 이름, 러닝 날짜, 예상 총 시간, 수행 여부, 블록 수, 단계 수를 표시함.
- 수행 여부는 계획 단위
is_performed 값을 기준으로 수행 전, 수행 완료를 표시함.
- 카드 선택은
/running-plans/{planId}/edit로 이동함.
- 카드 내부
수정 버튼은 같은 수정 화면으로 이동함.
- 카드 내부
러닝 시작 버튼은 /running-run/{planId}로 이동함.
- 하단 고정 액션바에는 새 계획 생성 버튼만 배치함.
- 러닝 기록 목록 화면은 별도로 제공하지 않음.
9.6 러닝 계획 편집
/running-plans/new, /running-plans/{planId}/edit는 같은 React 폼 화면을 사용함.
- 러닝 계획 편집 화면은 근력운동 세션 편집 화면을 확장하지 않고 별도
RunningPages 계열 컴포넌트에서 관리함.
- 상단 입력 영역은 러닝 날짜, 수행 여부, 러닝 이름, 메모, 전체 예상 운동시간과 예상 거리를 표시함.
- 러닝 날짜는
type=date 입력으로 관리하며 시간은 입력하지 않음.
- 수행 여부는 전체 러닝 계획 단위 토글로 관리하며 단계별 수행 여부는 관리하지 않음.
- 러닝 블록은 카드 하나가 연속 구간 또는 반복 패턴 하나를 의미함.
- 러닝 블록 카드는 카드 이름, 반복 횟수, 총 시간, 예상 거리, 단계 목록을 포함함.
- 블록 카드는 개별 접힘/펼침 상태를 가지며, 한 블록을 펼친다고 다른 블록이 자동으로 접히지 않음.
- 블록 제목 옆 요약 뱃지는
반복 횟수 · 총 시간 · 예상 거리를 한 뱃지에 표시함.
- 예상 거리는 단계별 목표 속도와 실행 시간을 기준으로 화면에서 계산하며 별도 저장값으로 관리하지 않음.
- 거리 표시는 소수점 둘째 자리까지 반올림하고 불필요한 trailing zero는 제거함.
- 블록 순서 이동은 카드 우측 상단의 위/아래 아이콘 버튼으로 수행함.
- 블록 삭제와 복사는 펼친 카드 하단의 아이콘 버튼으로 제공함.
- 단계 추가는 펼친 카드 하단의 텍스트 버튼으로 제공함.
- 러닝 단계는 별도 단계 이름 입력을 받지 않음.
- 러닝 단계의 표시명과 음성 안내명은 단계 유형 공통코드 라벨을 기준으로 함.
- 단계 입력 행은 유형, 분, 속도, 경사를 한 줄로 배치함.
- 단계 유형 선택은 네이티브 select가 아니라
shared-ui의 BottomOptionPicker 기반 모바일 하단 선택 시트를 사용함.
- 단계 유형 option은
GET /api/workout/running/meta/steps 응답을 사용함.
- 단계 시간 입력 헤더는
분으로 표기함.
- 목표 속도와 경사도는 선택 입력값이며 비어 있으면 저장 payload에서 null로 정규화됨.
- 하단 고정 액션바는 수정 화면에서
목록 아이콘 -> 삭제 아이콘 -> 복사 아이콘 -> 러닝 시작 -> 저장 구조를 사용함.
- 삭제 아이콘은 즉시 CUD를 일으키는 삭제 액션이므로
jb-button 강조 버튼 외형을 사용함.
- 복사 아이콘은 확인창을 거친 뒤
POST /api/workout/running/plans/{planId}/copy를 호출함.
- 복사 성공 시
러닝 계획을 복사했습니다. 성공 토스트를 표시하고 복사된 계획의 /running-plans/{newPlanId}/edit 화면으로 이동함.
- 생성 화면에서는 삭제 아이콘과 복사 아이콘을 표시하지 않음.
- 저장은 생성 시
POST /api/workout/running/plans, 수정 시 PUT /api/workout/running/plans/{planId}를 호출함.
- 러닝 시작은 저장을 먼저 수행한 뒤 저장된 계획 식별자를 기준으로
/running-run/{planId}로 이동함.
- 러닝 시작 버튼은 고스트 버튼을 사용하고, 저장 버튼은 흰색 강조 버튼을 사용함.
9.7 러닝 실행
/running-run/{planId}는 러닝 실행 전용 화면임.
- 러닝 실행 화면은 계획 편집 화면과 분리되며, 트레드밀 위에서 멀리 두고 볼 수 있도록 현재 단계명과 남은 시간을 크게 표시함.
- 러닝 실행 화면은 앱 셸 아래 별도 화면 제목을 표시하지 않고 실행 카드가 첫 콘텐츠가 되도록 구성함.
- 남은 시간은 CSS 빌드 산출 여부에 의존하지 않도록 컴포넌트에서 명시적인 font size를 적용하며 크게 표시함.
- 실행 화면 상단은 러닝 계획 이름과 실행 상태를 표시하고, 러닝 날짜와 백그라운드 소리 안내 보조 문구는 표시하지 않음.
- 실행 화면은 현재 단계 남은 시간, 현재 단계 유형명, 속도, 경사도, 현재 블록 이름, 반복 회차, 다음 단계, 전체 진행률, 경과 시간과 전체 시간을 표시함.
- 현재 단계 유형명은 타이머 위의 별도 대형 제목으로 표시하지 않고, 메트릭 카드 첫 번째 항목
유형으로 표시함.
- 메트릭 카드는
유형 -> 속도 -> 경사도 순서로 3열 배치함.
- 실행 카드의 상태 영역 아래, 남은 시간, 3열 메트릭, 현재 블록/반복 회차, 다음 단계, 진행 바, 경과/전체 시간은 트레드밀 위에서 빠르게 읽을 수 있도록 일관된 세로 여백으로 구분함.
- 실행 화면은 하단 고정 액션바를 피하기 위한 추가 페이지 하단 패딩을 두지 않음. 기준 모바일 viewport에서 카드와 조작 바 사이의 남는 공간은 문서 스크롤 영역으로 만들지 않으며, 터치 스와이프만으로 화면이 이동하지 않아야 함.
- 실행 화면의 여백과 하단 여백은 인라인 스타일이나 workout 전용 CSS가 아니라
shared-ui가 생성한 Tailwind 공통 유틸리티로 조합함. 새 유틸리티를 사용하면 Workout UI 빌드와 별도로 shared-ui 스타일 산출물을 다시 빌드해야 함.
- 실행 timeline은 저장된 블록과 단계 패턴을 실행 시점에 반복 횟수만큼 메모리에서 펼쳐 구성함.
- 반복 블록의 단계는 저장소에 반복 횟수만큼 복제되어 있지 않음.
- 러닝 진행 시간은 화면 갱신 interval의 누적값이 아니라 러닝 시작 기준 시각, 현재 기준 경과 시간, 수동 이동 시점의 offset을 기준으로 계산함.
- 화면 갱신 interval은 표시 상태를 자주 동기화하기 위한 트리거이며, 경과 시간의 소스 오브 트루스로 사용하지 않음.
- 시작 후 실제 경과 시간이 현재 단계 종료 시각을 지나면 다음 단계로 자동 이동함.
- 일시정지 상태에서는 단계 시간이 감소하지 않음.
- 다음 단계 수동 이동은 현재 단계의 남은 시간을 경과 시간에 반영하고 다음 단계로 이동함.
- 이전 단계 수동 이동은 해당 단계 시작 시점의 경과 시간과 남은 시간으로 위치를 갱신함.
- 처음 단계와 마지막 단계 이동은 실행 timeline의 첫 번째 또는 마지막 단계로 현재 위치를 이동함.
- 브라우저가 백그라운드 탭 또는 비활성 PWA의 JavaScript timer를 지연하거나 중단하더라도, 화면이 다시 visible 상태가 되면 실제 시각 기준으로 현재 단계, 남은 시간, 경과 시간, 완료 여부를 재계산함.
- 복귀 계산 결과 마지막 단계가 끝났으면
POST /api/workout/running/plans/{planId}/performed를 호출해 계획을 수행 완료로 표시함.
- 수행 완료 저장이 성공하면 러닝 목록으로 이동함.
- 운동 종료 버튼은 확인창을 표시하고, 조기 종료 시 수행 완료로 표시하지 않고 러닝 목록으로 이동함.
- 러닝 실행 하단 고정 액션바는 모두 아이콘 버튼으로 구성함.
- 러닝 실행 하단 버튼 순서는
목록 -> 처음 단계 -> 이전 단계 -> 시작/일시정지 -> 다음 단계 -> 마지막 단계 -> 종료임.
- 러닝 실행 하단 버튼은 하나의 조작 그룹처럼 붙어 배치하며, 목록 버튼만 버튼 그룹과 분리해 보이지 않도록 함.
- 시작/일시정지 버튼은 텍스트 없이 아이콘만 표시함.
- 음성/효과음/진동 안내 모드는 실행 화면 상단
jb-select 외형의 선택 버튼과 BottomOptionPicker 하단 선택 시트로 선택함.
- 안내 모드는
음성 + 효과음, 음성 안내, 효과음, 진동, 전체 무음을 제공함.
- 실행 중 다음 단계 10초 전 안내와 3초 카운트다운 안내를 제공함.
- 백그라운드 또는 비활성 상태에서 지나간 10초 전 안내와 3초 카운트다운 안내는 복귀 시점에 몰아서 재생하지 않음.
- 백그라운드에서 단계가 진행된 뒤 화면이 복귀하면 현재 단계 기준 시작 안내만 필요한 경우 1회 수행함.
- 브라우저가 Screen Wake Lock API를 지원하면 러닝 시작 시 화면 유지 요청을 수행함.
- 화면 유지가 지원되지 않거나 활성화 실패 시 실행 화면 상단 상태 문구로 안내함.
- wake lock release 이벤트를 감지해 화면 유지 상태 문구를 갱신함.
- 러닝 중 화면이 다시 visible 상태가 되면 wake lock을 다시 요청함.
- 화면 이탈 또는 완료, 종료 시 wake lock을 해제하고 speech synthesis를 정리함.
- Chrome PWA 환경을 주요 실행 환경으로 전제하되, 브라우저 API 지원 여부에 따라 화면 유지와 진동은 제한될 수 있음.
9.8 PWA 진입
- workout 프런트는 자체적으로 manifest를 만들지 않고,
workout-api가 제공하는 manifest와 service worker를 소비함.
- PWA manifest와 apple touch icon의 실제 아이콘 URL은
workout-api가 포털 메뉴의 APP_ICON + icon_val에서 해석한 shared-ui 앱 아이콘 slug를 기준으로 제공함.
- 브라우저가 standalone 설치형 진입을 할 수 있도록 base template의 관련 메타와 등록 스크립트를 그대로 사용함.
- workout PWA는 Android Chrome 설치형 실행을 주요 모바일 실행 환경으로 전제함.
- service worker는 설치형 진입 조건과 앱 셸 등록을 위한 최소 범위로 사용하며, 러닝 타이머, 음성 안내, 효과음, 진동을 백그라운드에서 지속 실행하는 주체로 사용하지 않음.
- PWA 백그라운드 실행은 브라우저와 OS 정책의 영향을 받으므로, workout-ui는 백그라운드 지속 실행 보장이 아니라 visible 복귀 시 실제 시각 기준 상태 보정을 기본 전략으로 사용함.
10. 운동 계획 화면과 규칙 엔진 응답 계약
- 계획 생성 완료 응답은 저장된 운동 계획 DTO를 반환함.
- 계획 생성 완료 응답의 운동 계획 DTO는
routine_mode_code, 연결 세션 식별자, 운동별 세트 목록을 포함함.
- 운동 계획 화면은 SSE, graph trace, usage log id를 사용하지 않음.
- 운동 계획 화면은 AI 조언 영역을 렌더링하지 않음.
- 최근 계획 조회와 특정 계획 조회는 연결 세션의 활성 세트 레코드를 기준으로 루틴 카드를 구성한 응답을 표시함.
- 루틴 카드는 운동별 카드와 세트별 행으로 구성하며, 각 행은 세트 번호, 중량, 반복수를 사용함.
- 러닝 계획 화면은
RunningPlanSavePayload 계열 타입을 사용하며, 러닝 세션 저장 payload 타입을 사용하지 않음.
- 러닝 단계 유형은
RUNNING_STEP 공통코드 기반 API 응답을 소비하며, 화면 내부 하드코딩 상수로 단계 유형 목록을 관리하지 않음.
- 러닝 실행 완료는 별도 기록 저장 payload 없이
POST /api/workout/running/plans/{planId}/performed만 호출함.
11. 관련 문서
- 프로젝트 전체 구조는
docs/junkbox/common/guide-jb-core.md를 따름.
- workout 백엔드 구조는
docs/junkbox/apps/guide-apps-workout-api.md를 따름.
- 공통 마스터 데이터 규칙은
docs/junkbox/common/guide-jb-common-master-data.md를 따름.
- 공통 UI 자산 구조는
docs/junkbox/libs/guide-lib-shared-ui.md를 따름.