---
title: "운영지침 — SQL 마이그레이션 이력 (적용 완료 DDL 전문)"
category: "operation"
document_type: "운영지침"
source_status: "generated"
knowledge_group: "01_rules"
priority: "Medium"
purpose: "SQL 에디터 저장 쿼리는 아니지만 DB 에 실제 적용 완료된 옛 마이그레이션 DDL 7건의 전문을 시간순 보존. 대부분 이후 골든셋(기록스키마·멀티유저RLS)에 흡수돼 에디터엔 별도 저장하지 않았다. 본레포 migrations/ 폴더 정리 시 삭제된 파일의 원문·적용일·흡수처를 추적하기 위한 이력 페이지. SQL관리대장(골든셋 체계)의 하위 페이지."
read_when: ["SQL","마이그레이션","이력","DDL","골든셋","run_type","strength","updated_at","profile_publications","RLS"]
updated: "2026-06-08"
work_timestamp: "20260608_222200"
context: "달록본레포CC (D:\\dallog\\dallog_git) — migrations/ 폴더 정리. 골든셋에 흡수돼 에디터 저장 쿼리가 아닌 옛 마이그레이션 7개를 본레포에서 삭제하고, 전문을 KB 이력으로 이관(전문 복사형 — 사장님 결정)"
source_of_truth: "https://dallog-tools.hansbridge.co.kr/knowledge/"
---

# 운영지침 — SQL 마이그레이션 이력 (적용 완료 DDL 전문)

> 상위 페이지: **SQL 쿼리 관리 지침 (골든셋 체계) = SQL관리대장** (워크리포트 68).
> 여기 담긴 7건은 **SQL 에디터에 저장된 쿼리가 아니다.** DB 에 적용 후 대부분 골든셋(기록 스키마·멀티유저 RLS)에 흡수돼, 에디터엔 별도 이름으로 저장하지 않았다. 본레포 `migrations/` 폴더 정리(2026.06.08) 시 삭제한 파일의 원문을 추적 가능하게 보존한다.
> **재실행 주의**: 대부분 멱등(IF EXISTS/IF NOT EXISTS)이나, 현행 스키마는 골든셋이 기준이다. 재적용이 필요하면 반드시 골든셋 파일을 우선 확인한다.

## 이력 요약

| 적용일 | 마이그레이션 | 목적 | 흡수처(현행 기준) | 상태 |
|---|---|---|---|---|
| 2026-05-16 | run_type_unlock | 런타입 기본 7종 잠금 해제(is_default/is_basic DROP) | 기록 스키마 (run_type_configs 구조) | 적용완료·흡수 |
| 2026-05-16 | strength_v2 | 근력 v2(카테고리·체중부하·맨몸+추가중량) + 베이직 종목 시드 | 기록 스키마 (exercise_configs/strength_sets 구조 + 글로벌 시드) | 적용완료·흡수 |
| 2026-05-22 | records_updated_at | body_records/running_logs updated_at 컬럼 + 자동갱신 트리거 | 기록 스키마 | 적용완료·흡수 |
| 2026-05-26 | running_logs_routine_id | running_logs.routine_id 컬럼(셀모드 루틴 연동) | 기록 스키마 | 적용완료·흡수 |
| 2026-05-27 | diag_rls_status | RLS 상태 진단(SELECT only, 데이터 무변경) | 없음(진단용) | 진단완료·무영향 |
| 2026-05-27 | fix_rls_delete_and_run_type | DELETE 정책 3테이블 + run_type authenticated 정책(단일사용자 임시) | 멀티유저 RLS(USING(true)→auth.uid()=user_id 재작성) | 후속 골든셋이 덮어씀(superseded) |
| 2026-06-02 | profile_publications | profile_publications 테이블 + RLS + 트리거 | golden_set/260602_프로필공개스냅샷_필수보존.sql (에디터 등록 미러본) | 중복 — 미러본으로 일원화 |

---

## 1. 2026-05-16 — run_type_unlock

```sql
-- 런타입 기본 7종 잠금 해제 — 이슈2 — 2026.05.16
-- 적용: Supabase SQL Editor에서 전체 복사·실행
-- 안전: DROP COLUMN IF EXISTS 가드로 멱등 실행 가능

-- 기본 7종(조깅/LSD/너프런/인터벌/템포런/대회/회복런)도 수정·삭제 가능하도록
-- run_type_configs 에서 잠금 플래그가 있다면 모두 제거
ALTER TABLE run_type_configs DROP COLUMN IF EXISTS is_default;
ALTER TABLE run_type_configs DROP COLUMN IF EXISTS is_basic;

-- 혹시 모를 RLS 정책 잔재 정리 (정책명이 다를 수 있어 IF EXISTS 사용)
-- 기본값으로는 모든 사용자가 CRUD 가능 (단일 사용자 앱)
DROP POLICY IF EXISTS "Block basic run types" ON run_type_configs;
DROP POLICY IF EXISTS "Lock default run types" ON run_type_configs;
```

## 2. 2026-05-16 — strength_v2

```sql
-- 근력운동 v2: 카테고리·체중부하·맨몸+추가중량 + 저장된 루틴/운동 지원 — 2026.05.16
-- 적용: Supabase SQL Editor에서 전체 복사·실행
-- 안전: ADD COLUMN IF NOT EXISTS / NOT EXISTS 가드로 멱등 실행 가능

-- ───────── exercise_configs 확장 ─────────
ALTER TABLE exercise_configs ADD COLUMN IF NOT EXISTS category TEXT;
ALTER TABLE exercise_configs ADD COLUMN IF NOT EXISTS bodyweight_ratio NUMERIC;
ALTER TABLE exercise_configs ADD COLUMN IF NOT EXISTS is_basic BOOLEAN DEFAULT FALSE;
ALTER TABLE exercise_configs ADD COLUMN IF NOT EXISTS sort_order INTEGER DEFAULT 0;

-- ───────── strength_sets 확장 (중량 / 추가중량 / 토글) ─────────
ALTER TABLE strength_sets ADD COLUMN IF NOT EXISTS weight_kg NUMERIC;
ALTER TABLE strength_sets ADD COLUMN IF NOT EXISTS additional_weight_kg NUMERIC;
ALTER TABLE strength_sets ADD COLUMN IF NOT EXISTS use_additional BOOLEAN DEFAULT FALSE;

-- ───────── 기존 미분류 종목은 '기타' 처리 ─────────
UPDATE exercise_configs SET category = '기타' WHERE category IS NULL;

-- ───────── 베이직 종목 프리셋 INSERT (이미 동일명 존재 시 스킵) ─────────
-- 맨몸
INSERT INTO exercise_configs (name, category, bodyweight_ratio, is_basic, sort_order)
SELECT v.name, v.category, v.bodyweight_ratio, v.is_basic, v.sort_order
FROM (VALUES
  ('풀업'::text,        '맨몸'::text, 100::numeric, TRUE, 100),
  ('친업'::text,        '맨몸'::text, 100::numeric, TRUE, 101),
  ('푸쉬업'::text,      '맨몸'::text,  65::numeric, TRUE, 102),
  ('다이브봄버'::text,  '맨몸'::text,  65::numeric, TRUE, 103),
  ('레그레이즈'::text,  '맨몸'::text,  50::numeric, TRUE, 104),
  ('딥스'::text,        '맨몸'::text, 100::numeric, TRUE, 105),
  ('인버티드로우'::text,'맨몸'::text,  60::numeric, TRUE, 106),
  ('피스톨스쿼트'::text,'맨몸'::text,  85::numeric, TRUE, 107),
  ('한손푸쉬업'::text,  '맨몸'::text,  65::numeric, TRUE, 108)
) v(name, category, bodyweight_ratio, is_basic, sort_order)
LEFT JOIN exercise_configs e ON e.name = v.name
WHERE e.id IS NULL;

-- 웨이트
INSERT INTO exercise_configs (name, category, is_basic, sort_order)
SELECT v.name, v.category, v.is_basic, v.sort_order
FROM (VALUES
  ('벤치프레스'::text,        '웨이트'::text, TRUE, 200),
  ('스쿼트'::text,            '웨이트'::text, TRUE, 201),
  ('데드리프트'::text,        '웨이트'::text, TRUE, 202),
  ('오버헤드프레스'::text,    '웨이트'::text, TRUE, 203),
  ('바벨로우'::text,          '웨이트'::text, TRUE, 204),
  ('덤벨컬'::text,            '웨이트'::text, TRUE, 205),
  ('덤벨플라이'::text,        '웨이트'::text, TRUE, 206),
  ('루마니안데드리프트'::text,'웨이트'::text, TRUE, 207),
  ('덤벨숄더프레스'::text,    '웨이트'::text, TRUE, 208)
) v(name, category, is_basic, sort_order)
LEFT JOIN exercise_configs e ON e.name = v.name
WHERE e.id IS NULL;

-- 머신
INSERT INTO exercise_configs (name, category, is_basic, sort_order)
SELECT v.name, v.category, v.is_basic, v.sort_order
FROM (VALUES
  ('렛풀다운'::text,        '머신'::text, TRUE, 300),
  ('시티드로우'::text,      '머신'::text, TRUE, 301),
  ('레그프레스'::text,      '머신'::text, TRUE, 302),
  ('레그익스텐션'::text,    '머신'::text, TRUE, 303),
  ('레그컬'::text,          '머신'::text, TRUE, 304),
  ('체스트프레스머신'::text,'머신'::text, TRUE, 305),
  ('펙덱플라이'::text,      '머신'::text, TRUE, 306),
  ('카프레이즈머신'::text,  '머신'::text, TRUE, 307)
) v(name, category, is_basic, sort_order)
LEFT JOIN exercise_configs e ON e.name = v.name
WHERE e.id IS NULL;

-- 아이소메트릭
INSERT INTO exercise_configs (name, category, is_basic, sort_order)
SELECT v.name, v.category, v.is_basic, v.sort_order
FROM (VALUES
  ('플랭크'::text,      '아이소메트릭'::text, TRUE, 400),
  ('사이드플랭크'::text,'아이소메트릭'::text, TRUE, 401),
  ('월싯'::text,        '아이소메트릭'::text, TRUE, 402),
  ('데드행'::text,      '아이소메트릭'::text, TRUE, 403)
) v(name, category, is_basic, sort_order)
LEFT JOIN exercise_configs e ON e.name = v.name
WHERE e.id IS NULL;

-- ───────── app_settings 키 메모 ─────────
-- 저장된 루틴   : key = 'saved_routines'    value = JSON array of { id, name, exercises:[{ exercise_name, sets:[{ reps, weight_kg?, additional_weight_kg?, use_additional? }] }], created_at, updated_at }
-- 저장된 운동   : key = 'saved_exercises'   value = JSON array of { id, exercise_name, sets:[...], created_at, updated_at }
-- 두 키 모두 별도 테이블 없이 app_settings 의 key-value(JSON 문자열)로 보관 — fitness_projects 와 동일 패턴
```

## 3. 2026-05-22 — records_updated_at

```sql
-- body_records / running_logs에 updated_at 컬럼 + 자동 갱신 트리거 추가 — 2026.05.22
-- C-6: /settings 수정 로그 진입 시 발생하는 Supabase 400 (updated_at 컬럼 부재) 해소
-- 적용: Supabase SQL Editor에서 전체 복사·실행
-- 안전: DO 블록 안에서 컬럼 존재 여부 확인 후 ADD + 백필 → 재실행해도 사용자 수정값 보존(멱등)

-- ───────── body_records.updated_at ─────────
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'body_records' AND column_name = 'updated_at'
  ) THEN
    ALTER TABLE body_records ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
    -- 기존 행 백필: created_at 으로 초기화 → 수정 로그 정렬이 자연스러움
    UPDATE body_records SET updated_at = COALESCE(created_at, now());
  END IF;
END $$;

-- ───────── running_logs.updated_at ─────────
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'running_logs' AND column_name = 'updated_at'
  ) THEN
    ALTER TABLE running_logs ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
    UPDATE running_logs SET updated_at = COALESCE(created_at, now());
  END IF;
END $$;

-- ───────── 트리거 함수: UPDATE 시 updated_at 자동 갱신 ─────────
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- ───────── 트리거 부착 (멱등) ─────────
DROP TRIGGER IF EXISTS body_records_set_updated_at ON body_records;
CREATE TRIGGER body_records_set_updated_at
  BEFORE UPDATE ON body_records
  FOR EACH ROW EXECUTE FUNCTION set_updated_at();

DROP TRIGGER IF EXISTS running_logs_set_updated_at ON running_logs;
CREATE TRIGGER running_logs_set_updated_at
  BEFORE UPDATE ON running_logs
  FOR EACH ROW EXECUTE FUNCTION set_updated_at();
```

## 4. 2026-05-26 — running_logs_routine_id

```sql
-- 러닝 루틴(프로젝트) 연동 컬럼 추가 — 셀기능 Phase 4 (2026.05.26 추가)
-- 적용 방법: Supabase Dashboard > SQL Editor 에서 본 파일 내용 실행
-- 본 컬럼은 nullable 이므로 기존 데이터·기능에 영향 없음.
-- 적용 후 src/components/cellmode/CellModeRunning.tsx 의 saveAll() 안 주석된
-- `routine_id: r.routine_id || null` 라인을 활성화하면 셀 모드 루틴 컬럼이 실제 저장된다.

ALTER TABLE running_logs ADD COLUMN IF NOT EXISTS routine_id text;

-- 후속(세컨드 페이즈) — 필요 시:
-- ALTER TABLE running_logs ADD CONSTRAINT running_logs_routine_id_check
--   CHECK (routine_id IS NULL OR routine_id ~ '^rr-[a-zA-Z0-9-]+$');
```

## 5. 2026-05-27 — diag_rls_status (진단용 · SELECT only)

```sql
-- 잔여 2 (셀 모드 삭제 차단) + 이슈 11 (OAuth RLS 차단) 진단용 SQL — 2026.05.27 추가
-- 적용 방법: Supabase Dashboard > SQL Editor 에서 본 파일 내용을 4개 쿼리 각각 실행하고
--           결과를 캡쳐(또는 텍스트 복사)해 Claude 컨텍스트에 붙여 보강 SQL을 정확히 만든다.
-- 본 파일은 SELECT만 사용 — 데이터 변경 없음.

-- ───────── 1. 테이블별 RLS 활성 여부 ─────────
-- rowsecurity = true 이면 RLS 활성. 정책이 없으면 모든 작업이 차단된다 (RLS 기본 동작).
SELECT
  schemaname,
  tablename,
  rowsecurity AS rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
  AND tablename IN (
    'body_records', 'running_logs', 'strength_logs', 'strength_exercises', 'strength_sets',
    'body_projects', 'running_projects', 'run_routines',
    'run_type_configs', 'shoe_configs', 'exercise_configs', 'app_settings',
    'fitness_projects', 'coach_notes'
  )
ORDER BY tablename;

-- ───────── 2. 테이블별 RLS 정책 목록 (cmd별) ─────────
-- cmd: ALL / SELECT / INSERT / UPDATE / DELETE
-- DELETE 정책이 누락된 테이블이 있으면 그 테이블의 DELETE는 자동 차단된다.
SELECT
  schemaname,
  tablename,
  policyname,
  permissive,
  roles,
  cmd,
  qual AS using_expression,
  with_check
FROM pg_policies
WHERE schemaname = 'public'
  AND tablename IN (
    'body_records', 'running_logs', 'strength_logs', 'strength_exercises', 'strength_sets',
    'body_projects', 'running_projects', 'run_routines',
    'run_type_configs', 'shoe_configs', 'exercise_configs', 'app_settings',
    'fitness_projects', 'coach_notes'
  )
ORDER BY tablename, cmd, policyname;

-- ───────── 3. user_id 컬럼 존재 여부 ─────────
-- user_id 컬럼이 없는 테이블은 사용자별 RLS(`auth.uid() = user_id`)를 적용할 수 없다.
-- 멀티유저 분리 시 user_id 컬럼 추가 + 백필 마이그레이션 필요.
SELECT
  table_name,
  column_name,
  data_type,
  is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
  AND column_name = 'user_id'
ORDER BY table_name;

-- ───────── 4. 현재 인증 상태 확인 (참고) ─────────
-- 본 쿼리를 Dashboard SQL Editor에서 실행하면 service_role 로 동작하므로 auth.uid() 가 null 일 수 있다.
-- 실제 앱에서 anon/authenticated 차이를 보려면 앱 콘솔에서 supabase.auth.getSession() 확인.
SELECT
  current_user AS db_role,
  current_setting('request.jwt.claim.role', true) AS jwt_role,
  auth.uid() AS current_auth_uid;
```

## 6. 2026-05-27 — fix_rls_delete_and_run_type (단일사용자 임시 · 후속 골든셋이 덮어씀)

```sql
-- 잔여 2 (셀 모드 삭제 차단) + 이슈 11 (OAuth 런타입 차단) 보강 — 2026.05.27 추가
-- 진단 결과 (migrations/from_user/) 분석:
--   1. body_records / running_logs / coach_notes : SELECT/INSERT/UPDATE 정책은 있지만 DELETE 정책 누락
--      → cmd별 분리 정책 패턴이므로 누락 cmd 는 자동 차단됨 (PostgreSQL RLS 표준 동작)
--   2. run_type_configs : 기존 정책 2개 모두 roles={anon} 전용 → OAuth(authenticated) 차단
--      → authenticated 역할용 ALL 정책 1건 추가 (기존 anon 정책 유지 — 데이터 손실 없음)
-- 본 SQL 은 단일사용자 모드 한정 임시 해소. v0.9 작업 2 (멀티유저 분리) 본격 진행 시
--   USING (true) → USING (auth.uid() = user_id) 로 재작성 예정.

-- 적용 방법: Supabase Dashboard > SQL Editor 에 본 파일 전체를 붙여 한 번에 실행.
-- 안전: DROP POLICY IF EXISTS 로 멱등 — 재실행해도 동일 결과.

BEGIN;

-- ───────── 잔여 2: DELETE 정책 추가 (3 테이블) ─────────
DROP POLICY IF EXISTS "public delete" ON body_records;
CREATE POLICY "public delete" ON body_records
  FOR DELETE TO anon, authenticated
  USING (true);

DROP POLICY IF EXISTS "public delete" ON running_logs;
CREATE POLICY "public delete" ON running_logs
  FOR DELETE TO anon, authenticated
  USING (true);

DROP POLICY IF EXISTS "public delete" ON coach_notes;
CREATE POLICY "public delete" ON coach_notes
  FOR DELETE TO anon, authenticated
  USING (true);

-- ───────── 이슈 11: run_type_configs authenticated 정책 추가 ─────────
DROP POLICY IF EXISTS "auth_full_run_type_configs" ON run_type_configs;
CREATE POLICY "auth_full_run_type_configs" ON run_type_configs
  FOR ALL TO authenticated
  USING (true) WITH CHECK (true);

COMMIT;

-- ───────── 검증 절차 ─────────
-- 1. dallog.pages.dev 에 master 계정(자격증명 생략) 으로 로그인
--    → 체성분/러닝 셀 모드 진입 → 1건 선택 삭제 → 성공 + NotifyDialog "삭제 완료" 메시지
-- 2. dallog.pages.dev 에 OAuth 계정 으로 로그인
--    → 동일 절차 → 성공
--    → Settings 페이지 런타입 섹션에서 추가/수정/삭제 → 성공
-- 3. F12 콘솔: [CellModeBody] / [CellModeRunning] delete response — error: null, count: N (>0) 확인
```

> ⚠ 위 §6 의 `USING (true)` 전체개방 정책은 **단일사용자 시절 임시본**이다. 현행은 골든셋 `멀티유저 RLS`(auth.uid()=user_id 분리)가 이를 덮어썼다. 절대 재적용 금지.

## 7. 2026-06-02 — profile_publications (중복 · 골든셋 미러본으로 일원화)

```sql
-- 프로필 공개 대시보드/기록 스냅샷 저장소 — 2026.06.02 추가
-- 작성: Claude Code Opus 4.8
-- 목적: 사용자가 자신의 대시보드/기록을 "저장 당시 스냅샷"으로 프로필에 선택 공개.
--       원본 기록 수정 시 자동 갱신 안 됨(불변). 사용자가 업데이트할 때만 갱신.
-- 보안: 공개(is_public)이거나 본인 행만 SELECT. 쓰기는 본인만(마스터 RLS 정책 — 본인 데이터만).
--       스냅샷은 클라이언트가 note 등 비공개 필드를 제거해 저장(앱 레벨). 삭제 데이터는 스냅샷에 남지 않음.

BEGIN;

-- ═════════════════════════════════════════════════════════════════
-- profile_publications — 공개 스냅샷 (대시보드/기록)
-- ═════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS public.profile_publications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL DEFAULT auth.uid() REFERENCES auth.users(id) ON DELETE CASCADE,
  kind TEXT NOT NULL CHECK (kind IN ('dashboard', 'history')),
  section TEXT NOT NULL CHECK (section IN ('body', 'running', 'strength')),
  -- 공개 범위: 전체/1주일/1개월/프로젝트/키워드/현재화면/직접기간
  range_type TEXT NOT NULL CHECK (range_type IN ('all', 'week', 'month', 'project', 'keyword', 'current', 'custom')),
  range_label TEXT NOT NULL DEFAULT '',         -- 사람이 읽는 범위 설명 (예: "최근 1개월", 프로젝트명, 키워드)
  range_meta JSONB,                              -- {start,end,project_id,keyword,view_mode} 등 부가정보
  view_mode TEXT,                                -- 기록(history) 보기 형식: calendar/large/weekly/row
  title TEXT NOT NULL DEFAULT '',                -- 카드 제목 (사용자 지정, 미지정 시 앱이 자동 생성)
  snapshot JSONB NOT NULL,                       -- 저장 당시 데이터 동결본 (note 등 비공개 필드 제거)
  is_public BOOLEAN NOT NULL DEFAULT true,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE public.profile_publications IS '프로필 공개 대시보드/기록 스냅샷. 저장 시점 동결(불변). 사용자가 업데이트할 때만 갱신. is_public 으로 공개/비공개 토글.';

CREATE INDEX IF NOT EXISTS profile_pub_user_idx ON public.profile_publications(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS profile_pub_public_idx ON public.profile_publications(user_id, is_public);

ALTER TABLE public.profile_publications ENABLE ROW LEVEL SECURITY;

-- SELECT: 공개본은 누구나(타 사용자·비로그인 포함), 비공개본은 본인만
DROP POLICY IF EXISTS profile_pub_select ON public.profile_publications;
CREATE POLICY profile_pub_select ON public.profile_publications
  FOR SELECT TO authenticated, anon
  USING (is_public = true OR user_id = auth.uid());

-- INSERT/UPDATE/DELETE: 본인만 (마스터 우회 없음 — project_master_rls_policy 일관)
DROP POLICY IF EXISTS profile_pub_insert_self ON public.profile_publications;
CREATE POLICY profile_pub_insert_self ON public.profile_publications
  FOR INSERT TO authenticated
  WITH CHECK (user_id = auth.uid());

DROP POLICY IF EXISTS profile_pub_update_self ON public.profile_publications;
CREATE POLICY profile_pub_update_self ON public.profile_publications
  FOR UPDATE TO authenticated
  USING (user_id = auth.uid())
  WITH CHECK (user_id = auth.uid());

DROP POLICY IF EXISTS profile_pub_delete_self ON public.profile_publications;
CREATE POLICY profile_pub_delete_self ON public.profile_publications
  FOR DELETE TO authenticated
  USING (user_id = auth.uid());

-- updated_at 자동 갱신 (소셜 스키마의 set_updated_at 재사용)
DROP TRIGGER IF EXISTS profile_pub_updated_at ON public.profile_publications;
CREATE TRIGGER profile_pub_updated_at BEFORE UPDATE ON public.profile_publications
  FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();

COMMIT;
```

> 위 §7 은 골든셋 `260602_프로필공개스냅샷_필수보존.sql`(에디터 등록 미러본)과 동일 내용이다. 현행 기준은 골든셋 미러본 하나로 일원화했고, 본 마이그레이션 파일은 중복이라 정리했다.
