모바일 운동·계획 편집 연결 — U02
화면 픽스처(앱 저장소):
src/react/ui/mobile/fixtures/WorkoutFlow.fixture.ts
이슈 #1420, 대상 release/v0.18.0. 구현·검증 및 병합 상태는 연결된 PR과 작업 기록을 따른다. 이 계약의 범위는 운동 기록·계획 편집과 보관·복구다. 모바일 앱 전체의 화면 이식 완료를 뜻하지 않는다.
1. 소유 경계
| 경계 | 소유자 | 화면이 받는 것 |
|---|---|---|
| 운동 입력 | A12 features/workout/editor, active-workout/mobileWorkoutEditor.ts | MobileWorkoutEditorPort: typed view와 동기 command |
| 계획 입력 | A13 features/plan/editor, active-workout/mobilePlanEditor.ts | 같은 모바일 view/action 표면, A13의 상태·관문·저장 행 |
| 편집 수명 | active-workout/mobileWorkoutFlow.ts | MobileFlowPort: 단계·휴식·완료 입력·저장 시도 상태와 명령 |
| 초안 | S08, mobileInputDraft.ts | 공통 초안 원문 + 모바일 IME/표시 토큰·수정 취소 원본·안정적인 자식 ID |
| 전송·확정 | A09, mobileSavePresentation.ts | 실제 대기열 행·영수증·통계 요청 세대에 근거한 상태 |
| 보관·복구 | S09 useRecoveryPanel·RecoveryPanelProps | 목록 → 서버 미리보기 → 명시 확인 → 요청 상태 |
WorkoutFlowBinding.MobileEditorFlow가 실제 화면에 이 표면을 주입한다. WorkoutFlow, WorkoutRecord, WorkoutRecordParts는 배열 사본을 재조립해 저장하지 않는다. 숫자 의미·단위 환산·완료 관문·저장 자식 ID는 공통 편집기의 책임이다. 화면에는 포커스·열림·애니메이션·단위 표시 선택만 남긴다.
2. 입력·계획 정책
- 빈칸, 입력 중인
12., 잘못된 값, 확정된0, IME 조합 상태를 구분한다. 조합 시작/종료는 주 입력·빠른 추가·복합·RPE·실패·휴식에서 같은 명령으로 전달한다. - kg/lb/%와 분:초/km의 원문·정규값은 같은 공통 명령에서 바뀐다. 분:초 blur는 공통 규칙으로 확정된 초의 범위/표기를 정리하며, 미완성 원문을 숫자로 꾸미지 않는다.
- 유산소의 켠 항목과 표시 단위는 종목 화면의 수명에 속한다. 입력이 잠시 비거나 미완성이 되었다는 이유로 칸이 사라지거나 단위가 바뀌지 않는다.
- 모바일 계획 저장 버튼은 행 0개 또는 저장 중일 때 잠긴다. 모든 종목의 확인 표시를 강제하는 데스크톱 정책을 이식하지 않는다. 실제 저장은 A13의 입력 관문을 통과해야 한다.
- 실패 0회는 빈칸과 다르다. 목표 횟수·실제 수행 횟수·실패 결과의 뜻은 공통 편집기와 저장 계약이 정한다. 점수와 통계 수식은 추가하지 않는다.
- 그룹 보드가 공유하는 기록 화면도 같은 typed editor를 쓴다. 보드의 memo/카탈로그 투영은
mobileGroupDraft.ts, 행 생성과 숫자 입력은 A12가 맡는다. 종목 ID를 이름으로 만들어 저장하지 않는다.
3. 저장 시도와 이후 편집
사용자 A가 저장한 뒤 입력을 더 바꾸면, 전송한 payload와 이후 초안은 서로 다른 operation으로 보관한다. 늦은 답장은 전송한 요청을 확정할 수 있지만 이후 원문을 덮거나 편집 화면을 닫지 못한다. 영수증의 서버/자식 ID는 현재 행에 연결하되 새 입력에 예전 점수를 덮지 않는다.
editor.durable은 이전 초안을 정리하기 전에 새 초안을 보존한다. editor.keepOpen은 owner·편집 수명·저장 시도와 이후 변경을 확인한다. S08 자체 debounce를 사용하며 UI의 추가 700ms debounce는 없다. 재시작 때 전송 중이던 요청은 보관된 동일 payload·operation으로 이어지고 이후 입력은 그대로 남는다.
완료 화면에서 기록으로 돌아오면 확정된 세션의 수정으로 이어진다. 로컬 pending: ID는 서버 확정 사본으로 취급하지 않는다. 계정 변경 또는 해제 뒤 도착한 응답은 현재 화면·초안을 갱신하지 않는다.
| 실제 근거 | 표시 |
|---|---|
| A09 queued 행 | 기기에 보관, 연결 후 동기화 |
| sending 행 | 서버 전송 중 |
| held / blocked | 로그인 확인 / 보관·복구 확인 |
| 확정 영수증 | 서버 저장 완료, 통계 확인 중 |
| 요청 세대 이상의 통계가 발행됨 | 서버 저장과 통계 반영 완료 |
| queued 반환 뒤 행·영수증 확인 전 | 서버 결과 확인 중 |
4. 명시적 복구
메뉴의 보관·복구는 기존 recordsTool 화면 스택의 tool=recovery다. 별도 history/popstate 체계를 만들지 않는다. RecoveryScreen은 RecoveryPanelProps만 받아 목록·이전 내용·현재/복구 후 값·부분 복구 경고를 보여 준다. 현재 값이 없으면 확인하지 못했다고 표시한다.
사용자가 이 내용으로 되돌리기를 누른 뒤에만 S09 명령을 기기에 적고 보낸다. 저장 공간 실패는 미전송, 권한 거절은 권한 오류, 변경 충돌은 새 미리보기 필요로 구분한다. 다시 미리보기를 받아도 자동 적용하지 않는다. committed 영수증만 복구 완료로 표시한다. 이미 확인한 요청의 재전송과 새로운 복구 확인을 구분한다.
5. 검증·인계
| 대상 | 증거·공개 경로 |
|---|---|
| 원문/IME/단위·입력 | tests/react/mobileWorkoutEditor.test.mjs, mobileEditorInputLifecycle.test.mjs |
| S08·owner·늦은 응답·재시작 | tests/react/mobileWorkoutFlowLifecycle.test.mjs |
| 복구 표시·S09 명령 | tests/react/mobileRecoveryScreen.test.mjs, recoveryBinding.test.mjs |
| 실제 브라우저 화면 | tests/browser/u02/mobile.spec.mjs, playwright.config.mjs, 포트 4420 |
| U03 공통 입력 | tests/fixtures/u03EditorParity.mjs + tests/react/mobileDesktopEditorParity.test.mjs 7개, 양쪽 실제 adapter의 validation/payload/ID 비교 |
| U07 상태 선택자 | 아래 표, 앱 contracts/designContract.ts 등록 |
| U06/N01 | native OS 키보드·네이티브 컨테이너의 뒤로가기/키보드 높이는 이번 Chromium 검사로 검증하지 않음 |
브라우저 검사는 실제 모바일 binding·화면, S08 IndexedDB, S09 binding을 실행한다. 저장/복구 서버 응답은 테스트가 제어한다. 실제 인증 제공자·Production DB 쓰기·모바일 OS 실기기 전체 여정의 증거로 확대하지 않는다. 날짜와 시계를 조작하지 않는다. 22개 브라우저 여정에는 드래그, owner 세대 변경 뒤 응답, recordsTool 스택의 브라우저 뒤로가기, 종료 overlay 취소 뒤 포커스도 포함한다.
| 종류 | 이름 |
|---|---|
| hook | workoutPersistenceStatus, mobileRecovery, mobileRecoveryList, mobileRecoveryPreview, mobileRecoveryRequests |
| action | recovery.open, recovery.refresh, recovery.preview, recovery.replan, recovery.confirm, recovery.flush, recovery.dismiss |
| 기존 입력/저장 | workout.set.load, workout.set.reps, workout.set.editDone, workout.set.perf.save, workout.plan.saveBar, workout.finishStep.confirm |
| 동적 상태 | data-lg-status: 대기열/영수증·S09 enum을 그대로 사용. .mobile-recovery는 @layer screen 안에서만 스타일 정의 |
UI에서 없앤 로컬 초안 팩토리·수정 여부·조기 종료·시각 계산의 이전 경로는 회귀 검사로 유지하지 않는다. 검사는 현재 편집기와 실제 feature를 부른다. 공통 시간 표기는 mobileWorkoutTime.ts를 실제 저장 수명에서 사용한다.
6. 남아 있는 공통 호환 API
A12의 adapters/mobileFlowAdapter.ts에서 openMobileWorkoutFlow는 실제 mobileWorkoutEditor의 열기 경계다. 기존 공개 guardMobileSavePayload는 앱 화면 호출이 0이고 A12 adapter 계약 검사에서만 남는다. U02가 이 guard를 새 입력/저장 경로로 감싸 사용하지 않는다. 공개 A12 API의 삭제는 A12/R01 소유이며, 해당 계약 검사와 외부 소비 여부를 정리할 때 제거할 수 있다. lgFlowInitialDraft는 계획·라이브·완료 기록에서 최초 편집 입력을 고르는 기존 컨테이너 경계로 남고, 숫자 변환·입력 상태·저장 판단을 소유하지 않는다.