---
title: "작업보고 — 코치AI 모델단가 완전자동 동기화 시스템 (F4 / coach-price-sync 워커) (PR#85)"
category: "workreport"
document_type: "작업보고"
source_status: "generated"
knowledge_group: "03_history"
priority: "High"
purpose: "코치AI 원가 계산의 기반인 모델 단가(coach_price_table)가 전부 placeholder였던 문제를, 주 1회 크론 워커가 공급사 단가 페이지를 LLM으로 추출해 활성단가와 비교(diff)하고, 안전 임계 내 변동만 자동 적용(새 price_version 행 추가)하며, 이상치·조회실패·신규모델은 보류하고 이메일·관리자화면 2채널로 보고하는 완전자동 동기화 시스템(F4, PR#85)으로 해결한 작업의 전모를 풀 깊이로 복원·기록한다."
read_when: ["모델단가","원가자동캐치","coach-price-sync","크론워커","어드민","최신상태복구"]
updated: "2026-06-11"
work_timestamp: "20260611_163200"
context: "달록본레포CC (D:\\dallog\\dallog_git) — 모델단가 자동 동기화 시스템(F4, PR#85) 작업보고"
source_of_truth: "https://dallog-tools.hansbridge.co.kr/knowledge/"
---

# 작업보고 — 코치AI 모델단가 완전자동 동기화 시스템 (F4 / coach-price-sync 워커) (PR#85)

> **이 문서가 무엇인가 (비개발자용 한 줄 설명)**
> 달록 코치AI를 한 번 돌릴 때 우리가 실제로 부담하는 "원가"는, AI 회사들이 매기는 모델 단가(토큰당 가격)를 그대로 가져와 계산한다. 그런데 그 단가표가 그동안 전부 가짜 임시값(placeholder)이었다. 이 작업은 매주 한 번 자동으로 돌아가는 작은 프로그램(크론=주기적 자동실행 워커)이 구글·오픈AI의 공식 단가 페이지를 읽고, AI(LLM)로 가격을 뽑아내, 우리 단가표와 비교해서 정상 범위의 변동만 자동으로 갱신하고, 이상하거나 위험한 변동은 손대지 않고 사람에게 보고하도록 만든 것이다. 평소엔 아무 말 없이 조용히 돌고, 문제가 있을 때만 이메일과 관리자 화면 두 군데로 알린다.

---

## 1. 무엇을 했나 (요약)

코치AI 단가 자동 동기화 시스템 **F4**를 신설했다. 핵심은 **단가표(coach_price_table)의 placeholder 졸업**이다. 더 이상 사람이 손으로 단가를 찾아 입력하지 않고, 워커가 주 1회 자동으로 점검·갱신한다.

| 구성요소 | 신규/변경 | 역할 |
|---|---|---|
| `workers/coach-price-sync/index.js` | **신규 272줄** | 단가페이지 fetch → LLM 추출 → 활성단가 diff → 안전 판정 → 버전 적용 → 2채널 보고 |
| `workers/coach-price-sync/wrangler.toml` | **신규 37줄** | 크론 트리거(주 1회)·환경변수·시크릿 가이드 |
| `migrations/2026-06-11_coach_price_autosync.sql` | **신규 100줄** | `coach_price_sync_log` 테이블 + `coach_price_apply` RPC(=DB 함수호출) |
| `workers/admin-analytics/index.js` | +26줄 | `GET /price-sync`(현황) + `POST /price-sync/seen`(확인처리) 엔드포인트 |
| `src/lib/admin/analytics.ts` | +83줄 | 프런트에서 단가 동기화 현황 조회·수동 트리거·확인 호출 |
| `src/components/admin/AnalyticsTab.tsx` | +54줄 | 관리자 화면 "단가 동기화" 패널 |

**커밋:** `44ae930` (2026-06-11 16:32 KST), PR#85 — `feat(coach): 모델단가 완전자동 동기화 시스템 (F4)`
**변경 규모:** 6파일, +571줄 / -1줄.

---

## 2. 왜 했나 (배경·문제)

- 직전 정책 작업에서 코치AI 과금 모델이 확정되었고([[project_pricing_charge_policy]]), 차감·충전금 회계의 토대가 **정확한 원가**다.
- 그런데 원가의 기반이 되는 **모델 단가(`coach_price_table`)가 전부 placeholder**였다 — 즉 `estimated_cost`(추정 원가) 계산이 가짜 숫자 위에서 돌고 있었다([[project_auto_cost_catch_system]]).
- 사장님이 2026-06-11 **자동 원가캐치 시스템 별도 구축**을 확정했다. 단가는 새 `price_version` 행을 추가하는 원칙(기존 행을 덮어쓰지 않고 이력 보존)으로 관리한다([[project_auto_cost_catch_system]]).

**문제의 본질**
- AI 공급사 단가는 자주 바뀌지 않지만, 바뀌면 원가가 통째로 틀어진다.
- 사람이 매번 단가 페이지를 확인·입력하는 것은 **선제 노동**이며 누락 위험이 크다.
- 단가 페이지는 HTML 구조(CSS 셀렉터)가 수시로 바뀌어, 셀렉터 스크래핑 방식은 깨지기 쉽다.

---

## 3. 어떻게 해결했나 (설계·동작)

### 3-1. 두 가지 운영 원칙 (사장님 지시)

1. **선제 노동 0** — 평소엔 사람이 아무것도 안 한다. 워커가 알아서 돈다.
2. **완전자동이 기본, 실패·변동 시에만 보고** — 정상 무변동(`ok_nochange`)이면 침묵. 문제가 있을 때만 2채널로 알린다.

### 3-2. 워커 동작 흐름 (크론 주 1회 / 마스터 수동)

```
크론(주1회) 또는 마스터 수동트리거
  └ 모델별 반복(TARGETS)
       1) 단가 출처 페이지 fetch (GET, UI변경 내성 위해 셀렉터 X)
       2) Gemini(LLM)로 입력/출력 단가 추출 + 1차 검증(0·음수·상한 거부)
       3) 활성단가(is_active) 조회 = 기준가(baseline)
       4) 변동·이상치 판정 (적용 "이전"에)
            - 무변동            → 건너뜀(정상)
            - 이상치(±임계 초과) → 보류(held, 미적용) + 사람확인
            - 기준가 조회실패    → 보류(held, 미적용)
            - 신규 모델(기준가 없음) → 보류(held, 1회 사람확인)
            - 임계 내 변동       → coach_price_apply RPC로 자동 적용
  └ 상태 집계(나쁜 순: failed > held > applied > ok_nochange)
  └ 로그 기록(coach_price_sync_log) + 상태≠ok_nochange면 2채널 보고
```

### 3-3. UI 변경 내성 — 셀렉터 대신 LLM 추출

단가 페이지 HTML을 그대로 받아 **Gemini로 가격을 추출**한다(`temperature:0`, JSON 강제). CSS 셀렉터가 아니라 자연어 추출이라, 공급사가 페이지 레이아웃을 바꿔도 잘 깨지지 않는다. 추출 모델은 저렴·빠른 `gemini-2.5-flash`(env `EXTRACT_MODEL`).

**대상 모델(TARGETS)**

| provider | model | 출처 |
|---|---|---|
| gemini | gemini-2.5-flash | ai.google.dev 단가 페이지 |
| gemini | gemini-2.5-flash-lite | ai.google.dev 단가 페이지 |
| openai | gpt-5.4-nano | platform.openai.com / openai.com 단가 페이지 |
| openai | gpt-5.4-mini-2026-03-17 | platform.openai.com / openai.com 단가 페이지 |

### 3-4. DB — 안전 버전적용 RPC와 로그 테이블

**`coach_price_apply` RPC (=DB 함수호출, service_role 전용)**
- 활성단가와 같으면 **`unchanged`**(no-op). 다르면 **기존 활성행 비활성 + 새 `price_version` 행 추가**(불변·되돌리기 가능).
- garbage(쓰레기 값) 차단: `0 이하`·`상한(1000 USD/Mtok) 초과`·`미지원 provider`·필수값 누락 → `invalid_args`.
- 동시성: `pg_advisory_xact_lock`(advisory lock=동시실행 방지 잠금)으로 같은 모델 직렬화 + `coach_price_one_active_idx` 유일 인덱스(활성행 1개 보장)로 **이중 안전**.
- 정규화: 입력/출력 단가 6자리 반올림(float 떨림 방어).
- 권한: PUBLIC·anon·authenticated **전부 REVOKE**, `service_role`만 EXECUTE.

**`coach_price_sync_log` 테이블 (append-only 운영 기록)**
- 컬럼: `run_at`, `trigger_kind`(cron/manual), `status`(ok_nochange/applied/held/failed), `source`, `models_checked`(JSONB), `changes`(적용), `held`(보류), `error`, `alert_email_status`, `alert_seen`.
- RLS 켜되 **정책 없음 = authenticated/anon 전면 차단**. `admin_action_log`와 동일 패턴. service_role과 admin-analytics 워커만 접근. 관리자 화면·이메일 보고의 **단일 출처(SoT)**.

### 3-5. 보고 2채널

| 채널 | 동작 |
|---|---|
| **이메일** | mock 게이트 — 기본 `mock`(미발송, DB 로그가 대체). `PRICE_ALERT_EMAIL_PROVIDER=resend` + `RESEND_API_KEY` 등록 시 실발송. 수신처는 [환경변수 등록됨]. |
| **관리자 화면** | `AnalyticsTab`의 "단가 동기화" 패널. admin-analytics 워커 `GET /price-sync`로 현황 조회, `POST /price-sync/seen`로 확인처리(`alert_seen=true`). |

상태가 `ok_nochange`가 아닐 때만 이메일 제목/본문에 적용·보류·실패 건수를 담아 보낸다.

### 3-6. 트리거·접근 제어

- **크론:** `0 18 * * 1` = 월요일 18:00 UTC(= 화요일 03:00 KST, 한가한 시간). 단가는 자주 안 바뀌어 주간이면 충분. `scheduled` 핸들러는 예외도 로그로 남겨 **조용한 실패** 방지.
- **수동 트리거:** 관리자 "지금 동기화" 버튼 → `fetch` 핸들러. `is_master` RPC를 호출자 토큰으로 평가해 **마스터만** 허용(`master_only` 403). CORS는 운영 도메인 allowlist(dallog.kr 등).

---

## 4. 판단 근거 (왜 자동 적용을 허용했나)

자동으로 라이브 데이터를 바꾸는 것은 보수적으로 가야 하지만, 이 경우 **자율 적용**이 정당하다.

- **피해 반경이 낮다** — `coach_price_table`이 만드는 것은 `estimated_cost`(내부 원가 추정)다. 사용자에게 직접 청구되는 값이 아니라 내부 회계·로깅용이다.
- **되돌리기 가능** — 단가는 **새 `price_version` 행 추가** 방식이라 기존 행이 남는다. 잘못 적용돼도 이전 버전을 다시 활성화하면 복구된다([[project_auto_cost_catch_system]]).
- **위험 변동은 애초에 보류** — 0/비정상·이상치(±40%)·기준가 조회실패·신규 모델은 적용하지 않고 사람 확인으로 넘긴다. "잘못된 단가가 조용히 라이브 적용"되는 경로 자체를 제거했다.

→ 임계 내 정상 변동만 자동 적용, 그 외는 전부 보류. 자율성과 안전성의 균형점.

---

## 5. 결과

- 단가표 placeholder 의존 해소 경로 확보 — 워커 가동 시 활성단가가 실측 단가로 채워진다(신규 모델 1회 사람 확인 후 이후 자동).
- DB 마이그레이션 적용 완료(`coach_price_sync_log` + `coach_price_apply` RPC, service_role 전용·멱등).
- 관리자 화면에 단가 동기화 현황 패널 노출, 마스터 수동 트리거 가능.
- 평시 무개입·실패 시만 보고하는 운영 부담 0 구조 확립.

---

## 6. Codex 검토 반영 결과

**Codex 3-pass GO** — 페일세이프 설계가 검수의 핵심 입력값이었고, 다음을 반영했다.

| 분류 | 항목 | 반영 |
|---|---|---|
| [즉시반영] | 적용 "이전"에 검증·이상치 판정 | runSync가 diff·이상치 판정을 끝낸 뒤에만 RPC 호출. "잘못된 단가 조용히 적용" 경로 제거 |
| [즉시반영] | 0/비정상값 차단 | LLM 추출 단계 + RPC 단계 **이중** 검증(0·음수·상한 1000·결측 거부) |
| [즉시반영] | 동시성 안전 | `pg_advisory_xact_lock` + 활성행 유일 인덱스(`coach_price_one_active_idx`) 이중 안전 |
| [즉시반영] | provider 화이트리스트 | RPC에서 `gemini/openai/anthropic`만 허용, 그 외 `invalid_args` |
| [즉시반영] | 기준가 조회실패·신규모델 보류 | baseline 없으면 이상치 판정 불가 → 보류(held), 미적용 |
| [검토후반영] | float 떨림 방어 | 추출·RPC 양쪽 6자리 반올림(`round6` / `round(...,6)`) |
| [검토후반영] | 조용한 실패 방지 | `scheduled` 핸들러 예외도 `writeLog`로 기록 |

페일세이프 요지 — **임계 내 변동만 자동, 그 외(0/비정상·이상치 ±40%·기준가조회실패·신규모델)는 보류**. 적용은 service_role 전용 RPC 단일 경로로만.

---

## 7. 미해결·후속

- **이메일 실발송 전환 대기** — 현재 `mock`(관리자 화면 DB 로그가 대체). `RESEND_API_KEY` 등록 + provider를 `resend`로 전환하면 실발송(코드 수정 불필요).
- **워커 배포·시크릿 등록** — `cd workers/coach-price-sync && wrangler deploy`. 시크릿 `SUPABASE_SERVICE_ROLE_KEY`·`GEMINI_API_KEY`는 `wrangler secret put`으로만(코드·toml 하드코딩 금지). 클라 측 `VITE_PRICE_SYNC_URL` 등록 필요.
- **신규 모델 첫 시드 1회 확인** — baseline이 없는 모델은 첫 추출값이 보류로 잡힌다. 사람이 1회 확인해 활성단가를 만들면 이후부터 자동(임계 내 변동만).
- **단가 정책 연동** — 확정 과금 모델([[project_pricing_charge_policy]])과 실측 단가가 연결되면 `estimated_cost` 신뢰도가 올라간다.

---

## 8. 관련 메모리·문서

- [[project_auto_cost_catch_system]] — 자동 원가캐치 구축 확정(2026-06-11)·placeholder 단가·새 price_version 행 추가 원칙
- [[project_pricing_charge_policy]] — 과금·차감·충전금 정책(B-라이트 확정)
- [[feedback_codex_review_handling]] — Codex 검수 협업 필수·4분류
- [[reference_studio_publish_worker]] / [[feedback_deploy_autonomy]] — 워커 배포·시크릿 자율 기준

---

## 작업 리드타임

- **시작:** 2026-06-11 (F4 자동 원가캐치 구축 지시 접수)
- **완료:** 2026-06-11 16:32 (KST) — 커밋 `44ae930` / PR#85 머지
- **경과:** 당일 완료(설계·DB·워커·관리자 UI·Codex 3-pass 일괄)

---
