/diagnose pain_points 백엔드 처리 완료

2026-05-05 · keeper-direct staging · handoff doc 🔴 항목 해제 · project hvcrbdpjxfszhwrtcygq

목차

  1. 한 줄 요약
  2. 왜 이 작업이 먼저였나
  3. DB 스키마 변경
  4. API 계약 (POST /api/keeper-out/diagnose)
  5. pain_points 6개 enum + 영업 분기 매핑
  6. 변경 파일 (3개 + migration 1개)
  7. 검증 결과
  8. 다음 분기 — 어디로 갈지

01.한 줄 요약

/diagnose의 SPIN Problem pain step에서 사용자가 고른 6개 객관식(출동지연·요금부담·약정부담·사각지대·AS불만·앱불편) + 자유 textarea(max 300자)가 이제 Supabase keeper_outbound_leads 테이블에 영구 저장됩니다. 영업맨이 outbound-dashboard에서 매장 방문 전 talking point 준비할 raw 데이터의 상류 3단(frontend → API route → DB)이 닫혔습니다.

★ INSIGHT

02.왜 이 작업이 먼저였나

어제 세션 끝에 frontend(DiagnoseFlow2.tsx)는 이미 pain_points/pain_note를 POST body에 넣어 보내고 있었지만, 서버 route는 그 필드를 조용히 무시 중이었습니다. 즉 사용자가 진단을 완료해도 pain 데이터는 DB에 안 들어가고 사라지는 상태. 이걸 그대로 두고 outbound-dashboard 시각화를 만들면 "데이터 없는 차트"가 됩니다.

03.DB 스키마 변경

새 마이그레이션 파일 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 인덱스가능하지만 느림
스토리지가벼움오버헤드

왜 CHECK constraint으로 enum 강제 안 했나

6개 옵션이지만 자동님이 향후 7번째(예: "야간 모니터링 부재")를 frontend에서 추가할 때 CHECK가 발목을 잡습니다. 진단 product 같은 fast-iteration 도메인에선 frontend value 파일을 SSOT로 두고 DB는 받아주기만 하는 게 정석.

04.API 계약

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,
  // ...
});
★ INSIGHT — 3단 graceful degradation
어느 단계에서 누락돼도 INSERT가 깨지지 않도록 모두 옵셔널 디자인.

05.pain_points 6개 enum + 영업 분기

handoff doc의 핵심 테이블. 영업맨이 매장 방문 전 lead의 pain을 보고 talking point를 매칭:

출동지연
→ 출동시간 SLA 보증 강조
요금부담
→ 결합 영업 / 가격 비교
약정부담
→ 위약금 대납 시나리오
사각지대
→ 카메라 위치/대수 보강 제안
AS불만
→ 커뮤니케이션 가치 강조
앱불편
→ 모던 UX 차별화

이 매핑은 outbound-dashboard에 lead 카드 클릭 시 talking point 자동 표시될 때 사용 예정. 현재는 raw 데이터 보존만 완료, 시각화는 다음 단계.

06.변경 파일

변경 종류경로비고
신규 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 라인

07.검증 결과

체크결과
npx tsc --noEmitPASS (출력 없음)
npx eslint app/.../route.ts lib/.../types.tsPASS (출력 없음)
Supabase schema dump에 컬럼 확인pain_points + pain_note 존재
git diff stat2 files changed, 6 insertions(+) — 1 migration 추가

08.다음 분기

outbound-dashboard 본체는 이 레포가 아니라 분산되어 있음. 셋 중 하나를 골라서 진행:

🅐 direct-market /admin/*

Next.js 영업맨 대시보드 통합 view + RBAC. handoff "5월 혜성 인계" 핵심. 다른 레포로 점프 필요.

🅑 outbound-recorder BFF

FastAPI + alembic. /admin이 호출할 BFF + visit_logs/TM 화법 데이터. 다른 레포 + Python.


자동님 결정 대기 중