Keeper Out Platform - SPEC-AND-PLAN v2
작성: 2026-05-06 KST 기반:
FEATURE-MATRIX.md126개 기능,SPEC-AND-PLAN.mdv1, 김푸른 5/6 통화 노트, 현재 repo/Atlas/Vhagar 실측 목적: 기존 MVP 위에 기능을 덧대는 계획이 아니라, 처음부터 누락 없는 기능명세 + API/DB/Graph 아키텍처를 다시 정의한다.
Part 0. BLUF
결론
Graph DB 구현은 가능하다. 단, Neo4j를 트랜잭션 원장으로 쓰면 안 된다. Keeper Out의 정답 구조는 다음이다.
Vercel UI
-> balerion.cloud /api/v2 (인증, 권한, 트랜잭션, 워커, 외부연동)
-> Atlas Postgres (4.54M Store SSOT, 검색/지도/타겟 매장)
-> Keeper OLTP Postgres (리드/정산/설치/콜/캠페인 원장)
-> Vhagar Neo4j (관계 분석, 온톨로지, 퍼널/추천 read model)
Neo4j는 Store, Lead, Agent, Campaign, Installation, Payout, Call의 관계 read model이다. 쓰기 원장은 Postgres에 두고, API 서버가 outbox/worker로 Neo4j에 투영한다.
v1 대비 핵심 보정
| 항목 | v1 | v2 보정 |
|---|---|---|
| 기능 수 | 121 기준 일부 남음 | 김푸른 5/6 통화 포함 126개, 완료 16 / 미구현 110 |
| Store 수 | 110K 전제 | Atlas live stores=4,543,356, Vhagar mirror stores=4,539,474, 과거 Neo4j verify Store=4,541,196 |
| Graph 상태 | Neo4j live 가정 | 현재 Vhagar Neo4j 컨테이너는 내려가 있고 재시동 시 exit 3. 데이터/dump는 존재 |
| balerion 역할 | /api/v2 서버 예정 | 현재는 SMS/Giftishow v1 프록시 중심. /api/v2는 아직 없음 |
| keeper-direct API | balerion 호출 가정 | 현재는 Next API Route가 Supabase와 balerion v1을 직접 호출 |
| DB 역할 | Atlas/Supabase 대략 분리 | Atlas=Store SSOT, Keeper Supabase=현재 OLTP, Vhagar PG=로컬 미러, Neo4j=projection |
| 보안 | role-based 예정 | 현재 hardcoded phone whitelist + 일부 admin mutation 인증 누락 |
v2 작성 중 정리한 문서 불일치
FEATURE-MATRIX.md상단 설명을 126개 기준으로 보정했다.SUPPLEMENTAL-EXTRACTION.md에 김푸른 통화 5개 추가 내역을 반영했다.- v1
SPEC-AND-PLAN.md는 121개 기준 historical 문서로 남기고, v2가 supersede한다는 주석을 추가했다. - v1에는
tiro-transcripts/19건이라고 적혀 있으나 현재 파일은 17개다.
Part 1. Current State Snapshot
1.1 keeper-direct repo
| 항목 | 실측 |
|---|---|
| 브랜치 상태 | 최신 커밋 3f1c6e0 docs: SPEC v1에 Codex v2 작업 지시 추가 로컬 반영 |
| 프레임워크 | Next 16.2.1, React 19.2.4 |
| 빌드 스크립트 | npm run build, npm run lint |
| 현재 API | app/api/keeper-out/* Next Route Handlers |
| v2 API | 아직 없음 |
| rewrite | /bapi/:path* -> https://atlas.balerion.cloud/api/v1/:path*만 존재 |
| env 상태 | 현재 shell에는 Supabase/Balerion/Giftishow env 미주입 |
현재 API route surface:
GET/POST /api/keeper-out/diagnose/request-otp
POST /api/keeper-out/diagnose/verify
POST /api/keeper-out/diagnose
GET /api/keeper-out/kpi
GET /api/keeper-out/leads
GET/PATCH/DELETE/PUT /api/keeper-out/lead/{id}
GET/POST /api/keeper-out/expenses
GET /api/keeper-out/incentives
GET /api/keeper-out/leaderboard
GET/POST /api/keeper-out/visits
GET/POST /api/keeper-out/places
GET/PATCH /api/keeper-out/places/{id}
POST /api/keeper-out/places/{id}/rank-snapshot
GET/POST /api/keeper-out/demo-stores
POST /api/keeper-out/payout/naver-pay
POST /api/keeper-out/notify/sms
GET/POST /api/keeper-out/sms/dispatch
GET/POST /api/keeper-out/catchrank/refresh
POST/GET /api/keeper-out/cron/*
POST /api/keeper-out/admin/auth
GET /api/keeper-out/admin/agents
GET /api/keeper-out/admin/agents/{code}
GET /api/keeper-out/admin/pending
GET /api/keeper-out/admin/fallback
POST /api/keeper-out/admin/leads/{id}/tm-call
POST /api/keeper-out/admin/leads/{id}/dispatch
POST /api/keeper-out/admin/leads/{id}/reject
GET /api/keeper-out/admin/leads/{id}/external
GET/POST /api/keeper-out/admin/dispatch/all-hold
1.2 현재 Supabase 스키마 (local migrations 기준)
Live Supabase는 세션 env/MCP가 없어 직접 조회하지 못했다. 현재 repo migrations 기준 테이블은 다음이다.
| 테이블 | 목적 |
|---|---|
keeper_outbound_agents |
운영원/promoter/tm/installer |
keeper_outbound_leads |
진단 제출부터 설치까지 리드 원장 |
keeper_outbound_funnel_events |
리드 이벤트 타임라인 |
keeper_outbound_payouts |
N페이/리드비/설치비 정산 |
keeper_outbound_places |
catchrank/keeper 자체 매장 마스터 |
keeper_outbound_owners |
점주 단위 통합 |
keeper_outbound_place_keywords |
네이버 키워드 |
keeper_outbound_rank_logs |
순위 스냅샷 |
keeper_outbound_gift_reports |
선물 3종/리포트 캐시 |
keeper_outbound_visit_logs |
현장 방문/녹음/사진 |
keeper_outbound_demo_stores |
현장 시연용 매장 |
keeper_outbound_otp_sessions |
진단 OTP 세션 |
keeper_outbound_fraud_signals |
반칙/부정 신호 |
keeper_outbound_system_state |
all-hold/cap 등 운영 상태 |
keeper_outbound_admin_sessions |
관리자 OTP/세션 |
keeper_outbound_expenses |
경비 |
스키마 드리프트:
- 코드가
scheduled_sms_at,sms_dispatched_at을 사용하지만 migrations에는 해당 컬럼 추가가 보이지 않는다. keeper_outbound_expenses.agent_id는text인데 타입/도메인은 Agent UUID다.- 김푸른 통화에서 나온
agent_submitted,agent_attributed,referral_source,referral_partner_id, field edit audit는 아직 없다. device_age는 text라"0년"저장은 가능하지만 프론트 Q2 옵션에는 아직 없다.
1.3 Atlas AWS / atlas.balerion.cloud
Public API 실측:
| 항목 | 결과 |
|---|---|
GET /healthz |
{"ok": true, "tiro_key_configured": true} |
GET /docs |
200 |
GET /api/v1/store/search?q=봉피양 |
정상 결과 반환 |
| OpenAPI title | outbound-recorder-api |
OpenAPI route surface:
/api/v1/address/building
/api/v1/address/lookup
/api/v1/admin/leads
/api/v1/admin/leads/{lead_id}/phone-reached
/api/v1/admin/timeline
/api/v1/auth/device
/api/v1/diagnose/full
/api/v1/estimates/quick-inquiry
/api/v1/franchises
/api/v1/franchises/{fr_id}
/api/v1/franchises/{fr_id}/activities
/api/v1/lead
/api/v1/recordings
/api/v1/recordings/{rec_id}
/api/v1/recordings/{rec_id}/chat
/api/v1/recordings/{rec_id}/download
/api/v1/recordings/{rec_id}/retry
/api/v1/recordings/{rec_id}/status
/api/v1/store/search
/api/v1/stores
/api/v1/stores/nearby
/api/v1/stores/search
/api/v1/stores/{store_id}/claim
/api/v1/stores/{store_id}/manual-tier
/api/v1/visits
/healthz
SSH/DB 실측:
| 항목 | 결과 |
|---|---|
SSH ubuntu@43.201.89.21 |
성공 |
| Docker | outbound_api, outbound_worker, outbound_db, outbound_redis |
| Postgres DB/user | outbound |
| Tables | stores, leads, diagnoses, claims, recordings, visits, crawl_tasks, franchises, franchise_activities, chat_messages, users, alembic_version |
stores count |
4,543,356 |
crawl_tasks count |
243,710 |
diagnoses count |
55 |
leads count |
9 |
| geo coverage | 4,516,058 rows with lat/lng |
| target stores | 4,533,595 rows is_target=true |
stores 주요 컬럼:
id, name, phone, address, naver_place_id, meta, created_at,
franchise_id, lat, lng, road_addr, jibun_addr, sigungu_cd, bjdong_cd,
building_info, owner_name, contact_phone, name_seq, deficiency_tags,
is_target, last_visited_at, manual_tier, manual_tier_set_at
1.4 balerion.cloud
| 항목 | 결과 |
|---|---|
GET https://balerion.cloud/api/v1/sms/balance |
401. 인증 필요이므로 정상 범주 |
GET https://balerion.cloud/ |
404 |
SSH balerion.cloud |
publickey denied. 현재 세션에서 PM2/router 직접 조회 불가 |
| 현재 사용처 | keeper-direct의 PPurio SMS / Giftishow NPay v1 forward |
현재 문서와 코드 기준 v1 SMS surface:
POST /api/v1/sms/send
POST /api/v1/sms/naverpay
GET /api/v1/sms/balance
/api/v2는 아직 운영 중이라고 볼 근거가 없다. Phase 0에서 새 서버 또는 기존 unified-api router에 명시적으로 추가해야 한다.
1.5 Vhagar / kontology
Hardware/OS 실측:
| 항목 | 값 |
|---|---|
| Hostname | kontology |
| Model | Bosgame BeyondMax Series / BOSGAME M4 Plus 계열 |
| CPU | AMD Ryzen 9 7940HS, 8C/16T |
| RAM | 25GiB total, 14GiB available at check time |
| Disk | 936G root, 698G available |
| OS | Ubuntu 24.04.4 LTS |
| Tailscale | 100.110.33.98, online |
Network 실측:
| 측정 | 결과 |
|---|---|
local -> 100.110.33.98 ping |
avg 0.034ms, same host path |
| balerion.cloud -> Vhagar latency | 미측정. balerion SSH denied |
Docker state:
| 컨테이너 | 상태 |
|---|---|
vhagar-neo4j |
시작 시도 후 Exited (3). 현재 stopped |
vhagar-postgres |
기동 가능, 확인 후 다시 stopped |
Neo4j state:
| 항목 | 값 |
|---|---|
| Image | neo4j:2026.02-community |
| Data dir | /home/tlswk/projects/vhagar/data/neo4j |
| Data size | 20G |
| Dump | neo4j.dump, 2,261,322,036 bytes |
| Historical verify | Store=4,541,196, Brand=6, USES=12,873, LOCATED_IN≈4.5M |
| Current live query | 실패. HTTP/Bolt port closed because container exits |
Neo4j 재가동 블로커:
- 2026-05-02 정상/수동 shutdown 로그가 있다.
- 2026-05-06 재기동 시
Logging config in use직후Neo4j Server shutdown initiated by request, exit code 3. - OOMKilled는 false다.
- Phase 0에서 compose/image/config/data store compatibility를 먼저 수리해야 한다.
Vhagar Postgres state:
| 항목 | 값 |
|---|---|
| DB | outbound |
| Tables | Atlas와 거의 동일 + advising_cache |
local mirror stores |
4,539,474 |
Atlas live stores |
4,543,356 |
| lag estimate | 3,882 rows |
| subscription | atlas_sub=true, but inactive |
| current error | publisher 100.113.6.15:5433 network unreachable |
결론: Vhagar PG는 로컬 미러로 가치가 있지만 현재 CDC source endpoint가 stale하다. Keeper v2의 실시간 source로 쓰면 안 된다.
Part 2. Target Architecture v2
2.1 권장 구조
┌────────────────────────────────────────────────────────────┐
│ Vercel / keeper-direct │
│ - UI only: ops admin, owner admin, diagnose, calculator, PWA │
│ - API 호출은 /api/v2 또는 NEXT_PUBLIC_API_BASE로 통일 │
│ - Next API Route는 Phase 1 이후 compatibility proxy로 축소 │
└───────────────────────┬────────────────────────────────────┘
│ HTTPS
┌───────────────────────▼────────────────────────────────────┐
│ balerion.cloud /api/v2 │
│ - Hono/FastAPI 중 하나로 통일 │
│ - auth/session/RBAC, audit log, rate-limit │
│ - leads/installations/payouts/calls/campaigns CRUD │
│ - PPurio/Giftishow/ChannelTalk/Tiro workers │
│ - Postgres transaction + outbox │
└───────────────┬───────────────────────┬────────────────────┘
│ │
┌───────────────▼──────────────┐ ┌──────▼─────────────────────┐
│ Atlas Postgres │ │ Keeper OLTP Postgres │
│ - 4.54M Store SSOT │ │ - Lead/Agent/Payout/Call │
│ - search/nearby/claim base │ │ - current: Supabase │
│ - store metadata │ │ - target: balerion PG │
└───────────────┬──────────────┘ └──────┬─────────────────────┘
│ outbox/sync │ outbox/sync
┌───────────────▼───────────────────────▼────────────────────┐
│ Vhagar Neo4j │
│ - Store Ontology projection │
│ - Agent 1/2/3 funnel path │
│ - referral/campaign/cross-sell graph │
│ - recommendation and analytical read model │
└────────────────────────────────────────────────────────────┘
2.1.1 Admin route policy
/keeper-out/admin is the current operations admin. It remains the active route for field operations, lead verification, NPay dispatch, expenses, and the existing dashboard. Treat it as a production surface.
/admin is the new owner-only admin. It is for Shin Woosung direct use, fast feedback, and dogfooding. It must not replace or mutate /keeper-out/admin workflows until a later explicit promotion decision. Build it as a separate product surface that calls /api/v2 contracts.
2.2 DB별 역할 분리
| DB | 역할 | 쓰기 주체 | 읽기 주체 | 주의 |
|---|---|---|---|---|
| Atlas Postgres | Store SSOT, 검색, 지도, claim base | Atlas API/ETL | balerion v2, Vercel fallback | 4.54M 규모. heavy query는 사전 인덱스 필수 |
| Keeper Supabase | 현재 keeper-outbound OLTP | Next API 현재 | Next API 현재 | Phase 0-1 유지. 신규 v2는 직접 의존 축소 |
| balerion Postgres | 목표 OLTP 원장 | /api/v2 |
/api/v2, workers |
Phase 1부터 신규 테이블 생성 권장 |
| Vhagar Postgres | Atlas 로컬 mirror/분석 | logical replication | Vhagar ETL | 현재 subscription broken |
| Vhagar Neo4j | graph projection/read model | sync worker | /api/v2/kpi/funnel, analytics |
현재 container restart failure |
2.3 Graph DB 채택 원칙
Neo4j에 넣을 것:
- Store와 Agent/Lead/Call/Installation의 관계
- 에이전트 1 -> 2 -> 3 전환 경로
- referral partner, campaign, QR/virtual number attribution
- store similarity, region density, cross-sell path
- long-term ontology labels: Brand, Device, Package, Contract, SecurityNeed
Neo4j에 넣지 말 것:
- 세션, OTP, 원본 전화번호, 원본 결제 토큰
- payout 원장성 금액의 단독 source
- 사진/녹음/영상 원본
- 실시간 발송 queue의 유일 상태
Part 3. Domain Model v2
3.1 Core entities
| Entity | Primary DB | Graph label | 핵심 필드 |
|---|---|---|---|
| Store | Atlas Postgres | :Store |
store_id, name, naver_place_id, geo, sigungu, category, is_target |
| Lead | Keeper OLTP | :Lead |
status, source, scenario, pain, purchase intent, timestamps |
| Agent | Keeper OLTP | :Agent |
role, phone hash, code, active, level, permissions |
| Owner | Keeper OLTP | :Owner |
phone hash, name, stores |
| ReferralPartner | Keeper OLTP | :ReferralPartner |
type, name, phone hash, region |
| Campaign | Keeper OLTP | :Campaign |
channel, asset, message, audience |
| Quote | Keeper OLTP | :Quote |
package, capex/opex, link, opened_at |
| Call | Keeper OLTP | :Call |
recording_url, stt, duration, outcome, score |
| Installation | Keeper OLTP | :Installation |
schedule, job sheet, exceptions, status |
| Payout | Keeper OLTP | :Payout |
type, amount, status, approved_by, paid_at |
| Expense | Keeper OLTP | optional | category, amount, receipt_url |
| Quest | Keeper OLTP | :Quest |
condition, evidence, reward |
| Device | Keeper/Atlas | :Device |
model, SKU, serial, channel |
3.2 Required lead attribution fields
김푸른 통화 반영으로 agent_1/2/3만으로는 부족하다.
agent_submitted_id -- 실제 입력한 사람
agent_attributed_id -- 성과 귀속 대상
agent_owner_id -- 운영상 lead owner, nullable
source_channel -- diagnose, qr_card, real_estate_referral, channeltalk, ad, manual
referral_partner_id -- 부동산/관리소/제휴처
display_channel -- 한화 노출용 채널
internal_source -- 내부 원천. 한화 화면에는 직접 노출 금지
3.3 Graph relationships
(:Agent)-[:SUBMITTED]->(:Lead)
(:Agent)-[:ATTRIBUTED]->(:Lead)
(:Agent)-[:TM_CALLED {outcome, duration, stt_score, at}]->(:Lead)
(:Agent)-[:INSTALLED {unit_count, at}]->(:Installation)
(:Lead)-[:AT]->(:Store)
(:Lead)-[:HAS_OWNER]->(:Owner)
(:Lead)-[:REFERRED_BY]->(:ReferralPartner)
(:Lead)-[:GENERATED_BY]->(:Campaign)
(:Lead)-[:HAS_QUOTE]->(:Quote)
(:Lead)-[:HAS_CALL]->(:Call)
(:Lead)-[:HAS_PAYOUT]->(:Payout)
(:Lead)-[:CONVERTED_TO]->(:Installation)
(:Store)-[:LOCATED_IN]->(:Sigungu)
(:Store)-[:USES]->(:Brand)
(:Store)-[:SIMILAR_TO]->(:Store)
(:Campaign)-[:SENT_TO]->(:Owner)
(:Agent)-[:COMPLETED]->(:Quest)
(:Installation)-[:USES_DEVICE]->(:Device)
Part 4. Feature Specification Index - 126 Features
FEATURE-MATRIX.md가 상세 문장 SSOT다. v2에서는 구현 모듈과 데이터 소유권을 붙인다.
4.1 Category summary
| Category | Count | Done | Todo | Primary module |
|---|---|---|---|---|
| 대시보드/KPI | 15 | 6 | 9 | dashboard, kpi |
| 게이미피케이션 | 7 | 1 | 6 | gamification |
| 리드 관리 | 14 | 2 | 12 | leads, attribution |
| 견적/계산기 | 6 | 1 | 5 | quotes |
| 데이터 연동/자동화 | 12 | 0 | 12 | sync, calls, workers |
| 설치팀 관리 | 11 | 0 | 11 | installations |
| 진단/설문 앱 | 8 | 4 | 4 | diagnose |
| CRM/파이프라인 | 7 | 0 | 7 | crm, store-ontology |
| 마케팅/캠페인 | 8 | 0 | 8 | campaigns |
| 영업 도구 | 12 | 1 | 11 | field |
| 정산/급여 | 7 | 1 | 6 | payouts, payroll |
| 패키지/가격 | 4 | 0 | 4 | packages |
| 랜딩/온보딩 | 4 | 0 | 4 | landing |
| 장기/탐색적 | 11 | 0 | 11 | long-term |
| Total | 126 | 16 | 110 |
4.2 Exact feature list by module
D. Dashboard/KPI (15)
| ID | Feature | Target |
|---|---|---|
| D01 | 월 목표 프로그레스 | current + v2 KPI |
| D02 | 전환 퍼널 시각화 | current + v2 KPI |
| D03 | Pain point 분석 카드 | current + v2 KPI |
| D04 | 시간대별 리드 히트맵 | current + v2 KPI |
| D05 | 리더보드 | current + v2 leaderboard |
| D06 | 인센 정산 요약 | current + v2 payouts |
| D07 | 세일즈 퍼널 대시보드, 에이전트 1/2/3 전환율 | Neo4j path query |
| D08 | 한화 보고용 KPI 뷰 | masked reporting API |
| D09 | 설치 일정 대시보드 | installations |
| D10 | 인력 배정 현황 | agents/assignments |
| D11 | 객단가/백마진/순익 리포트 | margin analytics |
| D12 | 수익 구조 시뮬레이션 | revenue simulator |
| D13 | Role-based Views | RBAC + frontend view model |
| D14 | PPurio/Giftishow 운영 헬스 패널 | dispatch health |
| D15 | 출격 준비 체크리스트 보드 | operations checklist |
G. Gamification (7)
G01 리더보드, G02 레벨 시스템, G03 도파민 피드백 루프, G04 3-way 커미션, G05 단계별 인센티브, G06 에이전트 간 경쟁 대시보드, G07 퀘스트/미션 엔진.
L. Lead Management (14)
L01 파이프라인, L02 Superadmin 삭제, L03 자동 필터링, L04 지역별 리드 자동 배포, L05 AI 리드 온도 측정, L06 리드 인정 자동 처리, L07 약정 만료 자동 타겟팅, L08 매장 온도 라벨링, L09 리드 귀속 추적, L10 Device ID 수집, L11 매장 Claim Lock, L12 리드 대리 등록/귀속 분리, L13 부동산 레퍼럴 채널 추적, L14 Superadmin 리드 필드 직접 수정.
Q. Quotes/Calculator (6)
Q01 견적 계산기, Q02 견적 계산기 고도화, Q03 건물관리자용 견적서, Q04 도면 기반 AI 견적 자동화, Q05 마진 계산기, Q06 견적계산기 링크 발송/열람 추적.
A. Automation/Sync (12)
A01 에어테이블 자동 연동, A02 콜 상태 자동 반영, A03 AI 노트 자동 기록, A04 TM 결과 자동 반영, A05 통화 기반 문자 자동 발송, A06 연락 금지 자동 필터링, A07 풀퍼널 세일즈 자동화, A08 전화 끊으면 자동 업데이트, A09 ChannelTalk 상담 인입 자동 동기화, A10 콜타임 추천 엔진, A11 유효콜 STT 자동 채점, A12 광고/브랜드콘텐츠 유입 데이터 병합.
I. Installation (11)
I01 설치 일정 조회, I02 작업 지시서 자동 생성, I03 LiDAR 층고 측정, I04 건축물대장 연동, I05 설치 리드타임 시각화, I06 재고/공정 대시보드, I07 현장 예외/추가공사비 플래그, I08 도면/첨부파일 인입 관리, I09 POC->신규 전환 추적, I10 SKU/시리얼-모델 매핑, I11 설치팀 이동거리/동선 최적화.
S. Diagnose/Survey (8)
S01 보안 진단 설문, S02 네이버페이 1만원 자동 지급, S03 Superadmin 수동 N페이 발송, S04 OTP 인증 게이트, S05 매장 사진 -> 설치 데이터 자동 추출, S06 셀프 실사 고객 포털, S07 태블릿 최적화 UI, S08 device_age="0년" 옵션.
C. CRM/Pipeline (7)
C01 에어테이블 대체 자체 CRM, C02 칸반 보드, C03 Cross-sell 파이프라인, C04 카테고리 자동 분류, C05 리드 통합 관리, C06 Store Ontology 통합 SSOT, C07 한화 노출용 데이터 마스킹.
M. Marketing/Campaign (8)
M01 문자 캠페인 자동화, M02 네이버 영수증 리뷰 프로그램, M03 자동 리뷰 답글, M04 바코드 기반 리뷰 수집, M05 인바운드 랜딩 추적, M06 키워드 플로우 분석, M07 인스타/네이버 광고 관리, M08 선물 3종/QR카드 캠페인 자산 관리.
F. Field/Sales Tools (12)
F01 경비 CRUD 모듈, F02 영업 웹앱, F03 녹음+AI 정리, F04 스크립트 관리, F05 A/B 테스트 프레임, F06 근처 매장 지도, F07 동별 브라우징, F08 페르소나 명함, F09 헌터/프로모터 개별 계정, F10 필드워크 PWA 설치/오프라인 모드, F11 베스트콜->스크립트 자동 진화, F12 운영원별 QR 명함/가상번호 발급 관리.
P. Payout/Payroll (7)
P01 인센 정산 요약, P02 프로모터 일급 자동 계산, P03 주급 시스템, P04 리드 보상 자동 분배, P05 비용 검증 + 지급 플로우, P06 녹음 품질 기반 정산 자동화, P07 TM N페이 발송 권한.
K. Packages/Pricing (4)
K01 CCTV+POS 번들 가격표, K02 인터넷 결합 할인 패키지, K03 대리점별 가격 차등 관리, K04 보조금 방식.
W. Landing/Onboarding (4)
W01 언브랜디드 랜딩 페이지, W02 전환율/PMF 트래킹, W03 강력한 CTA/부킹, W04 매장 전용 리포트/QR 랜딩 생성기.
X. Long-term (11)
X01 아웃바운드->인바운드 이식, X02 비상근 전환 완전 자동화, X03 ONVIF POS 연동, X04 출입통제 화이트라벨, X05 프랜차이즈 AI 품질 관리, X06 시스템 사용료/구독 모델, X07 토스앱 내 CCTV 모니터링, X08 도어락/보험/SaaS 확장, X09 Verkada식 Single Pane 물리보안 대시보드, X10 하이브리드 스토리지/메타데이터 업로드, X11 Plug-and-play 원격 핸드셰이크.
Part 5. API Contract v2
5.1 공통 규칙
Base URL:
https://balerion.cloud/api/v2
Headers:
Authorization: Bearer <session-or-service-token>
X-Request-Id: <uuid>
X-Client: keeper-direct-web | field-pwa | worker
Common response:
{
"ok": true,
"data": {},
"meta": {
"request_id": "uuid",
"served_at": "2026-05-06T20:00:00+09:00"
}
}
Error response:
{
"ok": false,
"error": {
"code": "permission_denied",
"message": "TM_NPAY_PERMISSION_REQUIRED",
"details": {}
}
}
5.2 Auth/RBAC
POST /auth/otp/request
POST /auth/otp/verify
GET /auth/session
POST /auth/logout
GET /auth/permissions
/auth/session response:
{
"user": {
"id": "agt_...",
"name": "김푸른",
"phone_masked": "010****5047",
"roles": ["tm"],
"permissions": ["lead.read", "lead.tm_call", "payout.npay.dispatch"]
}
}
권한은 DB 관리형이어야 한다. hardcoded phone whitelist는 Phase 1에서 제거한다.
5.3 Leads
GET /leads
POST /leads
GET /leads/{lead_id}
PATCH /leads/{lead_id}
DELETE /leads/{lead_id}
POST /leads/{lead_id}/status
POST /leads/{lead_id}/assign
POST /leads/{lead_id}/tm-call
POST /leads/{lead_id}/dispatch/npay
POST /leads/{lead_id}/reject-payout
GET /leads/{lead_id}/timeline
GET /leads/{lead_id}/graph
Create/diagnose request:
{
"store": {
"atlas_store_id": "uuid",
"naver_place_id": "123",
"store_name": "밀리클럽국수",
"geo": { "lat": 37.0, "lng": 127.0 }
},
"owner": {
"name": "대표",
"phone": "010..."
},
"diagnosis": {
"current_security": "에스원",
"device_age": "0년",
"camera_count": 0,
"monthly_fee": 0,
"combined_isp": "모름",
"pain_points": ["요금부담"],
"pain_note": "없음"
},
"attribution": {
"agent_submitted_id": "agt_pureun",
"agent_attributed_id": "agt_yunju",
"source_channel": "real_estate_referral",
"referral_partner_id": "ref_..."
},
"marketing_consent": true
}
Lead response:
{
"id": "lead_...",
"status": "pending_review",
"payout_status": "pending_review",
"display_channel": "field",
"internal_source": "real_estate_referral",
"created_at": "2026-05-06T20:00:00+09:00"
}
5.4 Stores/Atlas
GET /stores/search?q=
GET /stores/nearby?lat=&lng=&radius_m=
GET /stores/{store_id}
POST /stores/{store_id}/claim
POST /stores/{store_id}/release-claim
PATCH /stores/{store_id}/manual-tier
Store reads should call Atlas. Claim writes may remain in Atlas if outbound-recorder and Keeper share the same field-work pool.
5.5 KPI/Graph
GET /kpi/summary
GET /kpi/funnel
GET /kpi/hanwha
GET /kpi/margin
GET /kpi/dispatch-health
GET /graph/leads/{lead_id}
GET /graph/agents/{agent_id}/paths
GET /graph/stores/{store_id}/neighborhood
Graph endpoints are read-only. No public arbitrary Cypher endpoint.
5.6 Campaigns/Calls/Automation
GET/POST /campaigns
POST /campaigns/{id}/dispatch
GET /dispatches
GET /dispatches/health
POST /calls/import/tiro
POST /calls/{call_id}/score
POST /sync/channeltalk
POST /sync/airtable
5.7 Installations/Quotes/Payouts
GET/POST /quotes
POST /quotes/{id}/send-link
POST /quotes/{id}/open-event
GET/POST /installations
PATCH /installations/{id}
POST /installations/{id}/exceptions
GET/POST /payouts
POST /payouts/{id}/approve
POST /payouts/{id}/dispatch
GET /payouts/balance
GET/POST /expenses
Part 6. Data Model Delta
6.1 Minimum SQL delta for 김푸른 5/6 requirements
alter table keeper_outbound_leads
add column if not exists agent_submitted_id uuid references keeper_outbound_agents(id) on delete set null,
add column if not exists agent_attributed_id uuid references keeper_outbound_agents(id) on delete set null,
add column if not exists source_channel text default 'diagnose',
add column if not exists internal_source text,
add column if not exists display_channel text,
add column if not exists referral_partner_id uuid,
add column if not exists field_edit_count int default 0,
add column if not exists last_edited_by text,
add column if not exists last_edited_at timestamptz;
create table if not exists keeper_outbound_referral_partners (
id uuid primary key default gen_random_uuid(),
type text not null check (type in ('real_estate','building_manager','franchise','other')),
name text not null,
phone text,
region text,
active boolean default true,
created_at timestamptz default now()
);
alter table keeper_outbound_leads
add constraint keeper_outbound_leads_referral_partner_fk
foreign key (referral_partner_id)
references keeper_outbound_referral_partners(id)
on delete set null;
create table if not exists keeper_outbound_lead_edit_audit (
id uuid primary key default gen_random_uuid(),
lead_id uuid not null references keeper_outbound_leads(id) on delete cascade,
edited_by text not null,
patch jsonb not null,
reason text,
created_at timestamptz default now()
);
6.2 RBAC tables
create table if not exists keeper_outbound_admin_users (
id uuid primary key default gen_random_uuid(),
phone text unique not null,
name text not null,
active boolean default true,
created_at timestamptz default now()
);
create table if not exists keeper_outbound_roles (
id text primary key,
name text not null
);
create table if not exists keeper_outbound_permissions (
id text primary key,
description text not null
);
create table if not exists keeper_outbound_user_roles (
user_id uuid references keeper_outbound_admin_users(id) on delete cascade,
role_id text references keeper_outbound_roles(id) on delete cascade,
primary key (user_id, role_id)
);
create table if not exists keeper_outbound_role_permissions (
role_id text references keeper_outbound_roles(id) on delete cascade,
permission_id text references keeper_outbound_permissions(id) on delete cascade,
primary key (role_id, permission_id)
);
Minimum permissions:
lead.read
lead.write
lead.delete
lead.tm_call
lead.edit_fields
payout.npay.dispatch
payout.reject
agent.manage
system.all_hold
report.hanwha
report.internal
6.3 Dispatch/event columns currently missing
alter table keeper_outbound_leads
add column if not exists scheduled_sms_at timestamptz,
add column if not exists sms_dispatched_at timestamptz,
add column if not exists sms_dispatch_status text,
add column if not exists sms_dispatch_error text;
6.4 Graph projection constraints
CREATE CONSTRAINT ko_lead_id IF NOT EXISTS FOR (l:Lead) REQUIRE l.id IS UNIQUE;
CREATE CONSTRAINT ko_agent_id IF NOT EXISTS FOR (a:Agent) REQUIRE a.id IS UNIQUE;
CREATE CONSTRAINT ko_owner_id IF NOT EXISTS FOR (o:Owner) REQUIRE o.id IS UNIQUE;
CREATE CONSTRAINT ko_referral_id IF NOT EXISTS FOR (r:ReferralPartner) REQUIRE r.id IS UNIQUE;
CREATE CONSTRAINT ko_campaign_id IF NOT EXISTS FOR (c:Campaign) REQUIRE c.id IS UNIQUE;
CREATE CONSTRAINT ko_quote_id IF NOT EXISTS FOR (q:Quote) REQUIRE q.id IS UNIQUE;
CREATE CONSTRAINT ko_call_id IF NOT EXISTS FOR (c:Call) REQUIRE c.id IS UNIQUE;
CREATE CONSTRAINT ko_installation_id IF NOT EXISTS FOR (i:Installation) REQUIRE i.id IS UNIQUE;
CREATE CONSTRAINT ko_payout_id IF NOT EXISTS FOR (p:Payout) REQUIRE p.id IS UNIQUE;
CREATE INDEX ko_lead_status IF NOT EXISTS FOR (l:Lead) ON (l.status);
CREATE INDEX ko_lead_created_at IF NOT EXISTS FOR (l:Lead) ON (l.created_at);
CREATE INDEX ko_campaign_channel IF NOT EXISTS FOR (c:Campaign) ON (c.channel);
6.5 Projection worker pseudo-Cypher
UNWIND $rows AS row
MERGE (l:Lead {id: row.id})
SET l.status = row.status,
l.source_channel = row.source_channel,
l.display_channel = row.display_channel,
l.created_at = datetime(row.created_at)
WITH l, row
MATCH (s:Store {store_id: row.atlas_store_id})
MERGE (l)-[:AT]->(s)
WITH l, row
OPTIONAL MATCH (submitted:Agent {id: row.agent_submitted_id})
FOREACH (_ IN CASE WHEN submitted IS NULL THEN [] ELSE [1] END |
MERGE (submitted)-[:SUBMITTED]->(l)
)
WITH l, row
OPTIONAL MATCH (attributed:Agent {id: row.agent_attributed_id})
FOREACH (_ IN CASE WHEN attributed IS NULL THEN [] ELSE [1] END |
MERGE (attributed)-[:ATTRIBUTED]->(l)
);
Projection policy:
- All writes go to Postgres first.
keeper_outbound_outboxrecords entity changes.- Worker batches outbox rows every 1-5 seconds.
- Neo4j failure does not fail the user-facing transaction.
- Dashboard graph widgets degrade to Postgres fallback when Neo4j unavailable.
Part 7. Tech Insights and Risks
7.1 Current implementation risks
| Severity | Finding | Evidence | Fix |
|---|---|---|---|
| High | Admin mutation auth is inconsistent | admin/leads/{id}/dispatch, tm-call, pending routes do not validate session server-side |
Central requirePermission() middleware |
| High | PATCH /api/keeper-out/lead/{id} has no auth |
Lead status/agent assignment can be changed by route access | Require admin session + audit |
| High | NPay dispatch approval identity is client-supplied | approved_by accepted from request body |
Derive from session server-side |
| High | Giftishow readiness checks wrong env layer | diagnose route checks GIFTISHOW_*, while actual send uses balerion proxy key |
Check BALERION_API_KEY and /sms/balance health instead |
| High | Vhagar Neo4j currently not running | container exit 3 on restart | Fix before any graph dependency |
| Medium | /diagnose and /keeper-out/diagnose flows diverge |
active /diagnose/DiagnoseFlow2 differs from legacy keeper-out flow |
Deprecate legacy route or unify component |
| Medium | Schema drift | scheduled_sms_at used in code, missing in migrations |
migration + schema test |
| Medium | Hardcoded admin phones | admin-auth.ts constant map |
DB-backed RBAC |
| Medium | KPI loads up to 10k rows into app memory | fetchKpi() client-style aggregation |
SQL/materialized daily stats |
| Medium | Vhagar PG subscription broken | publisher 100.113.6.15:5433 unreachable |
update publication host or disable subscription |
| Low | Feature docs inconsistent | 121 vs 126 | v2 작성 중 matrix/supplemental 보정 완료 |
7.2 Graph DB feasibility
Scale is feasible if Neo4j is read model only.
| Dataset | Count | Feasibility |
|---|---|---|
| Store nodes | 4.54M | Already loaded historically; disk 20G |
| Lead nodes | 10K-1M near term | Fine |
| Agent nodes | <1K | Fine |
| Calls/events | 100K-5M | Fine if summarized and indexed |
| Relationships | 5M-30M | Fine on 25GiB RAM with tuned heap/pagecache, but not for arbitrary deep traversals |
Memory guidance for Vhagar:
- Keep Neo4j heap 6-8G, pagecache 6-10G after confirming system workload.
- Avoid loading all Store properties into Neo4j. Keep heavy JSON in Atlas/Postgres.
- For dashboard queries, precompute daily aggregates in Postgres and use Neo4j only for relationship-specific widgets.
- Do not expose GraphQL/arbitrary Cypher publicly.
7.3 Neo4j Community vs Enterprise
Enterprise is not required for MVP/Phase 1-5.
| Need | Community | Enterprise need? |
|---|---|---|
| Single-node Store/Lead graph | Yes | No |
| Constraints/indexes | Yes | No |
| APOC core | Yes | No |
| RBAC inside Neo4j | Limited | Avoid by API-layer RBAC |
| HA clustering | No | Not now |
| Fabric/multi-db federation | No | Not now |
| Online backups | Limited | Use dumps/snapshots |
Decision: keep Neo4j Community. Put RBAC and tenancy in /api/v2.
7.4 Security layer
Required baseline:
- Neo4j and Vhagar Postgres bind to Tailscale only.
- balerion -> Vhagar access limited by Tailscale ACL to Neo4j Bolt/HTTP and Postgres if needed.
- No phone numbers in Neo4j unless hashed. Use
phone_hashand keep raw phone in OLTP only. - All admin mutations require server-side permission checks.
- All payout dispatches require audit row: who, role, lead, amount, balance before/after, external result.
- Webhooks/worker endpoints require service token and replay protection.
- 한화 report API returns
display_channelonly, neverinternal_source.
Part 8. Implementation Roadmap v2
Phase 0 - Spec freeze and infra repair (W0-W1)
- Keep source doc count drift fixed: 126 = Codex 26 + 김푸른 5 + original 95.
- Add DB migrations for
device_age="0년", attribution split, referral partners, lead edit audit, scheduled SMS columns, RBAC. - Fix Vhagar Neo4j restart failure and run verify:
Store≈4.54MBrand=6USES>=10KLOCATED_IN>=4M
- Repair or intentionally disable Vhagar PG
atlas_sub. - Decide
/api/v2runtime: extend existing balerion unified-api or create new Hono service. - Create
/api/v2/health,/api/v2/auth/session,/api/v2/kpi/summaryskeleton.
Exit criteria:
https://balerion.cloud/api/v2/health200.- Vercel can call
/api/v2/auth/session. - Neo4j is either healthy or graph features explicitly feature-flagged off.
Phase 1 - Core API and security (W1-W3)
- Move admin auth to DB-backed RBAC.
- Protect all current admin mutation routes.
- Implement v2 leads CRUD and timeline.
- Implement NPay dispatch permission: superadmin + approved TM.
- Implement lead field edit UI/API with audit.
- Add
device_age="0년"in active/diagnoseUI. - Keep current Next APIs as compatibility layer only.
Exit criteria:
- 김푸른 can dispatch NPay only within assigned permission.
- Superadmin can edit bad lead fields with audit.
- Agent submitted vs attributed can diverge.
Phase 2 - Store/field workflow (W3-W5)
- Wire Atlas
/stores/search,/nearby,/claimthrough v2. - Field PWA: assigned stores, claim lock, visit log, offline queue.
- QR/virtual number attribution.
- Real estate referral partner flow.
- Tablet UI polish.
Phase 3 - Graph projection and KPI (W5-W7)
- Outbox table + projection worker.
- Graph constraints for Lead/Agent/Referral/Campaign.
- D07 agent 1/2/3 funnel.
- D08 Hanwha masked KPI.
- Call-time recommendation from historical lead/call events.
Phase 4 - Calls, campaigns, and health (W7-W9)
- Tiro/recording import.
- STT valid-call scoring.
- ChannelTalk sync.
- PPurio/Giftishow dispatch health panel.
- SMS campaign automation and do-not-contact filter.
Phase 5 - Installation and payout automation (W9-W11)
- Installation job sheet.
- Exceptions/additional-cost flags.
- Schedule and routing.
- SKU/serial mapping.
- Payroll and STT-quality-based payout.
Phase 6 - Marketing/landing/packages (W11-W12)
- Quote link tracking.
- Store report/QR landing generator.
- Review program.
- Package/pricing manager.
- PMF tracking.
Phase 7 - Long-term platform (W12+)
Verkada-style single pane, ONVIF/POS, hybrid video metadata, plug-and-play tunnel, subscription model, cross-sell expansions.
Part 9. Verification Checklist
Before implementation
[ ] FEATURE-MATRIX total script returns total=126 done=16 todo=110
[ ] Supabase live schema checked against migrations
[ ] Vhagar Neo4j starts and `verify_phase1.sh` passes
[ ] Vhagar PG subscription fixed or disabled
[ ] balerion.cloud SSH/API ownership confirmed
[ ] `/api/v2/health` deployed
[ ] Admin permissions seeded in DB
Per phase
[ ] API contract tests
[ ] Role/permission tests
[ ] Payout dispatch audit tests
[ ] Graph projection idempotency tests
[ ] Hanwha masking tests
[ ] Browser smoke test for admin dashboard
[ ] Migration rollback note
Production SLOs
| Metric | Target |
|---|---|
| Diagnose submit p95 | < 800ms excluding external payout |
| KPI summary p95 | < 500ms |
| Graph funnel p95 | < 1500ms with warm cache |
| NPay dispatch audit write | 100% |
| SMS/NPay health refresh | every 1-5 min |
| Outbox projection lag | < 30s normal |
Part 10. Cost Estimate
| Item | Current/target | Monthly |
|---|---|---|
| Vercel | keeper-direct frontend | existing plan |
| balerion.cloud unified API | current Lightsail/EC2 class | about $24-40 if Lightsail medium |
| Atlas AWS | outbound API + 4.54M Postgres | existing EC2/EBS cost |
| Supabase keeper-direct | current OLTP | Free/Pro depending usage |
| Vhagar | self-host power + network | electricity only |
| Neo4j | Community | $0 license |
| Tailscale | personal/team current | likely $0/current |
| Storage backups | S3/R2/B2 for dumps | small until video metadata grows |
Cost decision:
- Do not move 4.54M Store OLTP to Supabase.
- Do not put video/image originals in Neo4j/Postgres.
- Use Vhagar for graph/ontology and local analytical mirror, not internet-facing availability.
Part 11. Final Decisions
- The master feature count is 126, not 121.
- Graph DB layer is feasible and valuable, but only as projection/read model.
- Atlas remains Store SSOT. Keeper lead/payout/install/call state needs its own OLTP.
- balerion.cloud
/api/v2becomes the only public backend contract for new frontend work. /keeper-out/adminremains the current operations admin. New dogfooding admin work happens under/admin.- Vhagar must be repaired before any graph-dependent feature is scheduled as required.
- Security/RBAC must be Phase 1, not a later cleanup.
- 김푸른 통화 requirements are first-class:
device_age="0년"- submitted vs attributed agent split
- TM NPay permission
- real-estate referral channel
- superadmin field edit with audit
v2 SSOT: this document +
FEATURE-MATRIX.md. Implementation should not start until Phase 0 checklist is complete.