Skip to content

디자인 전달 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 3QA1 테스트 동조(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.json 19파일 사유), pgTAP session_photos_v1.test.sql 30 assert 추가. 앱 qa1.lock.json을 그 커밋에 고정.
  • 계약문 5개 델타 반입, 변경 노트, 이 문서.

Phase 4 — 세션 사진 첨부

  • 서버: 표 session_photos(원본 등급 등재, 보호 트리거 재생성), 비공개 버킷 session-images(8MB, jpeg/png/webp; insert/delete = 주인 폴더, select = 피드 열람 판정 session_image_readable_v1), RPC attach_session_photo_v1(주인·경로·객체 존재·오버레이·4장 상한)·remove_session_photo_v1·get_session_photos_v1, 피드 엔진 get_profile_feed_page_engine_v1photos 동봉(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를 세션 변수에 담도록 수정(QA1 0312ea96). QA1 배선 검사의 포트 묶음 수(18→19)·저장소 조립 목록·목적지 파일 수(13→14)도 새 사진 포트에 맞춰 갱신(QA1 32699b1d).
  • 통계 게시 동시성 테스트 1건("R05 publication fixture waits for an independent deferred publisher")이 로컬 게이트와 DB 단계를 동시에 돌린 1회 실행에서 시간 경쟁으로 실패(idledeferred). 세션 사진과 무관한 통계 코드이며 같은 스키마의 직전 실행과 단독 재실행에서 통과 — 두 검증을 겹쳐 돌리지 않는다.
  • 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파일이 충돌했고, 릴리스가 고정한 기대치를 채택했다(QA1 1117c373, 적응 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 checkmain b622cc80: QA1 14건 실패(#1593 미실행 병합)head c485b32a precheck: static 통과 · unit 3,864 passed / 0 failed / 149 conditional skipQA1 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개, 꼬리 20260915083000224개, 꼬리 20260915103000(사진 표 + 원본 보호 재생성), 위험 등급 low·lowmigration-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 이 같은 커밋을 포함).