/diagnose의 SPIN Problem pain step에서 사용자가 고른
6개 객관식(출동지연·요금부담·약정부담·사각지대·AS불만·앱불편) +
자유 textarea(max 300자)가 이제 Supabase
keeper_outbound_leads 테이블에 영구 저장됩니다. 영업맨이
outbound-dashboard에서 매장 방문 전 talking point 준비할 raw 데이터의
상류 3단(frontend → API route → DB)이 닫혔습니다.
어제 세션 끝에 frontend(DiagnoseFlow2.tsx)는 이미
pain_points/pain_note를 POST body에 넣어 보내고 있었지만,
서버 route는 그 필드를 조용히 무시 중이었습니다. 즉 사용자가 진단을 완료해도
pain 데이터는 DB에 안 들어가고 사라지는 상태. 이걸 그대로 두고 outbound-dashboard 시각화를
만들면 "데이터 없는 차트"가 됩니다.
새 마이그레이션 파일 supabase/migrations/20260505_keeper_outbound_pain.sql:
alter table public.keeper_outbound_leads
add column if not exists pain_points text[],
add column if not exists pain_note text;
-- "약정부담 lead 전체" 같은 array contains 쿼리 가속
create index if not exists idx_keeper_leads_pain_points
on public.keeper_outbound_leads using gin(pain_points);
text[]이고 jsonb[]가 아닌가| 축 | text[] | jsonb[] |
|---|---|---|
| 기존 패턴 | photo_urls text[] 동일 | 이 테이블엔 jsonb 컬럼 따로 (funnel_events.payload) |
| 쿼리 (영업맨 use-case) | pain_points @> ARRAY['약정부담'] + GIN 인덱스 | 가능하지만 느림 |
| 스토리지 | 가벼움 | 오버헤드 |
6개 옵션이지만 자동님이 향후 7번째(예: "야간 모니터링 부재")를 frontend에서 추가할 때 CHECK가 발목을 잡습니다. 진단 product 같은 fast-iteration 도메인에선 frontend value 파일을 SSOT로 두고 DB는 받아주기만 하는 게 정석.
POST /api/keeper-out/diagnose request body — 볼드체가 신규 필드:
{
store_name: string,
naver_place_id: string | null,
ceo_name: string,
phone: string,
current_security: string, // "에스원" | "캡스" | "KT" | "기타"
device_age: string, // "1년" | "2년" | "3년" | "4년" | "5년이상"
pain_points: string[], // ★ 신규 — 다중 선택, 빈 배열 허용
pain_note: string | null, // ★ 신규 — 자유 입력 (max 300자)
camera_count: number,
monthly_fee: number,
combined_isp: string, // "SK" | "KT" | "LG" | "단독" | "모름"
photo_urls: string[],
marketing_consent: boolean,
}
route 내부 처리:
await sbInsert<Lead>("keeper_outbound_leads", {
// ... 기존 필드들
current_security: body.current_security,
device_age: body.device_age,
pain_points: body.pain_points ?? [], // ← undefined 방어
pain_note: body.pain_note ?? null, // ← graceful
camera_count: body.camera_count,
// ...
});
painNote.trim() || null — 빈 string을 null로 normalize?? [] / ?? null — undefined 방어handoff doc의 핵심 테이블. 영업맨이 매장 방문 전 lead의 pain을 보고 talking point를 매칭:
이 매핑은 outbound-dashboard에 lead 카드 클릭 시 talking point 자동 표시될 때 사용 예정. 현재는 raw 데이터 보존만 완료, 시각화는 다음 단계.
| 변경 종류 | 경로 | 비고 |
|---|---|---|
| 신규 | supabase/migrations/20260505_keeper_outbound_pain.sql | 2 컬럼 + GIN 인덱스 |
| DB 적용 | Supabase hvcrbdpjxfszhwrtcygq |
apply_migration success |
| 수정 | lib/keeper-out/types.ts | Lead + DiagnoseSubmitPayload 인터페이스 +4 라인 |
| 수정 | app/api/keeper-out/diagnose/route.ts | sbInsert 페이로드 +2 라인 |
| 체크 | 결과 |
|---|---|
npx tsc --noEmit | PASS (출력 없음) |
npx eslint app/.../route.ts lib/.../types.ts | PASS (출력 없음) |
| Supabase schema dump에 컬럼 확인 | pain_points + pain_note 존재 |
| git diff stat | 2 files changed, 6 insertions(+) — 1 migration 추가 |
outbound-dashboard 본체는 이 레포가 아니라 분산되어 있음. 셋 중 하나를 골라서 진행:
/admin/*Next.js 영업맨 대시보드 통합 view + RBAC. handoff "5월 혜성 인계" 핵심. 다른 레포로 점프 필요.
FastAPI + alembic. /admin이 호출할 BFF + visit_logs/TM 화법 데이터. 다른 레포 + Python.
app/keeper-out/admin/ 안에서 진도방금 박은 pain_points를 이 레포 admin view에서 영업맨이 lead 클릭 시 talking point로 표시.
🅒로 가 — keeper-direct admin에 pain_points 카드 표시커밋 먼저 — 변경 3 파일을 staging 브랜치에 commit (현재 untracked 데이터 덤프/스크립트는 제외)🅐로 가 — direct-market 레포로 점프해서 영업맨 dashboard 본격