SessionScreen Props 계약
화면 픽스처(앱 저장소):
src/react/ui/mobile/fixtures/SessionScreen.fixture.ts
한국어 번역본
이 문서는 원문(영어)의 한국어 번역이다. 정본은 원문이며, 계약·게이트 판단이 갈리면 원문을 따른다. 원문: docs/contracts/session-screen-props.md
갱신: 2026-06-09 · 2026-07-21 일지 풀스크린 달력 개편 — 아래 "2026-07-21 개편" 섹션 참조
이 문서는 세션 달력 화면에 대한 Claude Design과 Codex 사이의 첫 파일럿 계약을 고정한다.
2026-07-21 개편 — 일지 탭 = 풀스크린 달력
- 일지 탭 메인 뷰 = 월 달력 전체 화면(
.sj-fullcal, 흰 지면 풀블리드): 월 헤더(오늘 칩 + ◀▶ 월 이동) + 큰 셀 그리드. 주간 스트립 아코디언·하루 카드·세션 리스트는 이 화면에서 제거. - 날짜 탭 = 하루 상세 드로어(2026-07-21 재지시): 셀 탭 →
onSelectDate(iso)+ 화면 로컬 바텀시트(journalDaySheethook, Codex 등록 필요)에UiDayDetailBody(day-summary-props.md) 렌더 —selectedDateSessions가 하이드레이션된 full 세션 배열이어야 함(계약 변경). 드로어 세션 행/셀 엔트리 블록 →onOpenSession(session.open).onOpenDaySummary는 이 화면에서 미호출(홈 진입용 페이지는 유지). - 계획 추가/지난 기록 추가 진입점은 하루 상세로 이동(day-summary-props.md 참조) —
onCreatePlan/onAddPastRecordprop은 계약 유지(이 화면에선 미사용). - 마커 변화:
calendarExpandToggle·dayDetailButton·mobileSessionsList·addPlanButton(calendar.plan.add)·addPastRecordButton이 이 화면에서 제거(뒤 3개는 UiDaySummary로 이동).mobileSessionCalendarGrid·mobileSessionTodayButton·prev/next 버튼(hook+calendar.prevMonth/nextMonth항상 활성)·calendar.today는 유지.
Claude Design이 표현용 화면을 소유한다. Codex가 컨테이너, 데이터 하이드레이션, 영속화, Supabase 호출을 소유한다.
컴포넌트
대상 표현 컴포넌트:
src/react/ui/screens/SessionScreen.jsx픽스처 / 독립 프리뷰 데이터:
src/react/ui/fixtures/sessionFixture.js앱 컨테이너는 세션 탭에 대해 window.UiSessionScreen을 렌더한다. 달력과 세션 상세 렌더링은 src/react/ui/screens/SessionScreen.jsx가 소유하며, 루트 레벨의 보조 화면 파일은 로드되지 않는다.
Props
<SessionScreen
today={today}
visibleMonth={visibleMonth}
selectedDate={selectedDate}
sessionsByDate={sessionsByDate}
selectedDateSessions={selectedDateSessions}
selectedSession={selectedSession}
monthSummary={monthSummary}
onSelectDate={onSelectDate}
onChangeMonth={onChangeMonth}
onGoToday={onGoToday}
onOpenSession={onOpenSession}
onCloseSession={onCloseSession}
onCreatePlan={onCreatePlan}
onEditPlan={onEditPlan}
onStartWorkout={onStartWorkout}
onUpdateSession={onUpdateSession}
onDeleteSession={onDeleteSession}
onDownloadImage={onDownloadImage}
/>입력 데이터
type SessionScreenProps = {
today: string;
visibleMonth: VisibleMonth;
selectedDate: string;
sessionsByDate: Record<string, SessionLite[]>;
selectedDateSessions: SessionLite[];
selectedSession: SessionFull | null;
monthSummary?: MonthSummary;
onSelectDate: (dateISO: string) => MaybePromise<void>;
onChangeMonth: (nextMonth: VisibleMonth) => MaybePromise<void>;
onGoToday: () => MaybePromise<void>;
onOpenSession: (session: SessionLite) => MaybePromise<void>;
onCloseSession: () => MaybePromise<void>;
onCreatePlan: (dateISO: string) => MaybePromise<void>;
onEditPlan: (session: SessionLite) => MaybePromise<void>;
onStartWorkout: (session: SessionLite) => MaybePromise<void>;
onUpdateSession: (sessionId: string, next: Partial<SessionFull>) => MaybePromise<void>;
onDeleteSession: (session: SessionLite | SessionFull) => MaybePromise<void>;
// Optional. Shown only on completed-session detail (read mode) as the "이미지로 저장" button.
// UI just raises the intent with the full session; Codex/native performs the actual image capture/share.
onDownloadImage?: (session: SessionFull) => MaybePromise<void>;
};
type MaybePromise<T> = T | Promise<T>;
type VisibleMonth = {
year: number;
month: number;
};
type MonthSummary = {
sessionCount: number;
};세션 타입
// 화면 구분값이며 테이블명이 아니다 — 두 종류 모두 `session` 테이블의 행(이슈 #1215, 2026-09-04).
// barbelicViewMappers의 `lgSessionSource(...)`가 만든다.
type SessionSource = "sessions" | "planned_sessions";
type SessionStatus = "completed" | "planned" | "missed";
type SessionLite = {
id: string;
source: SessionSource;
status: SessionStatus;
date: string;
title: string;
summary: string;
};
type SessionSet = {
id: string;
type?: string;
reps: number;
load: number;
perceivedRpe?: number | null; // RPE 1.0~10.0 (이슈 #1237)
memo?: string;
rest?: string | number | null;
done?: boolean;
};
type SessionExercise = {
id: string;
name: string;
review?: string;
sets: SessionSet[];
};
type SessionFull = SessionLite & {
condition?: string; // 기능 폐기(2026-08-24): 상세 화면에 표시하지 않음 — 데이터 보존용 통과 필드
postCondition?: string; // 기능 폐기(2026-08-24): 상세 화면에 표시하지 않음 — 데이터 보존용 통과 필드
startTime?: string;
endTime?: string;
durationLabel?: string;
note?: string;
exercises: SessionExercise[];
newPRs?: { name: string; value: string | number }[];
};콜백 규칙
모든 콜백은 Promise를 반환할 수 있다.
Claude UI는 대기 중인 promise를 다음 용도로 쓸 수 있다:
- 작업이 진행 중인 동안 저장/삭제 버튼을 비활성화한다
- 대기·성공·실패 토스트 상태를 표시한다
- 영속화가 실패하면 다이얼로그를 열어 둔다
Codex는 영속화 콜백이 오류를 삼키는 대신 실제 실패 시 reject하도록 만들어야 한다.
onUpdateSession은 계획 세션과 완료 세션 양쪽 모두에 대한 실제 영속화 콜백이다.
status: "planned"는session행(status = planned)을 갱신하고 그 아래 종목·세트 층을save_session_v5로 다시 쓴다.status: "completed"는session행(status = completed)을 갱신하고session_exercise/session_exercise_part/exercise_set/exercise_set_part를 같은save_session_v5로 다시 쓴다.- 완료 세션의 종목이나 세트를 편집할 때 UI는 다음
SessionFull전체 형태를 넘겨야 한다.
주요 결정
삭제는 세션 객체를 넘긴다
onDeleteSession은 sessionId만이 아니라 세션 객체 전체를 받는다.
onDeleteSession(session: SessionLite | SessionFull)이유: 삭제는 그 카드가 완료 기록인지 계획인지 알기 위해 source: "sessions" | "planned_sessions"가 필요하다. 이 값은 화면 구분값이며 테이블명이 아니다 — 두 종류 모두 session 테이블의 행이고 delete_session_v5로 지운다(이슈 #1215). id만 넘기면 계획 기록과 완료 기록을 혼동했던 이전 버그가 재발할 수 있다.
_source가 아니라 source를 쓴다
Claude를 향한 계약은 다음을 사용한다:
source: "sessions" | "planned_sessions"내부 어댑터는 마이그레이션 동안 구 _source 필드를 계속 읽어도 되지만, UI 픽스처와 신규 prop은 source를 써야 한다.
데스크톱 계획 편집은 내장 컴포저를 쓴다
v49 데스크톱 SessionScreen이 계획 컴포저 모달을 소유하고 그 안에 UiDesktopPlanEditor를 렌더한다. 모바일은 기존 운동 플로우를 통해 계획 작성을 계속 라우팅해도 된다.
데스크톱 영속화는 다음을 통해 경계를 넘는다:
onSavePlan(dateISO, {
...draft,
editSessionId?: string,
time?: string,
})editSessionId는 기존 session 행(status = planned)을 갱신한다. time은 선택적 예정 시작 시각(HH:mm)이다. 세트 행은 계산된 load 킬로그램과 사용자가 원래 입력한 loadPct를 함께 담을 수 있으며, 둘 다 저장/읽기/편집 왕복을 견뎌야 한다.
Claude가 소유하는 UI 로컬 상태
SessionScreen은 다음을 로컬 UI 상태로 유지해도 된다:
- 월 스와이프 / 표시 애니메이션 상태
- 선택된 팝오버 또는 시트 상태
- 세션 상세 펼침/접힘 상태
- 인라인 에디터 열림/닫힘 상태
- 삭제 확인 다이얼로그 상태
- 일시적 토스트 표시 상태
- 콜백 promise에서 파생된 저장/삭제 대기 상태
Codex가 소유하는 영속화
SessionScreen은 다음을 import해서는 안 된다:
- Supabase 클라이언트
src/react/services/barbelicApi.js- 정식 앱 데이터를 위한 영속 저장소 API
실제 데이터 변경은 콜백을 통해서만 일어난다.
마커 참고
화면이 여전히 DOM 수준 통합이나 계약 테스트를 필요로 하는 곳에서는 Claude가 기존 의미 마커를 유지해야 한다:
mobileSessionCalendarGridmobileSessionsListmobileSessionTodayButtonmobileSessionPrevMonthButtonmobileSessionNextMonthButtonsession.opensession.delete.confirmcalendar.todaycalendar.prevMonthcalendar.nextMonthcalendar.cond.edit
Claude가 새 data-lg-* 마커를 추가하면 PR 설명에서 요청한다. Codex가 이를 src/react/contracts/designContract.js에 등록한다.
2026-07-31 · 세션 수정 위임 (시트 내 에디터 제거)
- UiSessionDetail은 조회 전용. 시트 내 수정모드(draft·set-edit·저장 도크) 전면 제거.
- 신규 prop
onEditRecord(session)(optional): 완료 세션 ✎ 클릭 시 호출 — 컨테이너는 WorkoutFlow를backfill + initialDraft(세션→draft 어댑터)로 열고, onSaveSession 결과를 해당 세션 update로 라우팅한다(계획 ✎ = 기존 onEditPlan과 동일 패턴). 미배선 시 ✎ 자동 숨김(삭제 버튼은 onDelete 기준 유지). - 세션 exercises는 canonical exerciseId(UUID) 필요(flow draft 요건). 참조 구현: mobile/preview/_preview-local.jsx의 sessionToFlowDraft/builtToSession.
- 사용 중단 마커(등록 유지): session.detail.cancel/rename/save.bottom, session.set.expand/add, session.exercise.remove(+confirm).
pending (2026-08-12 추가 — 로딩 체계)
pending?: boolean— true면 화면이 "크롬 실물 + 데이터 자리 셔머" 스켈레톤을 즉시 렌더한다(노출은 300ms 지연 — 화면 CSS 소유라 컨테이너는 타이머 불필요). 컨테이너: 캐시가 없을 때만 true, 데이터 도착 시 false. 게이트/스피너로 화면 마운트를 막지 말 것.