SessionScreen Props Contract
화면 픽스처(앱 저장소):
src/react/ui/mobile/fixtures/SessionScreen.fixture.ts
Updated: 2026-06-09 · 2026-07-21 일지 풀스크린 달력 개편 — 아래 "2026-07-21 개편" 섹션 참조
This document freezes the first pilot contract between Claude Design and Codex for the session calendar screen.
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 owns the presentational screen. Codex owns the container, data hydration, persistence, and Supabase calls.
Component
Target presentational component:
src/react/ui/screens/SessionScreen.jsxFixture / standalone preview data:
src/react/ui/fixtures/sessionFixture.jsThe app container renders window.UiSessionScreen for the session tab. Calendar and session detail rendering is owned by src/react/ui/screens/SessionScreen.jsx; root-level auxiliary screen files are not loaded.
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}
/>Data In
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 Types
// UI-level discriminator only, not a table name: both kinds are rows of the `session` table
// (issue #1215, 2026-09-04). Derived by `lgSessionSource(...)` in barbelicViewMappers.
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 }[];
};Callback Rules
All callbacks may return a Promise.
Claude UI may use pending promises to:
- disable save/delete buttons while work is in flight
- show pending, success, or failure toast state
- keep a dialog open when persistence fails
Codex must make persistence callbacks reject on real failure instead of swallowing errors.
onUpdateSession is a real persistence callback for both planned and completed sessions.
status: "planned"updates thesessionrow (status = planned) and rewrites its exercise/set layers viasave_session_v5.status: "completed"updates thesessionrow (status = completed) and rewritessession_exercise/session_exercise_part/exercise_set/exercise_set_partvia the samesave_session_v5.- The UI should pass the complete next
SessionFullshape when editing completed session exercises or sets.
Important Decisions
Delete Passes The Session Object
onDeleteSession receives the full session object, not just sessionId.
onDeleteSession(session: SessionLite | SessionFull)Reason: deletion needs source: "sessions" | "planned_sessions" to know whether the card is a completed record or a plan. It is a screen-level value, not a table name — both live in the session table and are deleted through delete_session_v5 (issue #1215). Passing only an id can recreate the previous bug where planned and completed records were confused.
Use source, Not _source
The Claude-facing contract uses:
source: "sessions" | "planned_sessions"Internal adapters may still read old _source fields during migration, but UI fixtures and new props should use source.
Desktop Plan Editing Uses The Embedded Composer
The v49 desktop SessionScreen owns the plan composer modal and renders UiDesktopPlanEditor inside it. Mobile may continue routing plan authoring through its existing workout flow.
Desktop persistence crosses the boundary through:
onSavePlan(dateISO, {
...draft,
editSessionId?: string,
time?: string,
})editSessionId updates the existing session row with status = planned. time is the optional scheduled start (HH:mm). Set rows may contain both calculated load kilograms and the user's original loadPct; both must survive a save/read/edit round trip.
UI-Local State Owned By Claude
SessionScreen may keep these as local UI state:
- month swipe / visible animation state
- selected popover or sheet state
- session detail expand/collapse state
- inline editor open/closed state
- delete confirmation dialog state
- transient toast display state
- saving/deleting pending state derived from callback promises
Persistence Owned By Codex
SessionScreen must not import:
- Supabase clients
src/react/services/barbelicApi.js- persistent storage APIs for canonical app data
Real data changes happen only through callbacks.
Marker Notes
Claude should keep existing semantic markers where the screen still needs DOM-level integration or contract testing:
mobileSessionCalendarGridmobileSessionsListmobileSessionTodayButtonmobileSessionPrevMonthButtonmobileSessionNextMonthButtonsession.opensession.delete.confirmcalendar.todaycalendar.prevMonthcalendar.nextMonthcalendar.cond.edit
If Claude adds a new data-lg-* marker, request it in the PR description. Codex will register it in 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면 화면이 "크롬 실물 + 데이터 자리 셔머" 스켈레톤을 즉시 렌더한다(2026-09-10 #1554: 첫 프레임부터 표시하며 재방문 시 지연·투명화 없음). 컨테이너: 캐시가 없을 때만 true, 데이터 도착 시 false. 게이트/스피너로 화면 마운트를 막지 말 것.
ownerName (2026-08-26 추가 — 타인 세션 상세 소유자 표기)
ownerName?: string— 공급 시 세션 상세(UiSessionDetail, 시트 변형 제외) 맨 위에 워드마크 행 렌더: 좌 "●(라이브 점) {ownerName} 님의 바벨릭 페이지"(fband/mb-owner — 친구 페이지 4탭과 동일 문법) · 우 바벨릭 워드마크. 그룹 완료 행 → 멤버 세션 상세(group-props.md §4 doneSessions) 진입 표면용.profile은 그 멤버의 person(+self:false)으로 — 리뷰 말풍선 아바타 폴백에 사용.
sessionAsOverlay (2026-08-28 — 세션 수정 복귀 델타)
sessionAsOverlay?: boolean— 일지 탭(및 친구 스코프 달력) 인스턴스가 true로 공급.- true면 화면은 구형 풀페이지(UiSessionDetail) 분기를 발동하지 않는다. 세션 상세 오버레이 자체는 화면이 그리지 않는다 — 컨테이너의 공용
UiStackSessionOverlay가 연 탭 화면 스택의session/friendSession항목을 같은 문법(sj-mo: pg-slidein + 워드마크 행 + 행내 뒤로가기)으로 그린다(이슈 #1393 Phase 4). 세션이 열린 채 마운트되면(검색·리포트에서 연 세션·수정 플로우 복귀)sessionsByDate[session.date]에 세션이 있을 때만 하루 상세 층(journalDay)을 세션 아래에 끼운다(beneathTop). - 하루 상세의 열림 상태는 탭 화면 스택 항목(
journalDay)이다 —useTabStack()으로 읽고 push한다. 스택 호스트가 없는 문맥(조회 전용 등)에서는 push하지 않는다. (2026-09-14) 월간 요약 오버레이(journalMonth)는 폐지 — 월 타이틀 탭의 목적지가 월간 리포트로 바뀌었다(아래onOpenMonthReport). Codex:controllers/screenKinds.ts에서journalMonthkind 및 월 요약 데이터 배선(onSelectMonth·selectedMonth*) 제거 가능.
월 타이틀 탭 = 월간 리포트 (2026-09-14 지시)
- 달력 헤더의
YYYY년 M월버튼(hookcalendarMonthStats) 탭 →onOpenMonthReport?.(monthKey)(monthKey="YYYY-MM"). 컨테이너는 리포트 화면을periodUnit: "month"+ 해당 월로 열어야 한다(드로어 "월간 리포트" 항목과 같은 목적지, 기간만 달력의 표시 월). - 전환 = 화면 스택 push(탭 교체 아님): 홈 프로필 → 리포트와 같은
homeReportkind로 현재 탭(일지) 스택에 push한다 → 우→좌 슬라이드 진입, 리포트 워드마크 행 좌측 뒤로(onClose) · 좌→우 스와이프 · 안드로이드 백 = 좌→우 퇴장(공용UiStackScreen이 소유, 계약navigation-stack.md). 탭을 갈아끼우면(구 배선) 애니메이션이 없고 일지로 돌아올 수 없다. - 종전 목적지였던 월간 요약 오버레이(월 스텝 헤더 +
UiDayDetailBody mo)는 화면에서 삭제됐다. 삭제된 props:onSelectMonth·selectedMonthSessions·selectedMonthSummary·selectedMonthSetPurposeZones(공급해도 화면이 읽지 않는다). - 미공급 = 월 타이틀은 탭해도 아무 일도 없다(달력은 그대로). 월 이동은 종전처럼 좌우
‹ ›스테퍼(calendar.prevMonth/nextMonth). - 미공급(기존 기본): 종전과 동일 — 조회 문맥(그룹 멤버 세션·ownerName/onReturn 풀페이지) 불변.
- 컨테이너 의무: 수정 플로우 저장/취소 복귀 시 selectedSession을 비우지 말 것(비우면 달력 랜딩).
조회 문맥(친구·그룹)의 하루 상세 공급 의무 (2026-09-01 추가 — 이슈 #1095)
selectedDateSessions는 하이드레이션된 세션 배열이어야 한다 —exercises[].sets[]가 있어야 하루 상세가 합계(총 무게·운동 시간·세트 수)·존 도넛·탑세트를 계산하고 종목 줄을 그린다.{id, date, title, status}만 넘기면 화면은 오류 없이0kg / 0분 / 0세트와 빈 카드를 그린다(이것이 친구 일지 탭에서 실제로 일어났던 결함).- 목록 RPC가 세션 lite만 준다면(친구 일지 달력 =
get_following_calendar_month_v1) 컨테이너가 그날 세션 상세를 따로 받아 채운다. 도착 전에는 lite 행에_calendarDetailPending: true를 얹어 스켈레톤으로 높이를 잡는다(이슈 #905 문법).
- 목록 RPC가 세션 lite만 준다면(친구 일지 달력 =
- 세션 카드 클릭이 동작하려면
onOpenSession·selectedSession·onCloseSession3종을 함께 공급해야 한다.onOpenSession이 없으면 카드 클릭은 오류도 로그도 없는 no-op이고,selectedSession이 없으면 상세 오버레이가 렌더될 자리 자체가 없다. - 조회 전용(친구·그룹) 문맥은 수정 계열 콜백을 공급하지 않는 것으로 편집 표면을 봉인한다 —
onEditRecord/onEditPlan/onDeleteSession/onStartWorkout/onCreatePlan/onAddPastRecord. 세션의readOnly플래그는 화면이 읽지 않는다(컨테이너 전용). 공급하면 친구 세션에 수정·삭제 버튼이 그대로 노출된다. - 친구 페이지처럼 다른 사람의 세션을 보는 문맥은 앱 전역 세션 상태(
openSessionDetail)를 쓰지 않는다 — 전용 상태 슬롯 + 전용 스택 항목을 둔다(그룹 화이트보드doneSession→groupSession, 친구 페이지session→friendSession, 이슈 #1393). 모바일에서 전역 세션 상세는 어느 탭에서 열든 탭을 바꾸지 않고 연 탭 스택 위에 서며, 내 일지의 선택 날짜·표시 달은 일지 원점에서 열 때만 바뀐다.