Codex 작업지시서 #3 · STAGE 1 hotfix → STAGE 2 P3 v3

2026-05-06 · 출격 첫날 즉시 노출 차단 + 어뷰징 풀 방어 · 9 commits / ~7시간 / 2 라운드

두 STAGE 한 작업지시서, 단 중간 게이트 박힘: STAGE 1(2 commits) 완료 → production 배포 → 자동님 confirm → STAGE 2(8 commits) 진입. 이 게이트가 9 commit 한 방에 던지는 것보다 안전.

01.전체 흐름

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 머지 결정
★ idempotent 정합성
STAGE 1 — hotfix 4건 (2 commits, ~1-2h)
목표: production 즉시 배포로 출격 첫날 노출 차단

02.STAGE 1 — Migration (commit 1)

경로: 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 전 데이터 확인

코덱스가 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

03.STAGE 1 — Route hotfix (commit 2)

파일: app/api/keeper-out/diagnose/route.ts 수정

변경 1: checkBalance preflight

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,    // 클라이언트는 진단 완료로 처리
  });
}
⚠️ checkBalance 함수 존재 확인

lib/keeper-out/giftishow.tscheckBalance export 되어있는지 grep으로 확인. 없으면 코덱스가 신설 (Giftishow API /bizApi/balance 호출). 있으면 import만 추가.

변경 2: 일일 송금 cap

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)

STAGE 1 끝 — staging push → main merge → production

⚠️ STAGE 1 / STAGE 2 사이 게이트

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 받으면 진행
STAGE 2 — P3 v3 풀 구현 (8 commits, ~6h)
목표: anti-abuse 시스템 + 24h 휴먼 승인 큐 라이브

04.STAGE 2 — 06-brief 그대로 진입

참조: docs/keeper-out/anti-abuse/06-p3-implementation-brief.md

#커밋 메시지분량
1feat(keeper-out): migration v3 — agent code, leads approval columns, otp/fraud/system_state tablesmigration only
2feat(keeper-out): /diagnose request-otp + verify endpoints + AgentGateModal + OtpVerifyModalAPI 2개 + 컴포넌트 2개
3feat(keeper-out/admin): pending queue page + TM call APIpage 1 + API 2
4feat(keeper-out/admin): dispatch + reject API + balance preflight + cap checkAPI 2 + cap util
5feat(keeper-out/admin): fallback queue page (TM 6h escalate)page 1
6feat(keeper-out/admin): ALL HOLD toggle + system_state CRUDcomponent + API
7feat(keeper-out/admin): LeadDetailClient sections 14·15·16·17 (검증/외부매칭/승인/액션)4 섹션 추가
8feat(keeper-out): cron — recheck-completion + escalate-fallback (24h dual-layer)cron 2개

중간 결정 시점 (T+20~24h 정책)

commit 8 (cron) 작업 시 자동님께 옵션 (a)/(b)/(c) 결정 받기. 기본값 (c) 하이브리드. 받기 전엔 코드 PR 보류 또는 (c) 채택 명시 후 진행.

05.가드레일 (전체 STAGE 공통)

06.코덱스 프롬프트 (복붙)

📋 같은 세션 또는 새 세션에 붙여넣기
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 + 보고.

07.PM 검증 흐름

  1. STAGE 1 보고 받으면: 2 commit + main merge sha 검증, production URL hotfix 동작 확인 (예: phone 중복 → 409 응답), 자동님께 STAGE 2 진입 confirm 요청 보고서
  2. 자동님 confirm 후: 코덱스에게 "STAGE 2 진입 OK" 짧은 회신
  3. STAGE 2 보고 받으면: 8 commit 정합성 + grep 색깔 룰 + 새 endpoint 호출 테스트 + 기존 12섹션 + 신규 4섹션 production 안 됨 (staging만) → 자동님 main 머지 결정 보고서

참조 흐름

  1. P1 + 머지 — production live (4ab6eb4)
  2. v3 anti-abuse 자동님 승인 — 66KB 풀 보고서
  3. in-repo entry — docs/keeper-out/anti-abuse/06-p3-implementation-brief.md
  4. PM 컨텍스트 동기화 — 5/6 흡수
  5. 이 문서 — Codex Brief #3 (STAGE 1+2)