Skip to content

기간·달력 보정 쓰기를 완성 행 한 번으로 통합 (2026-09-10)

  • 기간: 2026-09-10, 1개 작업. 오너 요청: “바벨릭 #1416 진행해줘”, 분석·4개 Phase 계획 후 “go”.
  • 랜딩: release/v0.18.0 반영 완료(앱 PR #1518, 5b16a162). 앱 기준 a1d462a8, 직접 선행 D05 #1411 / PR #1506(8b796c1f) 포함. 앱 구현 ce08db2c, 최종 행동/고정 corpus 검사·중간 adapter 제거 ac8735e2. 마이그레이션 20260914063000, 20260914073000, 20260914083000. staging/Production/Vercel 배포는 이번 범위가 아니다.
  • 설계서: 이슈 계획, 필드·영향 계약.
  • 정본: 계산 DAG, 기간·달력 계약, 앱 supabase/definitions/stats/functions·calendar/functions의 assembler/writer와 stats-projection-writers.json.
  • 도구: 저장소의 sql:candidate, schema:snapshot, sql:extract, db:model, db:upgrade; 작업 디렉터리의 비추적 old/full shadow·쓰기 계측 스크립트. 과거 함수는 앱의 활성 호환 경로로 남기지 않았다.
  • 게이트: D06 실제 DB 10건, 단일 작성자 검사, 기존 출석·shared-set·시간대·신원 pgTAP 4파일/68단언, Phase별 npm run check. 최종 precheck는 7분 24초에 통과했고 Merge Check 잡은 54초에 통과했다. 브라우저/viewport·Production 검증은 미실행이다.
  • 버그리포트: 없음. 요청은 D06 구조 이식이며 별도 제품 결함 수리를 추가하지 않았다.
  • 계약: D04 scope v1과 완료된 D07 영향 v1의 합집합, 같은 assembler를 쓰는 full/listed/suffix, 누락 입력의 발행 중단.

Phase 현황

Phase내용상태
Phase 1기존 full 기준·경계 fixture·필드/입력 계약완료, 19cf9e0f
Phase 2종목 기간 최종 행·단일 writer완료, e6872d6a·1fc4aba4
Phase 3훈련 기간·달력·D04/D07 영향 통합완료, ce08db2c
Phase 4실패/재시도·실제 쓰기·문서·precheck·릴리스 통합완료, 앱 ac8735e2 → merge 5b16a162

1. 배경

운동을 한 번 저장하면 기본 합계 이후 강도·반복수·점수·출석 계산이 같은 기간 행을 다시 고쳤다. D05가 세트 관측과 세션 합계를 분리했으므로, D06은 그 결과와 순차 파생 결과를 모아 기간·달력의 최종 행을 완성하는 책임을 맡았다.

2. 문제 제기

종목 기간은 5개, 훈련 기간은 4개, 달력은 2개 계산 함수가 썼다. 달력은 먼저 기본 행을 삽입하고 파생 값을 0으로 초기화한 뒤 강도와 점수를 각각 UPDATE했다. 종목 기간의 기본 계산은 suffix 요청에도 이전 기간을 지우고 다시 넣었다.

Production 초기 프로브의 누적 INSERT/UPDATE/DELETE 수치는 기준 통계일 뿐이다. reset 시점이 없어 기간당 비용이나 운영 개선율로 해석하지 않았다. 변경 효과는 동일한 로컬 fixture의 실제 DML 트리거로 따로 측정했다.

3. 해결 방안

오너가 승인한 범위는 D06과 release/v0.18.0 통합이다. 수치 정책, 화면 DTO, 원본 저장, PR/e1RM 상태, applied generation 책임은 유지한다.

접근판단
최종 행 전체를 조립하고 각 표의 writer 하나가 값이 달라진 키만 저장채택. full과 증분이 같은 공식·같은 저장 경계를 사용
기존 보정 UPDATE 순서를 유지하며 조건문/재시도만 추가기각. 필드별 작성자 중복과 순서 의존성이 남음

합은 원시 합계, 평균은 원시 합/분모, 최댓값은 삭제 뒤 현재 후보 재평가를 사용한다. 출석은 주 목요일이 속한 월·분기·년에 귀속한다.

4. 적용한 내용

Phase 1 — 기준과 계약

ISO 연도·분기 경계, 같은 날 다세션, 복합 shared set, 날짜 이동, 최댓값 삭제를 고정했다. 같은 canonical ID에서 기존 full과 비교하며 기존 clock 제외 기준을 유지했다. 기존 a1d462a8 함수로 계산한 66행을 tests/db/fixtures/d06-period-calendar-baseline.json에 고정했다. 무작위 fixture owner는 실제 소유권을 먼저 단언한 뒤 논리 owner로 치환하고 모든 수치·키·null·분포 셀을 비교한다. D07의 완료 상태·사용자·세대·full/listed/suffix·old/new 날짜·종목 필드를 먼저 합의했다.

Phase 2 — 종목 기간

assemble_user_exercise_period_rows_v1가 모든 값을 계산하고 refresh_user_exercise_period_buckets_v1가 저장한다. mixed base는 종목 요약만 남긴 refresh_user_exercise_summary_from_rollups_v1로 대체했다. strength/purpose의 기간 보정 DML과 score/distribution 기간 전용 함수를 제거했다.

Phase 3 — 훈련 기간과 달력

훈련은 세션·날짜·shared-set·볼륨·반복수·시간대·출석을 함께 계산한다. 달력은 calendar_day_summary_source의 PR-only 날짜와 원본 의미를 유지하면서 관측·점수 필드를 결합한다. 훈련 보정 함수 4개와 calendar core를 제거했다. 날짜 범위 공개 진입점은 키 선택만 맡는다.

refresh_user_period_calendar_projection_v1는 D04 원본 영향과 완료된 D07 후속 영향을 합친다. 현재 순차 단계가 모두 성공하면 명시 legacy-sequential-range DTO를 전달한다. D07은 이 자리에 listed 결과를 연결할 수 있다. D07 완료를 새 선행으로 요구하지 않았다.

Phase 4 — 실패 복구와 계측

최종 consumer 연결 뒤 호출되지 않는 중간 기간 adapter는 세 번째 forward migration으로 제거했다. 단일 DROP은 4년 populated 상태의 원본 보존·조회·공존 검증을 통과했으며 자동 중단 위치 0/1은 문장 도중 실패 증거로 세지 않는다.

실제 worker에 complete가 빠진 파생 결과를 주입하면 계산이 실패하고 기존 출력·applied 세대를 유지한다. 정상 함수를 복원하고 기존 재시도 절차로 처리하면 원본 값이 변하지 않은 채 full과 같은 값으로 수렴했다. 새 날짜 추가는 종목 5행·훈련 5행·달력 1행을 각각 한 번만 썼고 이전 연도 ctid는 유지했다.

주요 결정과 그 근거

  • 격리 publisher는 별도 발행 경계로 유지한다. D06의 단일 작성자는 계산 책임을 뜻하며 최종 public 발행을 증분으로 바꿨다는 뜻이 아니다.
  • 빈 listed 입력은 빈 입력이다. 파생 값 누락과 정상적인 null/0 결과를 구분한다.
  • 달력 source_updated_at의 provenance 변화는 보존한다. 같은 값의 PR을 재물질화해 이 시각이 바뀐 경우와 숫자 변화는 계측에서 구분한다.

작업 중 드러난 것

업그레이드 증거 주석 추가 후 스키마 지문뿐 아니라 registry/model 지문도 함께 갱신해야 한다. 초기 기본 검사 실패 뒤 동기화하고 전체 기본 검사를 다시 통과했다. 준비 전 업그레이드 예비 실행은 중단해 증거에서 제외했다. DB 전용 실행 환경변수가 기본 테스트에 전달된 실행은 같은 sandbox의 cron 잠금 충돌 29건으로 실패해 제외하고, DB 없는 기본 검사와 직렬 DB 검사를 분리해 재실행했다. 테스트 fixture의 필수 source_ref를 채우고 예상 SQL 오류는 DB 안에서 포착해 출력 파이프 경합을 없앴다. 수치 단언이나 시간 예산을 낮추지 않았다.

5. 적용 결과

항목결과
계산 작성자종목 5→1, 훈련 4→1, 달력 2→1
동일 원본의 기존 full 비교종목 기간 39·훈련 20·달력 7행, 값 차이 0
원시 평균10·10·20 → 13.33, 일 평균의 평균 15로 바뀌지 않음
작은 append fixture 종목 기간 DMLINSERT 10/UPDATE 85/DELETE 5 → INSERT 5/UPDATE 0/DELETE 0
같은 fixture 훈련 기간 DMLINSERT 5/UPDATE 14 → INSERT 5/UPDATE 0
같은 fixture 달력 DMLINSERT 1/UPDATE 3 → INSERT 1/UPDATE 0
같은 fixture 총 물리 쓰기123→11. 2025년 세션 1개 뒤 2026년 세션 1개 추가의 로컬 유지보수 계산 경로
같은 fixture 처리 시간460→438ms, 각 1회·운영/p95 아님. 성능 개선율로 일반화하지 않음
4년 데이터 업그레이드세 migration 각각 1,043세션·원본 33,127행 보존, 공존 break 0, 잠금 대기 0. 첫 두 migration은 8/16·12/24문장 뒤 중단 rollback/replay, 마지막 단일 DROP은 중단 위치 0/1의 제한을 별도 표시
Phase별 기본 검사Phase 1/2: 각 3646 pass, Phase 3/4: 각 3647 pass. 모두 fail 0. 최종 Phase 4는 3707건 중 60 skip(DB 미지정 등)
실제 DB 추가 검사D06 10/10, 파생 누락·미완료·세대 불일치·빈 입력·재시도·연도 경계·물리 쓰기 포함
최종 precheck/릴리스 통합precheck 7분 24초: static/build·unit 3647 pass/0 fail/60 skip·pgTAP 140파일/2753단언·동시 저장/복구·dirty 세대·worker 임대·큰 이력 격리/동시 CRUD/수렴 통과. Merge Check 잡 54초, 실행 증거, 앱 #15185b16a162
운영·실기기staging/Production·브라우저/viewport 미실행. release 반영과 운영 배포를 구분

6. 이번 개선으로 향상된 것

숫자·출석·시간대가 준비된 한 행만 저장하므로 후속 보정 순서에 기대지 않는다. 새 날짜를 추가할 때 관계없는 이전 기간을 지우고 다시 만드는 비용이 줄었다. D07의 실제 후속 영향 목록을 받는 경계와 미완료 발행을 막는 행동 검사가 남았다.

남은 것

이번 요청의 구현·precheck·release 통합은 완료했다. D07 listed 연결, 최종 release의 staging/Production 승격·브라우저/viewport 검증은 각 캠페인 책임 범위다. D06 결과를 이미 운영 중이라고 표시하지 않는다.