"기록 보기"가 친구 세션·비슷한 이름 종목을 내 이력으로 보여 주던 것에서 서버 이력 RPC 한 곳·canonical 종목 id·색인 병합까지 — v0.18.0 A14 (2026-09-10)
- 기간: 2026-09-09 ~ 2026-09-10 (세션 2개 —
b55b728c-569d-4d7c-98ee-fe9e6cb1ea35분석·계획·Phase 1 착수,4b0f1d85-820d-4396-ad20-37eb6881b0f3Phase 1 마무리~Phase 4). 오너 지시 "1414 진행해줘" → 분석·Phase 계획 게시 → "묻지 않고 Phase 끝까지 진행" → "#1414 이어서 작업해줘". 계획 ID A14 / Phase 3 스텝 3-2. 직접 선행 A02(#1329)·A03(#1330)·A06(#1407)·A09(#1342)는 착수 시점에 전부release/v0.18.0(b074cbb1)에 병합돼 있었다. - 랜딩: 앱 PR #1503(base
release/v0.18.0; Phase 1099f3e0f· Phase 26edd7bc8· Phase 319458bd8· release 반영 병합5611e77c·4e953796) → 큐 병합 커밋209bbffd(2026-09-10 13:07 KST, 실행) — 마이그레이션 1건20260914043000_exercise_history_set_atoms.sql(함수 재발행만, 표·열 변경·원본 DML 없음), 엣지 함수·Vercel 설정 변경 없음. release 통합·staging 확인·Production 배포는 서로 다른 상태다 — release/v0.18.0 통합은 끝났고(209bbffd), staging·Production 은 아래 §5 상태 표가 갱신한다. - 설계서: 없음 — 착수 분석·해결 방안·Phase 계획·"예상 효과·개선사항" 표는 #1414 착수 댓글(2026-09-10).
- 정본:
src/react/contracts/ports/exerciseHistoryDto.ts(요청·페이지·행·세트 typed 모양) ·src/react/features/workout/history/**(계약 상수·loader·resource·병합·연도 창·view·port·binding) ·src/react/services/domains/codecs/exerciseHistoryCodec.ts(서버 페이지 → typed 페이지) · 서버supabase/definitions/pr/functions/get_exercise_pr_history_v3_core.sql· 이 문서 §4 "U02/U03 인계". - 도구:
npm run db:explain -- --only pr.history_page(대표 조회 목록에pr.history_page등재) ·scripts/performance/workload/load.mjs --workload history-10y(10년 fixture 적재, 레포 안) · 증거 파일은 레포 밖C:\Users\USER\AppData\Local\Temp\claude-wt\1414-evidence\(gitignored 대상 아님 — 임시 폴더). - 게이트: 새 행동 테스트 6파일 40건 —
exerciseHistoryRpcAtoms(4) ·exerciseHistoryFeature(7) ·exerciseHistoryResource(7) ·exerciseHistoryMergeThroughput(1, 1/4/10년 처리량 로그) ·exerciseHistoryBinding(5) ·exerciseHistoryDrawer(5), pgTAPexercise_history_set_atoms_v1.test.sql(15 assert). 명부 등재 테스트 수정 1건(mobileExperienceViewMappers.test.mjs— 삭제된 매퍼 단언 제거,pending-changes.json신고), 비명부 3파일 갱신(mobileExperienceMapperModules·workoutCalculations·pctBaseOneRmFallback). 로컬 pgTAP: 샌드박스cil095e2658에서 새 파일 15/15(2026-09-10 11:20 KST) · ci-local database 단계 137파일/2640 assert 중 새 파일 1건만 실패(plan 수 14→15 수정으로 해소).npm run check3541 pass · Precheck(ci:precheck-local) 결과는 §5. - 버그리포트: 없음(구조 개선). 부수 수리: 달리기·플랭크 같은 종목의 세트 원자(거리·시간·칼로리·보조무게)가 이력 RPC 에 없어 이 RPC 를 원천으로 쓰면 "—" 로 보일 것을 Phase 1 에서 막았다.
- 계약: app-screen-rpc-contract.md
get_exercise_pr_history절(세트 원자 4종·recording_fields추가 전용, v4 유지) · 총괄 A14 카드 · HQ 운영 장부.
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 서버 계약 최소 보강 — 세트 토큰에 거리·시간·칼로리·보조무게, 항목에 기록 칸(recording_fields); 마이그레이션·스냅샷·registry·어댑터 검증·pgTAP·EXPLAIN 증거 | ✅ 099f3e0f |
| Phase 2 | features/workout/history/** — typed 계약·A06 port 소비 loader·owner 범위 resource·Map 색인 병합·연도 창 정책·view/port; A06 결과에 원문 페이지 노출(추가 전용) | ✅ 6edd7bc8 |
| Phase 3 | 모바일 소비자 전환(운동 기록 드로어·그룹 보드 작성기), 이름 매칭·피드 창 재활용·세션 검색 선적재 배선 제거, 쓰기 답장의 이력 무효화, 테스트 갱신·신고 | ✅ 19458bd8 |
| Phase 4 | 증거·기록 — 이 문서, RPC 계약 문서, 총괄·HQ 장부, U02/U03 인계 | ✅ Barbelic-docs PR #32 |
1. 배경
운동 기록 화면에서 종목 옆 "기록 보기" 를 누르면 그 종목의 지난 훈련 목록이 뜬다(2026-07-30 도입, 2026-08-31 기록에서 "피드 창 + 세션 검색 병합"으로 범위를 넓혔다). 그때의 원칙은 "서버 변경 없이 이미 받아 둔 카드를 재활용"이었고, 종목을 고르는 열쇠는 종목 id 가 있으면 id, 없으면 이름이었다.
v0.18.0 리팩터링 총괄(A14)은 "이력의 정확도와 비용이 운동 이름이나 사용자가 방문한 피드 범위에 의존하지 않게 한다"를 목표로 잡았다. 직접 선행이 준 재료 — A02/A03 의 canonical 종목 id·codec, A06 의 통계 조회 port(loadPrExerciseHistoryData)와 응답 어댑터, A09 의 owner 범위 resource store 패턴 — 위에서 이력의 원천을 서버 RPC 한 곳으로 옮기는 트랙이다.
2. 문제 제기
친구의 세션이 내 이력으로 보였다
유저 A 가 B 를 팔로우하면 홈 피드에 B 의 세션 카드가 실린다. A 가 벤치프레스 "기록 보기"를 열면 B 의 벤치 200kg 세션이 A 의 이력으로 보였다. buildMobileExerciseHistory 는 카드의 소유자를 보지 않았다(재현: author 가 있는 카드를 넣으면 그대로 들어감 — 착수 세션 실측 friend bench in my history: ['friend-s1']).
비슷한 이름의 다른 종목이 섞였다
종목 id 가 없는 카드는 이름을 열쇠로 저장하고, 화면(legacyWorkoutHistoryValue)은 이름의 공백·대소문자를 지운 뒤 부분 문자열까지 맞췄다. "스쿼트"의 기록 보기에 "프론트 스쿼트" 세션이 나왔다(실측 lookup '스쿼트' -> ['내 세션'(프론트 스쿼트)]). 종목 이름을 바꾸거나 영문·한글 표기가 다르면 같은 종목이 갈라지거나 남의 종목과 합쳐졌다.
피드를 얼마나 봤느냐에 따라 결과가 달랐다
피드에서 본 세션은 세트 전량, 검색에서만 온 세션은 세트 5개·한 세션에 종목 6개까지만 실렸다. 앱을 다시 켜거나 피드를 더 내리면 같은 종목·같은 기간의 목록이 달라졌고 7번째 이후 종목은 아예 빠졌다.
열 때마다 전체 세션 검색을 통째로 내려받았다
onExerciseHistoryOpen → ensureSessionSearch 가 세션 검색(최대 40페이지 × 120건)을 전부 당겨 왔다. 10년치 유저는 드로어를 열 때마다 수천 건을 받았다.
합칠 때 행마다 배열 전체를 훑었다
rows.find(sessionId) 를 행마다 반복해 세션 수 × 종목 수에 비례하는 비용이었다.
서버 이력 RPC 에 유산소 세트 원자가 없었다
get_exercise_pr_history 의 세트 토큰은 load·stats_load_kg·reps·rest·memo 만 실었다. 그대로 원천을 바꾸면 달리기·플랭크의 기록 보기에서 거리·시간이 "—" 로 보인다.
3. 해결 방안
원칙 (오너 결정)
이번 트랙에 새 오너 결정은 없다(착수 댓글 "오너 결정 필요: 없음"). 전제: ① 부족한 세트 원자는 DB 담당 없이 이 세션이 최소 보강(추가 전용, 계약 v4 유지)으로 반영한다 ② 복합 종목 행의 "기록 보기" 비노출 정책은 유지한다(복합 안의 동작은 서버 롤업이 그 동작의 canonical id 로 세므로 단일 종목 이력에 포함된다) ③ 10년 창은 RPC 정책을 따르고 창 밖 여부를 화면 문구로 드러낸다 ④ 점수·PR 여부·합계는 서버 값을 그대로 쓰고 앱이 재계산하지 않는다(#1401 세트 스코어 DTO 보존).
접근
| 안 | 내용 | 채택 |
|---|---|---|
| A. 구조 개선 | 원천을 서버 RPC(owner·canonical 종목 id·연도·keyset 커서) 한 곳으로; typed 계약·resource store·Map 색인 병합·연도 창 정책을 feature 로 두고 화면은 port 만 소비. 이름 매칭·피드 창 재활용·세션 검색 선적재 경로 제거 | 채택 — 같은 종류 문제(소유자 혼입·이름 충돌·방문 범위 의존)가 다른 화면에서 재발하지 않고, U02/U03 이 같은 port 를 받는다 |
| B. 땜질 | 카드에 소유자 표시를 붙여 친구 카드만 거르고 이름 매칭에 정확 일치 조건을 더한다 | 기각 — 방문 범위 의존·전체 검색 다운로드·O(n·m) 병합이 그대로 남는다 |
| C. 세션 검색 RPC 에 종목 필터 추가 | 서버에 새 조회를 만들어 드로어 전용으로 쓴다 | 기각 — 이미 목적에 맞는 RPC(get_exercise_pr_history: owner·종목 id·연도·커서·색인 일치)가 있어 새 SQL 이 필요 없다. 부족한 것은 세트 원자 4종뿐(추가 전용) |
4. 적용한 내용
Phase 1 — 서버 계약 최소 보강 (099f3e0f, 마이그레이션 20260914043000)
get_exercise_pr_history_v3_core: 세트 토큰에distance_meters·duration_seconds·calories·assist_kg를 값이 있을 때만(jsonb_strip_nulls) 싣고, 항목마다 정렬된recording_fields(sort_recording_fields_v1) 를 싣는다. 60세션 × 12세트 최대 페이지의 바이트 상한을 유지하려고 null 키를 만들지 않는다. 문(get_exercise_pr_historyv4, 근력 지표 부착)에서도 같은 키가 남는다.- 앱 어댑터(
prExerciseRpcAdapters.validateHistory)·타입(screenRpc.ts): 새 키 검증(0 이상 유한수, 기록 칸은 정렬된 1..3 원자·중복 금지), 옛 응답(키 없음)도 그대로 통과. - 스냅샷·registry·db-objects 재생성,
deploymentManifest.EXPECTED_LATEST_MIGRATION갱신, 대표 조회 목록에pr.history_page등재. - pgTAP
exercise_history_set_atoms_v1(15): 달리기·어시스트 풀업·바벨 로우 fixture 에서 원자·기록 칸이 실리는지, 무게 종목엔 네 키가 없는지, 유산소만 한 종목도 롤업이 있어 페이지에 실리는지(피드 카드 의존 없음), 문 v4 에서도 남는지.
Phase 2 — 이력 feature (6edd7bc8)
contracts/ports/exerciseHistoryDto.ts: 요청(종목 id·연도·커서·크기·기준일)·페이지·행·세트의 typed 모양.services/domains/codecs/exerciseHistoryCodec.ts: 어댑터가 검증한 서버 페이지(snake) → typed 페이지. DB 행 모양(types/screenRpc)을 아는 것은 codec 층뿐이다(경계 테스트contractsImportBoundaries가 features 의 직접 import 를 막는다 — 처음 features 안에 두었다가 이 테스트에 걸려 옮겼다).features/workout/history/:exerciseHistoryQuery.ts(A06 port 소비, 취소 신호·10초 제한, 다른 종목·연도·로그아웃 응답 거부) ·exerciseHistoryResource.ts(키 = owner·get_exercise_pr_history·종목|연도|커서|크기·기준일·계약 버전; 시간으로 낡지 않고 통계 세대 stale·쓰기 답장invalidateOwner·owner 떠남만) ·exerciseHistoryMerge.ts(session_exercise_idMap 색인, 행 안 세트는set_id로 중복 제거; 우선순위 = 세트 안 잘린 행 > 통계 세대 높은 페이지 > 나중 페이지; 다른 종목 페이지는 오류) ·exerciseHistoryWindow.ts(기준일 연도 첫 페이지 → 같은 연도 커서 → 이전 연도 … 창 10년; 열 때 15줄이 찰 때까지 최대 10페이지, "더 보기"는 빈 연도를 건너뛰며 다음 페이지) ·exerciseHistoryView.ts(unresolved·pending·ready·error와 portread/open/loadMore/close).- A06 추가 전용:
PrExerciseHistoryResult에 검증된 원문 페이지history·exerciseRef노출, 이력 조회에 취소 신호 전달(statsDomain·statsRepository).
Phase 3 — 소비자 전환·구 경로 제거 (19458bd8)
features/workout/history/exerciseHistoryBinding.ts: store·loader·연도 창을 port 하나로 묶는다. 종목마다 소비자 취소 신호 하나 —close가 끊으면 열기 사슬이 그 자리에서 멈추고 마지막 소비자면 서버 요청도 끊긴다.read는 store 가 바뀌지 않았으면 같은 view 참조(렌더마다 다시 합치지 않는다).remoteDataController: 셸당 store·binding 하나(owner runtime 연결),invalidateSessionCollections(완료 기록 쓰기 답장) 에서 이력 전부 무효화.appControllerrenderCtx 의ensureSessionSearch제거,exerciseHistoryport 전달.mobileApp·WorkoutFlowBinding·WorkoutFlow·GroupComposer:exerciseHistoryport 한 prop 만 전달(exerciseHistoryLoading·onExerciseHistoryOpen제거).WorkoutRecord: 드로어가 열려 있는 동안만open(종목 id), 닫히거나 종목이 바뀌거나 화면이 사라지면close. 본문은WfExerciseHistoryBody(export) — canonical id 없는 행은 "종목이 연결되지 않아 기록을 조회할 수 없어요", 첫 페이지 대기는 스켈레톤, 실패는 문구+다시 시도, 줄은 그 줄의 기록 칸(없으면 종목의 칸)으로 세트를 표기하고 끝에 "더 보기" 또는 "최근 10년 기록을 모두 보여 드렸어요".- 제거:
buildMobileExerciseHistory(피드+검색 병합 매퍼)·legacyWorkoutHistoryValue·historyLookupKey(이름 매칭)·ensureSessionSearch배선. 세션 검색 자체는 세트 검색 화면이 계속 쓴다(보존). - 테스트: 명부 등재
mobileExperienceViewMappers.test.mjs의 삭제된 매퍼 단언 2곳 제거(pending-changes.json신고), 비명부 3파일 갱신. 새 테스트exerciseHistoryBinding(5)·exerciseHistoryDrawer(5, 정적 렌더 + 원문 앵커).
주요 결정과 그 근거
- 페이지 15행·연도 창 이어 읽기: 종전 드로어가 보여 주던 최대 15건과 같다. 첫 화면에 필요한 만큼만 읽고 전체 이력을 먼저 내려받지 않는다(이슈 완료 조건).
- 같은 세션의 같은 종목 두 행은 두 줄: 서버가
session_exercise_id단위로 세므로 그대로 둔다. 종전 매퍼는 같은 세션의 행을 세트 이어붙이기로 합쳤는데 그 규칙이 세트 중복 계산의 원인이었다. - 캐시 키에 기준일(asOf): 서버 연도 창의 기준이 바뀌면 다른 요청이다. 같은 (owner, 종목, 연도, 커서, 기준일) 요청은 같은 결과여야 한다(완료 조건 "피드를 열기 전후 결과 동일").
- 무효화 단위 = owner 전부: 완료 기록 하나가 어느 종목의 이력을 바꿀지 앱이 추적하지 않는다(세트 스코어·PR 여부는 서버 계산). 열 때 15행 × 최대 10페이지라 재조회 비용이 작다.
- 드로어 재렌더는 컨트롤러의 store 구독으로: port 는 셸당 하나로 안정적이어서 드로어의 열기/닫기 effect 가 페이지 도착마다 재실행되지 않는다(처음 memo 로 묶었다가 ESLint 억제 게이트에 걸려 memo 자체를 없앴다).
작업 중 드러난 것
- 문(v4)
get_exercise_pr_history가 10년치 실데이터(세션 2,609·세트 31,308·롤업 7,827)에서 3.6초 — core 함수는 18~75ms. 원인은 항목마다 불리는set_score_session_v2(PR #1401 세트 스코어): 세션당set_score_reference_v2약 140ms +exercise_set_part전체 해시 조인(31,308행 × 20항목 = 62만 행;with entries as materialized가 행 수 추정을 3,000으로 막아 planner 가 인덱스 대신 해시 조인을 고른다). 앱은 이 문을 PostgREST(authenticated statement_timeout 8초)로 부르므로 10년 유저는 페이지 20행에 3.6초, 60행이면 시간 제한에 걸릴 수 있다. A14 는 페이지 15행으로 묶지만 근본 수리는 세트 스코어·DB 담당 몫 — 인계(제안:entries를 not materialized 로 두거나exercise_set_part(session_exercise_part_id)인덱스 경로로 nested loop 유도, 또는 이력 문에서 세션당 점수 계산을 롤업 기반으로 대체). 증거explain-a14-populated.json(읽은 행 1,180,795 · 중첩 문장 74,677 · 100ms 초과 22건) ·door-explain.txt. - 샌드박스 pg_cron 격리 계산(
barbelic-stats-projection-compute, statement_timeout 60초)이 10년치 백필 1건을 완주하지 못해 통계 작업이 3회 실패로 멈췄다(부하 적재로 생긴 2,609건 세션 잡도 "추월됨"으로 전부 실패). cron 3개를 잠시 끄고process_user_exercise_stats_refresh_jobs_for_user_now인라인으로 정착시켜 증거를 확보했다. 긴 이력 유저의 첫 백필이 워커 시간 예산을 넘는 경우의 처리(분할·예산 상향)는 통계 워커 담당 인계. contractsImportBoundaries경계 테스트:features/**는types/screenRpc(DB 행 모양)를 직접 import 할 수 없다. codec 은services/domains/codecs/에, typed 모양은contracts/ports/에 둔다 — 다음 feature 이식(U02/U03·A10·A15)도 같은 자리.- ESLint 억제 게이트: 새
eslint-disable는 리뷰 없이 통과하지 않는다. 의존값이 남는 memo 대신 구조(안정적인 port + store 구독)로 푼다. pending-changes.json은 같은 파일 항목의reason에 이어 쓴다(항목을 새로 만들지 않는다).- 큐 요청은 precheck 직후에 바로 낸다. 12:26 precheck 뒤 문서 작업 동안 release 에 두 PR 이 들어와 첫 요청이 충돌로 실패했고, 그 뒤에도 한 PR 이 더 들어와 재번호를 두 번 했다. 같은 시각 마이그레이션 번호(
…023000·…033000)가 세 트랙에서 겹쳤다 — 번호는migrations:check -- --base origin/release/vX.Y.Z로 목적 release 꼬리와 대조하고, 스냅샷 재생성은 샌드박스 스택이 떠 있어야 한다(ci:precheck-local이 사용 후 스택을 내린다 —supabase --workdir <샌드박스> start -x …로 띄운 뒤schema:snapshot --reset). 스택이 내려간 채sql:extract를 돌리면 release 쪽 스냅샷에서 내 정의 파일 변경을 되돌리므로 순서는 반드시 스냅샷 → extract → model.
U02/U03 인계
- port:
ExerciseHistoryPort = { read(exerciseId) → ExerciseHistoryView; open(exerciseId); loadMore(exerciseId); close(exerciseId) }— 화면은 canonical 종목 id 만 넘긴다. 이름·행 id·카탈로그 조인은 받지 않는다. - view model:
status: "unresolved" | "pending" | "ready" | "error",rows[](key=session_exercise_id·date·title·isPr·note·fields(그 줄의 기록 칸 또는 null)·sets[](camelCase 원자, 값 있을 때만 키)·setsTruncated),hasMore·loading·exhausted·windowYears·setsTruncatedRows·message. 일부 페이지를 전체 이력처럼 취급하지 않는다 —hasMore/exhausted로 표시한다. - 취소 계약: 드로어를 열 때
open, 닫히거나 종목이 바뀌거나 화면이 사라질 때close(React 라면 effect 정리에서). 계정 전환은 store 가 처리한다(별도 처리 없음). - 재렌더:
useResourceStoreVersion(store)를 셸(또는 binding)에서 구독하고 화면은read를 다시 부른다. port 를 memo 로 새 객체로 만들지 않는다. - 데스크톱: 지금 데스크톱에는 이 드로어에 해당하는 소비자가 없다(
ui/desktop/**에 exerciseHistory 없음). PR 화면의ensurePrExerciseHistoryYear는 A10 소유의 다른 화면이다. U03 이 데스크톱 편집기에 기록 보기를 붙이면 같은 port 를WorkoutFlowBinding과 같은 방식으로 받는다.
5. 적용 결과
| 항목 | 전 → 후 | 확인 |
|---|---|---|
| 친구 세션·비슷한 이름 종목이 내 이력에 섞임 | 카드 소유자 무시·이름 부분 일치 → canonical 종목 id 로만 요청·병합, 다른 종목 페이지는 오류, id 없는 행은 unresolved | exerciseHistoryFeature "섞이지 않는다"(0건), exerciseHistoryDrawer unresolved |
| 같은 (owner, 종목, 연도, 커서, 기준일) 결과 동일 | 피드 방문 범위·앱 재시작에 따라 달라짐 → 캐시 키 동일·재요청 0 | exerciseHistoryFeature "열 때"·exerciseHistoryBinding open 재호출 시 서버 호출 0 |
| 열 때 내려받는 양 | 전체 세션 검색(최대 40페이지 × 120건) → 15행 페이지(창 이어 읽기 최대 10회) | 연도 창 테스트(3페이지로 18줄) |
| 병합 비용 | 행마다 배열 find(O(n·m)) → Map 색인(O(n)) | 1/4/10년 fixture(156/624/1,560행 + 중복 페이지 → 입력 201/783/1,962행) 행당 0.58 → 0.51 → 0.49µs, 10년 0.96ms |
| 세트 중복·최신값 덮어쓰기 | 같은 세션 행 세트 이어붙이기 → session_exercise_id·set_id 색인, 절삭<완전<세대 우선 | 중복/역순 페이지·같은 날짜·같은 세션 두 행·절삭+상세 혼합 테스트 |
| 유산소·시간 종목 세트 표기 | RPC 에 원자 없음 → 거리·시간·칼로리·보조무게 + 기록 칸 | pgTAP 15/15 · 드로어 렌더 "5000m in 30:00" |
| 서버 조회 실행계획 | — → core: Index Scan user_exercise_session_rollups_detail_cursor_idx, limit+1, 18~75ms(10년 실데이터) | explain-a14-populated.json(샌드박스, Production 미실측) |
| 문(v4) 응답 시간 | 3.6초(10년 실데이터, 20행) — 미개선, 인계 | §4 "작업 중 드러난 것" 1 |
| 취소·계정 전환·늦은 응답 | 취소 경로 없음(세션 검색 전체 대기) → 드로어 닫힘·종목 변경·owner 떠남에 요청 취소, 늦은 응답 폐기 | exerciseHistoryResource·exerciseHistoryBinding |
| 로컬 게이트 | npm run check 3541 pass / 0 fail · Precheck 통과(2026-09-10 12:26 KST) — ci:precheck-local precheck · static 통과 · unit-1 통과 1861 passed / 0 failed / 13 conditional skip · unit-2 통과 1726 passed / 0 failed / 28 conditional skip · database 통과 db reset(마이그레이션 전체 적용): pass · schema.sql 스냅샷 --check: pass · pgTAP: pass 137파일/2640 assert · 동시 저장 세대·영수증: pass · dirty 범위·세대 병합: pass · 큰 이력 계산 격리·동시 CRUD·통계 수렴: pass · 6분 43초 | |
| release 통합 / staging / Production | 통합 완료 209bbffd / 미확인 / 미배포 | 아래 상태 표 |
상태 표(작성 시점 2026-09-10; 승격 세션이 갱신한다):
| 단계 | 상태 |
|---|---|
Precheck(ci:precheck-local) | ✅ 통과 2026-09-10 12:26 KST(첫 head 4d2290d2) · 재통과 13:06 KST(재번호 뒤 head 4e953796; 12:50 실행은 격리 수렴 probe 가 단위 2샤드와 CPU 경합으로 실패, probe 단독 2회 통과)(기준 release/v0.18.0 2ab89770 합침 → HEAD 4d2290d2; 첫 실행은 격리 수렴 probe 게시 단계 문장 시간 제한(57014, 재계산 37.9초 — 단위 2샤드와 CPU 경합)으로 실패, 재실행 통과) · PR #1503 |
Merge Check(merge:request) → release/v0.18.0 | ✅ 병합 209bbffd 2026-09-10 13:07 KST(실행 34435877543). 첫 요청(f78b4132)은 #1501·#1502 선행 병합으로 생성 파일·remoteDataController 충돌 → 해결·같은 시각 마이그레이션(#1409·#1411) 뒤로 20260914043000 재번호·재생성·precheck 재통과 뒤 새 head 4e953796 로 재요청 |
| release → staging 승격 | 미실행 |
| staging → main(Production) | 미실행 |
실기기 확인은 하지 않았다(§14 — 자동 증거로 닫는다). 시각·감각 품질(드로어 스켈레톤·더 보기 버튼 배치)은 사람 눈으로만 판단할 수 있다는 정보로만 남긴다.
6. 이번 개선으로 향상된 것
내 기록만, 그 종목만
친구를 팔로우하든 이름이 비슷한 커스텀 종목을 만들었든, "기록 보기"에는 내가 그 종목(canonical id)으로 한 세션만 나온다. 종목 id 가 없는 옛 행은 이름으로 짐작하지 않고 "연결되지 않았다"고 말한다.
어디서 열어도 같은 목록
피드를 봤는지, 앱을 다시 켰는지와 무관하게 같은 종목·같은 기간의 목록이 같다. 통계가 아직 계산 중이면 페이지가 stale 로 표시되고 완료 기록을 저장하면 다음 열기가 서버를 다시 읽는다.
필요한 만큼만 읽는다
드로어를 열면 15줄이 찰 만큼만(최대 10페이지) 읽고 "더 보기"로 이어 읽는다. 10년 유저가 열 때마다 수천 세션을 받던 것이 사라졌다. 닫으면 진행 중 요청이 끊긴다.
구조적으로 남는 것
- 종목 이력의 원천 = 서버 RPC 한 곳(owner·canonical 종목 id·연도·커서). 이름 기반 매칭 경로가 코드에 없다.
contracts/ports/exerciseHistoryDto.tstyped 계약과features/workout/historyfeature(loader·resource·병합·연도 창·view·binding). U02/U03 이 그대로 받는 port.- codec 층만 DB 행 모양을 안다는 경계가 이 feature 에도 적용됐다(경계 테스트가 지킨다).
- 서버 RPC 계약: 세트 원자 4종·기록 칸(추가 전용, v4 유지)과 대표 조회
pr.history_page의 실행계획 증거.
남은 것
- 문(v4) 근력 지표 부착의 세션당 비용(§4 "작업 중 드러난 것" 1) — 세트 스코어·DB 담당 인계. A14 가 페이지를 15행으로 묶었지만 10년 유저 2초대는 남는다.
- 긴 이력 유저의 첫 통계 백필이 워커 60초 예산을 넘는 경우(같은 절 2) — 통계 워커 담당 인계.
- G05 장부:
mobileFeedViewMappers(피드 카드 전용으로 축소)·workoutCalculations(표시 도우미만) 의 소유 범위 갱신은 이 문서와 총괄 A14 카드에 적었고, 장부 표 재생성은 R01 종료 검사에서. - R04: 1/4/10년 병합 처리량·서버 EXPLAIN·페이지 한도(15행·10페이지)·측정 환경(샌드박스
cil095e2658, Postgres 17.6.1.127, 세션 2,609·세트 31,308)을 이 문서 §5 에서 넘긴다. - release 통합·staging·Production 상태는 승격 세션이 위 상태 표를 갱신한다.
세션: 4b0f1d85-820d-4396-ad20-37eb6881b0f3 (이전 b55b728c-569d-4d7c-98ee-fe9e6cb1ea35)