Skip to content

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) + 화면 로컬 바텀시트(journalDaySheet hook, Codex 등록 필요)에 UiDayDetailBody(day-summary-props.md) 렌더 — selectedDateSessions하이드레이션된 full 세션 배열이어야 함(계약 변경). 드로어 세션 행/셀 엔트리 블록 → onOpenSession(session.open). onOpenDaySummary는 이 화면에서 미호출(홈 진입용 페이지는 유지).
  • 계획 추가/지난 기록 추가 진입점은 하루 상세로 이동(day-summary-props.md 참조) — onCreatePlan/onAddPastRecord prop은 계약 유지(이 화면에선 미사용).
  • 마커 변화: calendarExpandToggle·dayDetailButton·mobileSessionsList·addPlanButton(calendar.plan.add)·addPastRecordButton이 이 화면에서 제거(뒤 3개는 UiDaySummary로 이동). mobileSessionCalendarGrid·mobileSessionTodayButton·prev/next 버튼(hook+calendar.prevMonth/nextMonth 항상 활성)·calendar.today는 유지.

Claude Design이 표현용 화면을 소유한다. Codex가 컨테이너, 데이터 하이드레이션, 영속화, Supabase 호출을 소유한다.

컴포넌트

대상 표현 컴포넌트:

txt
src/react/ui/screens/SessionScreen.jsx

픽스처 / 독립 프리뷰 데이터:

txt
src/react/ui/fixtures/sessionFixture.js

앱 컨테이너는 세션 탭에 대해 window.UiSessionScreen을 렌더한다. 달력과 세션 상세 렌더링은 src/react/ui/screens/SessionScreen.jsx가 소유하며, 루트 레벨의 보조 화면 파일은 로드되지 않는다.

Props

jsx
<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}
/>

입력 데이터

ts
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;
};

세션 타입

ts
// 화면 구분값이며 테이블명이 아니다 — 두 종류 모두 `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 전체 형태를 넘겨야 한다.

주요 결정

삭제는 세션 객체를 넘긴다

onDeleteSessionsessionId만이 아니라 세션 객체 전체를 받는다.

ts
onDeleteSession(session: SessionLite | SessionFull)

이유: 삭제는 그 카드가 완료 기록인지 계획인지 알기 위해 source: "sessions" | "planned_sessions"가 필요하다. 이 값은 화면 구분값이며 테이블명이 아니다 — 두 종류 모두 session 테이블의 행이고 delete_session_v5로 지운다(이슈 #1215). id만 넘기면 계획 기록과 완료 기록을 혼동했던 이전 버그가 재발할 수 있다.

_source가 아니라 source를 쓴다

Claude를 향한 계약은 다음을 사용한다:

ts
source: "sessions" | "planned_sessions"

내부 어댑터는 마이그레이션 동안 구 _source 필드를 계속 읽어도 되지만, UI 픽스처와 신규 prop은 source를 써야 한다.

데스크톱 계획 편집은 내장 컴포저를 쓴다

v49 데스크톱 SessionScreen이 계획 컴포저 모달을 소유하고 그 안에 UiDesktopPlanEditor를 렌더한다. 모바일은 기존 운동 플로우를 통해 계획 작성을 계속 라우팅해도 된다.

데스크톱 영속화는 다음을 통해 경계를 넘는다:

ts
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가 기존 의미 마커를 유지해야 한다:

  • mobileSessionCalendarGrid
  • mobileSessionsList
  • mobileSessionTodayButton
  • mobileSessionPrevMonthButton
  • mobileSessionNextMonthButton
  • session.open
  • session.delete.confirm
  • calendar.today
  • calendar.prevMonth
  • calendar.nextMonth
  • calendar.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. 게이트/스피너로 화면 마운트를 막지 말 것.