---
title: "작업보고 — 충전식 mock E2E 15/15 + 지갑 RPC 42702(컬럼 모호) 수정 + SQL 등록대장 동기화 + CC 직접실행 전환 (PR#82)"
category: "workreport"
document_type: "작업보고"
source_status: "generated"
knowledge_group: "03_history"
priority: "High"
purpose: "충전식(선불 지갑) 결제 시스템(PR#80·#81)의 정합성을 mock E2E로 전수 검증한 작업보고다. E2E가 RETURNS TABLE의 OUT 변수와 UPDATE 우변 컬럼이 충돌하는 PostgreSQL 42702(ambiguous column) 실버그를 적출했고, charge_admin·charge·refund 세 RPC에 테이블 한정자를 붙여 수정했다. 더불어 SQL 등록대장을 64런으로 동기화하고, 수동 적용에서 누락됐던 W3 UNIT7(42P13) 사건을 계기로 'SQL은 CC가 직접 실행·검증'하는 거버넌스 전환을 기록했다. 충전식 검증 상태·42702 재발 방지·SQL 거버넌스 맥락이 필요할 때 읽는다."
read_when: ["충전식","결제","E2E","지갑RPC","42702","SQL등록대장","CC직접실행","최신상태복구"]
updated: "2026-06-11"
work_timestamp: "20260611_144500"
context: "달록본레포CC (D:\\dallog\\dallog_git) — 충전식 mock E2E 검증·지갑 RPC 버그수정(PR#82) 작업보고"
source_of_truth: "https://dallog-tools.hansbridge.co.kr/knowledge/"
---

# 작업보고 — 충전식 mock E2E 15/15 + 지갑 RPC 42702 수정 + 등록대장 64런 동기화 + CC 직접실행 전환 (PR#82)

> **이 문서가 무엇인가 (비개발자용 한 줄 설명)**
> 달록 코치 AI의 "충전(선불 지갑)" 결제 흐름이 진짜로 돈 계산을 틀리지 않는지, 사람이 손으로 눌러볼 동작들을 **자동 테스트 스크립트(E2E)** 로 한 번에 15가지 단계를 돌려 검증했다. 그 과정에서 **숨어 있던 진짜 DB 버그**(데이터베이스가 "어느 잔액 컬럼을 말하는지 헷갈린다"며 멈추는 오류, 코드명 42702)를 발견해 고쳤고, 앞으로 이런 누락을 막기 위해 **DB 변경은 Claude Code(CC)가 직접 실행·검증**하기로 운영 방식을 바꾼 것까지 기록한 문서다. (E2E = end-to-end, 시작부터 끝까지 전 과정 자동 점검. RPC = DB 안에 든 함수 호출. 멱등 = 같은 요청을 두 번 보내도 한 번만 반영.)

---

## 0. 작업 개요

| 항목 | 내용 |
|---|---|
| 작업명 | 충전식 mock E2E 검증 + 지갑 RPC 42702 수정 + Codex fix 마이그레이션 + 등록대장 64런 동기화 |
| PR | **#82** (merge commit `00decb3`, 2026-06-11 14:45 KST) |
| 흡수 선행 커밋 | `f4de713` (2026-06-11 11:25) — 등록대장 W10 적용완료 + W3 UNIT7 미적용 발견·재적용 + CC 직접실행 전환 기록 |
| 작업 레포 | 달록 본레포 `D:\dallog\dallog_git` |
| 대상 시스템 | 충전식(선불 지갑) 결제 — PR#80·#81에서 구축한 `coach_wallet_*` 계열 |
| 변경 파일 | `migrations/2026-06-11_coach_wallet_42702_fix.sql`(133줄·신규), `scripts/e2e_charge_mock.cjs`(134줄·신규), `docs/sql/golden_set/SQL_등록대장.md`(+3행) |
| 실DB 변경 | 있음 — 42702 수정 마이그레이션을 **CC가 직접 적용·검증** (charge_admin·charge·refund 세 함수 `CREATE OR REPLACE`) |
| 검증 결과 | mock E2E **PASS 15 / FAIL 0** |
| 테스트 계정 | `mster`(=`master@hansbridge.co.kr`, 가짜 시드·공개 테스트 비밀번호 계정. "마스터 관리계정" 아님) |

---

## 1. 무엇을·왜 했나 (배경)

### 1-1. 충전식 시스템의 정합성을 확신할 근거가 없었다

PR#80·#81에서 **충전식(선불 지갑)** 결제 토대를 구축했다. 핵심은 다음과 같다.

- **선수금(prepaid) 분리회계** — 사용자가 충전한 돈은 아직 매출이 아니라 **선수금(받았지만 아직 서비스로 제공 안 한 돈)** 이다. 실제로 코칭을 소비(단발 150원 차감 등)할 때 비로소 **매출로 인식**된다.
- **환불 가드** — 환불은 본인 계좌 1원 인증을 통과한 사용자만, 잔액 범위 내에서만 가능하다.
- **멱등(idempotent) 충전** — 결제 webhook이 같은 결제건을 중복 전송해도 잔액이 두 번 오르지 않아야 한다.

이 토대는 **돈 계산**이므로 "코드가 컴파일된다 / 한두 번 눌러보니 되더라" 수준의 확인으로는 부족하다. 충전→소비→환불→복귀의 **전 사이클이 실제 DB·워커(worker)·RPC를 거쳐 정확한 숫자를 내는지** 한 번에 증명할 회귀 가능한(=언제든 다시 돌릴 수 있는) 검증 수단이 필요했다.

### 1-2. 그래서 mock E2E(A안)를 만들었다

설계자(사장님) 승인을 받은 **A안 시나리오**로 `scripts/e2e_charge_mock.cjs` 하네스(harness, 테스트 골격)를 작성했다. mock(가짜 결제)인 이유는 실제 PG(결제대행) 연동 전이라, 워커의 `/mock-confirm`·`/verify-account` mock 엔드포인트를 태워 **돈을 실제로 움직이지 않고도 전 흐름을 그대로 재현**하기 위해서다.

---

## 2. mock E2E — 무엇을 검증했나 (A안 15단계)

`node scripts/e2e_charge_mock.cjs` 한 번 실행으로 다음 순서를 자동 점검한다. 각 단계는 `✅/❌`로 집계되어 최종 `PASS n / FAIL n`을 출력한다.

| # | 단계 | 검증 포인트 |
|---|---|---|
| 0 | mster 로그인 + **원상태 백업** | 구독·엔타이틀먼트 plan_type·턴 수를 백업(종료 시 복원용) |
| 1 | `billing_phase=billing` 전환 | 결제 기능을 켜는 운영 스위치 |
| 2-a | 계좌 인증 시작(mock 1원 인증코드 발급) | `/verify-account/start` 200 + mock_code 발급 |
| 2-b | **오답 코드 거부** | 코드 `0000` → 400 거부 (가드 작동) |
| 2-c | 정답 코드 인증 성공 | `/verify-account/confirm` 200 + verified=true |
| 2-d | 지갑에 인증 계좌 기록 | `coach_wallet_account.refund_verified=true`·은행명 일치 |
| 3-a | **mock 충전 10,000원** | `/mock-confirm` 200 + ok=true + 잔액 반영 |
| 3-b | **멱등 — 동일 참조 재전송 차단** | 같은 payment_ref 재전송 시 duplicated=true (이중 가산 없음) |
| 3-c | 원장 charge 1행만 기록 | `coach_wallet_ledger` amount 10,000 단일 행(이중 기록 없음) |
| 4-a | **단발 150원 차감** | mster를 free로 임시 전환 후 `coach_consume_turn(single)` → 쿠폰0이라 실충전 150 차감 |
| 4-b | 원장 `deduct_single`(-150) | 매출 인식 차감 행 1건 |
| 4-c | **매출 분리표시** | `coach_revenue_view` 인식매출=150(🅑) / 미사용선수금=9,850(🅐) — 분리회계 정확 |
| 5-a | **전액 환불 9,850원** | `coach_wallet_refund` accepted=true + 잔액=0 |
| 5-b | 잔액 초과 환불 거부 | 1,000원 추가 환불 시 accepted=false·reason=`invalid_amount` |
| 6 | (finally) **원상 복원 + beta 복귀** | mster 구독·턴 원복 + `billing_phase=beta` (예외 발생해도 반드시 실행) |
| 7 | beta 가드 재확인 | beta 복귀 후 mock 충전이 다시 **409 거부**(billing_disabled) 돼야 정상 |
| 8 | `billing_phase=beta` 최종 확인 | 운영 상태가 원래대로 돌아왔는지 |

> **표기 주의** — PR 제목의 "15/15"는 E2E 어서션(assertion, 통과 판정) **PASS 개수**다. 위 표의 행은 라벨 기준이며 일부는 finally/로그(0·6번)라 어서션이 아니다. 어서션은 1·2-a·2-b·2-c·2-d·3-a·3-b·3-c·4-a·4-b·4-c·5-a·5-b·7·8 = **15개**로, 전부 통과해 **15/15**가 되었다.

### 2-1. 안전장치 (실서비스 오염 방지)

- **mster 테스트계정만** 사용한다. 실이용자 데이터를 건드리지 않는다.
- 전 구간 **1회성**으로 돌고, 종료 시 `finally`로 **billing_phase='beta' 복귀 + mster 원상 복원**을 **반드시** 실행한다. 중간에 예외가 터져도 최상위 `catch`에서 한 번 더 `billing_phase=beta`로 강제 복귀시킨다.
- **잔존 흔적(의도된 것)** — `coach_wallet_ledger`의 `mock:` 참조 행(충전·단발·환불)과 `coach_account_verify` 1행은 남는다. 원장(ledger)은 **append-only(추가만 가능, 삭제 안 함) 감사 기록**이라 일부러 지우지 않는다. 즉 이 흔적은 버그가 아니라 회계 무결성 설계의 결과다.

---

## 3. E2E가 적출한 실버그 — PostgreSQL 42702 (ambiguous column)

### 3-1. 문제 — 충전·환불 RPC가 런타임에 멈췄다

E2E를 처음 돌리자 충전·환불 단계에서 RPC가 **42702 (column reference is ambiguous, 컬럼 참조가 모호함)** 오류로 실패했다. 이 버그는 **코드 리뷰나 컴파일로는 드러나지 않았고**, E2E가 실제 DB를 태워봤기 때문에 비로소 적출됐다.

### 3-2. 원인 — RETURNS TABLE의 OUT 변수명과 UPDATE 우변 컬럼명이 충돌

문제 함수들은 `RETURNS TABLE (..., real_balance_krw BIGINT)` 형태다. PostgreSQL plpgsql에서 `RETURNS TABLE`의 컬럼은 **OUT 변수**처럼 함수 스코프에 등록된다. 그런데 함수 본문에서 잔액을 갱신할 때 이렇게 썼다.

```sql
UPDATE public.coach_wallet_account
   SET real_balance_krw = real_balance_krw + p_amount_krw   -- ← 우변 real_balance_krw 가 모호
 WHERE user_id = ...;
```

이때 우변의 `real_balance_krw`가 **(a) 갱신 대상 테이블의 컬럼**인지 **(b) RETURNS TABLE의 OUT 변수**인지 PostgreSQL이 구분하지 못해 42702를 던진다. 좌변(SET 대상)은 테이블 컬럼으로 해석되지만, 우변은 동명의 OUT 변수와 충돌한다.

### 3-3. 해결 — UPDATE 우변에 테이블 한정자 명시 (로직 변경 0)

`migrations/2026-06-11_coach_wallet_42702_fix.sql`에서 세 함수를 `CREATE OR REPLACE`로 다시 정의하며, UPDATE를 **별칭(alias) `w`로 한정**해 우변 모호성을 제거했다.

```sql
UPDATE public.coach_wallet_account w
   SET real_balance_krw = w.real_balance_krw + p_amount_krw, updated_at = now()
 WHERE w.user_id = p_user_id;
```

좌변은 SET 대상이라 한정자 불필요, **우변만 `w.` 한정**해 "테이블 컬럼"임을 못박았다. **비즈니스 로직·가드·반환 형태는 일절 바꾸지 않았다** — 순수하게 모호성만 제거한 외과적(surgical) 수정이다.

### 3-4. 세 함수 모두 동일 잠복 버그였다

| 함수 | 역할 | 수정 |
|---|---|---|
| `coach_wallet_charge_admin` | 결제 webhook 전용 원자 충전(service_role) — 멱등(unique_violation→already_processed) | UPDATE 우변 `w.real_balance_krw` 한정 |
| `coach_wallet_charge` | 사용자 직접 충전 경로 — billing 가드·멱등 유지 | 동일 잠복 버그 선제 수정 |
| `coach_wallet_refund` | 환불(DEFINER, billing+계좌인증 가드, 잔액 범위·음수 방지) | 잔액 차감 우변 `w.real_balance_krw` 한정 |

`charge`는 E2E 시나리오상 직접 안 탔지만 **같은 패턴의 잠복 버그**라 함께 고쳤다(나중에 사용자 직접 충전 경로가 열릴 때 동일 오류 재발 방지).

---

## 4. SQL 등록대장 동기화 + 거버넌스 전환

### 4-1. 등록대장 64런으로 동기화

`docs/sql/golden_set/SQL_등록대장.md`에 이번 결제·지갑 라인업 3건을 추가해 **64런(run)** 으로 맞췄다.

| # | 항목 | 상태 |
|---|---|---|
| 62 | 환불계좌 1원 인증 — verify 테이블 + 지갑 인증 플래그 + 환불 인증가드 | ✅적용(2026-06-11 CC 직접) |
| 63 | 부스트 월캡 가드 + 계좌인증 원자 confirm RPC + admin 함수 anon 회수 | ✅적용(2026-06-11 CC 직접) |
| 64 | **지갑 RPC 42702(컬럼-OUT변수 모호) 수정** — charge_admin·charge·refund | ✅적용(2026-06-11 CC 직접·**E2E 발견 버그**) |

선행 커밋 `f4de713`에서는 #60(change_plan 결제 가드)·#61(charge_admin 원자 충전)을 `⚠적용 대기` → `✅적용(CC 직접)`으로 전환 기록한 바 있다. 즉 #60~#64까지 결제·지갑 라인업 적용 이력이 이 시점에 정리됐다.

### 4-2. 핵심 교훈 — "수동 적용 = 조용한 실패" 위험

`f4de713`이 함께 기록한 사건이 이번 거버넌스 전환의 근거다.

> 사장님 **수동 적용분 중 W3 UNIT 7(#47 change_plan/cancel)이 구함수 반환타입 충돌(42P13)로 실제로는 미적용**이었음을, CC 전수검증으로 뒤늦게 발견했다. 적용한 줄 알았지만 DB에는 반영되지 않은 **조용한 실패(silent failure)** 였다. → 구함수 DROP 후 가드판으로 재적용·DEFINER 전환·GRANT 복구로 수습했다.

이 사건은 **사람이 SQL을 복붙(copy-paste)으로 수동 적용하는 방식**의 구조적 위험을 드러냈다. 에디터가 에러를 흘려보내거나 일부만 실행돼도 "적용했다"는 인식만 남고 DB와 코드가 **조용히 어긋난다**.

### 4-3. 거버넌스 전환 — SQL은 CC가 직접 실행·검증

이에 따라 운영 방식을 다음으로 전환했다(등록대장 메모로 명문화).

- **이후 SQL 적용·검증은 CC가 Supabase MCP / Management API로 직접 수행**한다. 복붙 가이드 방식은 종료한다.
- 가이드 HTML은 **기록·감사용**으로만 남긴다(실행 주체가 아님).
- 적용 후에는 CC가 실제 DB 상태를 조회해 **반영 여부를 검증**한다(적용했다는 선언이 아니라 조회로 증명).

이는 KB 운영지침 [[reference_supabase_direct_sql]](SQL은 CC가 직접 실행·검증)·[[feedback_sql_verify_no_drudgery]](검증을 한 줄씩 복붙시키는 노가다 금지, 검증은 CC 책임)와 정합한다. **이번 42702 버그를 CC가 직접 적용·E2E 재실행으로 검증한 것 자체가 이 전환의 첫 실증 사례**다.

---

## 5. Codex 검토 반영 결과

이번 결제·지갑 라인업은 돈을 다루는 민감 영역이라 Codex 검수 협업을 거쳤고, 지적사항을 마이그레이션에 반영했다. (지침 [[feedback_codex_review_handling]] — 민감할수록 2회+ 검수, 4분류 기록.)

| 분류 | Codex 지적 | 반영 내용 |
|---|---|---|
| [즉시반영] | **부스트 월캡 가드** 누락 — 월간 한도 우회 가능성 | confirm RPC에 월캡 가드 추가 (#63 `coach_boost_acctv_fix`) |
| [즉시반영] | 계좌인증 confirm이 비원자(non-atomic)일 경우 경합 위험 | **원자 confirm RPC**로 묶음 (#63) |
| [즉시반영] | **admin 함수에 anon(익명) 권한**이 남아 외부 호출 가능 | `coach_wallet_charge_admin` 등 admin 함수 **anon 권한 회수**(REVOKE) |
| [검토후반영] | 42702 모호 컬럼 — E2E가 1차 적출, Codex가 charge(미경유 경로)도 동일 잠복 확인 | charge까지 포함해 세 함수 일괄 한정자 수정 (#64) |

> 42702 자체는 **E2E 실행이 1차 발견**했고, Codex 검토가 "직접 안 탄 `coach_wallet_charge`도 같은 패턴"임을 짚어 세 함수 일괄 수정으로 확정했다. 자동 검증(E2E)과 정적 검토(Codex)가 상호 보완해 잠복 버그까지 닫은 사례다.

---

## 6. 결과·검증

- mock E2E **PASS 15 / FAIL 0** — 충전→단발 소비→매출 분리 인식→전액 환불→beta 복귀→가드 재확인 전 사이클 정합 확인.
- 멱등(중복 충전 차단)·매출 분리회계(인식 150 / 선수금 9,850)·환불 가드(인증·잔액 범위)·beta 가드(409 거부) **모두 정확 동작**.
- 42702 실버그 3함수 수정 후 E2E 재실행으로 **회귀 없음 확인**(CC 직접 적용·직접 검증).
- 등록대장 64런 동기화·CC 직접실행 거버넌스 명문화 완료.
- 종료 후 운영 상태는 `billing_phase=beta`로 원복(실서비스 결제는 여전히 OFF, 베타 상태 유지).

---

## 7. 미해결·후속

| 항목 | 내용 |
|---|---|
| 실 PG 연동 | 현재는 mock(`/mock-confirm`·`/verify-account` mock). 실제 PG(PortOne 등) 연동 시 동일 시나리오를 실결제 모드로 재검증 필요. |
| coach_wallet_charge 경로 | 사용자 직접 충전 경로는 이번에 잠복 버그만 선제 수정했고 E2E에서 직접 태우지 않았다. 직접 충전 UI 오픈 시 E2E에 단계 추가 권장. |
| 원장 mock 흔적 | mster 원장에 `mock:` append-only 행이 남음(의도). 운영 통계·정산에서 mock 행 제외 필터가 필요할 수 있음(향후 service_role 집계 설계 시 고려). |
| 단가 placeholder | 단발 150원 등은 정책 확정값이나, 코치 모델 원가 단가표는 별건([[project_auto_cost_catch_system]]) — 자동 원가캐치 시스템 구축 시 연동. |

---

## 8. 핵심 교훈 (요약)

1. **E2E는 코드 리뷰가 못 잡는 실DB 버그를 적출한다.** 42702는 컴파일·정적 리뷰로 안 보였고, 실제 DB를 태우는 E2E가 런타임에 적출했다. 돈을 다루는 흐름은 회귀 가능한 E2E로 전수 검증해야 한다.
2. **수동 SQL 적용은 조용히 실패한다.** W3 UNIT7 미적용(42P13) 사건이 증거다. → **SQL은 CC가 직접 실행하고, 조회로 적용을 검증**한다([[reference_supabase_direct_sql]]·[[feedback_sql_verify_no_drudgery]]).
3. **자동 검증 + 정적 검토의 상호 보완.** E2E(동적)가 42702를 발견하고, Codex(정적)가 미경유 경로의 동일 잠복까지 짚어 일괄 수정으로 닫았다.

---

## 작업 리드타임

| 구분 | 시각(KST) |
|---|---|
| 작업 시작 | 2026-06-11 11:25 (선행 등록대장 정정 `f4de713`) |
| 작업 완료 | 2026-06-11 14:45 (PR#82 merge `00decb3`) |
| 경과 | 약 3시간 20분 |

---
