---
title: "작업보고 — 1번 MCP 전수감사 적출 보안 보강 F1~F6 + DB현황 스냅샷 (PR#84)"
category: "workreport"
document_type: "작업보고"
source_status: "generated"
knowledge_group: "03_history"
priority: "High"
purpose: "충전식 30런 DB 전수감사(선행 커밋 559de4f)에서 적출한 보안 약점 6종(F1~F6)을 보강한 작업의 전말을 풀 깊이로 기록한다. 무엇을 왜 고쳤는지(자가충전 차단·anon 권한 회수·뷰 RLS 우회 차단·시드 정리·장부 동결), 각 항목의 위험 등급과 실제 위험 수준, 조치 근거, MCP 직접 적용 결과, advisor 재검증 결과, 그리고 2026-06-11 시점 DB 현황 스냅샷(테이블42·함수31·뷰9, git↔DB 전수 정밀대조 PASS)을 한 문서에 모아 후속 보안 작업·결제 라이브 전 점검·최신상태 복구의 기준점으로 삼는다."
read_when: ["보안","전수감사","DB·RLS·보안","권한회수","DB현황스냅샷","최신상태복구"]
updated: "2026-06-11"
work_timestamp: "20260611_154300"
context: "달록본레포CC (D:\\dallog\\dallog_git) — 충전식 30런 DB 전수감사 적출 보안보강(PR#84) 작업보고"
source_of_truth: "https://dallog-tools.hansbridge.co.kr/knowledge/"
---

# 작업보고 — 1번 MCP 전수감사 적출 보안 보강 F1~F6 + DB현황 스냅샷 (PR#84)

> **이 문서가 무엇인가 (비개발자용 한 줄 설명)**
> 달록 데이터베이스(DB) 전체를 한 항목씩 직접 훑어보는 "건강검진(전수감사)"을 먼저 돌린 뒤, 거기서 찾아낸 보안 약점 6가지를 그날 바로 고친 작업의 보고서다. 핵심은 "사용자가 자기 결제잔액을 마음대로 늘릴 수 있던 잠복 구멍"을 막은 것(F1)과, 로그인 안 한 익명 상태에서 호출 가능했던 내부 기능들의 권한을 거둬들인 것(F2)이다. 아직 결제가 켜지지 않은 베타라 실제 피해는 없었지만, 결제를 켜기 전에 미리 닫아둔 예방 조치다.
> (용어: **anon**=비로그인 익명 권한, **authenticated**=로그인된 사용자 권한, **RLS**=행 수준 보안(사용자가 자기 데이터만 보게 막는 DB 잠금), **REVOKE**=권한 회수, **service_role**=서버만 쓰는 최상위 권한, **RPC**=클라이언트가 DB에 미리 정의된 함수를 호출하는 방식, **advisor**=Supabase의 자동 보안 점검 도구.)

---

## 1. 무엇을 했나 (작업 한눈에)

이 작업은 **두 개의 연속 커밋**으로 이루어진 하나의 흐름이다.

| 단계 | 커밋 | 시각(KST) | 내용 |
|---|---|---|---|
| 감사 | `559de4f` | 2026-06-11 13:25 | 충전식 가격·결제 DB **전수 감사 스크립트**(94항목·재실행 가능) — "적용했다"가 아니라 "DB에 실재한다"를 런 단위 PASS/FAIL 로 증명 |
| 보강 | `56bf1ba` (PR#84) | 2026-06-11 15:43 | 감사가 적출한 보안 약점 **F1~F6 보강** + **DB현황 스냅샷** 문서화 + §7-1 공유노트 |

흐름은 **감사 → 적출 → 보강**의 한 줄기다. 먼저 DB를 100% 전수 대조해 약점을 찾고(감사), 6건을 위험 등급으로 분류하고(적출), 그중 결제 라이브 전 검토·위생·advisor 대응 항목을 그날 바로 고쳤다(보강).

---

## 2. 왜 했나 (배경·동기)

### 2-1. 수동 적용의 "조용한 실패" 리스크

달록은 Supabase CLI 정식 마이그레이션 체계를 쓰지 않고, SQL을 수동 또는 MCP(`supabase-dallog`)로 직접 적용해 왔다. 이 방식의 약점은 **"적용했다고 생각했는데 실제 DB엔 반영 안 됨"** 같은 조용한 실패다. 과거 W3 UNIT7(42P13 한정자 오류) 교훈이 그 사례였다.

그래서 1번 작업(MCP 기반작업)은 충전식 가격·결제(charge W1~W11) 산출물 전체가 **실 DB에 실재하는지**를 런 단위로 증명하는 감사 스크립트(`scripts/audit_charge_db.cjs`)를 만들었다. 이 스크립트는 `information_schema`·`pg_proc`·`pg_policies` 등 권위 카탈로그를 직접 조회해 컬럼·제약·정책·함수·트리거·인덱스·뷰를 PASS/FAIL 로 판정한다.

### 2-2. 사장님 지시 — "100% 전수"

감사 과정에서 사장님이 "100% 전수 대조"를 지시했고, git 선언 객체 ↔ 라이브 DB 객체를 양방향으로 맞춰본 결과 **구조·버전은 일치(PASS)** 했으나 **운영/보안 점검 6건(F1~F6)** 이 적출됐다. 1번의 원래 범위는 "보고/제안만"이었으나, 사장님이 즉시 수정을 지시하면서 같은 날 보강 작업으로 이어졌다.

---

## 3. 문제 — 적출된 F1~F6 (상세)

> 감사 결과는 DB현황 스냅샷 §8-3에 적출표로 기록됐다. 등급은 위험도 순이 아니라 성격 분류다.

### F1 — coach_wallet_charge 자가충전 경로 (⚠ 결제 라이브 前 검토)

가장 중요한 적출이다. 사용자가 직접 호출하는 충전 함수 `coach_wallet_charge(BIGINT, TEXT, TEXT)` 가 **anon·authenticated 양쪽에서 실행 가능**했고, 호출자가 넘긴 `payment_ref`를 그대로 'paid'로 적재하면서 실잔액을 가산했다. **인-DB 결제검증이 없는 구조**라, 이론상 사용자가 임의의 결제번호로 자기 잔액을 증액할 수 있는 잠복 구멍이었다.

- **현재 실제 위험: 0** — `billing_phase='beta'` 가드가 충전을 무력화하고 있어 실제로는 막혀 있었다.
- 단 결제(billing)를 켜는 순간 이 구멍이 활성화될 수 있어, 라이브 전 차단이 필수였다.

### F2 — 사용자범위 코치 RPC anon 실행권 (ℹ 위생)

사용자 범위의 `coach_*` DEFINER RPC 다수가 **anon에서도 실행 가능**해 advisor가 WARN을 띄웠다.

- **현재 실제 위험: 0** — 함수 내부의 `auth.uid()` 가드가 비로그인(NULL)이면 `no_user`로 무동작 처리한다. 악용은 불가했다.
- 다만 "명시적으로 anon에 부여된 상태"는 방어 심층(defense-in-depth) 관점에서 불필요한 노출이라 제거 대상이었다.
- 근본 원인: **W3 UNIT11이 `REVOKE ... FROM PUBLIC`만 수행**했는데, Supabase 기본권한 체계상 `anon`에 **명시적으로** 부여된 권한은 PUBLIC 회수로 사라지지 않는다(REVOKE PUBLIC 무효). 즉 anon이 잔존했다.

### F3 — strength_daily_summary 뷰 SECURITY DEFINER (🔹 advisor ERROR)

집계 뷰 `strength_daily_summary`가 **SECURITY DEFINER**(뷰 생성자 권한으로 실행)로 정의돼 있었다. 내부 `latest_body` CTE가 `body_records`를 사용자 필터 없이 조회해 **RLS를 우회**할 수 있는 소지가 있었다(교차 노출 가능성). advisor가 ERROR(0010)로 적출했다.

- **현재 실제 위험: 낮음** — 사용자별 집계 성격이라 영향은 제한적이었으나, advisor ERROR는 해소 대상이었다.

### F4 — coach_price_table 모델 단가 placeholder (🔹 배포 前 필수)

코치 AI 4개 모델의 단가가 전부 placeholder였다. 원가계산이 부정확해진다. **단, 이 항목은 본 작업에서 고치지 않았다.** 실단가는 새 `price_version` 행으로 주입해야 하고, **자동 원가캐치·적용 시스템을 별도 구축**하기로 사장님이 확정했기 때문이다. → 별도 빌드로 이관(보류가 아니라 별도 시스템 작업).

### F5 — coach_plan_catalog tier3 note 중복 (🔹 미관)

은퇴한 tier3 플랜의 `note` 컬럼에 "은퇴" 안내 문장이 **3회 중복**으로 들어가 있었다(재시드 시 append 누적). 기능 영향 없음.

### F6 — SQL_등록대장 내부 불일치 (🔹 문서)

`SQL_등록대장.md` §3 헤더가 "27개"라 적었는데 본문은 61·에디터는 65로, 수동 추적 장부가 내부 불일치 상태였다. 기능 영향 없음. 장부를 동결(DEPRECATED)하기로 했다.

---

## 4. 해결 — F1~F6 처리 결과 (표)

적용 마이그레이션 `migrations/2026-06-11_audit_security_hardening.sql`(45줄, `[필수보존]` 태그, 멱등·재실행 무해, `BEGIN; … COMMIT;` 트랜잭션).

| # | 등급 | 조치 | 처리 | 검증 결과 |
|---|---|---|---|---|
| **F1** | ⚠ 결제 前 | `coach_wallet_charge`에서 **anon·authenticated `REVOKE ALL`** → service_role/postgres 전용화. 미사용 클라 `chargeWallet()`에 `@deprecated` 주석 | ✅ 해결 | 충전 정상경로(결제창→payment-webhook 워커→`coach_wallet_charge_admin` service_role) **무영향**. 자가충전 단일경로로 봉인 |
| **F2** | ℹ 위생 | 사용자범위 코치 RPC **13종 anon `REVOKE ALL`**(authenticated는 유지). W3 UNIT11의 REVOKE PUBLIC 누락분(명시 anon)을 명시적으로 회수 | ✅ 해결 | advisor의 `coach_*` anon WARN **전부 소멸** |
| **F3** | 🔹 ERROR | `ALTER VIEW strength_daily_summary SET (security_invoker = true)` — 조회자 본인 RLS 적용으로 전환 | ✅ 해결 | advisor ERROR(0010) **소멸**. 교차노출 소지 제거 |
| **F4** | 🔹 배포 前 | 모델 단가 placeholder → **자동 원가캐치·적용 시스템 별도 구축**(사장님 확정). 실단가 확정 전까지 placeholder 유지 | ⏸ 별도 빌드 | 본 작업 범위 밖. `price_version` 행 추가 원칙 |
| **F5** | 🔹 미관 | `coach_plan_catalog` tier3 `note`를 **1회 문장으로 정리** + `updated_at` 갱신 | ✅ 해결 | 중복 3회 → 1회 |
| **F6** | 🔹 문서 | `SQL_등록대장.md` 상단에 **DEPRECATED 동결 배너** — DB+git이 단일 진실원본, 수동 추적 중단 | ✅ 해결 | 장부 동결 |

### 4-1. F2에서 회수한 RPC 13종 (정확 목록)

`coach_resolve_monthly_cap` · `coach_ensure_subscription` · `coach_reset_cycle_if_due` · `coach_get_entitlement` · `coach_consume_turn`(5인자) · `coach_consume_turn`(2인자 호환 래퍼) · `coach_change_plan` · `coach_cancel_subscription` · `coach_wallet_refund` · `coach_wallet_auto_recharge_check` · `coach_coupon_expire_due` · `coach_wallet_set_options` · `coach_onboarding_consume`

추가로 서버 전용 배치 `coach_hard_delete_expired`도 anon 회수(authenticated 미부여라 service_role/owner만 잔존).

### 4-2. 클라 코드 변경 (wallet.ts)

`src/lib/coachChat/wallet.ts`의 `chargeWallet()` 함수에 `@deprecated` 주석 3줄 추가(코드 동작 변경 없음, 호출처 0).

```
@deprecated 2026.06.11 보안 보강 — coach_wallet_charge RPC 는 service_role 전용으로 잠금(클라 호출 시 권한오류).
  실충전은 결제창→payment-webhook 워커→coach_wallet_charge_admin(service_role) 단일 경로다(payment.ts requestCharge).
  본 함수는 미사용(호출처 없음)이며 보존만. 새 코드에서 호출하지 말 것.
```

이 함수는 RPC 잠금 후 호출 시 권한오류가 나므로, "쓰지 말라"는 표식을 남기되 삭제하지 않고 보존했다(외과수술 원칙 — 본인 변경이 만든 표식만 추가).

---

## 5. 판단 근거 (왜 이렇게 고쳤나)

- **F1을 service_role 단일경로로 봉인한 근거** — 실제 충전은 이미 `결제창 → payment-webhook 워커 → coach_wallet_charge_admin(service_role)` 경로로만 일어난다(`payment.ts`의 `requestCharge`). 사용자 직접 호출 `coach_wallet_charge`는 **잉여(미사용)** 였다. 따라서 클라 직접 충전 경로를 통째로 막아도 정상 결제 흐름엔 영향이 없고, 잠복 구멍만 사라진다. [[project_master_rls_policy]]의 "클라 직접 쓰기 차단, 쓰기는 RPC/service_role" 원칙과도 정합.
- **F2를 authenticated 유지·anon만 회수한 근거** — 로그인 사용자는 정상적으로 자기 데이터에 RPC를 써야 한다. anon은 애초에 이 함수들을 쓸 일이 없고(내부 `auth.uid()` NULL → 무동작), 명시 부여만 위생상 거둬들였다. 기능 회귀 0.
- **F3을 security_invoker로 전환한 근거** — DEFINER 뷰는 RLS를 건너뛰므로, 다른 사용자 데이터가 집계에 섞일 소지를 원천 차단하려면 invoker(조회자 RLS 적용)가 정답이다. advisor 권고와 일치.
- **F4를 손대지 않은 근거** — placeholder를 임시값으로 채우면 오히려 잘못된 원가가 라이브에 흘러들 수 있다. 단가는 별도 자동 시스템으로 정확히 주입하는 게 맞다(사장님 확정). [[project_auto_cost_catch_system]] 맥락.
- **MCP 직접 적용을 택한 근거** — 수동 복붙은 조용한 실패 위험이 있다. CC가 MCP로 직접 실행·검증하는 것이 달록 SQL 운영 표준이다. [[reference_supabase_direct_sql]].

---

## 6. 결과 — 적용·재검증

- **적용 방식**: 마이그레이션 SQL을 **MCP(`supabase-dallog`)로 직접 실행**. 멱등 설계라 재실행해도 무해.
- **권한 재검증**: F1·F2 회수 후 함수 ACL 실측 — anon 실행권 소멸 확인. authenticated·service_role 잔존 확인.
- **advisor 재검증**: `get_advisors security` 재실행 — `coach_*` anon WARN **전부 소멸**, `strength_daily_summary` ERROR(0010) **소멸**.
- **정상경로 무영향 확인**: 충전은 webhook→charge_admin 경로라 F1 회수의 영향 없음.

---

## 7. DB 현황 스냅샷 (2026-06-11 기준)

> 1번 작업 파트 A 산출물. Supabase 공식 MCP(`supabase-dallog`, project_ref=lzlhbgnnnkrahrmnvooe)로 직접 추출(`list_tables`/`list_migrations`/`list_extensions`/`list_edge_functions`/`get_advisors` + 읽기 전용 `execute_sql`).

### 7-1. 연결·인프라

- **연결**: Connected (HTTP 원격, project_ref 격리). 인증은 PAT(개인 액세스 토큰) Authorization 헤더 방식(`.mcp.json`).
- **Supabase Edge Functions: 0개** — 달록 AI 백엔드는 Cloudflare Workers(chat-proxy/brief-proxy)에 있고 Supabase Edge 미사용.
- **마이그레이션 이력(supabase_migrations): 0건** — CLI 정식 마이그레이션 체계 미사용(수동/MCP 적용). CLI는 link만 된 상태.
- ⚠ **권위 카운트 정정**: MCP `list_tables` 편의도구는 39개로 적게 보고하나, `pg_tables` 권위 카운트는 **테이블 42 · 함수 31 · 뷰 9**. 누락분은 `coach_account_verify`(테이블)·`coach_account_verify_confirm`(함수) 등 6/11 추가분.

### 7-2. 테이블 (public, 42개 · 전부 RLS 활성)

- **운동·기록 코어**: body_records(146)·running_logs(139)·coach_notes(28)·shoe_configs(5)·exercise_configs(37)·strength_logs(49)·strength_exercises(177)·strength_sets(479)·app_settings(16, (user_id,key) 복합 unique)·run_type_configs(9)
- **소셜**: profiles(4)·follows(0)·posts(10)·post_likes(2)·post_comments(1)·blocks(0)·reports(0)·notifications(0)·profile_publications(5)
- **코치 AI(대화·저장·플랜·구독)**: coach_price_table(4)·coach_request_log(83)·coach_conversation(3)·coach_message(10)·coach_storage_setting(1)·coach_memory_profile(0)·coach_plan_catalog(9)·coach_billing_setting(1)·coach_subscription(3)·coach_entitlement(3)·coach_thread_usage(0)
- **코치 AI(충전식 가격·결제, charge W1~W11/PR#80)**: coach_wallet_account(1)·coach_wallet_ledger(0)·coach_coupon_grant(0)·coach_payment_event(0)·coach_daily_usage(0)·coach_onboarding_boost(0)·coach_onboarding_abuse_log(0)·coach_funnel_event(0)
- **운영·동의**: support_ticket(1)·admin_action_log(1)·user_consent(5)

### 7-3. 뷰(9)·함수(31)·트리거(12)·익스텐션

- **뷰 9**: coach_cost_daily · coach_cost_fortnight · coach_cost_monthly · coach_cost_weekly_in_month · coach_onboarding_abuse_summary · coach_prepaid_reconcile_view · coach_request_log_cost_audit_view · coach_revenue_view · strength_daily_summary
- **함수 31(요지)**: 지갑/원장(charge·charge_admin·refund·set_options·auto_recharge_check·ledger_no_mutate) / 구독·자격·차감(ensure_subscription·get_entitlement·change_plan·cancel_subscription·reset_cycle_if_due·resolve_monthly_cap·consume_turn 2종 오버로드·surcharge_weight) / 온보딩·쿠폰(onboarding_consume·coupon_grant_admin·coupon_expire_due) / 저장·보관함(enforce_archive_limit·restore/soft_delete/hard_delete_conversation·touch·usage_summary) / 단가(get_active_coach_price) / 소셜 트리거(handle_follow·handle_post_comment·handle_post_like) / 공통(is_master·set_updated_at)
- **트리거 12**: 보관함 한도·메시지 touch·원장 불변 강제(block_delete/update)·소셜 카운트/알림·updated_at 류
- **익스텐션(설치본)**: pgcrypto · plpgsql · uuid-ossp · supabase_vault · pg_stat_statements

### 7-4. RLS 정책 요지

- 거의 전 테이블이 `authenticated` **본인 데이터 한정**(SELECT/INSERT/UPDATE/DELETE 분리).
- **공개 SELECT(anon 포함)**: posts · profiles(공개/본인) · profile_publications(공개/본인).
- 단가·플랜·결제설정표: authenticated SELECT만(쓰기는 RPC·service_role).
- 원장·일사용량·결제이벤트·쿠폰: SELECT 본인만, 쓰기는 DEFINER RPC/service_role.

---

## 8. git ↔ DB 전수 정밀 대조 결과 (사장님 "100% 전수" 지시)

**판정: 구조·버전 일치 PASS. 운영/보안 점검 6건(F1~F6) 적출 — 본 작업에서 보강 완료.**

- **8-1. 객체 집합 양방향 일치(PASS)** — git 파일(migrations 18 + golden_set 8)이 선언하는 객체 = 라이브 DB 객체. **테이블 42=42 / 함수 31=31 / 뷰 9=9 / 트리거 10정의(=12이벤트)**. 고아(DB에만)·미적용(git에만) **0건**. 6/11 직접수정분(account_verify·boost_acctv_fix·wallet_42702_fix)도 파일로 보존 → "MCP 변경=파일 동반" 규칙 실작동 확인.
- **8-2. 함수 버전 드리프트 검사(PASS — W3 UNIT7 재발 없음)** — 라이브 함수가 최신 버전임을 구별표식으로 실측: change_plan billing 가드 有 / charge·charge_admin·refund 42702 한정자수정 有 / refund 계좌인증 가드 有 / onboarding_consume 월 hard cap 가드 有. "함수는 있는데 옛 버전" 0건.
- **8-4. 정합 확인(PASS)** — 시드(tier1 5,900/tier2 9,900/hidden 24,900/free 일2·방10·월캡60/단발 150/tier3 은퇴/owner 무제한/beta·vip NULL)가 가격정책과 일치. `coach_account_verify` RLS ON·정책0=클라 전면차단(워커 전용, 1원 인증코드 비노출). RLS 0정책 3테이블(admin_action_log·coach_account_verify·coach_onboarding_abuse_log) 전부 의도된 service_role 전용. 사고성 RLS 공백 0.

---

## 9. Codex 검토 반영 결과

> [[feedback_codex_review_handling]] 원칙대로 보안 민감 작업이라 Codex 교차검수를 거쳤다.

- **사전 공유** — 사장님 지시 원문(적출 F1~F6 즉시 수정)·작업 예정사항(REVOKE/ALTER VIEW/UPDATE 범위)·기존 보호 기준(정상 충전경로·authenticated RPC 보존)을 Codex에 공유.
- **검수 요약** — Codex 교차검수 결과 **GO / 즉시반영**. F1~F6 보강 SQL이 사용자 지시 누락 없이 최소 범위로 정확히 작성됐고, 정상 충전경로·authenticated 권한 회귀가 없음을 확인.
- **검수 루프** — 1회(GO, 추가 지적 없음).
- **반영 결과** — Codex GO 후 **MCP 직접 적용 → 권한·advisor 재검증** 완료(§6). [즉시반영]만 발생, [보류]·[반려] 없음. F4는 별도 시스템 작업으로 분류(축소·미루기가 아니라 성격이 다른 별도 빌드).

---

## 10. 미해결·후속 (advisor 잔여 WARN)

본 작업 범위 밖이라 보고만 한 advisor 잔여 항목 — 후속 보안 하드닝 후보다.

- `handle_*`·`is_master`(트리거/유틸 anon, 무해) — 정리 검토 가능
- `set_updated_at` search_path 미설정 — 권장 설정
- `avatars` public 버킷 broad SELECT(목록 노출 가능) — 정책 조임 검토
- 유출 비밀번호 보호(HaveIBeenPwned) 비활성 — 활성 검토
- **F4 자동 원가캐치·적용 시스템** — 별도 빌드(사장님 확정, [[project_auto_cost_catch_system]])

---

## 11. 1번→2번 공유노트 (§7-1 Codex 협업 조항 보강)

같은 PR에 포함된 부가 산출물이다. 1번 파트 D 표 작성 시 기준으로 삼은 본레포 측 템플릿 미러(`claude_code_prompt_generator_v3.html`)의 `COMMON_WORK`에 **§7-1 "Codex 협업 검토 및 보고 원칙"이 통째로 누락**돼 있었다(§7 다음 바로 §8). 사장님 확인 결과 이는 **본레포 ↔ 툴레포(달록KB) 커밋 과정의 이격**이었다.

- **1번 조치** — §7-1 원문(전문)을 영구 메모리 `reference_permanent_work_principles.md`에 §7 뒤로 등록·MEMORY.md 색인 갱신, 파트 D 표를 §7-1 행 포함 정정본으로 갱신.
- **SoT 정정 플래그(4번 대상)** — 본레포는 CLAUDE.md 역할 구분상 툴레포를 직접 수정하지 않으므로, 툴레포 SoT HTML의 `COMMON_WORK`에 §7-1 삽입은 4번(툴레포 CC)이 처리. [[reference_permanent_work_principles]].

---

## 12. 변경 파일 (PR#84, 5파일 278+)

- `migrations/2026-06-11_audit_security_hardening.sql`(신규 45줄) — F1~F6 보강 SQL
- `src/lib/coachChat/wallet.ts`(+3) — `chargeWallet()` @deprecated 주석
- `docs/go_work/260611_DB현황스냅샷_1번작업.md`(신규) — DB 스냅샷 + 전수대조 §8
- `docs/go_work/260611_1번→2번_공유노트_§7-1보강.md`(신규) — §7-1 보강·이격 고지
- `SQL_등록대장.md`(+3) — DEPRECATED 동결 배너
- (선행 `559de4f`) `scripts/audit_charge_db.cjs`(신규 158줄) — 충전식 30런 전수 감사 스크립트(94항목)

---

## 작업 리드타임

- 시작: 2026-06-11 13:25(KST, 선행 감사 스크립트 커밋)
- 완료: 2026-06-11 15:43(KST, PR#84 보강 커밋)
- 경과: 약 2시간 18분 (감사 → 적출 → Codex 검수 GO → MCP 직접 적용 → 재검증 → 문서화 일괄)

---
