두 STAGE 한 작업지시서, 단 중간 게이트 박힘: STAGE 1(2 commits) 완료 → production 배포 → 자동님 confirm → STAGE 2(8 commits) 진입. 이 게이트가 9 commit 한 방에 던지는 것보다 안전.
STAGE 1 hotfix 4건 (2 commits, ~1-2h)
├─ commit 1: migration (naver_place_id NOT NULL + phone UNIQUE)
└─ commit 2: route hotfix (checkBalance preflight + day cap)
↓ staging push → main merge → production 배포
↓ 자동님 confirm 게이트 ★
STAGE 2 P3 v3 풀 구현 (8 commits, ~6h)
└─ docs/keeper-out/anti-abuse/06-p3-implementation-brief.md 따라
↓ staging 누적 → PM 검증 → 자동님 main 머지 결정
naver_place_id NOT NULL + phone UNIQUE는 STAGE 2 v3 migration에도 똑같이 들어있음.alter column ... set not null + create unique index if not exists는 idempotent → STAGE 2가 다시 시도해도 no-op.경로: supabase/migrations/20260506_keeper_outbound_hotfix.sql
-- hotfix: naver_place_id NOT NULL + phone lifetime UNIQUE
-- 출격 첫날 노출 차단. P3 v3 migration이 이걸 다시 시도해도 idempotent.
-- (1) naver_place_id NOT NULL 격상
-- 기존 NULL row 처리: 없을 가능성 높지만 명시적 처리
delete from public.keeper_outbound_leads
where naver_place_id is null
and created_at < now() - interval '1 day';
-- 24h 이내 NULL row는 보존 (영업원 대응 시간)
alter table public.keeper_outbound_leads
alter column naver_place_id set not null;
-- (2) phone lifetime UNIQUE
create unique index if not exists uniq_leads_phone
on public.keeper_outbound_leads(phone);
코덱스가 delete 실행 전에 다음 query로 영향 row 수 확인 필수:
SELECT count(*) FROM keeper_outbound_leads WHERE naver_place_id IS NULL AND created_at < now() - interval '1 day';
0건이면 안전. 1건 이상이면 PM에게 보고 후 진행 결정.
커밋: feat(keeper-out/hotfix): naver_place_id NOT NULL + phone lifetime UNIQUE
파일: app/api/keeper-out/diagnose/route.ts 수정
import { sendNaverPayCoupon, generateTrId, checkBalance } from "@/lib/keeper-out/giftishow";
// 진단 INSERT 직후, sendNaverPayCoupon 직전에 추가
const balance = await checkBalance().catch(() => -1);
if (balance >= 0 && balance < 50000) {
// 잔액 5만원 미만이면 발송 건너뜀 + funnel_event 기록
await sbInsert("keeper_outbound_funnel_events", {
lead_id: lead.id,
event_type: "naver_pay_skipped_low_balance",
payload: { balance },
}).catch(() => {});
// payouts.naver_pay_10k는 status='pending' 그대로 — 추후 수동 발송
return NextResponse.json({
ok: true,
lead_id: lead.id,
scenario, expected_benefit: benefit, payback_token: lead.id,
payout_deferred: true, // 클라이언트는 진단 완료로 처리
});
}
lib/keeper-out/giftishow.ts에 checkBalance export 되어있는지 grep으로 확인.
없으면 코덱스가 신설 (Giftishow API /bizApi/balance 호출).
있으면 import만 추가.
const DAY_CAP = parseInt(process.env.KEEPER_OUT_DAY_CAP || "200", 10);
// checkBalance 직후, sendNaverPayCoupon 직전에 추가
const today = new Date().toISOString().slice(0, 10);
const sentToday = await sbSelect(
"keeper_outbound_payouts",
`select=id&payout_type=eq.naver_pay_10k&status=eq.sent&paid_at=gte.${today}T00:00:00Z`
);
if (sentToday.length >= DAY_CAP) {
await sbInsert("keeper_outbound_funnel_events", {
lead_id: lead.id,
event_type: "naver_pay_skipped_day_cap",
payload: { day_cap: DAY_CAP, sent_today: sentToday.length },
}).catch(() => {});
return NextResponse.json({
ok: true,
lead_id: lead.id,
scenario, expected_benefit: benefit, payback_token: lead.id,
payout_deferred: true,
});
}
커밋: feat(keeper-out/hotfix): checkBalance preflight + day cap (DAY_CAP env, default 200)
npx tsc --noEmit exit 0npx eslint <변경 파일> 0 hitnpm run build successgit push origin staginggit checkout main && git pull && git merge staging -m "Merge staging: keeper-out hotfix 4건"git push origin mainhttps://www.keeper-direct.ceo/diagnose 200git checkout staging 복귀STAGE 1 끝나면 코덱스가 PM에게 다음 형식으로 1차 보고하고 STAGE 2 진입 confirm 받기. 자동님이 production URL 직접 확인하고 OK 받기 전엔 STAGE 2 진입 X.
## STAGE 1 hotfix 결과 보고
1. 커밋 sha 2개 + main merge sha
2. delete 영향 row 수
3. tsc/eslint/build 결과
4. production URL 200 확인 결과
5. STAGE 2 진입 대기 — 자동님 confirm 받으면 진행
참조: docs/keeper-out/anti-abuse/06-p3-implementation-brief.md
| # | 커밋 메시지 | 분량 |
|---|---|---|
| 1 | feat(keeper-out): migration v3 — agent code, leads approval columns, otp/fraud/system_state tables | migration only |
| 2 | feat(keeper-out): /diagnose request-otp + verify endpoints + AgentGateModal + OtpVerifyModal | API 2개 + 컴포넌트 2개 |
| 3 | feat(keeper-out/admin): pending queue page + TM call API | page 1 + API 2 |
| 4 | feat(keeper-out/admin): dispatch + reject API + balance preflight + cap check | API 2 + cap util |
| 5 | feat(keeper-out/admin): fallback queue page (TM 6h escalate) | page 1 |
| 6 | feat(keeper-out/admin): ALL HOLD toggle + system_state CRUD | component + API |
| 7 | feat(keeper-out/admin): LeadDetailClient sections 14·15·16·17 (검증/외부매칭/승인/액션) | 4 섹션 추가 |
| 8 | feat(keeper-out): cron — recheck-completion + escalate-fallback (24h dual-layer) | cron 2개 |
commit 8 (cron) 작업 시 자동님께 옵션 (a)/(b)/(c) 결정 받기. 기본값 (c) 하이브리드. 받기 전엔 코드 PR 보류 또는 (c) 채택 명시 후 진행.
#009090·#00b4b4·#006B6B·#007575·#e6f7f7 Xapp/diagnose/* 격리 영역 수정 X (예외: AgentGateModal.tsx·OtpVerifyModal.tsx만 추가 OK)--no-verify·--force push Xnext.config·package.json 수정 X (의존성 추가 금지)npx tsc --noEmit + npx eslint + npm run build#ff5c00 / hover #cc4900 / light #fff6f1await params 필수PM 디스패치 받음. 두 STAGE 진행. 중간 게이트 ★ 박혀있음.
[FULL CONTEXT]
- 리포: /home/tlswkehd/projects/keeper-direct (cd 가정)
- 브랜치: staging
- Supabase: hvcrbdpjxfszhwrtcygq
- 시작 전 docs/keeper-out/anti-abuse/06-p3-implementation-brief.md §0 필독
- AGENTS.md / .claude/CLAUDE.md 룰 준수
- pain_points / lead detail / layout 분리 / P2 4섹션은 이미 main에 박혔음 (4ab6eb4 머지)
═══════════════════════════════════════════════
STAGE 1 — hotfix 4건 (2 commits, ~1-2h)
═══════════════════════════════════════════════
[목적]
출격 첫날 즉시 노출 차단. production 즉시 배포.
[commit 1] supabase/migrations/20260506_keeper_outbound_hotfix.sql 신설
먼저 영향 row 수 확인:
Supabase MCP로 SELECT count(*) FROM keeper_outbound_leads
WHERE naver_place_id IS NULL AND created_at < now() - interval '1 day';
→ 0건이면 진행. 1건+ 이면 stop + PM 보고.
migration 내용:
delete from public.keeper_outbound_leads
where naver_place_id is null
and created_at < now() - interval '1 day';
alter table public.keeper_outbound_leads
alter column naver_place_id set not null;
create unique index if not exists uniq_leads_phone
on public.keeper_outbound_leads(phone);
Supabase apply_migration MCP로 적용 (project_id=hvcrbdpjxfszhwrtcygq).
파일도 마이그레이션 디렉토리에 추가.
커밋 메시지:
feat(keeper-out/hotfix): naver_place_id NOT NULL + phone lifetime UNIQUE
[commit 2] app/api/keeper-out/diagnose/route.ts 수정
먼저 lib/keeper-out/giftishow.ts에 checkBalance 함수 export 확인:
grep -n "checkBalance" lib/keeper-out/giftishow.ts
- 있으면: import만 추가
- 없으면: Giftishow /bizApi/balance 호출 함수 신설 후 export
route.ts 수정 위치 (sendNaverPayCoupon fire-and-forget 직전):
const balance = await checkBalance().catch(() => -1);
if (balance >= 0 && balance < 50000) {
await sbInsert("keeper_outbound_funnel_events", {
lead_id: lead.id,
event_type: "naver_pay_skipped_low_balance",
payload: { balance },
}).catch(() => {});
return NextResponse.json({
ok: true, lead_id: lead.id, scenario, expected_benefit: benefit,
payback_token: lead.id, payout_deferred: true,
});
}
const DAY_CAP = parseInt(process.env.KEEPER_OUT_DAY_CAP || "200", 10);
const today = new Date().toISOString().slice(0, 10);
const sentToday = await sbSelect("keeper_outbound_payouts",
`select=id&payout_type=eq.naver_pay_10k&status=eq.sent&paid_at=gte.${today}T00:00:00Z`);
if (sentToday.length >= DAY_CAP) {
await sbInsert("keeper_outbound_funnel_events", {
lead_id: lead.id,
event_type: "naver_pay_skipped_day_cap",
payload: { day_cap: DAY_CAP, sent_today: sentToday.length },
}).catch(() => {});
return NextResponse.json({
ok: true, lead_id: lead.id, scenario, expected_benefit: benefit,
payback_token: lead.id, payout_deferred: true,
});
}
커밋 메시지:
feat(keeper-out/hotfix): checkBalance preflight + day cap (DAY_CAP env, default 200)
[STAGE 1 머지]
1. npx tsc --noEmit → 0
2. npx eslint app/api/keeper-out/diagnose/route.ts lib/keeper-out/giftishow.ts → 0
3. npm run build → 성공
4. git push origin staging
5. (대기 ~2분) Vercel preview URL 200 확인
6. git checkout main && git pull origin main
7. git merge staging -m "Merge staging: keeper-out hotfix 4건 (naver_place_id+phone+balance+cap)"
8. git push origin main
9. (대기 ~2분) curl -s -o /dev/null -w "%{http_code}\n" https://www.keeper-direct.ceo/diagnose → 200
10. git checkout staging
[STAGE 1 끝 — 게이트 ★]
다음 형식으로 PM에게 1차 보고:
## STAGE 1 hotfix 결과 보고
### 1. 커밋 sha 2개 + main merge sha
### 2. delete 영향 row 수 (0이면 명시)
### 3. tsc/eslint/build 결과
### 4. production /diagnose HTTP code
### 5. STAGE 2 진입 대기 — 자동님 confirm 받으면 진행
자동님이 OK 주기 전까지 STAGE 2 진입 절대 X.
═══════════════════════════════════════════════
STAGE 2 — P3 v3 풀 구현 (8 commits, ~6h)
═══════════════════════════════════════════════
[자동님 confirm 받은 후 시작]
docs/keeper-out/anti-abuse/06-p3-implementation-brief.md 섹션 1~8 순서대로 진행.
룰:
- 각 commit 후 tsc/eslint/build 자가 점검
- §0 시작 전 필독 (절대 금지 8건 + 따를 것 8건) 준수
- T+20~24h 정책 (commit 8 cron 작업 시) 자동님께 (a)/(b)/(c) 결정 받기.
받기 전엔 (c) 하이브리드 채택 명시 후 진행 OR PR 보류
- 커밋 단위 8개 (06-brief §5 그대로)
- staging에만 push, main 머지는 PM 검증 후 자동님 결정
[STAGE 2 끝 — 보고 형식]
## P3 v3 풀 구현 결과 보고
### 1. 커밋 sha 8개 + 메시지
### 2. 변경 파일 (신규/수정 분리)
### 3. 검증 결과 (각 commit 별 tsc/eslint/build)
### 4. T+20~24h 정책 자동님 결정 + 채택 옵션
### 5. 새 endpoint 11개 동작 확인 (POST /diagnose/request-otp 등)
### 6. UI 신규 페이지 2건 + 신규 섹션 4건 + 신규 컴포넌트 5건 렌더 확인
### 7. cap 룰 + ALL HOLD 동작 확인
### 8. staging push 후 preview URL 200
### 9. main 머지 대기 — PM 검증 후 자동님 결정
[가드레일 — 전체 공통]
- 청록 #009090 / #00b4b4 / #006B6B / #007575 / #e6f7f7 사용 X
- /diagnose/* 디렉토리 수정 X (예외: AgentGateModal.tsx · OtpVerifyModal.tsx만 추가 OK)
- "한화"·"키퍼"·"하나비전" 사용자 face 텍스트 X
- next.config / package.json 수정 X
- main 강제 푸시 X / --no-verify X
- untracked 13개 보존 (CLAUDE.md / catchrank / landing / context / data / scripts / docs / public/assets)
[시작]
지금 STAGE 1부터 진행. 각 단계 후 다음으로. 충돌/장애 시 즉시 stop + 보고.
docs/keeper-out/anti-abuse/06-p3-implementation-brief.md