v1 보고서가 설계만 다뤘다면 v2는 이미 박힌 코드(Codex P1 8섹션, staging에 3 commits) + 외부 시스템(키퍼 admin·Referio) RE 결과를 같은 도면 위에 겹친다. 3-layer 통합 스키마(L1 keeper-out 자체 DB / L2 키퍼 admin / L3 Referio)에 어뷰징 검증 시스템(V1~V8)이 박힐 정확한 위치를 못박는 것이 v2의 목표. 코드 수정은 자동님 명시 지시로 보류 — P3(어뷰징 시스템) 진입 시점만 자동님 결정 대기.
다른 Claude 세션(PM/CTO 모드) → Codex CLI 디스패치로 /keeper-out/admin/leads/[id]/ 신규 페이지 8섹션이 staging에 박혔음. 어제 박은 pain_points 백엔드도 이 라운드에 포함.
PM 검증 6/6: 3 commit 분리 / 색깔 룰(admin 청록 0건) / 텍스트 룰(키퍼·한화·하나비전 미박입) / untracked 보존 / tsc clean / 8섹션 모두 셔브. 실제 라이브 데이터 확인: 고반식당 이수역점 / B 92만 통신결합 / 기대혜택 ₩920,000 / 지급 합계 ₩30,000.
keeper-out lead 1건이 시스템 3개에 어떻게 매핑되는지 단일 도면:
010-3111-1165 형식 동일. 별도 통합 키 발급 없이 한 줄 쿼리로 ②③룰 통과 검증.leads.agent_1(L1) ↔ admin User.mobile(L2) ↔ Referio Partner.code(L3) 3축 모두 keeper-out에서 박힘.Codex P1이 박은 LeadDetail 인터페이스 (Phase별 필드 표시):
// lib/keeper-out/types.ts (commit dc4d9a0 + P2 예정)
export interface LeadDetail extends Lead {
// === P1 박힘 ===
agent_1_data: Agent | null;
agent_2_data: Agent | null;
agent_3_data: Agent | null;
payouts: Payout[]; // lead_30k / install_50k_per_unit / naver_pay_10k
events: FunnelEvent[]; // diagnose.* / naver_pay.* / 시간순
// === P2 대기 ===
visit_logs: VisitLog[]; // promoter+step+outcome+GPS+사진+녹음
place: Place | null; // 매장 마스터 (closure_rate, priority_score)
owner: Owner | null; // 점주 (total_payback)
other_leads: Lead[]; // 같은 owner의 다른 매장
// === P3 어뷰징 시스템 (이번 보고서 신규 제안) ===
agent_attribution: AgentAttribution | null; // V3 게이트 결과
otp_session: OtpSession | null; // V1 OTP 검증 ts
fraud_signals: FraudSignal[]; // V7 패턴 탐지 hit
external_user: KeeperUser | null; // L2 phone match 결과
}
| endpoint | 상태 | 역할 |
|---|---|---|
POST /api/keeper-out/diagnose | 박힘 | 진단 제출 + payouts 3건 + Giftishow fire-and-forget |
GET /api/keeper-out/lead/[id] | 박힘 P1 | LeadDetail PostgREST embed 한 방 |
PATCH /api/keeper-out/lead/[id] | 박힘 | status 이동 |
POST /api/keeper-out/visits | 박힘 | 방문 메모 (P2 폼이 사용) |
POST /api/keeper-out/diagnose/request-otp | P3 신설 | V3 게이트 통과 + V1 OTP 발송 |
POST /api/keeper-out/diagnose/verify | P3 신설 | V1 OTP 검증 → leads INSERT + 발송 |
GET /api/keeper-out/lead/[id]/external | P3 신설 | L2 admin User phone match (사후 검수) |
자동님 본인 권한(ceo@bizdirect.co.kr ADMIN, group=13 "키퍼 아웃바운드")으로 직접 호출 가능. JWT는 RS256 + 5h 만료. 우리 cron이 사후 검수에 사용.
// 사후 검수 핵심 쿼리 — 24h cron이 leads.phone으로 admin 조회
GET https://api-account.keeperplatform.com/v1/account/admin/users
?role=OWNER&phone={leads.phone}
Authorization: Bearer {RS256_JWT}
// Response
{
"data": {
"items": [
{
"id": 6245,
"name": "이정희",
"mobile": "010-3111-1165",
"shopName": "아지트",
"subscribes": [] // ★ 비어있음 = 계약 X
}
],
"total": 1
}
}
| L1 → L2 매핑 | 일치 시 의미 | dual-layer 룰 |
|---|---|---|
leads.phone == User.mobile | 사장님 키퍼 가입 확인 | ② 사업자 확인 통과 |
User.subscribes.length > 0 | 키퍼 요금제 구독 | ③ 도입 의향 통과 (계약 성사 직전) |
Order.status === '설치 완료' | 설치 완료 매장 | ④ 계약 완료 = install_50k 인센 트리거 |
키퍼메이트 = Puzl 운영 Referio. 자동님 본인이 가입돼있어 본인 대시보드 접근 가능(referio.puzl.co.kr/login). 우리 keeper-out과 직접 통합은 없으나, 5자리 영숫자 mixed case 추천코드 형식이 keeper-out agent.code 표준이 됨.
// keeper-out agent ↔ Referio partner 매핑 가능 컬럼
interface Agent {
id: string;
name: string;
phone: string | null;
role: 'promoter' | 'tm' | 'installer' | 'multi';
code: string; // ★ 5자리 영숫자 mixed case (예: K9bR2)
active: boolean;
daily_rate: number;
// === P3 어뷰징 + 향후 통합 컬럼 ===
referio_partner_id: string | null; // L3 매핑 (예: 86Af9)
keeper_partner_id: number | null; // 키퍼 partners 통합 시
kakao_user_id: string | null; // 향후 SSO
invalid_rate_30d: number; // V7 누적 invalid률
day_count: number; // V3 day cap (25/일)
week_count: number; // V3 week cap (100/주)
}
Referio 어뷰징 정책 차용(FAQ Q5): "허위/과장 광고, 검색 광고 집행, 스팸성 홍보, 자기 추천(셀프 가입) 금지 — 위반 시 자격 해지". 이걸 keeper-out V7 cron 룰 매트릭스에 반영.
| 축 | L1 keeper-out | L2 키퍼 admin | L3 Referio |
|---|---|---|---|
| 사장님 | leads.phone + ceo_name |
User.mobile + name |
(직접 없음, ref URL로만) |
| 매장 | leads.naver_place_id + store_name |
User.shopName + Order.주문자.매장명 |
(없음) |
| 영업원/메이트 | agents.code (5자리) |
(별도 — partners 테이블 있지만 미통합) | Partner.code (자동님 86Af9) |
| 계약 시점 | leads.status='installed' |
Order.status='설치 완료' + User.subscribes>0 |
(없음) |
| 보상 | payouts(naver_pay_10k + lead_30k + install_50k) |
(자체 정산) | 유효리드 1.5만 + 계약 2만 = 3.5만 (월별) |
// [24h cron — TM 통화 후]
async function checkLeadCompletion(lead: Lead) {
// L2 admin BFF 호출 (자동님 JWT 사용)
const res = await fetch(
`https://api-account.keeperplatform.com/v1/account/admin/users`
+ `?role=OWNER&phone=${lead.phone}`,
{ headers: { Authorization: `Bearer ${KEEPER_ADMIN_JWT}` } }
);
const { data: { items } } = await res.json();
if (items.length === 0) {
// 사장님 키퍼 가입 안 함 = ②③ 미달
await updatePayoutStatus(lead.id, 'lead_30k', 'invalid');
await incrementAgentInvalidRate(lead.agent_1);
return;
}
const user = items[0];
if (user.subscribes.length > 0) {
// ②③ 모두 통과
await updatePayoutStatus(lead.id, 'lead_30k', 'confirmed');
// 보너스: external_user 캐싱 (LeadDetail에 표시용)
await upsertExternalUser(lead.id, user);
} else {
// 가입했지만 미구독 — 7일 추가 대기
await scheduleRecheck(lead.id, 7 * 24 * 3600);
}
}
| Phase | commits | 예상 시간 | 핵심 파일 |
|---|---|---|---|
| P1 | 3 ★ 박힘 | — | type.ts·route.ts·LeadDetailClient.tsx 615줄 |
| P1.5 | 1 | 15m | admin/layout.tsx 신설 |
| P2 | 3 | 90m | GET embed 확장 + 4섹션 + 메모 폼 |
| P3 | ~8 | 1~2일 | migration(otp/agent.code/fraud) + request-otp/verify 라우트 분리 + agent 모달 + cron + admin BFF 호출 + LeadDetailClient에 검증 결과 섹션 |
| 파일 | 변경 |
|---|---|
app/diagnose/DiagnoseFlow2.tsx | "지원금 받기" 버튼 클릭 → AgentGateModal 띄움 (자동님 5/5 정정 흐름) |
app/diagnose/components/AgentGateModal.tsx 신규 | 이름 + 5자리 코드 입력 + 본사 확인 톤 |
// 현재 (단일 route)
POST /api/keeper-out/diagnose
→ 4 non-null + sbInsert + payouts 3 + Giftishow fire-and-forget
// P3 분리 (3 단계)
POST /api/keeper-out/diagnose/request-otp
→ V3(agent code+name) + V4(place_id NOT NULL+phone unique)
+ V5(잔액·cap) + V8(place 실재) + V2(GPS)
→ 통과 시 OTP 발송 (balerion SMS)
POST /api/keeper-out/diagnose/verify
→ V1(OTP 5분/3회) + V6(EXIF)
→ leads INSERT (agent_id=verified) + funnel_event 'diagnose.verified'
→ Giftishow 발송 (사장님 1만원 즉시)
→ payouts.lead_30k status='pending' (사후 검수 대기)
GET /api/keeper-out/lead/[id]/external
→ L2 admin User phone match (LeadDetail 표시용)
Codex P1이 박은 8섹션에 P3에서 2개 더:
| # | 섹션 | 표시 데이터 |
|---|---|---|
| 9 | 방문 로그 P2 | visit_logs |
| 10 | 점주 다른 매장 P2 | other_leads |
| 11 | 매장 마스터 P2 | place |
| 13 | 메모 폼 P2 | visits POST |
| 14 | 검증 결과 P3 | otp_session(언제·횟수·통과여부) / agent_attribution(이름·코드 매칭) / fraud_signals[] |
| 15 | 키퍼 admin 매칭 P3 | external_user (User.id·shopName·subscribes·createdAt 비교) / 매칭 시 24h ②룰 통과 표시 |
-- supabase/migrations/2026MMDD_keeper_outbound_anti_abuse.sql
-- agents 테이블 컬럼 추가
alter table public.keeper_outbound_agents
add column if not exists code text unique, -- 5자리 영숫자
add column if not exists referio_partner_id text, -- 86Af9 매핑
add column if not exists keeper_partner_id int, -- 향후 통합
add column if not exists kakao_user_id text,
add column if not exists invalid_rate_30d numeric default 0;
-- leads 테이블 컬럼 추가
alter table public.keeper_outbound_leads
alter column naver_place_id set not null, -- V4
add constraint uniq_phone unique (phone), -- V4 lifetime
add column if not exists ip_address inet, -- V9
add column if not exists user_agent text, -- V9
add column if not exists external_user_id int, -- L2 매칭 캐시
add column if not exists external_subscribed_at timestamptz; -- ③룰 ts
-- OTP 세션
create table public.keeper_outbound_otp_sessions (
id uuid primary key default gen_random_uuid(),
phone text not null,
code char(6) not null,
expires_at timestamptz not null,
attempts int default 0,
verified_at timestamptz,
created_at timestamptz default now()
);
-- 어뷰징 신호
create table public.keeper_outbound_fraud_signals (
id uuid primary key default gen_random_uuid(),
lead_id uuid references public.keeper_outbound_leads(id) on delete cascade,
rule text not null, -- self_referral / unknown_responses / cluster / ...
severity text check (severity in ('low','med','high')),
payload jsonb,
flagged_at timestamptz default now()
);
| Q | 결정 | 출처 |
|---|---|---|
| agent.code 형식 | 5자리 영숫자 mixed case (Referio 표준) | v1 |
| ref URL 패턴 | ?ref={code} | v1 |
| UX — 영업원 코드 시점 | "지원금 받기" 클릭 후 모달 | v1 (자동님 5/5 정정) |
| 코드 수정 보류 | 계획 끝난 후 추후 지시 | v1 |
| v1 보고서 수용 + v2 통합 | 대부분 수용 + 병렬 세션 합본 | v2 (이번) |
이미 staging에 P1 박혀있으니 layout 분리 + 4섹션 추가가 자연스러운 다음. 대시보드를 먼저 완성한 후 P3(어뷰징) 진입.
예상: 105분 (P1.5 15m + P2 90m). 4 commits.
5/5 출격 직후라 어뷰징 노출 위험이 P1.5/P2보다 큼. 다만 LeadDetail에 fraud 섹션 추가는 P2 끝난 후가 깔끔.
예상: 1~2일. ~8 commits.
① naver_place_id NOT NULL ② phone unique lifetime ③ Giftishow checkBalance preflight ④ 일일 송금 카운터. P3 풀 시스템 전 즉시 효과 큰 4건.
예상: 30분. 1 migration + route.ts 수정.
staging 3 commits를 main에 fast-forward. keeper-direct.ceo/keeper-out/admin/leads/<id> 라이브.
v1에서 보류한 B 옵션. 자동님 코드 86Af9 데이터 모델 + 정산 흐름 직접 확인.
같은 Giftishow 키. balerion.cloud SSH (call-center.pem). P3 설계 보강용.
05-policy.md 어뷰징 정책 명문화 별도 문서