Skip to content

종목 정체 컬럼 정리와 id uuid 전환 — "외부 인입 종목은 커스텀인가"라는 모호함에서 한 줄 규칙·타입 보장까지 (2026-09-03)

  • 기간: 2026-09-03 (세션 2개 — 분석·계획 b56663ea, 구현·랜딩 5d407216; 오너 질의 원문 "외부 인입 종목은 왜 남아 있나 → 유저 커스텀과 어떻게 다르나 → origin과 source가 같은 역할인가 → BRID 3필드만 남기고 나머지 드롭 + id uuid 타입")
  • 랜딩: PR #1190 (Phase 0~3, d23c9418) — 마이그레이션 20260906005900_exercise_identity_columns_v1 · 20260906015900_exercise_id_uuid_v1, Vercel 배포 O(릴리스 PR 뒤)
  • 설계서: 이슈 #1175 첫 댓글(분석·Phase 계획·"예상 효과·개선사항" 포함) · 오너 결정 댓글 · 인수인계 댓글 · Phase 완료 보고 2건
  • 정본: docs/data/brid.md(행 판별 = owner_user_id·source, 라벨 공식 v2) · docs/data/exercise-identity-hard-cutover.md(en/ko) · 마이그레이션 2개의 머리 주석(순서·치환 규칙) · exercises_source_ref_shape_check·exercises_external_owner_source_ref_uidx
  • 도구: 세션 스크래치패드(레포 밖) — probe.mjs(Production 읽기 전용 프로브 17종), build/edit-fns.mjs·edit-p2-g1~g3g.mjs(Production 원문에 "정확히 N회 일치" 치환), assemble.mjs·assemble-p2.mjs(템플릿 + 함수 본문 조립), phase2-proto.sql(타입만 바꾼 롤백 트랜잭션 + plpgsql_check 전수), p2-report.mjs(형 오류를 함수별로 묶어 원문 줄과 함께 보고), 샌드박스 sbx/(config + migrations/tests junction)
  • 게이트: pgTAP exercise_identity_columns_v1(18단언) + 기존 pgTAP 45파일 개정(origin 제거)·27파일 개정(uuid) · 마이그레이션 자가 검증 DO 2개(컬럼 부재·함수 본문 구 참조 0·정책/제약/인덱스·라벨 일치·FK 24·트리거 9·시그니처 text 잔존 0) · 단위 계약 테스트 재앵커(schemaRls·screenRpcContracts·exerciseIdentityHardCutover 등 20파일)
  • 버그리포트: 없음(정리 트랙 — 드러난 결함 2건은 4절 "작업 중 드러난 것")
  • 계약: docs/data/app-screen-rpc-contract.md(카탈로그 origin은 소유자 파생값) · docs/data/rpc-catalog.md(시그니처 uuid·은퇴 34) · docs/contracts/identity-passing.md·desktop-screens-props.md · docs/data/import-pipeline.md 점검 SQL · docs/data/exercise-naming.md · docs/data/admin-health-report.md(placeholder 항목 삭제)

Phase 현황

Phase내용상태
Phase 0퇴역 함수 2개 드롭(remap_wodup_user_customs_to_canonical_v1·wodup_provider_exercise_key_for_placeholder_v1, 호출처 0 실측) + 전용 테스트 삭제✅ PR #1190
Phase 1정체 컬럼 4개 드롭(origin·external_payload·is_external_placeholder·client_request_id), source_ref 제약·인덱스, 라벨 공식 v2, 함수 19개 재발행, 앱 발급 id(D3), 클라이언트·관리자 화면✅ PR #1190
Phase 2exercises.id와 참조 컬럼 29개 text → uuid, 함수 48개 재서명·88개 재발행, brid_uuid_v1 uuid 반환, 권한 복원✅ PR #1190
Phase 3문서 9종 개정 + 이 작업 기록✅ PR #1190 · docs PR 이 문서 PR

1. 배경

종목(exercises) 테이블에는 "누구 것이고 어디서 왔나"를 말하는 정체 컬럼이 8개(id·source·owner_user_id·brid·origin·source_ref·external_payload·is_external_placeholder·client_request_id) 있었다. 2026-09-03 오너가 관리자 화면에서 외부 인입 종목이 남아 있는 이유를 물었고, 질의가 "유저 커스텀과 무엇이 다른가 → origin과 source가 같은 역할인가"로 이어졌다. Production 실측(종목 1,072개: 정식 793·커스텀 110·외부 인입 169)으로 originsource+owner_user_id로 100% 유추되고, is_external_placeholder는 전부 false, external_payload는 어느 화면도 읽지 않으며, 커스텀 110개의 source_ref에는 앱 요청 토큰이 의미 없이 복사돼 있었다. 세션·세트·유저 id는 uuid인데 종목만 text라 함수마다 ::text 변환과 정규식 CHECK가 붙어 있던 것도 같은 자리에서 드러났다.

2. 문제 제기

외부 인입 종목이 앱의 커스텀 관리에서 빠져 있었다

"내 커스텀 종목" 목록·숨기기(set_own_custom_exercise_active)·이름 중복 검사(create_custom_exercise)가 전부 origin='user'만 커스텀으로 취급했다. 박주희(145)·성근(19)·봉천곰돌이(5)의 외부 인입 종목 169개는 카탈로그에는 보이지만 관리 목록에는 0개 노출.

WodUp 중복 방지 인덱스가 아무것도 지키지 않았다

exercises_wodup_owner_source_ref_uidxorigin='user' 조건이라(8월 중순 user로 만들던 시기의 잔재) 현행 외부 인입 164개를 0개 보호.

컬럼 8개가 4개 일을 겹쳐서 했다

정체를 읽으려면 컬럼 조합을 알아야 했고, 그 겹침이 위 결함을 만들었다. id는 text라 정규식 CHECK 19개·길이 CHECK 3개가 모양을 대신 지켰다.

3. 해결 방안

원칙 (오너 결정, 2026-09-03)

  • D1 id 타입 uuid 전환은 이 트랙에 포함하되 마지막 코드 Phase로.
  • D2 퇴역 함수 3개 드롭 승인(실측으로 1개는 이미 없어 2개).
  • D3 앱이 종목 id를 직접 발급하고 client_request_id 컬럼은 드롭.
  • D4 불필요 — source_ref는 남긴다(재결정): 외부 인입만 채우고 커스텀·정식은 빈 값, 제약으로 강제.
  • D5 랜딩 단위 = 전 Phase 완료 후 PR 1개(§16).
  • D6 관리자 "전체 종목"의 "외부 인입 종목" 세그먼트는 "유저 커스텀 종목"에 합침.
  • details는 범위 밖.

접근

대안판단
채택 정식 = 소유자 없음 / 내 종목(커스텀·외부 인입) = 소유자 있음 / 외부 인입 = source <> 'barbelic', source_ref는 외부 인입만컬럼 1개(origin)를 두 컬럼 조합으로 대체, 결함 2건이 조건 교체만으로 해소
origin 유지 + 결함만 수리겹침이 남아 같은 종류의 조건 누락이 재발할 수 있어 기각
RPC 응답에서 origin 키 제거(계약 버전 올림)옛 앱 번들 파손 위험 → 기각. 파생값(소유자 없음=system, 있음=user)으로 키 유지, 카탈로그 계약 버전 8 불변
uuid 전환을 별도 이슈로D1로 이번 트랙 마지막 Phase에 포함
함수 재발행을 손으로Production 원문(pg_get_functiondef)에 "정확히 N회 일치" 치환 스크립트 + diff 전수 검토로 대체

4. 적용한 내용

Phase 0 — 퇴역 함수 정리 (#1190)

remap_wodup_user_customs_to_canonical_v1·wodup_provider_exercise_key_for_placeholder_v1 드롭, 전용 테스트 2개 삭제(pending-changes 신고 대상 아님 — manifest 미등재), 원격 스키마 게이트 퇴역 목록 등재.

Phase 1 — 정체 컬럼 정리 (#1190, 20260906005900_exercise_identity_columns_v1)

① 컬럼 드롭 전에 의존 객체 선정리(정책 exercises_select_authenticated 재생성, 인덱스 4 드롭·1 재생성, CHECK 2 드롭) ② 커스텀 110행 source_ref := ''exercises_source_ref_shape_check + exercises_external_owner_source_ref_uidx (owner_user_id, source, source_ref) where source <> 'barbelic'brid_label_exercise_v2(source, owner_user_id, id) → 정규화 트리거 재발행 → v1 드롭(라벨 값 전 행 불변) ⑤ 함수 19개 재발행 ⑥ 컬럼 4개 드롭 ⑦ 자가 검증. D3는 create_custom_exercise·create_catalog_exercise(_engine)가 선택 키 id를 받도록: 같은 id 재전송이면 소유자·내용 일치 시 기존 행, 불일치면 23505 id is already assigned to a different … payload, 모양 불량은 22023 id must be a UUID. 클라이언트는 barbelicRepositoryid를 발급해 보내고(createClientUuid), 재시도는 같은 제출 키의 id를 재사용(appController.customExerciseRequestIdsRef). 직접 select(EXERCISE_SELECT_COLUMNS·관리자 카탈로그 컬럼)에서 origin 제거, 내 커스텀 목록은 소유자 필터만, 어댑터는 구 캐시의 external을 소유자 기준으로 정규화, 관리자 세그먼트 합침(D6), 건강 리포트 placeholder_exercises 항목 삭제.

Phase 2 — id uuid 전환 (#1190, 20260906015900_exercise_id_uuid_v1)

선정리(뷰 1·정책 1·트리거 9·인덱스 1·CHECK 22·FK 24) → 타입 변경 29컬럼(using ::uuid; jobs 배열은 default를 내렸다 올림) → 재생성(FK 24 원문 옵션 유지, 의미 CHECK 3 — 노트 행은 종목 없음·종목 행은 필수·배열 null 없음) → 시그니처 변경 함수 48개 drop+재생성·본문 88개 재발행·brid_uuid_v1 uuid 반환·라벨 v2 id uuid·Production ACL 복원 → 자가 검증. 클라이언트는 무변경(JSON에서 uuid는 문자열).

Phase 3 — 문서

정본 문서 9종 개정(상단 "계약" 항목).

주요 결정과 그 근거

  • RPC origin 키는 파생값으로 유지: exercise_legacy_read_keys_v1'origin' 파생 키를 넣어 등록 RPC 응답(to_jsonb(행) || 키) 전부가 자동으로 유지되고, 카탈로그·PR 요약 스냅샷은 파생식으로 치환. 계약 버전 올림 없이 옛 번들이 깨지지 않는다.
  • 외부 인입도 "내 종목": 소유자가 있는 행은 전부 소유자 본인 것으로 취급(정체성 트리거·가시성·숨기기·중복 검사). 공급자 매핑 행만 외부 인입을 가리킬 수 있고 앱 커스텀은 못 가리킨다(source 분기).
  • 함수 수리의 오라클은 정적 검사기: 타입만 바꾼 롤백 트랜잭션에서 plpgsql_check 2.8(로컬 이미지 내장)로 전 함수를 검사 → 67함수 149건의 진짜 형 오류를 원문 치환으로 0건까지. SQL 언어 함수는 재생성으로 검사.

작업 중 드러난 것

  • 타입 변경을 막는 객체 목록: 뷰·정책·update of <컬럼> 트리거(컬럼 목록만 있어도)·collate 인덱스·정규식 CHECK·FK 양쪽. 전부 명시 drop → 변경 → 명시 재생성. 마이그레이션 게이트는 add constraint/create policy 직전의 drop … if exists를 요구한다.
  • drop 후 재생성한 함수는 실행 권한이 기본값으로 돌아간다 — Production ACL을 실측해 새 시그니처에 복원(revoke public + 역할별 grant). pgTAP identity_passing_contract가 잡았다.
  • 같은 함수 다중 트랙 되덮기가 리베이스로 재발: #1182가 get_session_presentation_mains_json_v2_engine을 먼저 재발행 → 원문을 그 본문으로 교체하고 치환 재적용. 리베이스 뒤엔 upstream 마이그레이션 함수 ∩ 재발행 목록 교집합 확인.
  • 임시 마이그레이션 번호가 다른 트랙과 겹침(둘 다 20260905235900) → 리베이스 후 migrations:renumber20260906005900/015900으로 해결.
  • $function$ 인용은 $$로 바꿔야 계약 테스트(finalSqlFunction)가 다음 함수를 삼키지 않는다 · 계약 테스트 헬퍼는 시그니처가 바뀐 함수를 "오버로드"로 봐 시그니처 명시 필요 · tableColumns/indexesFor는 베이스라인만 읽어 append 마이그레이션의 drop column은 스냅샷 본문으로 단언.
  • 플랫폼 검사기의 착각 1건(get_admin_mapping_page CTE 컬럼) — 런타임 pgTAP 정상.

5. 적용 결과

항목전 → 후
종목 정체 컬럼9개(id·source·owner_user_id·brid·origin·source_ref·external_payload·is_external_placeholder·client_request_id) → 5개
외부 인입 종목의 "내 커스텀 종목" 노출·숨기기·중복 검사0/169 → 169/169 (pgTAP·소유자 필터; 실기기 미확인)
WodUp 종목 중복 보호인덱스 보호 0/164 → (owner, source, source_ref) 유일 164/164
커스텀 source_ref 잔여 토큰110 → 0 (제약이 재발 차단)
죽은 컬럼·퇴역 함수컬럼 2·함수 2 → 0
종목 id 타입text + 정규식 CHECK 19·길이 CHECK 3 → uuid(참조 29컬럼 포함), CHECK 0
함수 시그니처의 text 종목 id48 → 0 (postcheck 단언)
pgTAP95파일 1,473건 PASS (샌드박스, 마이그레이션 2개 적용)
npm run check2,449/2,450 (1건 = Windows CRLF 전용 perf_tab_switch, CI 통과)
e2e crud·empty·cardio24/24
CIverify·migration-smoke 초록(2차 — 1차는 브라우저 e2e 3건의 구 입력 키 clientRequestId·origin 단언 수정, run 33742556484)
Production 적용릴리스 v0.15.0(PR #1177, 2026-09-03 10:56 UTC 머지)에 포함 — Production 마이그레이션 꼬리 20260906015900, exercises.id·session_exercises.exercise_id uuid, 드롭 컬럼 4종 부재를 읽기 전용 조회로 확인

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

유저: 외부 인입 종목도 내 종목처럼 관리된다

인입 유저가 종목 관리 화면에서 인입 종목을 보고 숨길 수 있고, 같은 이름 커스텀 재등록이 막힌다. 화면 응답 모양은 그대로다.

개발: 정체 규칙이 한 줄이 됐다

"정식 = 소유자 없음, 내 종목 = 소유자 있음, 출처는 source, 외부 인입만 source_ref". 조건 누락 재발의 원인이던 origin 컬럼이 없어졌고, 인입 엔진은 (owner, source, source_ref)로 기존 행을 찾는다.

구조적으로 남는 것

  • exercises_source_ref_shape_check·exercises_external_owner_source_ref_uidx(제약이 규칙을 강제).
  • uuid 타입이 id 모양을 보장 — 정규식 CHECK와 ::text 변환 관습 소멸.
  • 앱 발급 id 계약(D3): 세션·세트와 같은 "클라이언트 발급 + 기본키 중복 흡수" 철학으로 통일.
  • 마이그레이션 기법 정본: 정적 검사기(plpgsql_check) 오라클 + 원문 치환 조립(스크래치패드 도구 목록은 상단).

남은 것

  • 릴리스 v0.15.0(2026-09-03)으로 Production 적용 완료 → 이슈 #1175 [반영완료].
  • 시각적 확인은 사람 몫: 관리자 "유저 커스텀 종목" 세그먼트에 외부 인입 종목이 합쳐진 모습, 앱 종목 관리 목록의 인입 종목 표시 — 정보로만 남긴다(§14).
  • details 컬럼 정리는 범위 밖(오너 별도 결정).