Biz Admin 리드 대시보드 — Supabase용 CRM 스타일 /admin
Next.js 프로젝트(App Router)에 1CRM 스타일 /admin 페이지를 생성합니다. 3개의 탭 — 개요(KPI + 차트), 고객(테이블 + 필터), 이메일 마케팅(작성 + 대량 발송 + 기록)으로 구성됩니다.
철학: 1CRM보다 단순합니다(공통 비밀번호 1개, 다중 사용자 없음). 하지만 1-2인 사업자가 사용하기에 충분한 차트, 델타(변화율), 이메일 마케팅 기능을 갖추고 있습니다. 모든 상태는 Supabase에 저장되며, 파일 생성은 기존 프로젝트와 독립적입니다.
/admin → 팝업 "관리자 코드 입력" → 각 탭은 /api/admin/{leads|dashboard|campaigns} 호출
→ 각 요청은 x-admin-pass 헤더 전송
→ 서버에서 ADMIN_PASSWORD 환경 변수와 timing-safe 비교
비밀번호는 React state에 저장됩니다 — 페이지 새로고침 시 팝업이 다시 나타납니다(의도된 동작이며, 간단하고 세션 쿠키가 필요 없습니다).
Supabase만 지원 — KV 변형은 제거되었습니다. 이유: campaigns + campaign_sends는 SQL 조인과 service_role을 통한 RLS 우회가 필요하지만, KV는 이를 기본적으로 지원하지 않습니다.
이 스킬을 사용해야 하는 경우
- 프로젝트에 Sepay 결제 및
lib/leads-supabase.ts를 통한 리드 저장소가 이미 설정되어 있는 경우(biz-setup-sepay-payment에서 Supabase 선택). 뷰 레이어, 대시보드, 이메일 마케팅이 필요합니다. - 사용자가 CRM 스타일 페이지(델타가 있는 KPI 카드, 일별 라인 차트, 전환율 도넛 차트, 등록 고객에게 프로모션 이메일 발송)를 원하는 경우.
사용하지 말아야 하는 경우:
- 프로젝트에
lib/leads-supabase.ts가 없는 경우 → 사용자에게 먼저/biz-setup-sepay-payment(Supabase 선택)를 실행하도록 안내하세요. - 프로젝트가 Supabase 대신 Vercel KV를 사용하는 경우 → 이 스킬은 KV를 더 이상 지원하지 않습니다. 사용자는 Supabase로 마이그레이션하거나 수동으로 포팅해야 합니다.
- 역할/권한이 있는 다중 관리자가 필요한 경우 → 이 스킬은 공통 비밀번호 1개를 사용하므로 적합하지 않습니다.
- 수신자별 상세 이메일 추적(열람/클릭)이 필요한 경우 → 이 스킬은 발송 성공/실패만 추적합니다. 사용자는 Resend/SendGrid/Brevo 등 추적 기능이 있는 대량 발송 서비스가 필요합니다.
워크플로우 (6단계)
Phase 0: Next.js App Router 감지 + lib/leads-supabase.ts 확인 + SMTP 환경 변수 확인
Phase 1: 사용자에게 비밀번호 요청 (기본값 123456) — GATE
Phase 2: npm 종속성 `recharts` 설치
Phase 3: Supabase SQL 편집기에서 supabase-migration-admin.sql 실행 — GATE
Phase 4: ADMIN_PASSWORD를 .env.local에 기록
Phase 5: 9개 파일 생성 (5 lib + 4 route + 1 page)
Phase 6: 3단계 테스트 (curl dashboard, UI smoke, 테스트 캠페인 발송)
Phase 0 — 감지 및 종속성 확인
# Check Next.js + App Router
test -f next.config.js -o -f next.config.mjs -o -f next.config.ts && echo "Next.js: ✓"
test -d app && echo "App Router: ✓" || { echo "✗ App Router가 필요합니다. 이 스킬은 Pages Router를 지원하지 않습니다."; exit 1; }
# Verify Supabase lead store
test -f lib/leads-supabase.ts && echo "Supabase store: ✓" || { echo "✗ lib/leads-supabase.ts가 없습니다 — 먼저 /biz-setup-sepay-payment를 실행하고 Supabase를 선택하세요."; exit 1; }
test -f lib/leads-store.ts && echo "Provider-agnostic wrapper: ✓"
test -f lib/supabase-admin.ts && echo "Service-role client: ✓"
# Verify SMTP env (from biz-email-setup)
grep -E "^SMTP_HOST=" .env.local && echo "SMTP_HOST: ✓" || { echo "✗ SMTP 환경 변수가 없습니다 — 먼저 /biz-email-setup를 실행하세요."; exit 1; }
grep -E "^SMTP_USER=" .env.local
grep -E "^SMTP_PASS=" .env.local
grep -E "^MAIL_FROM=" .env.local
# Verify package
grep '"nodemailer"' package.json && echo "nodemailer: ✓"
grep '"@supabase/supabase-js"' package.json && echo "@supabase: ✓"
lib/leads-supabase.ts가 없는 경우:
⚠ 프로젝트에 Supabase 리드 저장소가 없는 것 같습니다. 먼저
/biz-setup-sepay-payment를 실행하고(Supabase 백엔드 선택) 결제 시스템과leads테이블 및 RLS를 설정하세요. 그런 다음 다시 돌아와서 관리자 대시보드를 생성하겠습니다.
SMTP 환경 변수가 없는 경우:
⚠ SMTP가 구성되지 않은 것 같습니다. 이메일 마케팅 탭은 발송을 위해 SMTP가 필요합니다. 먼저
/biz-email-setup를 실행하여 제공업체(Gmail/Resend SMTP/Brevo/SendGrid/Zoho...)를 선택한 후 다시 돌아와서 관리자를 생성하세요. 이메일 탭 없이 계속 진행할 수도 있습니다 — 확인해 주시겠습니까?
사용자 요약:
감지 결과: Next.js App Router ✓, Supabase store ✓, SMTP 연결 ✓. 다음을 수행합니다:
- /admin 비밀번호 요청
recharts설치(라인 차트 + 도넛 차트용)- Supabase에
campaigns+campaign_sends테이블을 생성하는 SQL 마이그레이션 실행ADMIN_PASSWORD를 .env.local에 기록- 9개 파일 생성 (5 lib + 4 route + 1 page)
- 테스트 안내
Phase 1을 시작할까요?
Phase 1 — 비밀번호 요청 (GATE)
정확히 이렇게 질문하세요:
/admin 페이지에 사용할 비밀번호를 입력해 주세요:
- 기본값:
123456- 또는 프로덕션용 강력한 비밀번호를 입력해 주세요 (12자 이상, 문자 + 숫자 + 특수 문자 포함)
비밀번호를 입력하거나(또는 기본값을 사용하려면 "123456" 입력):
사용자 응답을 기다리세요. 사용자가 붙여넣은 비밀번호를 사용하세요. 비워두면 123456을 사용합니다.
사용자가 비밀번호를 확인할 때까지 Phase 2로 진행하지 마세요.
Phase 2 — recharts 설치
# Detect package manager
test -f pnpm-lock.yaml && PM=pnpm
test -f yarn.lock && PM=yarn
test -z "$PM" && PM=npm
$PM add recharts # 또는: pnpm add recharts / yarn add recharts
nodemailer는 /biz-email-setup에 의해 이미 설치되었습니다. 다시 설치하지 마세요.
Phase 3 — SQL 마이그레이션 실행 (GATE)
사용자에게 안내:
Supabase Dashboard → SQL Editor → New query를 열고 → 아래 파일의 전체 내용을 붙여넣은 후 → Run을 클릭하세요:
[templates/supabase-migration-admin.sql 내용을 여기에 붙여넣으세요]이 SQL은 2개의 새 테이블(
campaigns(캠페인 헤더) +campaign_sends(수신자별 로그))을 생성합니다. 기존leads테이블은 건드리지 않습니다.실행 후 SQL 끝부분에서
campaigns: 0및campaign_sends: 0이 표시되는지 확인하세요.
사용자가 "SQL 실행 완료"를 확인할 때까지 기다린 후 Phase 4로 진행하세요.
Phase 4 — .env.local에 기록
.env.local에 추가:
# Admin /admin page password
ADMIN_PASSWORD=<user-paste-from-phase-1>
ADMIN_PASSWORD가 이미 있는 경우 → 사용자에게 덮어쓸지 물어보세요(기본값: 덮어쓰지 않음).
.env.local이 .gitignore에 포함되어 있는지 확인하세요:
grep -qxF ".env.local" .gitignore 2>/dev/null || echo ".env.local" >> .gitignore
Phase 5 — 9개 파일 생성
lib/admin-auth.ts ← templates/lib-admin-auth.ts
lib/admin-stats.ts ← templates/lib-admin-stats.ts
lib/campaigns.ts ← templates/lib-campaigns.ts
lib/email-render.ts ← templates/lib-email-render.ts
lib/mailer.ts ← templates/lib-mailer.ts
app/admin/page.tsx ← templates/app-admin-page.tsx
app/api/admin/leads/route.ts ← templates/app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← templates/app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← templates/app-api-admin-campaigns-route.ts
이메일 렌더링 모듈: lib/email-render.ts는 순수 모듈입니다(nodemailer를 import하지 않음). textToHtml / wrapEmailHtml / personalize / renderCampaignEmail 함수를 포함합니다. lib/mailer.ts는 이러한 함수를 다시 export하고 SMTP 전송을 추가합니다. 이렇게 분리하면 app/admin/page.tsx(클라이언트 컴포넌트)가 nodemailer를 클라이언트 번들로 가져오지 않고도 미리보기 버튼을 위한 렌더링 헬퍼를 import할 수 있습니다.
biz-email-setup에서 이미 lib/mailer.ts가 있는 경우: 덮어쓰지 마세요 — 이 스킬의 mailer를 lib/admin-mailer.ts로 생성하고 app/api/admin/campaigns/route.ts의 import를 @/lib/admin-mailer로 변경하세요. lib/email-render.ts는 항상 안전합니다(고유 이름, 충돌 없음).
import alias 참고: 모든 템플릿은 @/lib/...를 사용합니다. 프로젝트에 tsconfig.json paths "@/*": ["./*"](Next.js 기본값)가 있는지 확인하세요. 없으면 상대 경로 import로 변경하세요.
lib/leads-store.ts 참고: leads API 라우트는 @/lib/leads-store(biz-setup-sepay-payment의 provider-agnostic wrapper)에서 listLeads를 import합니다. 사용자가 이전에 KV를 설정하고 이름을 변경한 경우 → wrapper가 Supabase를 가리키는지 확인해야 합니다.
데이터 흐름 다이어그램
Browser (admin/page.tsx)
│ fetch (header x-admin-pass)
├──► GET /api/admin/dashboard?days=30 → lib/admin-stats.ts → leads table → { kpi, timeseries, deltas }
├──► GET /api/admin/leads?status=... → lib/leads-store.ts → leads table → { leads, stats }
├──► GET /api/admin/campaigns → lib/campaigns.ts → campaigns table → { campaigns: [...] }
└──► POST /api/admin/campaigns (body에 bodyFormat: 'text' | 'html' 포함)
└─► lib/campaigns.queryAudience(leads.where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (1회 렌더링)
└─► lib/campaigns.createCampaign (campaigns 행)
└─► loop (throttle 200ms):
lib/mailer.personalize(html/text per recipient)
lib/mailer.sendOne(to, subject, html)
lib/campaigns.logCampaignSend (campaign_sends 행)
└─► lib/campaigns.setCampaignStatus (최종 카운트 + sentAt)
return { successCount, failCount }
클라이언트 미리보기 (서버 호출 안 함): admin/page.tsx가 lib/email-render를 직접 import하고,
"미리보기" 버튼이 { html } + personalize(템플릿)을 렌더링하여 <iframe srcDoc sandbox>에 표시합니다.
Phase 6 — 3단계 테스트
테스트 1 — Curl dashboard 엔드포인트
PASS="<phase-1-비밀번호>"
# 잘못된 비밀번호 → 401
curl -i http://localhost:3000/api/admin/dashboard -H "x-admin-pass: wrong"
# 예상: 401, {"error":"invalid_password"}
# 올바른 비밀번호 → 200 (KPI + timeseries)
curl -s http://localhost:3000/api/admin/dashboard?days=7 -H "x-admin-pass: $PASS" | jq '.period, .deltas'
# 예상: period {days, leads, paid, revenue, conversionRate}, deltas {leadsPct, ...}
테스트 2 — 브라우저 UI 수동 테스트
1. pnpm dev (또는 yarn/npm)
2. http://localhost:3000/admin 열기
3. "관리자 페이지" 팝업 표시 → 잘못된 비밀번호 입력 → "잘못된 코드입니다. 다시 시도해 주세요."
4. 올바른 비밀번호 입력 → 대시보드 진입
5. 개요 탭: 4개의 KPI 카드, 라인 차트, 도넛 차트, 7d/30d/90d 기간 선택 pill이 작동하는지 확인
6. 고객 탭: 테이블 + 필터 + CSV 내보내기 확인
7. 이메일 마케팅 탭: 작성 폼 + 기록(처음에는 비어 있음) 확인
8. 이메일 마케팅 탭: 내용 입력 → "👁 미리보기" 클릭 → 이메일 렌더링 모달 표시
(일반 텍스트 ↔ HTML 전환 토글을 변경하여 두 모드 모두 확인)
테스트 3 — 소유자 이메일로 테스트 캠페인 발송
중요: 대상 고객을
last_days= 7로 선택하고, 본문에{{name}}을 포함하세요. 실제 대량 발송 전에 흐름을 확인하기 위해 DB에 있는 단일 리드(소유자의 테스트 리드)에게만 테스트 발송하세요.
1. 이메일 마케팅 탭으로 이동
2. 캠페인 이름: "테스트 발송"
3. 제목: "/admin에서 보내는 테스트 이메일 — 받으셨나요?"
4. 대상: "최근 날짜 기준" → 7일
5. 내용:
안녕하세요 {{name}},
이것은 SMTP가 작동하는지 확인하기 위해 관리자 페이지에서 보내는 테스트 이메일입니다.
받으셨다면 실제 캠페인 발송을 시작하겠습니다.
6. "👁 미리보기" 클릭 → 이메일이 올바르게 표시되는지 확인 → 모달 닫기
7. "지금 보내기" 클릭 → 확인
8. 10-30초 대기 (200ms × 수신자 수)
9. 확인:
- 녹색 배너 "X/X 이메일 발송 완료 (0 오류)"
- 기록 테이블에 새 행 상태 "발송 완료"
- 소유자 받은편지함 확인 → {{name}}이(가) 실제 이름으로 대체된 이메일 확인
최종 출력
✓ Supabase + recharts + 이메일 마케팅을 갖춘 CRM 스타일 /admin을 성공적으로 생성했습니다.
📁 새 파일 (9):
lib/
admin-auth.ts (timing-safe 비밀번호 확인)
admin-stats.ts (KPI + 시계열 + 델타)
campaigns.ts (CRUD + 대상 고객 쿼리)
email-render.ts (순수 함수: textToHtml + wrapEmailHtml + renderCampaignEmail — 서버 및 클라이언트 미리보기에서 공유 사용)
mailer.ts (nodemailer 전송기 + email-render 재-export + 옵트아웃 푸터)
app/admin/page.tsx (1CRM 3-탭 UI — 개요 / 고객 / 이메일 마케팅 — 본문 텍스트/HTML + 미리보기)
app/api/admin/
leads/route.ts (GET 목록 + 필터)
dashboard/route.ts (GET KPI + 시계열)
campaigns/route.ts (GET 기록 + POST 대량 발송)
🗄 Supabase 마이그레이션:
- campaigns + campaign_sends 테이블 (RLS deny-all, service_role 우회)
- templates/supabase-migration-admin.sql 실행 완료 ✓
📦 NPM 종속성:
- recharts (새로 설치)
- nodemailer (biz-email-setup에서 이미 설치)
🔐 환경 변수:
- ADMIN_PASSWORD가 .env.local에 기록됨
- .gitignore에 .env.local 포함됨
- SMTP_* + MAIL_FROM은 biz-email-setup에서 재사용
🧪 테스트:
1. ✓ Curl dashboard 엔드포인트 (테스트 1)
2. ✓ 브라우저 UI 수동 테스트 (테스트 2)
3. ✓ 소유자 이메일로 테스트 캠페인 발송 (테스트 3)
🔧 사용자 할 일:
1. (프로덕션 배포 시) Vercel 환경 변수에 ADMIN_PASSWORD 추가:
vercel env add ADMIN_PASSWORD production
2. 재배포: vercel --prod
3. https://<domain>/admin 열기 → 비밀번호 입력 → 3개 탭 작동 확인
4. 첫 번째 실제 캠페인 발송: 소규모 대상(last_days 7)으로 시작하여
제공업체의 SMTP 속도 제한을 확인한 후 'all'로 확장
템플릿
| 파일 | 설명 |
|---|---|
templates/supabase-migration-admin.sql | campaigns + campaign_sends 테이블 생성 마이그레이션 |
templates/lib-admin-auth.ts | checkAdminPass() timing-safe 비교 |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + 시계열 + 델타 |
templates/lib-campaigns.ts | campaigns CRUD + queryAudience() |
templates/lib-email-render.ts | 순수 모듈 — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (텍스트/HTML). nodemailer를 import하지 않아 클라이언트 컴포넌트에서 미리보기용으로 사용 가능 |
templates/lib-mailer.ts | nodemailer 전송기 + sendOne() + sleep() + email-render의 렌더링 헬퍼 재-export |
templates/app-admin-page.tsx | 사이드바 + 3개 탭 + recharts를 갖춘 1CRM 스타일 페이지. 이메일 탭에는 텍스트/HTML 형식 전환 토글 + 미리보기 버튼(iframe 모달) 포함 |
templates/app-api-admin-leads-route.ts | 리드 테이블 API (listLeads() 호출) |
templates/app-api-admin-dashboard-route.ts | 대시보드 분석 API |
templates/app-api-admin-campaigns-route.ts | GET 목록 + POST 생성 및 발송 |
안티 패턴 (하지 말 것)
- ❌
===로 비밀번호 비교 → 타이밍 공격 위험. 템플릿은lib/admin-auth.ts를 통해crypto.timingSafeEqual을 사용합니다. - ❌ 소스 코드에 비밀번호 하드코딩 → 변경하는 것을 잊어 프로덕션에 테스트 비밀번호를 배포하게 됩니다. 항상
process.env.ADMIN_PASSWORD를 사용하세요. - ❌ URL 쿼리 문자열로 비밀번호 전송 (
?pass=123456) → 브라우저 기록 및 액세스 로그에 노출됩니다.x-admin-pass헤더를 사용하세요. - ❌ 별도 로그인 페이지 + 세션 쿠키 + JWT 생성 → 사용자가 간단한 관리자 페이지만 필요한 경우. 인라인 팝업으로 충분합니다.
- ❌ localStorage에 비밀번호 저장 → JS로 읽을 수 있고 XSS로 유출됩니다. 템플릿은 React state에 저장합니다(새로고침 시 손실, 의도된 동작).
- ❌ 타이밍 공격 확인 건너뛰기 →
crypto.timingSafeEqual대신 단순 비교 사용. 항상lib/admin-auth.ts를 사용하세요.