Skip to content

group-props — 그룹 (목록 · 화이트보드 · 커피라운지 · 작성 · 만들기) props 계약

화면 픽스처(앱 저장소): src/react/ui/mobile/fixtures/GroupListScreen.fixture.ts, src/react/ui/mobile/fixtures/GroupScreen.fixture.ts, src/react/ui/mobile/fixtures/GroupInfoScreen.fixture.ts

<a id="group-navigation-integration"></a>

2026-09-08 통합 — 보드 진입·복귀

  • 목록의 onOpenBoard(groupId)는 오늘 보드를 열며 groupState.boardReturnTo = "list"를 게시한다. 라운지는 마운트하지 않고 뒤로가기·스와이프는 목록으로 복귀한다. 보드의 backLabel은 "그룹 목록으로"다.
  • 라운지의 onOpenBoard(date?)는 공급한 ISO 날짜(미공급이면 오늘)를 보존하며 boardReturnTo = "lounge"로 돌아온다. selectCurrentGroup은 현재 groupId와 일치하는 라운지/목록 행만 공급한다.

<a id="group-data-availability"></a>

2026-09-08 통합 — 실제 공급 데이터와 선택 기능

  • list_my_groups_design_v1은 기존 전체 가입 그룹(현재 생성·수락 한도 50개)을 보존하면서 오늘 보드 전체(기존 종목·세트 물리 한도), 최신 가시 공지, 오늘 완료자(기존 보드 완료 RPC의 100세션 창을 사람별 최신 1개로 접음), 그룹 사진 경로를 한 응답으로 제공한다. client의 그룹별 네트워크 fan-out은 없다. 그룹별 상세가 조회된 뒤에는 같은 그룹/날짜의 최신 스토어 스냅샷을 즉시 반영한다.
  • get_group_attendance_month_v1은 한 달 28~31일의 실제 source=barbelic / origin_kind=group-board / origin_ref=groupId:boardDate 완료 세션을 공급한다. 해당 날짜 보드 존재, 현재 멤버십, 양방향 차단·정지·내림 정책은 기존 완료행과 동일하다. 세션 실제 날짜와 보드 계보 날짜를 혼동하지 않는다. attendanceLoading/attendanceError는 0일과 구분해 표시하며 월 이동 응답은 마지막 그룹·계정·월 요청만 반영한다.
  • update_group_profile_v1은 현재 그룹장만 이름(기존 1~20자·금칙어 정책)과 group-images private bucket의 그룹별 업로드 경로를 변경한다. 멤버만 사진 서명/조회, 그룹장만 업로드/삭제 가능하다. 5MB 이하 JPEG/PNG/WebP/GIF; 새 파일 업로드 뒤 DB 실패는 새 파일만 정리, 성공은 이전 사진을 정리한다. photo_path도 유저 원본 보호 이력에 포함한다. 사람 아바타와 그룹 이미지는 서로 대체하지 않는다.
  • 공지 작성은 과거 공지를 삭제하지 않는다. 라운지는 최신 10개를 조회하여 최신 카드와 지난 공지 모달에 공급하며 clock_timestamp()로 연속 작성 순서를 보존한다.
  • 대기 중 초대는 셸의 워드마크 알림에 연결한다(초대 알림 계약). 알림 컨텍스트 미공급 화면에서는 초대 카드를 계속 그리며 invitePendingId 동안 수락/거절을 비활성화한다.
  • 검증: tests/react/groupDesignIntegration.test.mjs, tests/react/notificationIntegration.test.mjs, tests/react/groupScreens.test.mjs, supabase/tests/database/group_design_notifications.test.sql.

대상: mobile/screens/GroupScreen.tsx(UiGroupListScreen · UiGroupScreen · UiGroupInfoScreen · GpCommentForm) · mobile/screens/GroupComposer.tsx(UiGroupComposer · UiGroupCreateSheet). 원칙: data in via props, mutations out via callbacks. 저장·권한·서버 왕복은 컨테이너 소유(20260821800000 groups v1 + groupStore, 이슈 #822). data-lg 마커 = 뷰 4종(dashboard.group·group.lounge·group.board·group.composer) + 액션 7종(designContract 등재, 2026-08-26 배선). 진입: 하단 탭바 "그룹"(tab key "group" — DashboardTab 유니온 등록 후속. 구 리포트 탭 자리, 드로어에도 동일 항목).

1. 공통 타입

  • role: "owner"(그룹장) | "writer"(작성 권한 부여) | "member". canWrite = owner | writer.
  • person: { id, name, handle?, short?, photo?, me?: boolean } — photo 실패 시 이니셜 폴백(GpAvatar).
  • 페이지 전환: 진입 = 우→좌 슬라이드 인(공용 .pg-slidein, base/primitives), 뒤로 = 좌→우 아웃 후 언마운트. 오버레이 화면은 .pg-overlay(absolute, 아래 화면 유지).

2. UiGroupListScreen (그룹 탭 기본 화면)

  • groups: Array<{ id, name, photo?, short?, notice?: { id, author, when, date?, text } | null, desc?, memberCount, role, canWrite, avatars: person[], todayTitle: string|null, today?: { title, exercises: string[], board?: board & { date }, done: [{ id, member: person, completedAt: ISO }] } | null }>.
    • 카드 상단(2026-09-08 재지시) = 한 행: 좌 그룹 프로필(photo/short — 이니셜 폴백, GpAvatar 30px 라운드 사각 --r-sm) + 그룹명 + 멤버 수 작게(사람 아이콘 + memberCount) · 우 › 셰브런(행 전체 = onOpenGroup; 구 아바타 스택 삭제, avatars는 계약 유지). 그 아래 최신 공지 밴드(notice = §3 notices[0] 동형 + 선택 date ISO, accent-wash: "공지" 라벨 + 1줄 ellipsis + 시각 — gpNoticeWhen: 오늘/N일 전(≤7일)/YY.MM.DD, today prop 기준, date 없으면 when "YY/MM/DD" 파싱). 신규 prop today?. role 칩·desc 행은 목록에서 비노출(데이터 계약은 유지 — 라운지 멤버 모달이 role 사용). 상단 ↔ 오늘 블록 가로선 없음.
    • today(2026-09-08 신설) — 오늘 보드 업데이트를 목록 카드에서 바로 보인다: 미니 화이트보드(today.board 공급 시 — §4 board shape + date, 연한 무채 회색 지면(테두리 없음) + 본문 서체(손글씨 보드 문법 아님, 2026-09-08 재지시): 헤더 = 세션 제목(EB) + 우측 끝 그룹명 작게, 날짜 YY.MM.DD, 종목 최대 4행 "A. 이름 / 세트 축약" + "+N개 더", 탭 = onOpenBoard(id)) 또는 종목 이름 한 줄(board 미공급 폴백, 최대 3개 + "+N") + 하단 행(좌 "완료한 사람 N명"(아바타·이름 없음, 2026-09-08 재지시) / 우 "운동하러 가기" onOpenBoard). 구 "오늘 보드 — 제목" 행·최신 완료 시각은 2026-09-08 재지시로 삭제. done은 §4 doneSessions와 같은 shape·같은 소스(보드 계보 세션) — 화면이 gpDoneByMember로 사람 단위로 접는다. 미공급 = todayTitle만으로 제목 행 1줄(구 동작). 컨테이너: 그룹별 오늘 보드 스냅샷 + 오늘 완료 세션 목록을 목록 로드에 동반(Codex 후속 — 서버 RPC는 §4 get_group_board_day_sessions_v1를 그룹별 오늘 날짜로).
  • invites: Array<{ id, groupName, inviter: person, memberCount? }> — 그룹장 초대 확인 카드(수락/거절). 2026-09-08: 목록 카드 비노출(GP_INVITE_CARD_UI=false, 마크업·콜백 보존) — 초대 수락/거절은 워드마크 알림 목록의 행 인라인 액션으로 이동(shell-frames.md §notify actions). 컨테이너: 초대함을 notify.items에 합성(kind:"group", actions:{accept:"수락",decline:"거절"}, onAccept/onDecline = 기존 수락/거절 배선).
  • 콜백: onOpenGroup(id) · onOpenBoard?(id)(2026-09-02 신설 — 오늘 보드 있는 카드의 "운동하러 가기"·미니 보드, 컨테이너는 라운지를 건너뛰고 해당 그룹 오늘 화이트보드로 진입. 2026-09-08: 이 경로의 보드는 라운지를 밑에 마운트하지 않는다 — 뒤로/좌→우 스와이프 = 목록으로 복귀(UiGroupScreen backLabel="그룹 목록으로" 공급, 라운지 진입은 라운지 경유 보드만); 없으면 버튼 미렌더. 마커는 group.list.board.open — 목록은 라운지 아래 상시 마운트라 라운지의 group.board.open과 이름을 나누어야 e2e .first() 선택이 라운지 버튼을 잡는다) · onCreateGroup()(워드마크 행 좌측 + 아이콘, 2026-09-08) · onAcceptInvite(id) · onDeclineInvite(id).

3. UiGroupInfoScreen — "커피라운지" (그룹 진입 기본 화면)

  • 워드마크 행 중앙 라벨 "커피라운지" · 뒤로 = 그룹 목록. 목록 위 .pg-overlay — 목록은 라운지 표시 중에도 아래에 상시 마운트(컨테이너: 라운지·보드 = 조건부 형제 렌더), 뒤로가기 = 슬라이드 아웃 후 언마운트로 목록이 재마운트 없이 그대로 드러남 (2026-08-26).
  • 구성: 상단 행(2026-09-08 재지시: 좌 그룹 프로필(group.photo/short, 32px 라운드 사각) + 그룹명 19px + 바로 옆 사람 아이콘+멤버 수(탭 = 멤버 모달, 행 탭 = onOpenMember(id)) · 우측 끝 "설정" 필 버튼 → 메뉴 모달(.gpi-menu): 초대하기(onInvite) · 공지작성(onAddNotice 모달) · 그룹 설정(그룹장 + onOpenSettings(), 마커 group.settings.open — Codex 등록 후속); 셋 다 없으면 버튼 미렌더) → 공지 워시 박스(헤드 좌 = "공지" 라벨 + 바로 옆 시각 gpNoticeWhen 오늘/N일 전(≤7일)/YY.MM.DD · 우상단 › = 지난 공지 모달(notices[] 전체, 최신순, 작성자·시각·본문, 타인 공지 신고) — 2026-09-08 재지시) → 출석왕(달력과 표시 월 공유 — 이번 달 = "이번달 출석왕", 지난달 = "M월 출석왕"; attendance에서 멤버별 그 달 출석 일수 집계 — 표시용 집계, 동률 = 같은 순위·이름순; 라벨 행 "이번달 출석왕" + › / TOP 3 행(3등분, 🥇🥈🥉·이름 말줄임·N일), 탭 = 전체 순위 모달(행 탭 = onOpenMember); 그 달 출석 0 = 칸 유지 + "이 달에는 출석왕이 없어요"(탭 없음), 2026-09-08) → 출석 달력(attendance?: { [iso]: person[] } = 그날 보드 운동 완료 멤버(§4 doneSessions 소스와 동일, 컨테이너 집계) · onAttendanceMonth?(y, m0) 월 이동 시 그 달 데이터 요청(이번 달 이후로는 이동 불가 — 다음 달 버튼 비활성) · 헤더 "YYYY년 M월 · 출석 N일" · 운동한 사람이 있는 날(나 제외) = 채워진 무채 회색 원(surface-3) · 내가 운동한 날(person.me === true 포함) = 채워진 accent 원 · 오늘 = 숫자 위 4px accent 점 · 탭 = 그날 화이트보드로 이동 onOpenBoard(iso)(모든 날짜, 2026-09-08 — onOpenBoard 미공급이면 출석 날만 완료 멤버 모달)(2026-09-08 재지시 — 구 워시 박스+미니 아바타·인라인 행 폐지) · 구 액션 3버튼 행은 2026-09-08 삭제, 운동하러 가기는 목록 카드 미니 보드로) → 자유 대화(입력창이 목록 위 — 최신 댓글 아래, 전송 ↓). (2026-08-26 재지시 반영)
  • 데이터: members: person[] & { role }(나 포함) · notices: [{ id, author, when: "YY/MM/DD", date?, text }]본문 표시 = 최신 1개(notices[0]), 나머지 = 지난 공지 모달(2026-09-08 — 컨테이너는 최신순 전체(상한 권장 50)를 공급; 구 "교체 의미론" 폐지, onAddNotice는 목록 맨 앞에 추가) · chat: [{ id, body, createdAt(ISO), mine?, canDelete?, author }](날짜 무관 누적) · today.
  • 콜백: onBack · onOpenBoard(iso?)(달력 날짜 탭 시 iso 동반 — 컨테이너는 그 날짜로 보드 오픈, 인자 없음 = 오늘) · onOpenMember(id) · onInvite()(2026-08-26 신설) · onOpenSettings()(2026-09-08 신설 — 그룹장 설정 진입, 화면은 컨테이너 몫) · onSaveSettings({ name, photo?: File, removePhoto?: boolean }) → Promise<boolean|void>(2026-09-08 — 공급 시 화면 내 그룹 설정 모달(.gpi-spop: 76px 라운드 사각 프로필 미리보기 + 사진 변경/삭제 + 그룹명, 저장 = 이 콜백; onOpenSettings보다 우선). 이미지 리사이즈·업로드·URL 왕복은 컨테이너 몫 — 저장 후 group.photo 갱신으로 반영, false 반환 = 모달 유지) · onAttendanceMonth(y, m0)(2026-09-08 신설, 선택) · onAddNotice(text) · onSubmitComment(body)→Promise · onDeleteComment(id).
  • 신고·차단(그룹 UGC 안전, 이슈 #922 · 2026-09-02, 서버 표면 group_ugc_safety_v1): 타인 대화 행과 타인 공지에 "신고" 버튼(onReportComment(row) · onReportNotice(notice) — 공급 시에만 그린다). 컨테이너는 피드와 같은 moderationStore로 ⋯ 메뉴(신고하기·차단하기) → 신고 사유 시트/차단 확인을 그룹 판에 띄운다(대상 종류 group_chat_message / group_notice; 보드 댓글은 §4 group_board_comment). 신고·차단 직후 로컬 즉시 숨김은 피드 댓글과 같은 규칙(행 id·작성자 id) — notices[]authorId·mine을 함께 나른다(서버 mine 추가 키). 서버 규칙: 차단 쌍·정지 계정의 글과 초대는 목록에 오지 않고, 내려진 글·내가 신고한 글은 재조회에서 빠지며, 정지 계정은 그룹 RPC 전체가 42501(토스트 "계정이 일시 정지되어 있어요.").

4. UiGroupScreen — 화이트보드 (라운지 위 오버레이)

  • 워드마크 행 중앙 = 그룹명(2026-09-02 — 구 "화이트보드" 라벨·보드 카드 우측 손글씨 그룹명 폐지) · 뒤로 = onBack(라운지 경유 = 라운지로, 목록 직행 = 목록으로 — 컨테이너 라우팅; backLabel? = 뒤로 버튼 aria 라벨, 2026-09-08). 날짜 옆 오늘 칩 = 오늘이 아닌 날짜에서만, 탭 = onSelectDate(today). onOpenInfo는 보드 화면에서 더 이상 읽지 않는다(멤버 진입 = 라운지 인원 버튼; 컨테이너가 넘겨도 무해).
  • 보드(서체 = 나눔스퀘어라운드 Bold/EB — public/fonts 셀프호스팅 B·EB 2종, v78·#831 Phase 3): 좌상단 "YY/MM/DD X요일 운동"(탭 = 커스텀 미니 달력 onSelectDate(iso), boardDates = 보드 있는 날 워시 마크) + 연필(누구나 — canWrite 게이트 해제 2026-08-26, onOpenComposer) · 우상단 그룹명 · 종목 = "A. 이름" + 세트 축약 "무게 반복수x세트수"(연속 동일 구성 묶음, x1 포함, '회' 없음) · 맨 아랫줄 날짜 내비(SVG 셰브런 ‹ › = onStepDate(±1)) · min-height 33vh. 빈 날 = 프레임 유지 + 쓰기 버튼(누구나).
  • board | null: { title, whenLabel?, authors: person[], exercises: [{ name, memo?, pctBase?, pctRoundKg?, sets: [{ type: "main"|"warmup", load, reps, loadPct?, loadLb?, durationSeconds?, distanceMeters? }] }] } — title/authors는 데이터 계약만(보드 미표시). 제목 상한 50자(#950 — 입력칸·서버 동시). loadPct 있는 세트는 보드에 "N% 반복수x세트수"로 표시(오너 D3, 이슈 #837). 스냅샷의 환산 kg(load)는 작성자 기준값이라 세션 프리필이 더 이상 쓰지 않는다(이슈 #1150 — 아래 시작 바 항). pctRoundKg(5 | 없음) = 작성자가 켠 %→kg 5kg 반올림 설정(이슈 #1150 D2 — 컨테이너는 pct_round_kg로 왕복, 서버 검증기 20260904010100). 시간·거리 세트는 "5000m in 30:00 x세트수"로 표시(채워진 원자만 — 이슈 #926, 유산소 세트의 load 0·reps 0은 미표기).
  • comments(날짜 단위 — 보드 유무 무관, §3과 동일 shape) + onSubmitComment/onDeleteComment/onReportComment(row)(#922 — 타인 댓글 "신고" → 그룹 판의 ⋯ 메뉴, 대상 종류 group_board_comment) — 표시 = 보드 아래 인라인, 완료 멤버 행과 시간순 혼합(2026-09-08 — 정렬 키 = completedAt/createdAt, 행 사이 가로선 없음, 입력창 = 보드 바로 아래·목록 위) + 날짜 내비 말풍선 탭 = 댓글만 모아보는 드로어(FdCommentsSheet) 유지. 좋아요 = 날짜 내비 가운데 하트(board.likes {count, likedByMe} + onToggleLike() — 보드 있는 날만, 채움·빨강 = likedByMe) + 완료 인원 체크(+수, 사람 단위 — gpDoneByMember(doneSessions).length, 표시 전용).
  • exerciseRecords?(2026-08-26): 종목 행 탭 → 보드 안 슬라이드 전환 — 그날 그 종목을 한 멤버들의 기록 뷰. 헤더(날짜·그룹명)·날짜 내비는 유지, 콘텐츠만 우→좌 인(복귀 ‹ = 좌→우 인, prefers-reduced-motion 무애니). shape { [종목명]: [{ id, member: person, sets: [{type, load, reps, loadPct?}] }] } — 행 = 아바타(sm)+이름+세트 축약(보드 표기와 동일 — loadPct면 % 표시), 목록 = 보드 안 스크롤(max-height 45vh), 빈 목록 = "이 날 이 종목을 기록한 사람이 아직 없어요". prop 미공급 = 종목 행 탭 없음(표시 전용). 날짜 이동 = 종목 목록으로 복귀. 집계·매칭은 컨테이너 소유.
  • 완료·기록의 공급 기준(2026-08-29, 이슈 #880 — 오너 D1·D2=a안): doneSessions(와 그로부터 파생되는 exerciseRecords)의 소스는 이 보드에서 시작해 완료한 세션만 — 서버 RPC(get_group_board_day_sessions_v1)가 세션 source_ref 계보 접두 group-board:<group_id>:<board_date>:로 매칭한다(세션의 date 무관 — 자정 넘긴 완료도 그 보드 소속, 보드 없는 날 = 빈 목록). 계보는 시작 바 프리필이 발급한다(아래 시작 바 항). 그날 개인 운동으로 같은 종목을 한 기록은 더 이상 뜨지 않는다.
  • doneSessions?(2026-08-26): 이 보드 운동을 완료한 멤버 행 — 댓글 영역 맨 위(완료자 → 댓글 → 입력 순), 댓글 행과 같은 문법(fd-cmrow: 아바타 32px + 본문), 한 줄 고정(nowrap ellipsis). 표시는 사람 단위(#875 Phase 1, 오너 D1): 같은 member.id의 세션 여러 개는 화면이 gpDoneByMember로 최신 completedAt 1줄로 접는다(행 탭 = 그 최신 세션 상세) — 원본 배열은 세션 단위 그대로 공급(종목 기록 뷰가 전 세션 사용). 행 = "NN님이 운동을 완료하셨습니다 {상대 시각}"(gpDoneWhen(completedAt): 방금/N분 전/N시간 전/N일 전(≤7일), 그 너머 = "YY.MM.DD HH:MM" — 메타 12px/500 ink-3, "오늘" 표기 없음). shape [{ id, member: person, sessionId?, completedAt: ISO datetime }]. 행 탭 = onOpenDoneSession(item) — 컨테이너가 해당 멤버 세션 상세를 UiGroupSessionOverlay(보드 위 3겹째 pg-overlay, 우→좌 진입·좌→우 퇴장, children=함수면 requestClose 수신)로 띄운다(미공급 시 표시 전용). 타인 세션 상세 = UiSessionDetail ownerName 공급(fband 소유자 표기 + 워드마크). 빈 배열 = 섹션 미렌더. 매칭·집계·세션 열람 권한은 컨테이너 소유.
  • 세션 공유 시작 = 셸 하단 시작 바(홈 .home-startfab 동일 문법): 그룹 탭 + 보드(exercises 있음) + 보드 화면 열림 + 비진행일 때만 마운트, 문구 = "[그룹명]의 M/D 운동으로 시작"(2026-08-26 개정 — 구 "M월D일")(BottomNav startLabel prop). 프리필 draft 변환은 컨테이너: { title, exercises: [{ id, exerciseId(정규화 후속), name, fields, measurementType, bwFactor, pctBase?, pctRoundKg?, done:false, sets: [{ id, type, load, loadPct, reps, durationSeconds, distanceMeters, done:false, restSeconds:null, freeRest:false }] }] }(fields·measurementType·bwFactor = 매칭된 카탈로그 행에서 — 없으면 load/reps 격하 + 세션 저장 프로필 대조 22023, 이슈 #926). % 세트는 시작하는 사람의 1RM 기준으로 환산(이슈 #1150, 오너 D1 a·D2 — groupBoardPrefill.ts): 보드 세트에 loadPct가 있으면 loadPct·종목 pctBase·pctRoundKg를 그대로 동반하고 load는 시작자의 기준 1RM(실측→추정, oneRms 조회 표 — 기준 우선순위는 운동 화면 activePctRm과 동일: 명시 기준 → 복합 1RM 보유 첫 동작 → 자기 종목, 체중 계수 종목은 기준 없음)으로 round(% × 1RM, 0.5kg | pctRoundKg 5면 5kg)로 다시 계산한다. 시작자에게 기준 1RM이 없으면 loadPct만 남기고 load는 빈 값 — 운동 화면의 종목 설정 창이 "1RM 기록이 없어 %를 kg로 바꿀 수 없어요" 안내(D3)와 저장 시점 카드(#837 D2)로 kg 입력을 유도한다. 작성자의 환산 kg(스냅샷 load)는 kg 세트(loadPct 없음)에서만 그대로 쓴다. 매칭 실패 종목은 같은 이름의 커스텀 종목을 자동 등록해 잇는다(이슈 #945, 오너 D1~D3): 기록 항목은 보드 세트 값으로 추정(inferRecordingFieldsFromSets — 시간만→시간·거리만→거리·둘 다→거리+시간·그 외→무게·횟수), 복합 구성 동작도 대상(프로필 미상 = 무게·횟수 기본), 별도 안내 없음, 같은 정규화 이름은 1회만 등록(서버도 같은 이름 재등록에 기존 행 반환 — 반복 시작 중복 생성 없음). 등록 실패(오프라인 등) 종목만 기존 "빼고 시작" 개수 토스트로 제외. 계보 발급(2026-08-29, 이슈 #880 a안): 프리필 start에 origin: {groupId, date}를 동반 — 컨테이너(appController)가 드래프트 sourceRef를 group-board:<groupId>:<date>:<operationId>로 선발급해 세션 BRID에 출처가 내장된다(groupBoardWorkoutSourceRef). 좌표가 어긋나면 조용히 기본 local:* 발급(보드 사정이 저장을 막지 않는다).

5. UiGroupComposer / UiGroupCreateSheet

  • Composer(보드 작성/수정, 탭 내 전면) — 운동 계획 작성과 동일 편집기(2026-08-26: WfRecord planMode + workout.picker 재사용, 자체 폼 폐기): group · date · initial: board|null · catalog · onSave({title, exercises}) · onClose + 선택 onAddCustomExercise(draft)→Promise<종목>(픽커 커스텀 추가 — 운동 플로우와 동일 계약) · profile · oneRms · exerciseHistory. appbar = 계획 헤더 문법(제목+연필 인라인 수정 · 그룹명·날짜 서브라인 · 취소 · 올리기/수정 완료 필).
  • catalog: 운동 플로우 exerciseCatalog와 동일 공급 권장(synonym 검색·기록 항목 프로필). [{id,name}] 최소형도 동작 — 픽 = id 통과, UUID 강제 없음(정규화 = 컨테이너 후속 ⑤).
  • 유효성 = 종목 ≥1 (제목 미입력 시 "M월 D일 운동" 자동). onSave out 계약: {title, exercises:[{name, exerciseId?, pctBase?, memo?, sets:[{type:"main"|"warmup", load, reps, loadPct?, loadLb?, durationSeconds?, distanceMeters?}]}]}기록 프로필에 값 있는 세트만 직렬화(= planMode 세트 추가 게이트와 동일식 exMeasures().some(mHasValue) — 구 reps>0 필터는 유산소·요가 세트를 전부 떨궈 보드 저장 불가였다, 이슈 #926. 시간·거리는 durationSeconds/distanceMeters로 동반 — 컨테이너는 duration_seconds/distance_meters로 왕복 보존. 수정 재진입 시 종목 기록 프로필은 catalog에서 재수화 — 스냅샷은 fields를 나르지 않는다), %로 입력한 세트는 loadPct(원본 %)·pctBase(기준 종목)를 동반(오너 D3, 이슈 #837 — 컨테이너는 load_pct/pct_base로 왕복 보존, 수정 재진입은 loadPct 생존으로 %가 유지된다), 종목의 5kg 반올림 설정은 pctRoundKg: 5로 동반(이슈 #1150 D2 — 운동 화면 체크박스가 화면 상태가 아니라 종목 값 pctRoundKg를 쓴다. 컨테이너는 pct_round_kg로 왕복, 수정 재진입·프리필 모두 복원), lb로 입력한 세트는 loadLb(원본 lb)를 동반(이슈 #950 — 컨테이너는 load_lb로 왕복 보존, 수정 재진입은 lb 입력 상태로 복원. 보드 화면 표시는 kg 유지), 수정 재진입 재수화는 bwFactor까지(프리필과 동일 폭), 자유 기록 = {kind:"note", name: 제목(≤50자), memo: 내용(≤1000자), sets:[]}(상한 = 오너 확정 #950, 편집기·서버 동시. kind 통과 = 수정 재진입 시 자유기록 카드로 복원 — 컨테이너는 kind 필드 왕복 보존). 기존 보드 memo는 종목 note로 왕복 보존(계획 모드 미편집).
  • CreateSheet(센터 모달): directory: person[](초대 후보 = 팔로잉 + 서버 닉네임 검색 결과 병합) · onSearchDirectory(q)(타이핑마다 — 컨테이너가 search_user_profiles_v1로 결과를 directory에 병합, 2026-08-26 배선) · onCreate({ name, invited: person[] }) · onClose.

6. 홈 연동 (HomeScreen — 별개 화면, 이번 델타 포함)

  • onOpenReport() 신설: 히어로 프로필 사진(lvt)·이름 텍스트(idtx)·우측 › 탭 = 리포트 페이지 슬라이드 오버(.pg-overlay .pg-slidein, 좌상단 .pg-back = 복귀). 미공급 시 구 동작(주요 종목 드로어) 유지 — 친구 스코프는 컨테이너가 friendView("report")로 배선.

좌→우 스와이프 뒤로가기 — 그룹 3표면 (2026-08-27 → 2026-09-08 스택 셸)

이슈 #1393 Phase 4: 커피라운지·화이트보드·그룹 세션 상세는 그룹 탭 화면 스택의 항목(groupLounge/groupBoard/groupSession, entry prop)이며 공용 셸 UiStackScreen이 진입/퇴장·스와이프(navigation-stack.md §4 판정값)·뒤로가기 4경로(‹ 버튼·앱 안 스와이프·iOS 가장자리·Android 뒤로)를 맡는다. 보드 작성창(groupComposer)과 보드 안 종목 기록 뷰(groupBoardExercise)도 보드 위 항목(뒤로 한 겹). 닫힐 때 데이터 상태를 접는 것은 컨테이너가 등록한 closer(closeLounge/closeBoard/closeDoneSession). 아래 표는 종전 판정(2026-08-27)의 기록이다:

트리거상태 변화복귀
오버레이 루트 touchstart시작점(x,y) 기록 — 렌더 변화 없음
touchend에서 dx > 45 && `dx>
touchend에서 조건 미충족(세로 스크롤 등)무반응
prefers-reduced-motion: reduce애니메이션 없이 즉시 onBack()
  • 판정·집계 로직은 화면에 없다(제스처 임계값만) — 라우팅·origin 복원은 컨테이너 소관.

A11 resource 연결 (2026-09-10)

목록·초대·라운지·보드·완료자·출석·상세 props는 group resource에서 파생한다. 보드/완료자 로딩은 독립적이다. 권한 상실 시 보드/목록의 이전 값을 숨기고, 화면 이탈은 해당 조회를 취소한다. 알림 대상의 실제 세션 owner 검증과 읽음 cutoff를 유지한다. 소유·무효화 계약을 따른다.

작성·수정 후 원래 화면 보존 (#1554, 2026-09-10)

그룹 화이트보드 작성·수정(groupComposer) 중 기존 그룹 목록·라운지·보드와 미전송 입력은 마운트를 유지한다. 작성기 아래 화면은 표시·접근성 탐색에서 숨기고 하단 바 신고를 잠시 해제한다. 작성기를 닫으면 동일한 화면과 입력 상태를 다시 보이고 신고를 복원한다. 작성기 초안의 저장·취소 정책과 서버 쓰기 계약은 기존 동작을 유지한다.