디자인 전달 3건 반입과 후속 배선 — 월간 카드·일지 월 리포트·내 피드·세션 사진 (2026-09-14)
- 기간: 2026-09-14 (세션 1개, 사용자 지시 "요거 3개 디자인 업데이트좀 해줄래? 후속작업이랑 이슈도 하나 만들어서").
- 랜딩: 앱 이슈 #1598 · 앱 PR #1602 (base
release/v0.19.0, Phase 1~4 한 PR) — release/v0.19.0 통합c63372a5(2026-09-15 00:50 KST, 큐 실행 34858097943, staging 승격 대기) · QA1 PR #3 · 문서 PR #87. 마이그레이션 2개(20260915093000_session_photos_v1,20260915103000_user_fact_history_and_protection_v1), Edge 없음. release 병합·staging·Production 반영 상태는 이슈의 최신 댓글을 따른다. - 설계서: 이슈 #1598 본문(분석·Phase 계획·예상 효과·D1). 첨부 전달문 3건(
delivery-20260914b)의 지시문은 구현 참고 자료이며 별도 실행 권한으로 해석하지 않았다. - 정본: 리포트 계약 §월간 화면, 일지 계약 §월 타이틀 탭, 피드 계약 §피드 범위 탭·§포스트 사진 카드, 기록 계약
ownerName, 운동 화면 계약 §완료 화면 리뷰 입력; 앱src/react/contracts/designContract.ts(마커),controllers/screenKinds.ts(화면 kind),supabase/definitions/workout/functions/{attach_session_photo_v1,remove_session_photo_v1,get_session_photos_v1,session_photos_json_v1,session_image_readable_v1}.sql,supabase/contracts/user-fact-columns.json(session_photos등급). - 도구: 첨부 manifest의
baseBlobs와 저장소 Git blob 대조 → 델타만 반입(겹친primitives.css·workout.css·feed-props.md는 3-way 병합).sql:candidate(마이그레이션 후보),generate-user-fact-protection.mjs(보호 재생성),migrations:risk,schema:snapshot·sql:extract·db:model. - 게이트: 로컬
npm run check(정적 게이트 + QA1 단위 스위트),ci:full-local -- --only db(마이그레이션 전체 적용·스냅샷 대조·pgTAP),ci:precheck-local. 결과는 §5. 원격 Full CI는 release→staging 승격 후보에서 돈다(이번 트랙에서 실행하지 않음). - 버그리포트: 없음(디자인 반입·기능 배선).
- 계약: 위 정본 5개 계약문의 2026-09-14 절,
SessionPhotoPort(contracts/ports/writes.ts), 피드 카드photos(types/screenRpc.ts·types/feedView.ts·types/sessionPhoto.ts).
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 델타 3건 반입 + 타입·마커·화면 kind 정식 등재 | ✅ 커밋 3074ae6a |
| Phase 2 | 일지 월 타이틀 → 월간 리포트 push, 월간 카드 이미지 저장, 드로어 › 내 피드, 종목 평균 중량 | ✅ 커밋 b9fac406 |
| Phase 3 | QA1 테스트 동조(19파일 + pgTAP 1)·lock 고정, 계약문·변경 노트·작업 기록 | ✅ QA1 73322d20, 이 문서 |
| Phase 4 | 세션 사진 첨부 서버·클라이언트 | ✅ 커밋 31f6dabf(서버 마이그레이션 2개·클라이언트 첨부 상태·피드 사진, pgTAP 30/30) · 릴리스 병합 cc4cfd55 · 재번호 32296460 |
1. 배경
디자인 세션(Claude Design)이 2026-09-14 하루에 세 묶음을 전달했다. 월간 리포트를 공유 가능한 카드 한 장으로 바꾸고 일지 달력의 월 제목을 그 리포트로 잇는 것, 종목 상세 그래프·숫자 정렬·피드 범위 탭·내 피드·세트 시트 종목명 탭 같은 화면 손질, 그리고 운동 완료 화면에서 사진을 첨부해 피드 카드로 보여 주는 것이다. 전달문마다 "Codex 후속"으로 타입 등재·화면 kind·마커·서버·배선 항목이 남아 있었고, 사용자는 디자인 반영과 후속 작업을 하나의 이슈로 처리하도록 지시했다.
전달 시점 기준은 앱 main b622cc808b47(= release/v0.19.0 head), 문서 main ddd6dd1a63b1이었고 파일별 base blob이 모두 일치했다.
2. 문제 제기
월간 리포트가 연간과 같은 긴 화면이라 공유·이동이 어색했다
월간을 열면 연간과 같은 섹션 10개가 이어졌고, 일지 달력의 월 제목을 누르면 월간 요약 오버레이라는 별도 화면이 열렸다. 같은 달의 통계를 보는 길이 둘이었다.
피드에서 내 포스트만 보는 길이 없었고, 완료 화면은 글만 남길 수 있었다
드로어 프로필 옆 ›는 닫기였고, 완료 화면 리뷰 입력은 텍스트뿐이었다. 사진을 남기려면 다른 앱을 써야 했다.
테스트 스위트가 앱 main과 어긋나 있었다
#1593(PR #1594)이 테스트를 돌리지 않고 병합되면서 완료 문구("~하였습니다")·토스트 배지 마크업이 바뀌었는데 QA1 테스트 14건은 옛 값을 기대해 main에서 이미 실패하고 있었다.
3. 해결 방안
원칙
- D1(이슈 #1598, 미결): "추천" 피드에 무엇을 보여줄지 — 결정 전까지 범위 탭을 숨기고 종전 피드를 유지한다(A안 권장: 같은 그룹 멤버의 최근 포스트).
- 전제(이슈 본문): 목적 릴리스
release/v0.19.0; 사진 정책 = 세션당 최대 4장·장당 8MB·jpeg/png/webp·비공개 버킷 + 서명 URL·세션 삭제 시 연쇄 삭제. - 표현은 디자인이, 의미론·데이터는 컨테이너·서비스·서버가 소유한다(전달 계약).
접근
| 대안 | 판단 |
|---|---|
| 일지 바인딩이 리포트 기간 컨트롤을 직접 바꾼다 | 기각 — 일지 하니스가 허용하는 기능 소유자 밖이고 의존이 넓어진다. 스택 항목 params(월간·해당 월)로 요청하고 리포트가 열릴 때 적용 |
| 내 피드를 오너 스토어 세트에 피드 자원을 노출해 읽는다 | 기각 — "세트의 모든 항목은 독립 인스턴스" 계약과 충돌. 원격 데이터 컨트롤러·소셜 기능 계약에 내 피드 창(owner=target=나) 추가 |
| 내 피드용 서버 함수 신설 | 기각 — 친구 피드 RPC의 열람 판정이 본인을 이미 허용한다(새 서버 함수 0) |
| 사진을 세션 행의 jsonb 열에 넣는다 | 기각 — 원본 등급 열 변경과 삭제·용량 관리가 얽힌다. 별도 표 session_photos + 비공개 버킷 + RPC |
| 추천 탭을 빈 목록으로 먼저 노출 | 기각 — 빈 화면만 보인다. 결정 전 숨김 |
4. 적용한 내용
Phase 1 — 델타 반입·정식 등재
- 앱 파일 21개(신규
PhotoCard.tsx·PhotoComposer.tsx)를 base 위에 그대로 반입. 타입 7건(onDownloadMonthCard·onOpenMonthReport·RecordsDetailProps.ownerName·MobileFeedScreenProps.scope/onChangeScope·NavDrawerProps.onOpenMyFeed·photos), 화면 kind(journalMonth제거·myFeed등록), 마커(mobileReportMonthCard·mobileReportMonthCardDownload·사진 마커 3건 등록·부착,journalMonthSheet제거), 월 요약 데이터 배선 4 props 제거.
Phase 2 — 화면 배선
- 리포트 스택 항목(
homeReport)을 탭에 무관하게 공용 셸이 그린다. 일지 월 제목은{ periodUnit: "month", monthKey }를 실어 push하고 리포트가 열릴 때 기간 컨트롤에 적용한다(mobileReportMonthOffset). - 월간 카드 이미지 저장: iOS는 세션 이미지 저장 브리지(
sessionDownloadImage+targetHook)로 카드 영역 캡처, 웹은 카드 DOM → PNG, Android 셸은 기존 미지원 안내. - 내 피드: 같은
profile_feed자원의 owner = target = 나 창(get_following_profile_feed_v1), 열 때마다 첫 페이지를 새로 읽는다. 피드 판은 내 피드가 열린 동안 시트를 접는다(친구 페이지와 같은 규칙). - 종목 행
avgLoad(volume/reps, reps를 아는 행만).
Phase 3 — 테스트·문서
- QA1
test/1598-design-delivery: 새 계약으로 바뀐 3건 + Phase 2가 건드린 2건 + #1593 기존 실패 14건 갱신(단언 완화·skip 없음,migration-adaptations.json19파일 사유), pgTAPsession_photos_v1.test.sql30 assert 추가. 앱qa1.lock.json을 그 커밋에 고정. - 계약문 5개 델타 반입, 변경 노트, 이 문서.
Phase 4 — 세션 사진 첨부
- 서버: 표
session_photos(원본 등급 등재, 보호 트리거 재생성), 비공개 버킷session-images(8MB, jpeg/png/webp; insert/delete = 주인 폴더, select = 피드 열람 판정session_image_readable_v1), RPCattach_session_photo_v1(주인·경로·객체 존재·오버레이·4장 상한)·remove_session_photo_v1·get_session_photos_v1, 피드 엔진get_profile_feed_page_engine_v1에photos동봉(session_photos_json_v1). - 클라이언트: 페이로드 검증(어댑터, 최대 4)·카드 codec·서명(
sessionPhotoSigning, 45분 재사용)·저장소(sessionPhotoRepository: 업로드 → RPC, 실패 시 객체 삭제)·도메인·포트(SessionPhotoPort)·API; 완료 화면useSessionPhotos(저장 확정 세션에만 귀속, 미저장 동안 보관 후 업로드, 기존 세션 수정 진입 시 목록 읽기, 4장이면 카메라 버튼 숨김).
주요 결정과 그 근거
- 사진 첨부는 서버에 저장이 확정된 세션(자동 저장
saved또는 기존 세션 수정)에만 귀속한다. 대기열(queued) 상태에서는 서버 행이 없어 FK가 성립하지 않으므로 훅이 보관했다가 확정 뒤 올린다. - 저장소 객체 삭제 실패는 행 삭제를 되돌리지 않는다(화면에서는 이미 사라짐, 고아 객체는 후속 정리 몫).
작업 중 드러난 것
- main
b622cc80에서 QA1 테스트 14건이 이미 실패(#1593 미실행 병합). 이번 QA1 PR에 함께 갱신. - 전달본 월간 분기 안의
unit === "month"비교가 타입 오류를 내 한 줄 보정,SessionScreen미사용 import 1건 제거. - 원본 보호 재생성 마이그레이션은 이벤트 트리거 보호 때문에 수리 티켓(
issue#1598) 선언이 필요했다(2026-09-13 재발행과 같은 방식). 원본 값 변경은 없다. - 새 pgTAP 파일이
authenticated역할로 바뀐 뒤 표를 직접 읽어 권한 오류(테스트 결함, 제품 함수는 정상) → 역할 전환 전에 사진 id를 세션 변수에 담도록 수정(QA10312ea96). QA1 배선 검사의 포트 묶음 수(18→19)·저장소 조립 목록·목적지 파일 수(13→14)도 새 사진 포트에 맞춰 갱신(QA132699b1d). - 통계 게시 동시성 테스트 1건("R05 publication fixture waits for an independent deferred publisher")이 로컬 게이트와 DB 단계를 동시에 돌린 1회 실행에서 시간 경쟁으로 실패(
idle≠deferred). 세션 사진과 무관한 통계 코드이며 같은 스키마의 직전 실행과 단독 재실행에서 통과 — 두 검증을 겹쳐 돌리지 않는다. - Phase 4 커밋 시점에
release/v0.19.0가 base 이후 #1597(도메인 분리)·#1599/#1601(데이터 공개 상태·데스크톱 온보딩)로 167개 파일만큼 앞서 있었고, 마이그레이션 번호20260915093000이 릴리스의report_design_freshness와 겹쳤다. 릴리스를 병합(cc4cfd55, 충돌 13개: 화면 8·생성 파일 3·lock·manifest)해 릴리스의 데이터 공개 상태 표시(monthStatus·publicationStatus·StatDataRegion)와 전달본을 합치고, 폐지된 월간 요약 오버레이 props(selectedMonth*·onSelectMonth·onRetryMonthRange)는 친구 스코프 달력까지 제거했으며, 리포트 로딩 스켈레톤과 그 CSS는 릴리스(공용 공개 경계)를 따랐다. 번호는 꼬리 뒤20260915113000·20260915123000으로 재배정(32296460). - QA1 lock도 릴리스가 고정한 suite 커밋
a0e0fad1(#1592 suite 브랜치, QA1 main 미병합)과 병합해야 했다 — 두 트랙이 같은 #1593/#1594 완료 문구 테스트를 각자 고쳐 13파일이 충돌했고, 릴리스가 고정한 기대치를 채택했다(QA11117c373, 적응 61건).
5. 적용 결과
| 항목 | 전 | 후 | 근거 |
|---|---|---|---|
| 월간 리포트 화면 | 연간과 같은 긴 화면(추이·분포 4종) | 월간 카드 1장(rp-mcard) + 이전/다음 달 이동 + 이미지 저장 버튼 | QA1 setScoreReportBands, e2e report-design-detail |
| 일지 달력 월 제목 탭 | 월간 요약 오버레이(journalMonth) | 월간 리포트 스택 항목(homeReport, periodUnit=month) | QA1 daySummarySetPurposeBoundary·statsCentralizationPhase4, e2e bottom-clearance |
| 월간 카드 이미지 저장 | 없음 | iOS 네이티브 캡처(sessionDownloadImage) / 웹 PNG 저장 / Android 미지원 안내 | features/volume/monthCardImage.ts + 마커 mobileReportMonthCard(Download) |
| 내 피드 | 없음(드로어 항목 없음) | 드로어 › → myFeed 스택 항목, 같은 피드 자원의 owner=target=나 창 | MobileMyFeedBinding, remoteDataController.loadMyFeed, QA1 friendScope |
| 피드 범위 탭(팔로잉·추천) | 없음 | 화면·타입은 준비, 탭은 D1 결정 전 숨김(onChangeScope 미공급) | feedPresentation.ts FeedScopeKey |
| 세션 사진 | 없음 | 완료 화면에서 최대 4장 첨부·제거, 비공개 버킷 session-images, 피드 카드 사진 슬라이드 | 표 session_photos·RPC 4개·pgTAP session_photos_v1 30/30 |
로컬 게이트 npm run check | main b622cc80: QA1 14건 실패(#1593 미실행 병합) | head c485b32a precheck: static 통과 · unit 3,864 passed / 0 failed / 149 conditional skip | QA1 32699b1d 고정 |
DB 단계 ci:full-local -- --only db | — | 마이그레이션 225개 빈 DB 적용 통과 · schema.sql 스냅샷 일치 · pgTAP 123파일 2,483건 · 노드 DB 테스트(동시 저장·복구·통계 게시·격리 수렴) 통과, 8분 39초 | 샌드박스 supabase CLI 2.113.0 · Postgres 17.6.1.127 |
| 마이그레이션 | 222개, 꼬리 20260915083000 | 224개, 꼬리 20260915103000(사진 표 + 원본 보호 재생성), 위험 등급 low·low | migration-risk, check-supabase-migrations |
| 원격 Full CI | — | 미실행(명시 지시 없음) — release→staging 승격 후보에서 1회 | §게이트 |
6. 이번 개선으로 향상된 것
월간 리포트는 카드 한 장으로 읽히고 그대로 저장해 공유할 수 있다. 일지에서 월 제목을 누르면 같은 달의 리포트로 바로 간다. 드로어 › 한 번으로 내 포스트만 본다. 완료 화면에서 사진을 붙이면 워드마크·총 kg·종목 요약이 얹힌 카드로 피드에 실린다.
구조적으로: 리포트 스택 항목이 탭과 무관하게 같은 셸에서 그려지는 규칙, 세션 사진 계약(표·버킷·RPC·타입·포트), 월간 카드 캡처 경로(네이티브/웹).
남은 것
- D1 "추천" 피드 정책(이슈 #1598) — 결정 뒤 서버 RPC + 탭 노출.
- 친구 스코프 종목 상세(
ownerName타입은 등재, 친구 PR 상세 RPC가 없어 진입 미배선). - Android 셸의 이미지 저장(세션·월간 카드 공통) — 네이티브 후속.
- 세션 사진의 공유·이미지 베이킹(사진 + 오버레이 → 한 장), 고아 객체 정리 작업.
- QA1 PR #3 병합 뒤 앱 lock(
32699b1d)은 그대로 유효(merge commit 이 같은 커밋을 포함).