피드 카드 복합 종목 합치기 — 동작 두 줄로 쪼개진 카드에서 세션 상세와 같은 한 종목 표시까지 (2026-09-03)
- 기간: 2026-09-03 (세션 1개, 오너 보고 "이거 복합종목으로 했는데, 피드 세션카드에 별개 종목으로 표시되는 문제 해결해줘")
- 랜딩: PR #1185(Phase 1~3,
c5f0aead) — 마이그레이션20260905235900_feed_presentation_composite_meta_v1.sql, Vercel 자동 배포. Production 적용 = 릴리스 레인 - 설계서: 없음(이슈 #1182 본문의 Phase 계획·"예상 효과·개선사항" 절이 정본)
- 정본:
restorePresentationMains·presentationMainSetCount(src/react/services/compositeExerciseModel.ts) · 카드 응답 키composite_meta·position(src/react/types/screenRpc.ts) - 도구: Production 조회(Management API 쿼리 러너, 세션
a380c940…) - 게이트:
tests/react/feedCompositeMains.test.mjs(7건) · pgTAPfollowing_feed_v1.test.sql(+8) - 버그리포트:
bug-069-20260903.md - 계약: 피드/검색 카드 응답에 추가 전용 키 3개(계약 버전 불변)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 서버: 카드 엔진 재발행 — 종목 카드 position·composite_meta, 세트 토큰 position + pgTAP | ✅ #1185 |
| Phase 2 | 앱 계약·합치기: 어댑터 검사, 조립이 묶음 정보 전달, 카드 조립 4곳이 세션 상세와 같은 함수로 합침 + 세트 수 재계산 | ✅ #1185 |
| Phase 3 | 표시: 피드·데스크톱 홈/검색 카드의 복합 한 줄 표기, 데스크톱 검색 동작 일치 | ✅ #1185 |
| Phase 4 | 랜딩·기록: CI 1회·랜딩 잠금·문서 | ✅ 이 문서 / Production 확인은 5절(릴리스 v0.15.0) |
1. 배경
복합 종목(예: 하이 클린 + 저크)은 서버에 동작마다 한 행으로 저장되고 composite_meta의 묶음 키로 묶인다. 세션 상세와 하루 상세는 이 묶음 키로 한 종목으로 다시 합치며, #1151(2026-09-02)에서 합쳐진 세트의 표기("50kg × (3+1)")까지 정리했다. 피드·검색 카드는 별도 서버 함수가 만든 축약 응답을 쓰는데, 이 응답은 그 범위 밖이었다.
2. 문제 제기
카드 응답에 묶음 정보가 없어 앱이 합칠 수 없었다
카드용 서버 함수 get_session_presentation_mains_json_v2_engine은 종목 행을 하나씩 나열하며 composite_meta와 세트 position을 싣지 않았다. Production 실측(2026-09-02 "역도" 세션)에서 하이 클린·저크 두 행이 같은 묶음 키를 갖고 있음을 확인했지만, 카드 응답만 보면 두 행이 한 종목이라는 것을 알 수 없다.
카드 머리 세트 수가 세션 상세와 달랐다
서버 집계표(user_exercise_session_rollups)는 동작 행마다 한 줄이라 복합 종목 세트가 동작 수만큼 중복된다. 카드 21세트 vs 세션 상세 16세트.
3. 해결 방안
원칙
오너의 명시 결정 없음(수리 건). 이슈 본문 계획대로: 서버는 묶음 정보만 더 싣고, 앱은 세션 상세의 합치기 함수를 재사용한다.
접근
| 안 | 판단 |
|---|---|
서버가 composite_meta·position을 싣고 앱이 합친다 | 채택 — 추가 전용 키, 검증된 합치기 로직 재사용, 옛 응답은 종전과 동일 |
| 서버 함수가 합쳐진 종목을 바로 만든다 | 기각 — 세트 토큰 계약 변경·합치기 규칙 SQL 재구현 |
| 앱만 고친다 | 기각 — 응답에 묶음 정보가 없어 추측 불가 |
| 집계표 자체를 복합 기준으로 | 보류 — 통계 전반이 쓰는 표. 카드 머리만 합쳐진 기준으로 재계산(잘린 카드는 서버 값 유지) |
4. 적용한 내용
Phase 1 — 서버 (#1185, 마이그레이션 20260905235900)
카드 엔진 재발행: candidate_mains에 composite_meta, 종목 카드에 'position'·'composite_meta'(bounded_home_composite_meta로 크기 제한), 세트 토큰에 'position'. 감싸는 함수(get_session_presentation_mains_json)는 카드·토큰을 그대로 통과시키므로 재발행 없음. pgTAP following_feed_v1에 백스쿼트 + 풀업 복합 픽스처와 8단언(plan 19 → 27).
Phase 2 — 앱 계약·합치기 (#1185)
screenRpcAdapters.ts: 피드/검색 카드의position·composite_meta, 세트position을 있을 때만 검사(허용 키 = 달력 묶음 키 목록, 768바이트, 정수).barbelicMappers.buildProfileFeedItems:compositeMeta·position·setsTruncated·mainsTruncated전달.compositeExerciseModel.ts:restorePresentationMains(묶음 정보가 있고 모든 묶음이 동작 2개 이상·종목 id 유효일 때만restoreCompletedCompositeExercises호출, 아니면 입력 그대로) ·presentationMainSetCount(합쳐진 기준 메인 세트 수, 잘린 카드는 null).mobileFeedViewMappers.buildMobileFeedPosts·barbelicViewMappers.lgDesktopProfileFeed: 합치기 + 합쳐진 카드만 세트 수 재계산.
Phase 3 — 표시 (#1185)
FeedPost.FdAttach는 formatSetRunsWithComposite, 데스크톱 DkH3Mains·DkSrchMains는 formatCompositeSetLine ?? formatSetMeasurements. 데스크톱 검색은 합쳐진 종목의 동작 이름·id로도 일치(dkSrchMainMatches).
주요 결정과 그 근거
- 합치기는 화면 조립 계층에서:
buildProfileFeedItems결과는 종목 훈련 기록(#1002)·모바일 세트 검색이 동작별로 읽으므로 그대로 두고, 카드 조립 2곳에서만 합친다. - 세트 수 재계산은 합쳐진 카드에서만: 복합이 없는 카드는 서버 값 그대로(집계 규칙 차이의 파급을 막는다). 잘린 카드는 전체를 알 수 없으므로 서버 값 유지.
- 동작 하나만 남은 묶음은 합치지 않는다: 합치기 함수가 그 묶음을 조용히 버리는 것보다 동작 행이 따로 보이는 쪽이 낫다.
작업 중 드러난 것
- dual-key 게이트: 합치기 도우미와 카드 정규화에서
camel ?? snake이중 키를 쓰면 게이트가 막는다 — 경계(buildProfileFeedItems)가 camel로 확정하므로 camel만 읽는다. npm run check의tabSwitchLatencyProbe실패 1건은 수정 전 main에서도 같은 Windows 줄바꿈 원인으로 실패(#1161 계열), 이 변경과 무관.- 워크트리
node_modules는 PowerShellNew-Item -ItemType Junction으로(cmd mklink는 경로 인용 문제로 실패).
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 실측 세션 모양 픽스처의 카드 종목 줄 | 3줄(하이 클린 / 저크 / 클린 풀) → 2줄(하이 클린 + 저크 / 클린 풀) — 단위 테스트 |
| 카드 머리 세트 수(픽스처) | 16(5+5+6) → 11(5+6) — 단위 테스트 |
| 세트 토큰 | "100kg 1x1" 두 줄 → "100kg (1+1)x1" 한 줄 — 단위 테스트 |
| 옛 응답(키 없음) | 종전과 동일(종목 3줄·서버 세트 수) — 단위 테스트 |
| 잘린 카드 | 합치되 세트 수는 서버 값 유지 — 단위 테스트 |
| pgTAP | 8단언 — CI에서 실행 |
Production 카드 응답에 composite_meta | 릴리스 v0.15.0(2026-09-03) 뒤 확인: 마이그레이션 20260905235900 적용, 카드용 서버 함수 본문에 composite_meta·position 포함(읽기 전용 조회). 카드 응답 직접 호출은 조회 계정 권한(42501)으로 미실측 |
| 어제 "역도" 카드의 실제 표시(4줄·21세트 → 3줄·16세트) | 미검증(자동 검증 밖) — Production 반영됨(v0.15.0), 픽스처는 실측 데이터와 같은 모양 |
6. 이번 개선으로 향상된 것
복합 종목이 카드에서도 한 종목으로 보인다
모바일 피드·친구 피드·데스크톱 홈·데스크톱 검색 카드가 세션 상세와 같은 이름("하이 클린 + 저크")과 세트 구성으로 그려진다.
카드 머리와 세션 상세의 세트 수가 같아진다
복합 종목이 든 카드의 세트 수가 동작 수만큼 부풀지 않는다.
구조적으로 남는 것
카드 응답이 세션 상세와 같은 묶음 정보를 싣고, 카드류 조립은 restorePresentationMains 하나를 공유한다. 복합 픽스처가 pgTAP·단위 테스트에 고정됐다.
남은 것
- 릴리스 v0.15.0(2026-09-03)으로 Production 반영, 이슈 #1182 [반영완료]. 카드 응답 직접 실측만 권한 문제로 남았다(앱 화면에서 확인되는 항목).
- 집계표 세트 수의 복합 중복(통계·리포트)은 범위 밖 — 필요 시 별도 이슈.