1# Korean Data MCP Hub — Deployment Guide
2
35월 30-31일 라이브 직전 배포 작업 = 이 가이드대로 진행.
4한 번에 끝까지 박는 게 목표 = 각 Step 검증 OK 받고 다음으로.
5
6`[TODO]` 마크 = 추측 불가 = 실시간 화면 확인 후 박을 부분.
7
8---
9
10## Prerequisites
11
12- [ ] Apify 계정 = `teddyan35@gmail.com` (로그인 상태)
13- [ ] Supabase 프로젝트 = `aevpcppnmceuxchsyeoz`
14- [ ] 도메인 = `korean-data.com` (Namecheap)
15- [ ] Paddle 계정 = K-Star Hub 공유 (Seller ID `334227`)
16- [ ] OpenAI API 키 = $10+ 잔액
17- [ ] GitHub repo = `teddyan35-create/kstar-mcp` (이미 push 완료)
18- [ ] Apify CLI 설치됨 (`apify --version` 확인. 없으면 `npm install -g apify-cli`)
19- [ ] Supabase CLI 설치됨 (`supabase --version`. 없으면 `brew install supabase/tap/supabase`)
20
21---
22
23## Step 0 — Pending Supabase 마이그레이션 적용
24
25작업 36/37에서 만든 SQL = 아직 미적용.
26
27```bash
28# 부모 레포에서
29cd ~/projects/kstar-automation
30
31# 1. system_config 테이블 (작업 36)
32# 2. mcp-paddle-webhook은 SQL 아님 = 함수 = Step 5에서 deploy
33```
34
35**Supabase Dashboard → SQL Editor → New query** 에 박을 파일:
36- `supabase/migrations/20260519_system_config.sql`
37
38확인 SQL:
39```sql
40SELECT key, value, description FROM system_config ORDER BY key;
41-- 결과 4행: trial_calls_limit=100, trial_per_call_limit=5,
42-- trial_date_window_days=7, trial_cooldown_hours=24
43```
44
45---
46
47## Step 1 — Lovable 랜딩 페이지 도메인 연결
48
491. Lovable 대시보드 → Korean Data MCP Hub 프로젝트
502. **Publish** 또는 **Connect Domain** 클릭
513. 도메인: `korean-data.com` 입력
524. Lovable이 표시하는 CNAME/A record 값 = 화면 캡처
53 - `[TODO]` Lovable 안내 화면대로 정확한 record 타입/값 확인
545. Namecheap → Domain List → korean-data.com → **Advanced DNS**
556. `[TODO]` Lovable이 표시한 record 그대로 박기 (보통 CNAME `www` → `xxx.lovable.app`, A record `@` → Lovable IP)
567. DNS 전파 = 5-30분 대기. 확인:
57 ```bash
58 dig korean-data.com +short
59 dig www.korean-data.com +short
60 ```
618. `https://korean-data.com` 브라우저 접속 = 랜딩 표시 + HTTPS 자물쇠 ✅
629. 페이지 내 검증:
63 - [ ] Sign up 버튼 → 회원가입 폼
64 - [ ] Pricing 섹션 = Starter $29 / Pro $79 / Business $199
65 - [ ] Docs / Status 링크 작동
66
67---
68
69## Step 2 — Apify Actor 빌드 + 배포
70
71```bash
72cd ~/projects/kstar-automation/mcp-server
73
74# 1. 로컬 build 확인 (사전 sanity)
75npm run build
76# → dist/index.js, dist/actor_entry.js 둘 다 생성 확인
77
78# 2. Apify 로그인 (한 번만)
79apify login
80# 브라우저 인증 → teddyan35@gmail.com
81
82# 3. Actor 배포
83apify push
84# = .actor/actor.json + Dockerfile + 전체 소스 업로드
85# = Apify 클라우드에서 Docker build 실행 (apify/actor-node:20 base)
86# = 빌드 로그 = 터미널에 스트리밍
87```
88
89빌드 실패 시 = 로그에서 에러 확인. 흔한 원인:
90- `npm ci` 실패 = `package-lock.json` 누락/손상 → 로컬에서 `npm install` 후 재push
91- `npm run build` 실패 = TS 에러 → 로컬 `npm run build` clean 확인 후 재push
92
93배포 성공 → Apify Console:
94- Actor 이름: `korean-data-mcp-hub`
95- URL: `https://console.apify.com/actors/[ACTOR_ID]`
96- `[TODO]` Actor 공개 설정 = "Public" 토글 ON (Store 노출)
97
98---
99
100## Step 3 — Apify Actor 환경변수 설정
101
102Apify Console → korean-data-mcp-hub → **Settings → Environment variables** → 다음 7개 박기:
103
104
105|---|---|---|
106| `SUPABASE_URL` | `https://aevpcppnmceuxchsyeoz.supabase.co` | No |
107| `SUPABASE_ANON_KEY` | Supabase Dashboard → Settings → API → `anon public` | Yes |
108| `SUPABASE_SERVICE_ROLE_KEY` | Supabase Dashboard → Settings → API → `service_role` (= 작업 38 captureError 컨텍스트 보강용 / quota RPC는 anon key 안 됨 → service role 필요) | Yes |
109| `OPENAI_API_KEY` | OpenAI dashboard. `sk-...` | Yes |
110| `SENTRY_DSN` | `[TODO]` Sentry 프로젝트 생성 후 DSN 복사 | Yes |
111| `DISCORD_WEBHOOK_URL` | `[TODO]` Discord 서버 → 채널 → Integrations → Webhooks → New | Yes |
112| `NODE_ENV` | `production` | No |
113
114⚠️ `MCP_USER_API_KEY`는 **박지 마라** — Actor input에서 사용자별 API key를 받아 entrypoint(`src/actor_entry.ts`)가 동적으로 set.
115
116---
117
118## Step 4 — Supabase Edge Function 배포 (mcp-paddle-webhook)
119
120```bash
121cd ~/projects/kstar-automation
122
123# 1. Supabase CLI 프로젝트 연결 (한 번만)
124supabase login
125supabase link --project-ref aevpcppnmceuxchsyeoz
126
127# 2. 함수 deploy (2개)
128# = --no-verify-jwt = Paddle / 브라우저는 JWT 안 보냄 = HMAC 또는 Paddle API로 인증
129supabase functions deploy mcp-paddle-webhook --no-verify-jwt
130supabase functions deploy get-api-key --no-verify-jwt
131supabase functions deploy signup-trial --no-verify-jwt
132supabase functions deploy top-up --no-verify-jwt
133supabase functions deploy balance-check --no-verify-jwt
134supabase functions deploy auto-topup-trigger --no-verify-jwt
135supabase functions deploy refund-check --no-verify-jwt
136
137# 배포 URL:
138# - mcp-paddle-webhook = Paddle transaction.completed webhook
139# - get-api-key = /welcome 페이지에서 호출 (= top-up 완료 후 key/balance 표시)
140# - signup-trial = 카드 없는 $5 trial credit 발급
141# - top-up = Paddle Hosted Checkout URL 동적 생성
142# - balance-check = AI agent가 본인 balance 자율 확인 (= api_key auth)
143# - auto-topup-trigger = saved-card 자동 충전 (admin-token auth)
144# - refund-check = 환불 정책 자동 분류 (admin-token auth)
145```
146
147`[TODO]` 기존 `paddle-webhook` (K-Star Hub 웹앱 결제) = **건드리지 마라.** 위 명령은 `mcp-paddle-webhook` + `get-api-key` 둘만 deploy.
148
149---
150
151## Step 5 — Supabase Secrets (Edge Function 환경변수)
152
153Paddle MCP product/price 먼저 생성 (Step 6.1) → 받은 price_id를 여기에 박음.
154
155```bash
156# Paddle webhook secret = Paddle 대시보드 → Notifications → endpoint 생성 후 받음
157supabase secrets set MCP_PADDLE_WEBHOOK_SECRET=pdl_ntfset_xxxxx
158
159# Top-up product price IDs = Paddle 대시보드 → Catalog → Products
160supabase secrets set MCP_PADDLE_TOPUP_PRICE_10=pri_xxxxx
161supabase secrets set MCP_PADDLE_TOPUP_PRICE_25=pri_xxxxx
162supabase secrets set MCP_PADDLE_TOPUP_PRICE_50=pri_xxxxx
163supabase secrets set MCP_PADDLE_TOPUP_PRICE_100=pri_xxxxx
164
165# Paddle API key (top-up, balance-check, refund-check, auto-topup-trigger)
166# Paddle 대시보드 → Developer Tools → Authentication → API keys → Create
167supabase secrets set MCP_PADDLE_API_KEY=pdl_apikey_xxxxx
168# sandbox 환경이면 추가:
169# supabase secrets set MCP_PADDLE_ENV=sandbox
170
171# Resend (mcp-paddle-webhook + signup-trial = welcome email 발송)
172# Resend 가입 → API Keys → Create → re_xxx 받음
173supabase secrets set RESEND_API_KEY=re_xxxxx
174# from 주소 = 디폴트 noreply@korean-data.com. 다른 주소 쓰면:
175# supabase secrets set WELCOME_EMAIL_FROM="Korean Data <hi@korean-data.com>"
176
177# Admin tokens (각자 별도 = 권한 분리)
178supabase secrets set REFUND_CHECK_ADMIN_TOKEN=$(openssl rand -hex 16)
179supabase secrets set AUTO_TOPUP_ADMIN_TOKEN=$(openssl rand -hex 16)
180supabase secrets set OPS_REPORT_ADMIN_TOKEN=$(openssl rand -hex 16)
181
182# 확인
183supabase secrets list
184# = 약 10개 표시 (SUPABASE_URL / SERVICE_ROLE_KEY는 자동 주입 = 별도 set 불필요)
185```
186
187⚠️ `MCP_PADDLE_WEBHOOK_SECRET` = K-Star Hub `paddle-webhook`의 secret과 **다름** = MCP endpoint 전용 secret.
188
189⚠️ Resend = `from:` 도메인이 verified 이어야 발송 성공.
190- Resend 대시보드 → **Domains → Add** → `korean-data.com` → DNS records (TXT/CNAME) 표시
191- Namecheap DNS에 박기 → Resend 대시보드에서 "Verify" 클릭 → 5-15분 후 verified
192- `[TODO]` 도메인 인증 미완료 시 = 임시로 `WELCOME_EMAIL_FROM=onboarding@resend.dev` 박으면 Resend test 도메인으로 발송 가능 (개발 검증용)
193
194---
195
196## Step 6 — Paddle 대시보드 설정
197
198### 6.1 MCP product / prices 생성 (v2 — one-time top-ups)
199
200⚠️ 가격 모델 v2 = **pay-per-call prepaid credit**. Paddle subscription 제품 X = one-time payment 제품만.
201
202Paddle Dashboard → **Catalog → Products → New product**:
203- Name: `Korean Data MCP Hub — Credit Top-Up`
204- Tax category: SaaS / Digital service
205
206각 충전 금액마다 **one-time price** 추가:
207- `[TODO]` $10 top-up → price_id 복사 → `MCP_PADDLE_TOPUP_PRICE_10`
208- `[TODO]` $25 top-up → `MCP_PADDLE_TOPUP_PRICE_25`
209- `[TODO]` $50 top-up → `MCP_PADDLE_TOPUP_PRICE_50`
210- `[TODO]` $100 top-up → `MCP_PADDLE_TOPUP_PRICE_100`
211
212⚠️ Trial ($5 free credit) = **Paddle 제품 X** = `signup-trial` Edge Function이 직접 INSERT (= 결제 안 일어남).
213
214⚠️ Enterprise (`$79`/`$199` monthly) = **별도 Paddle subscription products** (= 수동 영업, 자동 결제 안 함). 영업 후 = `is_enterprise=true` + `monthly_quota` 수동 UPDATE.
215
216### 6.1b Paddle Hosted Checkout — v2 = `top-up` Edge Function이 동적 생성
217
218v2 = Top-up 금액 4개 × CTA 4개 ≠ 정적 URL 4개. 대신 = `top-up` Edge Function 호출 = 매 결제마다 Paddle Hosted Checkout URL 동적 생성.
219
220**호출 흐름**:
2211. Lovable `/topup` 페이지 → 금액 버튼 클릭 → POST `https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/top-up`
222 ```json
223 {
224 "customer_id": "<existing paddle customer_id OR trial_xxx synthetic>",
225 "amount_usd": 10,
226 "email": "user@example.com",
227 "k_star_user_id": "<K-Star UUID>",
228 "ref": "<channel>",
229 "save_payment_method": true
230 }
231 ```
2322. 응답: `{ checkout_url: "https://pay.paddle.io/...", transaction_id, amount_usd }`
2333. Frontend = `window.location = checkout_url` (= 사용자 Paddle Hosted Checkout 으로 redirect)
2344. 결제 완료 → Paddle Success URL = `https://korean-data.com/welcome?transaction_id={transaction_id}` (Paddle이 자동 치환)
2355. `/welcome` 페이지 = `get-api-key` 호출 → API key + 새 balance 표시
236
237⚠️ Trial signup (= 카드 없음) = Paddle 거치지 X = Lovable이 `signup-trial` 직접 호출 = `{ api_key, balance_usd: 5 }` 즉시 받음.
238
239**Success URL + Cancel URL 설정** (Paddle Catalog → Product 상단):
240- Success URL: `https://korean-data.com/welcome?transaction_id={transaction_id}`
241- Cancel URL: `https://korean-data.com/topup?canceled=1`
242
243### 6.1c Lovable 랜딩 = Pricing v2 CTA + 트래픽 출처 추적
244
245**Pricing 페이지 (v2)** — 단일 메인 카드 = pay-per-call:
246- **$0.005 per call** | **$10 minimum top-up** | **$5 free trial credit** | **No card for trial**
247- CTA 메인: `[Start free (no card)]` → POST `signup-trial` (= Trial credit 즉시 발급)
248- CTA 보조: `[Top up $10]` → POST `top-up` (= Paddle Hosted Checkout)
249
250작은 글씨 = 엔터프라이즈 안내:
251- "Need 20,000+ calls/month? See enterprise plans →" → `/pricing/enterprise`
252
253⚠️ 임시 placeholder (`#`, `/signup`, 기존 subscription CTA 전부) = 제거.
254
255**트래픽 출처 (referrer) 추적**:
256
257각 채널별 = Paddle Checkout URL에 `?ref=<source>` 박음:
258
259
260|---|---|---|
261| Hacker News | `hn` | `…?custom_data[ref]=hn` |
262| Reddit r/ClaudeAI | `reddit_claude` | … |
263| Reddit r/mcp | `reddit_mcp` | … |
264| X (Twitter) | `x` | … |
265| Reddit Ads | `ad_reddit` | … |
266| korean-data.com Playground 위젯 | `playground` | … |
267| 직접 방문 / 출처 미상 | `direct` | … |
268
269**구현 옵션 (Lovable 측)**:
270- A. **각 채널별 별도 Paddle 링크 박음** = 가장 단순. 6-7개 링크 메모.
271- B. **공유 한 개 링크 + URL에 ref 동적 박음** = Lovable JS가 `document.referrer` 또는 `?utm_source=` 파싱해서 자동 부착. 더 정밀.
272
273`[TODO]` 어느 방식으로 박을지 = 라이브 직전 결정. 추천 = B (UTM 표준 + 동적).
274
275저장 위치 = `mcp_subscriptions.referrer` (자동 by mcp-paddle-webhook). 매일 `ops_daily_signups` view에서 referrer GROUP BY로 확인 = 어디서 결제됐는지 즉시 박힘.
276
277### 6.1d `/welcome` 페이지 = API key 표시
278
279Lovable에 `/welcome` 라우트 신규 추가:
2801. URL 파라미터 `transaction_id` 읽기
2812. POST `https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/get-api-key`
282 ```json
283 { "transaction_id": "<from URL>" }
284 ```
2853. 응답 `{ ok: true, api_key, status, tier }` 받음 → 화면에 박음
2864. 응답 `404` ("subscription not found … retry") → 3초 후 재시도 (webhook 지연)
2875. 응답 `401` ("transaction not verified") → "결제 정보 확인 실패" 메시지
288
289`[TODO]` Lovable에서 페이지 컴포넌트 작성. fetch 호출 예시:
290```js
291const res = await fetch("https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/get-api-key", {
292 method: "POST",
293 headers: { "Content-Type": "application/json" },
294 body: JSON.stringify({ transaction_id: new URLSearchParams(location.search).get("transaction_id") }),
295});
296const data = await res.json();
297```
298
299⚠️ 백업 경로 = Resend welcome email (Step 5). `/welcome` 못 보거나 새로고침 시 = 이메일로도 키 받음.
300
301### 6.2 Webhook endpoint 등록
302
303Paddle Dashboard → **Developer Tools → Notifications → New endpoint**:
304- URL: `https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/mcp-paddle-webhook`
305- Description: `MCP Hub subscriptions`
306- Events (5개 체크):
307 - `subscription.created`
308 - `subscription.activated`
309 - `subscription.updated`
310 - `subscription.canceled`
311 - `subscription.past_due` (선택적으로 `transaction.payment_failed` 추가)
312- **Save** → 생성된 endpoint secret 복사 → Step 5의 `MCP_PADDLE_WEBHOOK_SECRET`에 박기 (안 박았으면 지금)
313
314기존 `paddle-webhook` endpoint (K-Star Hub 웹앱용) = 별도 row로 그대로 유지.
315
316### 6.3 Seller 인증
317
318`[TODO]` Paddle Seller 인증 = 본인 신원증 + 사업자 정보 입력 = 별도 절차 (Paddle 대시보드 안내). 인증 완료 전엔 실제 결제 처리 안 됨.
319
320### 6.4 Lovable trust badges + Footer legal links
321
322**Pricing 페이지** = 메인 카드 하단 공통 배지 한 줄로:
323
324> ✓ 24-hour money-back on any top-up · 💳 Paddle MoR · 🇰🇷 Made by Korean dev
325
326**Hero 섹션** (랜딩 페이지 상단):
327
328> 🔒 Secure payments via Paddle · ↩ 24h refund on top-ups · 🎁 $5 free credit on signup
329
330⚠️ "unlimited refund" / "no questions asked forever" 같은 카피 **절대 박지 마라** = 정책 (legal/refund.md) 과 모순. 24h 무조건 환불은 **top-up 단위** = "Trial credit ($5)은 환불 X" 임을 작은 글씨에 박을 것.
331
332**Footer 링크 3개**:
333- `/terms` → `legal/terms.md` 내용 렌더 (또는 Markdown 변환된 페이지)
334- `/privacy` → `legal/privacy.md`
335- `/refund` → `legal/refund.md`
336
337Lovable는 정적 페이지로 markdown 컴포넌트 또는 별도 라우트로 박을 것.
338
339### 6.5 K-Star Hub `/settings/mcp` self-service page (Lovable)
340
341Reduces 1-인 운영 CS 부담 — 사용자가 quota / API key 직접 관리.
342
343**3개 신규 Supabase Edge Function**:
344- `GET /functions/v1/mcp-user-keys` — 구독 요약 + masked api_key
345- `GET /functions/v1/mcp-user-usage` — quota + daily breakdown + 최근 50건
346- `POST /functions/v1/mcp-revoke-key` — api_key 회전
347
348모두 K-Star Supabase JWT 필요 (`Authorization: Bearer <jwt>` 헤더).
349
350```bash
351# 배포 (3개 동시) — JWT 검증 활성화 필수 = --no-verify-jwt 박지 마라
352supabase functions deploy mcp-user-keys
353supabase functions deploy mcp-user-usage
354supabase functions deploy mcp-revoke-key
355```
356
357**K-Star → MCP 매핑**: K-Star JWT의 `auth.users.email` 을 `mcp_subscriptions.email` 과 매치 (= Paddle 결제 시점 동일 이메일 가정). 메일 변경 시나리오는 별도 처리 필요.
358
359**Lovable 페이지 구조** (`/settings/mcp`):
360
3611. **구독 상태 카드** (= `mcp-user-keys` 응답):
362 - tier + status + `monthly_quota`
363 - `trial_ends_at` (trialing) 또는 `current_period_end` (active)
364
3652. **API key 섹션**:
366 - 기본 = masked (`kdmcp_XXXX...abcd`)
367 - "Reveal" 버튼 = 클릭 시 = 알림 "key는 회전 시점에만 표시됩니다 - 잃었으면 Revoke & Regenerate"
368 - "Revoke & Regenerate" 버튼 + 확인 모달:
369 - 모달 본문: "현재 키 즉시 무효화. 새 키 1회만 표시됨. 진행?"
370 - 확인 → POST `mcp-revoke-key` → 응답의 `new_api_key` 를 큰 글씨로 표시 + Copy 버튼 + 경고 "이 화면 닫으면 다시 못 봄"
371 - 동시에 Discord ops 알람 자동 발사
372
3733. **사용량 그래프** (= `mcp-user-usage`):
374 - `usage_pct` = 진행 바 (color: <80%=green, 80-100%=yellow, >100%=red)
375 - `daily_breakdown` = 최근 30일 막대 그래프
376 - `recent_calls` = 최근 50건 테이블 (`tool_name`, `called_at`)
377
3784. **결제 관리** = 기존 K-Star Hub 결제 페이지 링크
379
380`[TODO]` Lovable에서 페이지 컴포넌트 + 3개 fetch 호출 작성. JWT = K-Star Supabase Auth 세션에서 자동 (`supabase.auth.getSession()`).
381
382### 6.6 "Try It Live" public playground (랜딩 페이지)
383
384B2B SaaS API 제품의 신뢰 빌더 — 결제 전에 데이터 직접 만져 보게 함.
385
386**새 Supabase Edge Function** = `public-playground` (parent repo commit에서 생성).
387- GET `?category=<kpop|kdrama|...>&limit=3`
388- 인증 없음 (= 무료, 무로그인)
389- IP 기반 rate limit 5/min/IP (= `check_playground_rate` RPC)
390- 결과 hard cap = 3건
391- **데이터 = 7일 이상 된 것만** (= 실시간 누출 X)
392- `category_confidence` 등 LLM enrichment 응답에서 제거
393
394**배포** (= public endpoint = `--no-verify-jwt`):
395```bash
396# 0. SQL 마이그레이션 (rate-limit 테이블 + RPC) 먼저 적용
397# supabase/migrations/20260520_playground_rate_limit.sql
398
399# 1. Edge Function deploy
400supabase functions deploy public-playground --no-verify-jwt
401
402# 2. IP hash salt (선택적이지만 권장 = IP rainbow table 방어)
403supabase secrets set PLAYGROUND_IP_HASH_SALT=$(openssl rand -hex 16)
404```
405
406**Lovable 랜딩 페이지 작업** (`[TODO]`):
407
408Hero 바로 아래 신규 섹션 "Try It Live (No Signup)":
409
410```
411┌─────────────────────────────────────────────────────────┐
412│ Try It Live [No signup] │
413│ │
414│ Category: [ K-Pop ▾ ] │
415│ │
416│ [ Fetch Latest Samples ] │
417│ │
418│ ┌─────────────────────────────────────────────────┐ │
419│ │ { "samples": [ {...}, {...}, {...} ], ... } │ │
420│ │ (pretty-printed JSON) │ │
421│ └─────────────────────────────────────────────────┘ │
422│ │
423│ Sample data only. Subscribe for real-time + full │
424│ features. │
425└─────────────────────────────────────────────────────────┘
426```
427
428JavaScript (Lovable 컴포넌트):
429```js
430const cat = document.querySelector("#playground-category").value;
431const res = await fetch(
432 `https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/public-playground?category=${cat}&limit=3`,
433);
434if (res.status === 429) {
435 showMessage("Rate limit. Subscribe for unlimited access.");
436} else {
437 const data = await res.json();
438 showCodeBlock(JSON.stringify(data, null, 2));
439}
440```
441
442⚠️ 카테고리 11개 드롭다운 = `kpop / kdrama / kmovie / kbeauty / kfood / kgame / ktech / kstock / ktravel / kwebtoon / ktrends`.
443
444### 6.7 운영자(Teddy) 일일 대시보드
445
446매일 아침 한 번에 = 어제 매출 / 가입 / 사용량 / 이상 패턴 확인. Discord 알람과 별개 = 종합 일일 리포트.
447
448**구성**:
449- 6개 SQL views (parent repo: `supabase/migrations/20260520_ops_views.sql`)
450- 1개 Edge Function (`daily-ops-report`) = 6 views 모두 조회 + JSON 통합
451- 1개 GitHub Actions cron (parent repo: `.github/workflows/daily-report.yml`) = 매일 08:00 KST 자동 Discord 발사
452
453**배포**:
454```bash
455# 0. SQL views 적용 (Supabase Dashboard → SQL Editor)
456# supabase/migrations/20260520_ops_views.sql
457
458# 1. Admin token (refund-check와 별도 = 권한 분리)
459supabase secrets set OPS_REPORT_ADMIN_TOKEN=$(openssl rand -hex 16)
460
461# 2. Function deploy (public 호출 X = --no-verify-jwt + admin-token 헤더로 fail-closed)
462supabase functions deploy daily-ops-report --no-verify-jwt
463```
464
465**수동 확인** (= 매일 아침 30초):
466```bash
467curl -H "X-Admin-Token: <your-token>" \
468 "https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/daily-ops-report" \
469 | jq
470```
471
472**응답 필드**:
473- `signups_last_24h` — 어제 가입자 수 (status별 + tier별 합산)
474- `active_subscribers` — 현재 살아있는 가입자 ({trialing, starter, pro, business})
475- `estimated_mrr_usd` — 이번 달 MRR 추정 (Paddle 데이터 X = mcp_subscriptions만으로 추정)
476- `total_calls_last_24h` + `top_tools_last_24h` (top 5)
477- `anomalies` — 1분 100+ calls 사용자 (봇 감지)
478- `high_usage_users` — quota 사용률 top 10 (upsell 타겟)
479- `near_trial_end` — 트라이얼 24h 내 만료 예정자
480
481**GitHub Actions cron 활성화**:
4821. parent repo (`teddyan35-create/kstar-automation`) → Settings → **Secrets and variables → Actions → New repository secret**
4832. `OPS_REPORT_ADMIN_TOKEN` = 위에서 만든 토큰 (= `supabase secrets`에 박은 값과 동일)
4843. Actions 탭 → **Daily Ops Report** → 첫 수동 발사 (`Run workflow`) → green ✅ 확인
4854. 이후 매일 08:00 KST 자동 = Discord `#alerts` 채널에 markdown 요약 박힘
486
487⚠️ `OPS_REPORT_ADMIN_TOKEN` ≠ `REFUND_CHECK_ADMIN_TOKEN`. 둘 다 별도 발급 권장 = 노출 시 영향 격리.
488
489---
490
491## Step 7 — Registry 6개 등록
492
493### 7.1 Apify Store (자동)
494Step 2에서 `apify push` + Public 토글 ON → 자동 등록.
495`https://apify.com/upstanding_wax/korean-data-mcp-hub` 페이지 확인.
496
497### 7.2 Smithery (자동)
498`smithery.yaml` + `README.md` 이미 push됨.
4991. https://smithery.ai 로그인 (GitHub OAuth)
5002. **Add server** → `teddyan35-create/kstar-mcp` repo 선택
5013. Smithery가 `smithery.yaml` 읽어 자동 빌드
5024. 공개 URL: `https://smithery.ai/server/korean-data-mcp-hub`
503
504### 7.3 Glama (자동)
505`README.md` 기반 크롤링.
5061. https://glama.ai → MCP servers → **Submit**
5072. GitHub repo URL: `https://github.com/teddyan35-create/kstar-mcp`
5083. 자동 메타데이터 추출 + 24-48h 후 게재
509
510### 7.4 Anthropic 공식 MCP Registry (수동 PR)
5111. `https://github.com/modelcontextprotocol/servers` fork
5122. README 또는 등록 디렉토리에 entry 추가
5133. PR body = `.registry/anthropic-pr-body.md` 내용 복붙
5144. 리뷰 ~1주 (사전 푸시 권장)
515
516### 7.5 MCP Compass (수동)
5171. `https://mcphub.io` 또는 `https://mcp-compass.xyz` 의 Submit 폼
5182. 입력값 = `.registry/server-metadata.json` 의 필드 그대로
519
520### 7.6 MCPfinder (수동)
5211. MCPfinder Submit 폼
5222. 입력값 = `.registry/server-metadata.json` 동일
523
524---
525
526## Step 8 — Healthcheck 크론 설정
527
528옵션 A = Apify Scheduler (권장 = 같은 인프라에서 실행):
5291. Apify Console → **Schedules → Create schedule**
5302. Cron expression: `*/15 * * * *` (15분마다)
5313. Actor: 별도 Actor 생성 (`korean-data-healthcheck`) 또는 기존 Actor에 input 분기
532 - `[TODO]` healthcheck 전용 Actor 따로 만들지, 같은 Actor에 mode flag로 분기할지 결정
5334. 환경변수: `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `DISCORD_WEBHOOK_URL`
5345. 실패 시 = 스크립트가 `process.exit(1|2)` + Discord 알람 발화
535
536옵션 B = GitHub Actions cron:
537```yaml
538# .github/workflows/healthcheck.yml (=[TODO] 별도 PR로 박을 것)
539on:
540 schedule:
541 - cron: '*/15 * * * *'
542jobs:
543 health:
544 runs-on: ubuntu-latest
545 steps:
546 - uses: actions/checkout@v4
547 - uses: actions/setup-node@v4
548 with: { node-version: '20' }
549 - run: npm ci
550 - run: npm run healthcheck
551 env:
552 SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
553 SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
554 DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
555```
556
557---
558
559## Step 9 — K-Star Funnel 검증 + End-to-End Live Test
560
561### 9.1 K-Star Hub 웹앱 결제 = 영향 없는지 확인
5621. K-Star Hub 웹앱에서 기존 가입 흐름 1회 실행 (테스트 계정)
5632. Supabase `users` 테이블 = `plan_status=pro` 정상 update 확인
5643. = 기존 `paddle-webhook`이 mcp-paddle-webhook 추가로 영향 받지 않음 검증
565
566### 9.2 MCP Hub 신규 가입 + 결제 = E2E (credit 모델)
567
568#### 9.2.A — Trial signup (카드 없음)
5691. `https://korean-data.com` → "Start free" 클릭 → 이메일 입력 → POST `signup-trial`
5702. 응답 = `{ api_key, balance_usd: 5 }` 즉시 표시
5713. Supabase 확인:
572 ```sql
573 SELECT user_id, status, api_key, credit_balance_usd, total_topup_usd, email_verified
574 FROM mcp_subscriptions
575 ORDER BY created_at DESC LIMIT 1;
576 ```
577 기대값: `status='active'`, `credit_balance_usd=5.0000`, `total_topup_usd=0`, `user_id` starts with `trial_`
5784. 발급된 `api_key`로 Apify Actor 호출. tool 호출 1회 → Supabase 확인:
579 ```sql
580 SELECT credit_balance_usd FROM mcp_subscriptions WHERE api_key='kdmcp_...';
581 ```
582 기대값: `4.9950` (= 5 - 0.005)
5835. `search_news` (같은 query 2회) → 2회차 = `COOLDOWN_ACTIVE` (= trial cooldown 작동)
584
585#### 9.2.B — 첫 top-up (Paddle test card)
5861. 위 trial 사용자 → `/topup` → "$10" 클릭 → POST `top-up`
5872. 응답 = `{ checkout_url }` → window.location 으로 redirect
5883. Paddle Hosted Checkout → test card `4000 0000 0000 0002` 결제 완료
5894. Paddle Success URL → `/welcome?transaction_id=...` 도달
5905. `transaction.completed` webhook → `mcp-paddle-webhook` 처리:
591 ```sql
592 SELECT credit_balance_usd, total_topup_usd, last_topup_at FROM mcp_subscriptions
593 WHERE api_key='kdmcp_...';
594 SELECT amount_usd, is_auto_topup FROM mcp_topups
595 WHERE user_id=(SELECT user_id FROM mcp_subscriptions WHERE api_key='kdmcp_...')
596 ORDER BY created_at DESC LIMIT 1;
597 ```
598 기대: `credit_balance_usd ≈ 14.99` (= 4.99 + 10), `total_topup_usd = 10`, mcp_topups row `amount_usd=10, is_auto_topup=false`
5996. 같은 query 2회 search_news → 두 번 다 통과 (= paid user = cooldown 없음)
600
601#### 9.2.C — Auto-topup (선택 검증)
6021. 위 사용자 → `/settings/mcp` → auto-topup 토글 ON + threshold $5 + amount $10
6032. balance 가 $5 미만으로 떨어지도록 사용 (= 약 1,800 calls)
6043. POST `auto-topup-trigger { customer_id }` 수동 트리거 (또는 sweep 모드)
6054. Paddle saved card charge → 새 `transaction.completed` webhook → balance +$10
6065. ⚠️ Paddle 측 saved card 미저장 시 = `auto_topup` 실패 = Discord alert + 사용자 이메일
607
608### 9.3 Sentry / Discord 알람 작동 확인
609- 의도적 에러 유발 (잘못된 api_key 입력) → Sentry에 captureError 도달
610- `npm run healthcheck` 수동 실행 → 정상 시 stale 알람 없음 / DB 끊고 실행 시 error 알람
611
612### 9.4 Apify Actor timeout 검증
613
614현재 = `defaultRunOptions.timeoutSecs = 3600` (1시간). 라이브 직후 24h 안에 모니터링:
615
616- Apify Console → Actor → **Runs** → 평균 `Duration` 측정
617- Claude Desktop 세션의 실제 평균 길이 = 30분 정도 추정 → 1시간 충분
618- **Duration ≈ 3600s에 도달하는 run이 늘면** = 사용자가 1h 통째로 stdio 점유 = idle disconnect 처리 필요
619 - 단기 대응 = Apify Console run 단위로 timeoutSecs 즉시 raise (재배포 X)
620 - 장기 대응 = Apify Standby Mode + idle stdio timeout 5분 (= 별도 PR)
621- **Duration이 평균 5분 미만이면** = timeoutSecs를 낮춰 비용 절감 가능 (라이브 1주 후 측정)
622
623---
624
625## Step 10 — Marketing 발사
626
627### 10.1 Reddit posts (5/30 미국 동부 오전)
628- `r/ClaudeAI` — 제목: "I built an MCP server for real-time Korean media (K-Pop, K-Drama + 9 more) — 7-day free trial"
629- `r/mcp` — 제목: "Korean Data MCP Hub: 25 tools, 4 languages, cluster-deduped JSON"
630- 각 post 본문 = `[TODO]` 라이브 직전 작성 (README 요약 + 1개 demo screenshot + 가격 + Sign up 링크)
631- 댓글 = 첫 1시간 가장 중요 = 적극 응답
632
633### 10.2 X (Twitter) pinned tweet
634`[TODO]` 본문 + GIF demo 준비. 핵심 카피:
635> Just launched Korean Data MCP Hub — real-time K-pop/K-drama news for AI agents.
636> 25 tools, 4 languages, pre-clustered for 70-80% token savings.
637> 7-day free trial → korean-data.com
638
639### 10.3 Reddit Ads ($500 credit)
6401. https://ads.reddit.com → 새 캠페인
6412. Targeting: r/ClaudeAI, r/cursor, r/LocalLLaMA, r/SaaS, r/sideproject (Tier 1)
6423. Budget: $50/day × 10일 = $500
6434. Creative: 동일 demo GIF + "Real-time Korean media for Claude/Cursor"
6445. Landing: `https://korean-data.com/?utm_source=reddit&utm_campaign=launch`
645
646### 10.4 Product Hunt
647`[TODO]` 5/31 PT 12:01 AM 등록 (Reddit/X 24h 반응 본 후 결정)
648
649---
650
651## Step 11 — Emergency Manual Recovery (1인 운영 서바이벌 키트)
652
653라이브 첫 30일 = webhook 실패 / 사용자 키 분실 / 환불 처리 등 = 다음 SQL 그대로 박으면 해결. **모두 Supabase Dashboard → SQL Editor** 에서 실행.
654
655### 시나리오 1 — Paddle 결제 성공인데 mcp_subscriptions row 없음 (webhook 실패)
656
657```sql
658INSERT INTO mcp_subscriptions (
659 user_id, email, api_key, status, tier,
660 trial_started_at, trial_ends_at, monthly_quota, email_verified
661) VALUES (
662 '<paddle_customer_id>',
663 '<email>',
664 'kdmcp_' || replace(gen_random_uuid()::text, '-', ''),
665 'trialing',
666 '<starter|pro|business>',
667 NOW(),
668 NOW() + INTERVAL '7 days',
669 100,
670 true
671)
672ON CONFLICT (user_id) DO NOTHING
673RETURNING api_key;
674```
675
676Paddle webhook log 먼저 확인 → 재배달 가능하면 = `mcp-paddle-webhook` 자동 처리가 우선. 위 SQL은 webhook이 끝까지 실패할 때만.
677
678### 시나리오 2 — 사용자: "Welcome 페이지를 못 봤어요 / 키를 잃었어요"
679
680```sql
681SELECT api_key, status, tier, api_key_last_viewed_at
682FROM mcp_subscriptions
683WHERE email = '<email>'
684 OR user_id = '<paddle_customer_id>';
685```
686
687⚠️ 평문 키 전달 = 안전한 채널만 (= K-Star `/settings/mcp` 페이지 또는 본인 인증 후 1:1 DM). Slack/Discord 일반 채널 절대 X. 더 안전한 옵션 = 사용자에게 시나리오 3 (회전)으로 안내.
688
689### 시나리오 3 — API key 회전 (사용자 요청 or 키 노출 의심)
690
691```sql
692UPDATE mcp_subscriptions
693SET api_key = 'kdmcp_' || replace(gen_random_uuid()::text, '-', ''),
694 api_key_last_viewed_at = NULL
695WHERE user_id = '<paddle_customer_id>'
696 OR email = '<email>'
697RETURNING api_key;
698```
699
700`api_key_last_viewed_at = NULL` 박는 이유 = `/welcome` 또는 `mcp-revoke-key` 응답에서 `first_view: true` 트리거 = "save this now" UX 다시 발사.
701
702### 시나리오 4 — Subscription 강제 취소 (어뷰즈 대응)
703
704```sql
705UPDATE mcp_subscriptions
706SET status = 'canceled',
707 updated_at = NOW()
708WHERE user_id = '<paddle_customer_id>';
709```
710
711⚠️ 환불 처리는 별도 = `refund-check` Edge Function 호출 → Paddle 대시보드 수동 환불. 위 SQL은 즉시 API 차단만.
712
713### 시나리오 5 — k_star_user_id 수동 매핑 (이메일 변경 사용자)
714
715```sql
716UPDATE mcp_subscriptions
717SET k_star_user_id = '<new_k_star_uuid>'
718WHERE email = '<old_email>'
719 AND k_star_user_id IS NULL;
720```
721
722K-Star 사용자가 이메일 변경 후 `/settings/mcp` 매핑 깨질 때.
723
724### 시나리오 6 — Credit 수동 부여 (CS 보상 / 프로모션)
725
726```sql
727UPDATE mcp_subscriptions
728SET credit_balance_usd = credit_balance_usd + 5.00,
729 updated_at = NOW()
730WHERE email = '<email>'
731RETURNING credit_balance_usd;
732```
733
734⚠️ `total_topup_usd` 는 안 건드림 = "유료 사용자" 카운트와 다른 분류 유지.
735
736### 시나리오 7 — 환불 후 Credit 차감
737
738```sql
739UPDATE mcp_subscriptions
740SET credit_balance_usd = GREATEST(0, credit_balance_usd - <refunded_amount_usd>),
741 total_topup_usd = GREATEST(0, total_topup_usd - <refunded_amount_usd>),
742 updated_at = NOW()
743WHERE user_id = '<paddle_customer_id>';
744```
745
746Paddle 대시보드에서 환불 실행 후 = 우리 DB에서도 차감. `refund-check` 함수가 추천하는 금액 사용.
747
748---
749
750## Step 12 — Lovable: `/topup` + `/pricing/enterprise` 페이지
751
752가격 모델 v2 = pricing 페이지 + 두 페이지 신규.
753
754### 12.1 `/topup` 페이지
755
756레이아웃:
757```
758┌──────────────────────────────────────────────────┐
759│ Top up your credit │
760│ │
761│ Current balance: $4.97 (~994 calls) │
762│ │
763│ [ $10 ] [ $25 ] [ $50 ] [ $100 ] │
764│ 2,000 5,000 10,000 20,000 │
765│ calls calls calls calls │
766│ │
767│ ☐ Enable auto top-up (saves payment method) │
768│ │
769│ [ Continue to checkout ] │
770└──────────────────────────────────────────────────┘
771```
772
773JS:
774```js
775const res = await fetch("https://aevpcppnmceuxchsyeoz.supabase.co/functions/v1/top-up", {
776 method: "POST",
777 headers: { "Content-Type": "application/json" },
778 body: JSON.stringify({
779 customer_id: currentSession.paddleCustomerId,
780 amount_usd: selectedAmount,
781 email: currentSession.email,
782 k_star_user_id: currentSession.kStarUserId,
783 ref: getQueryParam("ref") || "direct",
784 save_payment_method: autoTopupCheckbox.checked,
785 }),
786});
787const { checkout_url } = await res.json();
788window.location.href = checkout_url;
789```
790
791⚠️ `save_payment_method: true` = 자동 충전 가입 = Paddle Vault에 카드 저장. 사용자에게 체크박스로 명시 동의 받아야 함.
792
793### 12.2 `/pricing/enterprise` 페이지
794
795레이아웃:
796```
797┌──────────────────────────────────────────────────┐
798│ Enterprise Plans (flat-rate monthly) │
799│ │
800│ Enterprise Pro Enterprise Business │
801│ $79/month $199/month │
802│ 20,000 calls/mo 50,000 calls/mo │
803│ Volume discount SLA + custom limits │
804│ │
805│ [ Contact sales ] │
806│ │
807│ For higher volume: enterprise@korean-data.com │
808└──────────────────────────────────────────────────┘
809```
810
811CTA = 폼 또는 mailto 링크 (= Paddle 자동 결제 안 함 = 수동 영업). 문의 접수 후 = 직접 Paddle Subscription 만들고 `is_enterprise = true`, `monthly_quota = 20000/50000` 수동 UPDATE.
812
813### 12.3 `/welcome` 페이지 (= top-up 완료 도착 페이지) 갱신
814
815기존 `/welcome` (작업 46) = `transaction_id` → `get-api-key` 호출. v2에서 응답 형식 확장됨:
816```js
817const data = await res.json();
818// data.api_key, data.first_view
819// v2 추가: data.credit_balance_usd, data.total_topup_usd, data.is_trial
820showBalance(data.credit_balance_usd);
821if (data.first_view) showSaveKeyWarning(data.api_key);
822```
823
824`first_view=true` 시 = "이 화면 닫으면 키 다시 못 봐. 지금 저장해." UX 강조.
825
826### 12.4 `/settings/mcp` 페이지 갱신 (= 작업 49 → v2)
827
828기존 섹션 + 신규:
829
830
831|---|---|---|
832| 구독 상태 | GET `mcp-user-keys` | tier (= `null` 이면 "Pay-per-call"), status, masked api_key |
833| **잔액** (신규) | GET `balance-check?api_key=<masked키X = full session 가져온 key>` | balance_usd, approx_calls_remaining, is_trial |
834| 사용량 | GET `mcp-user-usage` | usage_pct, daily_breakdown, recent_calls |
835| **충전 이력** (신규) | direct query `mcp_topups` (또는 K-Star Edge Function 추가) | amount_usd, created_at, is_auto_topup |
836| **자동 충전 설정** (신규) | direct UPDATE via K-Star Edge Function | toggle ON/OFF, threshold input, amount input |
837| API key 회전 | POST `mcp-revoke-key` | 새 키 1회 표시 |
838
839`[TODO]` 자동 충전 토글/threshold/amount UPDATE 용 Edge Function = 별도 추가 필요 (= 현재는 SQL로만 변경 가능). 라이브 후 우선순위 박을 것.
840
841---
842
843## Rollback Plan
844
845### Rollback A — Apify Actor 문제
846Apify Console → Actor → **Builds** → 이전 빌드 → **Set as latest**.
847또는 git revert + `apify push` 다시.
848
849### Rollback B — mcp-paddle-webhook 문제
850즉시:
851```bash
852# 함수 일시 비활성화 = Paddle 대시보드에서 webhook endpoint 토글 OFF
853# (코드 deploy 보다 빠름 = Paddle 측에서 송신 중단)
854```
855영구:
856```bash
857# 이전 버전 git checkout 후
858supabase functions deploy mcp-paddle-webhook --no-verify-jwt
859```
860주의: 이미 발생한 결제는 Paddle 대시보드에서 수동 처리 또는 webhook 재발송 트리거.
861
862### Rollback C — 도메인 / 랜딩 문제
863Lovable 대시보드에서 이전 Publish 버전으로 revert.
864또는 Namecheap DNS를 임시 메인터넌스 페이지로 전환.
865
866### Rollback D — Supabase RPC / 쿼터 시스템 문제
867**환경변수 임시 토글**: Apify Actor env에 `QUOTA_BYPASS=1` 추가 → 모든 호출 통과.
868= 안전망 = 결제·인증은 건너뛰지만 라이브 유지 가능 = 그 사이에 root cause 디버깅.
869복구 후 = `QUOTA_BYPASS` env 제거.
870
871### Emergency 정지
8721. Apify Actor → Stop 모든 Run
8732. Paddle webhook endpoint OFF
8743. Discord 알람 채널에 incident 박기
8754. korean-data.com landing → "Temporarily unavailable" 안내 (Lovable에서 페이지 교체)
876
877---
878
879## 최종 Go/No-Go 체크리스트
880
881라이브 직전 (5/30 발사 1시간 전):
882
883- [ ] Step 0 = `system_config` 4행 SELECT 확인
884- [ ] Step 1 = `https://korean-data.com` HTTPS 접속 OK
885- [ ] Step 2 = Apify Actor 빌드 success + Public ON
886- [ ] Step 3 = Apify env 7개 박힘 + `MCP_USER_API_KEY` 없음 확인
887- [ ] Step 4 = mcp-paddle-webhook + get-api-key deploy success
888- [ ] Step 5 = `supabase secrets list` = 6개 (MCP_PADDLE_WEBHOOK_SECRET, MCP_PADDLE_PRICE_*, MCP_PADDLE_API_KEY, RESEND_API_KEY)
889- [ ] Resend 도메인 verified (또는 `WELCOME_EMAIL_FROM=onboarding@resend.dev` 임시)
890- [ ] Lovable `/welcome` 페이지 = get-api-key 호출 동작 확인 (placeholder transaction_id로 401 응답이라도 나면 routing OK)
891- [ ] Step 6 = Paddle endpoint Active + 5 events + Seller 인증 완료
892- [ ] Step 7.1 = Apify Store 페이지 노출
893- [ ] Step 9.2 = E2E test 1회 success (`mcp_subscriptions` 새 row + MCP call 성공)
894- [ ] Step 9.3 = Sentry 1건 도달 / Discord 알람 1건 발사 검증
895- [ ] Healthcheck cron 작동 (Apify Schedules에서 마지막 run timestamp 확인)
896
897전부 ✅면 → Step 10 발사.