셸 골격 프레임 계약 (shell frames)
화면 픽스처(앱 저장소):
src/react/ui/mobile/fixtures/MobileShellBody.fixture.ts
<a id="group-invitation-notifications"></a>
2026-09-08 통합 — 실제 이벤트 워드마크 알림
- production
notify는groupDesignAdapter.buildNotificationBrandNotify로 서버user_notificationsledger를 매핑한다. 실제 그룹 초대·팔로우·앱 운동 완료·좋아요·댓글·기준선이 아닌 PR 이벤트의 트리거가 기록한다.recipient_user_id + event_keyunique로 같은 사건·PR projection 재생을 중복 알림으로 만들지 않는다.system은 현재 실제 발행원이 없으므로 생성하지 않는다. 최초 배포는 현재 미처리 초대만 이관하고 과거 소셜 활동을 새 알림으로 꾸미지 않는다. - 초대 문구·canonical invite id·수락/거절 callback은 controller adapter가 공급한다. 본문 탭은 그룹 탭으로 이동하며 초대 응답을 대신 실행하지 않는다. 차단한 초대자의 행은 목록·워드마크 모두 숨긴다.
list_user_notifications_v1은 수신자만 읽으며 원본 존재·현재 팔로우/멤버십·차단·정지·콘텐츠 내림을 재검사한다. 1~200개 keyset 페이지와has_more, 서버as_of를 반환한다. 알림 원문 직접 조회/임의 생성은 authenticated에 열리지 않는다.read_user_notifications_v1은 해당 수신자의read_at만 저장한다. 모두 읽음은 가장 최근 성공한 목록 페이지의as_ofcutoff가 필수다. 이전 페이지를 더 읽었을 때도 그 페이지의 cutoff로 갱신하여 뒤늦게 기록된 과거 알림의 서버·클라이언트 읽음 상태를 일치시킨다. 사건의created_at과 ledger의recorded_at을 구분하여 과거 날짜의 사건이 뒤늦게 기록돼도 이전 화면의 모두 읽음에 포함되지 않는다. 새로 도착한 항목은 클라이언트에서도 미읽음으로 남는다.BrandNotice.actionPending?: boolean이 true면 수락/거절 둘 다 비활성이다. 응답 중 중복 제출은 스토어도 차단하며 성공·실패 뒤 초대 목록을 다시 읽는다. 계정 전환 뒤 도착한 이전 응답은 새 계정 상태나 토스트를 바꾸지 않는다.BrandNotifyContext미공급 화면은 기존 그룹 초대 카드로 접근을 보존한다. 워드마크 메뉴는 Escape로 닫고 원래 버튼으로 포커스를 반환한다.- 알림·초대 목록의 초기 워밍은 온라인일 때만 실행한다. 오프라인 캐시로 재진입한 화면은 연결 복구 이벤트 뒤 두 목록을 불러오며, 사용자 변경 시 이전 계정의 대기 리스너를 정리한다.
- 페이지 밖 세션 이동은 단건 알림 대상 계약을 따른다.
friend는 실제 actor 프로필,pr는 실제 세션/내 PR 화면으로 이동한다. - 검증:
tests/react/notificationIntegration.test.mjs,supabase/tests/database/group_design_notifications.test.sql.
2026-08-25 이슈 #753(화면 표현 전량 ui 이사, 오너 D1(a)) 산출물. 앱에서 실제로 보이는 모든 표면이 클로드 디자인 가시권(
src/react/ui/**+docs/contracts/**) 안에 있도록, 셸 골격 DOM의 소재와 규칙을 여기 명문화한다.
1. 프레임 인벤토리
셸 골격(화면을 감싸는 래퍼 DOM)은 전부 ui 프레임 컴포넌트가 그린다. 컨테이너는 상태를 props로 주입할 뿐 골격 DOM을 직접 만들지 않는다.
| 프레임 | 파일 | 그리는 것 | 컨테이너(배선 소유) |
|---|---|---|---|
MobileThemeRoot | ui/mobile/shell/MobileShellFrames.tsx | 테마 지면(data-accent/card/density, 기준 16px) | mobileApp.tsx |
MobileAppFrame | 〃 | .app 컬럼 + .safe-top (+ 옵션 .screen) — 부팅/온보딩 전용 잔존 | vite/mobileRoot.tsx(온보딩) |
MobileShellBody | 〃 | 통합 앱 셸(이슈 #855 Phase 8, 오너 D3=B): 탭 모드·운동 플로우가 같은 .app 엘리먼트 공유 — flow일 때만 wf-live+workoutView 마커+운동 기본 채널 none 및 도크 등 실제 신고 게시, nav 슬롯. 모드 전환 = 방문 탭 숨김·복원과 운동 층 표시(탭·기둥 재마운트 없음, 2026-09-10 #1554) | mobileApp.tsx |
MobileTabFrame | 〃 | .screen > .tab-keep/.tab-pane 토글 + 탭별 스켈레톤. 깊이 화면(세션·종목 상세·일 요약·기록 도구·친구 페이지·그룹 라운지/보드)은 판 안 화면 스택 항목(이슈 #1393, navigation-stack.md) — 레이어 슬롯·scopePane은 은퇴(2026-09-08) | mobileApp.tsx(MobileTabsRegion) |
DesktopThemeRoot | ui/desktop/shell/DesktopShellFrames.tsx | 데스크톱 테마 지면 | desktopApp.tsx |
DesktopAppFrame | 〃 | .dk-app(sky·compact 기본값은 속성 생략) | desktopApp.tsx |
DesktopMainFallback | 〃 | 본문 lazy 로딩 중 .dk-main 폴백 | desktopApp.tsx |
MobileHostFrame | ui/shared/gates/HostFrames.tsx | .gym-app-host > .gym-app-device(폰 컬럼/목업) | vite/mobileRoot.tsx |
DesktopHostFrame | 〃 | .gym-app-host.desktop > .gym-app-desktop(1913×1063 캔버스) | vite/desktopRoot.tsx · vite/admin/AdminStandaloneRoot.tsx |
PlatformGate | ui/shared/gates/ShellGates.tsx | .dk-app.dk-shell-gate.gym-app-platform-gate 게이트 래퍼 | app.tsx · AdminStandaloneRoot |
HostLoadingScreen·AppErrorScreen | ui/shared/gates/ | 부팅 스플래시·복구 카드·렌더 에러 폴백 | vite/main.tsx·vite/AppErrorBoundary.tsx |
ImpersonationBannerView·ImpersonationLauncherView | ui/shared/gates/ImpersonationViews.tsx | 유저 열람 배너·런처·검색 모달(.lg-imp-*) | src/react/ImpersonationOverlay.tsx |
AdminSidebar | ui/desktop/shell/AdminSidebar.tsx | 관리자 독립 셸 사이드바(.dk-side·.adm-shell-*) | AdminStandaloneRoot |
호스트·부팅·에러 표면의 CSS는 ui/shared/styles/app-host.css(호스트 스코프라 의도적으로 @layer 밖), 나머지는 기존 ui 스타일 트리에 있다.
2. keep-alive 계약 (⚠ 구조 변경은 Codex 경유)
MobileTabFrame의 클래스 토글은 표현의 옷을 입은 의미론이다(탭 렌더 개편 2026-08-24):
- 탭 전환 = 활성 판만
.tab-pane, 나머지는.tab-pane off(display 전환). 방문한 판을 언마운트하는 구조(조건부 렌더)로 바꾸면 keep-alive(스크롤·상태 보존)와 렌더 입자성 (탭 전환 조립 루트 커밋 0)이 깨진다. - (이슈 #1393 Phase 5, 2026-09-08) 판을 대신 가리던 레이어 슬롯(
.tab-keep off)은 은퇴 — 기록 도구·일 요약은 판 안 화면 스택 항목이다..tab-keep.offCSS 규칙과 가드 문법은 그대로 둔다(가드 특정도 계약 — 아래). - 숨은 판 가드(이슈 #1195):
.app/.app .screen을 주어로 하는 모든:has()인자는:not(.tab-pane.off *):not(.tab-keep.off *)로 끝난다 — 예외 없음(레이어·플로우 전용 표식도 같은 가드: 특정도 균일이어야 규칙 간 우선순위가 선언 순서로 유지된다). 게이트 =tests/react/hiddenPaneGateGuards.test.mjs(전 PR). 깊이 화면의 하단 크롬은 각 스택 셸(UiStackScreen chrome)이 신고한다(none/fsb). - 판 요소(panes)의 정체성은 컨테이너가 관리한다 — 프레임은 요소를 만들지 않고 배치만 한다.
- 숨은 판 구독 게이트(이슈 #1610 Phase 6, 2026-09-15): 숨은 판(
.tab-pane off, 운동 층 아래.tab-keep off)은 CSS 로만 숨을 뿐 React 갱신은 받았다. 이제 판 래퍼(mobileAppMobileFeaturePane)의 게이트(ui/shared/paneGate)가 숨은 동안 판 안의 스토어 구독(feature 컨텍스트·자원·직접 구독·탭 스택·워드마크 알림)을 "밀림" 으로 적어 두고, 판이 보일 때 한 번에 따라잡는다. keep-alive(스크롤·로컬 상태 보존)와 렌더 입자성(탭 전환 루트 커밋 0)은 그대로다 — 언마운트가 아니라 구독 게이트라서. 정본 = 렌더 불변식 계약 §2-1 규칙 15. - 스타일링(각 클래스의 생김새)은 자유. 골격 중첩·토글 구조를 바꾸고 싶으면 Codex에 요청한다. 회귀 판정은
e2e/profile/tabSwitch.profile.mjs(재방문 탭 전환 조립 루트 커밋 0 기준).
2-1. 크롬 상태 채널·기하 계약 (⚠ 이슈 #855 — 구조 변경은 계약 개정 선행)
- 하단 크롬 상태는 단일 채널: 컨테이너(MobileTabsRegion·MobileShellBody flow)가
.app[data-bottom-chrome="tabbar|tabbar-start|tabbar-resume|dock|fsb|none"]로 게시하고(none= 레이어가 하단 크롬을 전부 가림 — 일 요약, 이슈 #1195 Phase 4), 높이 사상표(--bottom-chrome-h)와 탭바 숨김·클리어런스 유도식(--chrome-hush)이 이 채널만 소비한다(nav.css·core.css). 탭 구독은 리전에 격리 —.app리프트 금지, 게시는 MobileTabFrame의 레이아웃 이펙트. - 기하 소유권: 도크 스테이지
--dock-stage-h(workout.css 계약 C′) · 탭바height: --tabbar-h + safe· 워드마크 골격은lib/BrandRow.tsx단일 렌더러. 게이트 =tests/react/dockGeometryContract.test.mjs(전 PR) +e2e/dock-geometry.spec.mjs(full-ci, safe 0/34 실측) — 스위치 소등·규칙 삭제도 게이트 위반이다. - 잔존 :has() 예외(승격 대상 밖): 세션 상세(시트 스왑 예외 = 레이어 내부 상태)·종목 상세·댓글 시트(#775)·빈 홈 FAB 회수. 일 요약은
none채널로 승격(이슈 #1195 Phase 4). - 이슈 #1250(2026-09-04): 위 잔존 :has() 4건은 채널로 승격되고(
return-bar·none신고), 바닥 여백은.screen뿐 아니라 모든 하단 스크롤 컨테이너가--bottom-clearance하나만 소비한다. 값 사상표·유예 목록·도달 게이트 정본 =bottom-chrome-clearance.md.
3. ui가 될 수 없는 표면 (예외 3종 — 스펙만 여기)
| 표면 | 소재 | 왜 ui가 아닌가 | 변경 절차 |
|---|---|---|---|
| 정적 부팅 스플래시 | index.html(인라인 CSS #gym-app-critical-loading-paint + .gym-app-static-splash 마크업) | JS 로드 전 첫 페인트여야 해서 React·외부 CSS 불가 | Codex에 요청. 시각은 ui의 AuthLoadingGate(.lg-splash)와 쌍 — 한쪽을 바꾸면 둘 다 바꾼다(entrance 이어받기 계약: gym-app-splash-resume) |
| 네이티브 셸 스플래시 | iOS/Android 셸 자산(splashOwnerV1) | 웹 코드가 아님 — 환경당 한 장 원칙(2026-08-23) | 셸 트랙에 요청. 웹은 html.gym-app-native-splash로 자기 스플래시를 숨긴다 |
| 법률 정적 페이지 | public/legal/(legal.css) | 심사·링크용 독립 정적물(앱 밖 URL) | Codex에 요청 |
부트 스크린의 문구·복구 카드·에러 카드(React가 그리는 쪽)는 예외가 아니다 — 1절의 HostLoadingScreen·AppErrorScreen이 ui 소유다.
참고: src/react/vite/AppRoot.tsx + vite-scaffold.css는 Vite 마이그레이션 잔재 스캐폴드로 앱에서 렌더되지 않는다(참조 0) — 화면 표면이 아니므로 이 계약의 대상 밖이다.
MobileShellBody — 워드마크 알림 notify (2026-09-07)
MobileShellBodyProps에notify?: BrandNotify | null추가(선택). 셸이BrandNotifyContext(ui/mobile/lib/brandNotify.tsx)로 한 번 제공하고, 워드마크 단일 렌더러(UiBrandRow/UiBrandMark)가 컨텍스트로 읽는다 — 화면 props 계약 무변경. 미공급(null) = 워드마크는 종전과 동일한 순수 텍스트.- 타입(ui 소유):
BrandNotice { id, text, time?, unread?, kind?, actions?: {accept, decline}|null, onAccept?, onDecline?, sessionId? }(sessionId + kind "feed"/"like"/"comment" = 친구 운동 완료·내 포스트 좋아요·댓글 알림, 탭 → 컨테이너가 피드 탭focusSessionId공급 — feed-props.md §알림 → 포스트 점프)(actions = 2026-09-08 신설 — 그룹장 초대 수락/거절이 그룹 목록 카드에서 알림 행으로 이동. 행 아래 [수락][거절] 2버튼(마커 기존group.invite.accept/group.invite.decline), 탭 결과는 컨테이너가 items 갱신으로 반영, 본문 탭 onPick과 독립) ·BrandNotify { items, onOpen?, onPick?(item), onReadAll? }.text는 컨테이너가 완성 문장으로 공급(UI는 조립·판정하지 않는다),time은 표시용 상대 시각 문자열. - 표면: 배지 =
unread === true수(99 초과99+), 워드마크 탭 = 목록 드롭다운(.app포털,data-lg-hook="brandNotifyList"), 항목 탭 =onPick+ 즉시 닫힘,모두 읽음=onReadAll(미공급 시 버튼 비노출). 홈 히어로 워드마크(.hbrand)도 같은 렌더러를 쓴다(b → span, 굵기는 CSS 700 명시). - Codex 후속: ①
mobileApp.tsx가 알림 목록·읽음 처리·이동 배선을notify로 공급 ② 마커brandNotify/brandNotifyListdesignContract.ts등록 ③ 알림 문구 카피 등재.
// MobileShellFrames.tsx
type MobileShellBodyProps = { flow?: boolean; nav?: React.ReactNode; notify?: BrandNotify | null; children: React.ReactNode };NavDrawer — 커스텀 종목 관리 항목 (2026-08-27)
NavDrawerProps에onCustomExercises?: () => void추가(선택 배선). ACCOUNT 그룹 마지막에 연필 글리프 항목으로 렌더되며, 콜백 미공급 시 항목 자체가 비노출.- 라벨 =
MOBILE_SHELL_COPY.drawer.customExercises?.label→ 폴백"커스텀 종목 관리"(어휘 원본은 Codex 소유barbelicCopy.ts, 카피 등재 후 폴백 제거 가능). - ⚠ 타입 선언 파일
src/react/types/reactComponents.ts는 전달 zip 범위(src/react/ui/) 밖** — 아래 2줄을 Codex가 직접 반영해야 MobileShell.tsx가 type-check를 통과한다:
// 커스텀 종목 관리 드로어 진입(2026-08-27 디자인 — 내 정보 인라인 카드에서 분리, 선택 배선).
onCustomExercises?: () => void;NavDrawer — 연간·월간 리포트 2행 + 라벨/글리프 (2026-09-03)
- ANALYSIS 그룹의 단일
리포트(keyvolume) 행 →연간 리포트·월간 리포트2행. 둘 다 리포트 화면 진입이고 차이는 기간 단위뿐이다. 새 콜백onPickReport?(unit: "year" | "month")— 공급되면 그것으로, 없으면onPick("volume")폴백(구 계약 하위 호환).- 컨테이너 몫(Codex 후속):
mobileApp.tsx가onPickReport를 받아reportPeriodUnit을 그 값으로 세팅 +reportOffset = 0후volume탭 선택, 현재 단위는reportUnit으로 되돌려 준다(행 하이라이트 =tab === "volume"∩reportUnit === unit, 미공급 시 연간 기본). 마커는 기존dashboard.nav/data-lg-option-value="volume"재사용(신규 등록 0). - 라벨은 안정축(unit) 로컬 리터럴 —
MOBILE_SHELL_COPY.drawer에reportYear/reportMonth등재되면 그걸로 교체(어휘 원본 = Codex 소유barbelicCopy.ts).
- 컨테이너 몫(Codex 후속):
주요 종목 목록 수정→나의 주요 종목, 글리프 = 목록(feed) → 별(star). 액션 마커pr.favorites.open불변. 카피 원본MOBILE_SHELL_COPY.drawer.favoriteEdit.label은 아직 구 문구라 로컬 리터럴 사용 — Codex 후속: 카피 갱신 후 참조 복원.- 드로어 닫기 버튼 글리프 =
x→chevR(›), 테두리 없음(기존.drawer-close-chev규칙 그대로 — CSS 변경 0). - 신규 아이콘 2종(
shared/icons/appIcons.tsx):star(나의 주요 종목),bars(연간 리포트 — 기존chart는 돋보기 글리프라세션 검색과 시각 충돌).
// 리포트 기간 단위 직행(2026-09-03 디자인 — 드로어 연간/월간 2행, 선택 배선).
onPickReport?: (unit: "year" | "month") => void;
// 현재 리포트 기간 단위 — 두 행 중 어디에 하이라이트를 줄지 결정(미공급 = 연간).
reportUnit?: "year" | "month";
// 탭 위 도구 화면이 열린 상태(2026-09-03 디자인) — "favorites" | "manualPr" | "customExercises".
// 설정되면 탭·리포트 행 강조를 모두 끄고 해당 행만 하이라이트한다(드로어 하이라이트 = 열린 화면 1개 불변식).
// 이슈 #1393 Phase 5: 값은 화면 스택의 recordsTool 항목(params.tool)에서 드로어 바인딩이 직접 읽는다.
activeItem?: "favorites" | "manualPr" | "customExercises";BottomNav — 드로어 진입 화면 = 목록 탭 활성 (2026-09-03)
- 하단 탭바의 활성 표시: 탭바에 있는 4탭(home·calendar·feed·group)이 아닌 화면은 전부 목록(메뉴) 탭 활성. 탭 값으로 판정 가능한 것(volume·search·profile·pr)은 내부 처리, 탭 위에 얹는 레이어(나의 주요 종목·1RM 직접 입력·커스텀 종목 관리 — tab 불변)는 컨테이너가
menuActiveprop으로 알린다.menuActive가 true면 다른 탭 강조는 끈다. BottomNavProps.menuActive—MobileTabsRegion이 화면 스택의 recordsTool 항목 유무를 구독해 전달한다(이슈 #1393 Phase 5 — 종전 컨테이너 지역 state 폐지).
// 드로어 진입 레이어 열림 = 목록 탭 활성 강제(2026-09-03 디자인). 미공급 시 tab 기준 판정.
menuActive?: boolean;U03 Desktop intrinsic host (2026-09-10)
DesktopHostFrame은 배율 style prop을 받지 않는다. desktopRoot의 1913×1063 상수·0.7 하한·resize listener·CSS zoom 변수를 제거했다. desktop host가 존재할 때만 html/body/#vite-root는 document 스크롤을 사용한다. 모바일 device의 pointer 분기·520/600 폭·safe area·overflow와 검은 고정 splash는 기존 계약을 유지한다.
DesktopAppFrame은 sidebar+minmax(0,1fr), 본문은 dk-main inline-size container다. 화면 최소 폭을 루트에 강제하지 않고 auto-fit과 wrapping으로 대응한다. compose는 dk-editor container다. viewport가 바뀌어도 JS 배율을 다시 계산하지 않는다. scroll/selector 예외는 시각 계약에 한정하며 실측을 참조한다.
메인 데이터 준비 후 본문 공개 (#1554 후속, 2026-09-11)
오너는 메인과 하단 바벨릭 프로필의 뱃지가 순서대로 나타나는 대신, 필요한 데이터가 준비될 때까지 전체 스켈레톤을 유지한 뒤 본문을 한 번에 보여주도록 지정했다. 메인 준비 조건은 홈 요약·PR 개요(등급 기준표 포함)·계정별 최초 즐겨찾기 조회다. 부분적인 종목명·기록이 먼저 있어도 준비 조건을 우회하지 않는다. 다른 탭의 프리로드는 이 조건에 포함하지 않는다.
usePrController.prFavoritesPending은 기존 즐겨찾기 저장소의 owner/hydrated 상태에서 파생한다. 최초 조회 성공 또는 기존 실패 캐시 폴백이 적용되면 해제한다. homePrSectionsPending은 이 신호와 미도착 PR 조회를 합치며, UiHomeScreen은 homeBootstrapPending || prSectionsPending 동안 HomeBootstrapSkeleton만 그린다. 이미 조회가 완료된 일반 탭 왕복은 다시 스켈레톤으로 돌아가지 않는다. 조회·등급 산식·저장 동작은 기존 그대로다.
이는 초기 메인의 부분별 공개 정책 변경이다. 기존 리포트 기간 갱신과 편집 복귀 화면 보존의 동작은 유지한다. 근거와 검증은 후속 기록에 있다.
화면 픽스처와 심사 계정 데이터 — 2026-09-11 (#1568)
적용 대상은 앱 v0.17.11 후보다. 원격 반영·실제 심사 환경 생성은 별도 실행 결과로 판단한다.
화면 데이터의 정본은 앱 src/react/ui/shared/fixtures/reviewScenario.ts(합성 인물·상대 날짜)와 reviewWorkouts.ts(종목·운동 원본), 각 화면의 src/react/ui/mobile/fixtures/<screen>.fixture.ts다. 화면 props 계약을 바꾸는 앱 PR은 해당 데이터 타입과 fixture를 같은 PR에서 수정하고, 문서 저장소 계약 변경을 연결한다. 데이터 타입은 앱 src/react/contracts/ui/(공용 데이터)와 src/react/ui/mobile/contracts/(모바일 계약·타입 재수출)에 있고 화면 export와 fixture의 satisfies가 공유한다. 기존 선택 props는 화면 호출의 호환을 유지하며 fixture에서 요청 상태에 필요한 필드는 필수로 둔다.
프리뷰는 화면 파일·fixtures·관련 데이터 타입·ui/shared/fixtures·ui/shared/assets를 함께 verbatim 반입한다. 디자인 로컬 _fixtures/를 중복 원본으로 유지하지 않는다. 날짜가 필요한 named export는 create…Fixture(today?: string)이며 프리뷰가 업무 날짜 YYYY-MM-DD를 한 번 결정해 전달한다. 원본은 day offset이고 소비 직전에 ISO 날짜를 만든다. import 시점에 날짜를 고정하지 않는다. 상대 기본 날짜는 Asia/Seoul이며 다른 업무 날짜는 인자로 지정한다.
import { createGroupListScreenFixture } from './fixtures/GroupListScreen.fixture';
const data = createGroupListScreenFixture(today);
<UiGroupListScreen {...data} onOpenGroup={previewActions.openGroup} />콜백은 프리뷰가 주입한다. fixture는 nested on* 함수도 포함하지 않는다. 알림 actions.accept/decline의 문자열은 버튼 문구이므로 데이터다. oneRms·oneRmExercises는 콜백이 아닌 데이터 필드다. search처럼 데이터와 콜백이 섞인 props는 {...fixture.search, ...previewSearchActions}로 조합한다.
npm run type-check는 fixture를 포함한 앱 TypeScript 검사다. 앱 진입점은 fixture를 runtime import하지 않으며 seed CLI·테스트·디자인에서만 소비한다. 생산 빌드 산출물에 합성 인물·원본이 포함되지 않는지도 로컬 빌드로 확인한다.
상호 배타 상태와 미지원 상태는 구분한다. 시작 전/진행 중/동기화 실패는 프리뷰 변형이며 새 DB 계정의 기본은 진행 중 운동 없음이다. 동기화 실패는 클라이언트의 sync_blocked 상태이며 DB status로 저장하지 않는다. WorkoutFlow의 현행 계획 prop은 initialDraft, 복귀 상태는 initialSnapshot, 종목은 exerciseCatalog다. 셸에는 isWorkoutActive가 없으므로 실제 소비 화면에 별도로 공급한다.
현재 피드 사진은 작성자 아바타이고 포스트 첨부 사진 필드는 없다. 친구 비공개 설정은 현재 제품 기능이 아니므로 fixture의 접근 불가는 조회 오류 상태로 표현한다. 연결/미연결 소셜 identity도 표시 변형이며 비밀번호 계정에 OAuth 연결을 가짜로 생성하지 않는다. DB 사진은 레포 원본 이미지를 private Storage에 업로드하고, 프리뷰는 같은 이미지의 레포 경로만 사용한다. 서버가 발급한 Auth UUID·Storage URL·생성시각과 fixture의 안정적인 키/offset은 seed manifest와 읽기 검증 결과로 대응시킨다. 이를 문자 그대로 동일한 DB 응답이라고 보고하지 않는다.