Codex 작업지시서 · keeper-out admin Lead Detail P1

2026-05-05 · PM/CTO → Codex CLI · keeper-direct staging · 8섹션 + GET embed + 클릭 이동

이 문서 사용법

  1. 자동님이 § 11의 "코덱스 프롬프트 (복붙 박스)"를 통째 복사해 코덱스 세션에 붙여넣기
  2. 코덱스가 작업 후 § 12의 보고 템플릿 형태로 결과 회신
  3. 자동님이 그 회신을 PM(저)에게 전달 → 검증 → 다음 라운드 디스패치

01.미션 한 줄

app/keeper-out/admin/leads/[id]/ 신규 페이지를 만들어 lead 한 건의 고객·매장·pain·사진·영업자·지급·이벤트 8섹션을 한 화면에 펼친다. 스코프는 어제 박은 보고서 §05의 P1 컬럼만 — visit_logs / owner / place 마스터는 P2로 미룸.

02.전제 (이미 박힌 것)

상태내용
DB 박힘keeper_outbound_leads.pain_points text[] + pain_note text + GIN 인덱스 (Supabase hvcrbdpjxfszhwrtcygq에 적용 완료)
untracked3 파일 변경 + 1 마이그레이션 신규 — staging에 아직 commit 안 됨. 코덱스가 P1 작업 시작 전 이 변경분을 별도 commit으로 박은 뒤 P1 진행
컨벤션 살아있음색깔 룰: /keeper-out/admin/*은 키퍼 오렌지 #ff5c00 (청록 금지). 영업원 컨텍스트.
기반 코드AdminClient.tsx + PipelineRow + lib/keeper-out/supabase.ts + lib/keeper-out/types.ts

03.리포 / 인프라 컨텍스트

리포 root/home/tlswkehd/projects/keeper-direct
현재 브랜치staging (main push 금지 — AGENTS.md 룰)
Supabase 프로젝트hvcrbdpjxfszhwrtcygq (URL https://hvcrbdpjxfszhwrtcygq.supabase.co)
service_role keySUPABASE_SERVICE_ROLE_KEY 환경변수 — lib/keeper-out/supabase.ts가 사용
Photo bucketSupabase Storage keeper-out-photos (public, URL은 photo_urls text[]에 그대로 박힘)
Vercel Blob로고/브랜드 이미지용 별도 (이번 작업 무관)
Next.js15+ App Router, RSC + Client Component 혼합. params는 Promise — await params 필수

04.색깔 / 디자인 룰 (절대 어기지 말 것)

✗ 금지

★ 따를 것 (admin 키퍼 오렌지 토큰)

primary#ff5c00
hover#cc4900
alt#e65200
light bg#fff6f1
border#e8e4db
text dim#666
page bg#ffffff (라이트 모드 — 기존 admin 톤 유지)

참조 파일: app/keeper-out/admin/AdminClient.tsx + components/PipelineRow.tsx의 className 패턴 그대로 차용.

05.새/수정 파일 정확한 경로

종류경로역할
신규app/keeper-out/admin/leads/[id]/page.tsxRSC 셸 — id 받아서 LeadDetailClient에 props 패스
신규app/keeper-out/admin/leads/[id]/LeadDetailClient.tsx"use client". 8섹션 렌더 + status 액션
수정app/api/keeper-out/lead/[id]/route.ts기존 PATCH 유지 + GET 추가 (PostgREST embed)
수정app/keeper-out/admin/components/PipelineRow.tsxrow를 <Link href="/keeper-out/admin/leads/{id}">로 감싸기 (또는 store_name 클릭만)
수정lib/keeper-out/types.tsLeadDetail 인터페이스 신규 (Lead + agents/payouts/events embedded)

06.API GET endpoint 스펙

기존 app/api/keeper-out/lead/[id]/route.ts의 PATCH는 그대로 두고 GET 함수 추가:

import { sbSelect } from "@/lib/keeper-out/supabase";

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  try {
    // PostgREST embed 한 방 — agent_1/2/3 FK + payouts/events 역참조
    const rows = await sbSelect<LeadDetail>(
      "keeper_outbound_leads",
      `select=*,
        agent_1_data:keeper_outbound_agents!keeper_outbound_leads_agent_1_fkey(id,name,phone,role),
        agent_2_data:keeper_outbound_agents!keeper_outbound_leads_agent_2_fkey(id,name,phone,role),
        agent_3_data:keeper_outbound_agents!keeper_outbound_leads_agent_3_fkey(id,name,phone,role),
        payouts:keeper_outbound_payouts(id,payout_type,recipient_agent_id,recipient_phone,amount,status,paid_at,created_at),
        events:keeper_outbound_funnel_events(id,event_type,payload,created_at)
        &id=eq.${id}&events.order=created_at.asc&payouts.order=created_at.asc`
    );
    if (!rows[0]) {
      return NextResponse.json({ error: "not found" }, { status: 404 });
    }
    return NextResponse.json({ ok: true, lead: rows[0] });
  } catch (e) {
    return NextResponse.json({ error: (e as Error).message }, { status: 500 });
  }
}

⚠️ FK 이름 확인

!keeper_outbound_leads_agent_1_fkey 형식의 hint는 PostgREST가 모호한 관계 추론 시 필요합니다. 실제 FK 이름은 Supabase에서 자동 생성되므로 다음 중 하나로 시도:
(1) !agent_1 (컬럼명 hint), (2) !keeper_outbound_leads_agent_1_fkey, (3) Supabase introspect 후 실 이름 박기. 코덱스가 둘 다 시도하고 동작하는 쪽 채택.

Response shape — types.ts에 박을 것

import type { Lead, Agent, Payout, FunnelEvent } from "./types";

export interface LeadDetail extends Lead {
  agent_1_data: Agent | null;
  agent_2_data: Agent | null;
  agent_3_data: Agent | null;
  payouts: Payout[];
  events: FunnelEvent[];
}

07.UI 8섹션 — 데이터 매핑

모든 섹션은 한 페이지에 세로 스택. 모바일 우선(max-w-3xl mx-auto) — 영업원이 폰에서 본다.

#섹션 이름표시 데이터특이사항
1Hero store_name (큰 글씨), business_no, scenario badge (A 72만 / B 92만), expected_benefit, status pipeline (4단계 emoji), created_at relative PipelineRow의 STAGES 재사용. 큰 사이즈로 다시 렌더.
2고객 ceo_name, phone (tel: 링크), naver_place_id (네이버 플레이스 새 탭), marketing_consent_at 전화 클릭 시 즉시 다이얼.
3현재 보안 current_security, device_age, camera_count, monthly_fee (₩), combined_isp 5개 stat 카드 그리드.
4★ Pain Points pain_points 배열을 chip으로 → 영업분기 매핑 표시 (출동지연→SLA 등). pain_note는 인용 박스. 매핑 사전을 LeadDetailClient 안에 const로 박기.
5사진 갤러리 photo_urls.map 그리드. 클릭 시 새 탭 (또는 lightbox — Phase 1은 새 탭으로 충분). next/image는 외부 도메인 설정 필요해서 일단 <img> 사용 OK.
6영업자 1·2·3 agent_1_data / 2_data / 3_data 각 카드 (이름 + role + phone). null이면 "미배정". 배정 폼은 P2. 일단 read-only.
7지급/인센티브 payouts 배열을 type별 그룹 (lead_30k·install_50k_per_unit·naver_pay_10k) — 각 행에 amount + status + recipient. 합계 카드 1개 (총 지급 / 대기). status badge 컬러: pending=yellow, sent=green, failed=red.
8이벤트 타임라인 events 시간순 (오래된→최근). 각 항목: created_at + event_type + payload 요약. event_type 한글 매핑: diagnose_submitted="진단 제출", naver_pay_sent_auto="네이버페이 자동발송", naver_pay_failed="네이버페이 실패", installed="설치완료" 등.

섹션 4 매핑 사전 (그대로 박을 것)

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

08.네비게이션 wiring

  1. PipelineRow의 store_name 영역 (또는 row 통째)을 <Link href={`/keeper-out/admin/leads/${lead.id}`}>로 감싸기. 단, 4 stage 진행 버튼은 e.stopPropagation()로 클릭 전파 차단 (advance 클릭이 라우팅으로 가지 않게).
  2. app/keeper-out/admin/leads/[id]/page.tsx 상단에 "← 대시보드" 링크 (/keeper-out/admin으로 복귀).

09.검증 체크리스트 (코덱스가 자가 점검)

10.커밋 + 보고 규칙

커밋 단위

  1. 커밋 1 — pain_points backend (선행 정리):
    git add supabase/migrations/20260505_keeper_outbound_pain.sql lib/keeper-out/types.ts app/api/keeper-out/diagnose/route.ts
    메시지: feat(keeper-out): /diagnose pain_points·pain_note 백엔드 처리
  2. 커밋 2 — GET endpoint + types:
    git add app/api/keeper-out/lead/[id]/route.ts lib/keeper-out/types.ts
    메시지: feat(keeper-out/admin): lead GET endpoint with PostgREST embed
  3. 커밋 3 — detail page + nav:
    git add app/keeper-out/admin/leads/[id]/ app/keeper-out/admin/components/PipelineRow.tsx
    메시지: feat(keeper-out/admin): lead detail view P1 (8섹션)
⚠️ git add 주의 — 다음 untracked는 건드리지 말 것:

11.코덱스 프롬프트 (복붙 박스)

아래 박스 통째 복사해서 코덱스 세션에 붙여넣으세요:

📋 CODEX PROMPT — 통째 복사 (아래 코드 블록 전체)
당신은 Codex CLI로 keeper-direct 레포에서 작업한다.

[CONTEXT]
- 리포: /home/tlswkehd/projects/keeper-direct (이미 cd 되어있다고 가정)
- 브랜치: staging (확인 필수, main push 금지)
- Supabase 프로젝트: hvcrbdpjxfszhwrtcygq
- DB 컬럼 pain_points text[] / pain_note text는 이미 박혀있음
- AGENTS.md / .claude/CLAUDE.md 존재 — 시작 전 한 번 읽기

[MISSION]
P1: app/keeper-out/admin/leads/[id]/ 신규 페이지로 lead detail 8섹션을
한 화면에 펼친다. visit_logs / owner / place 마스터는 P2 (이번 작업 X).

[SCOPE — 정확히]

(0) 선행 정리 — 이미 박혀있지만 untracked인 3 파일을 staging에 commit:
  - supabase/migrations/20260505_keeper_outbound_pain.sql
  - lib/keeper-out/types.ts (DiagnoseSubmitPayload + Lead에 pain_points/pain_note 추가됨)
  - app/api/keeper-out/diagnose/route.ts (sbInsert에 pain_points/pain_note 추가됨)
  - 커밋 메시지: "feat(keeper-out): /diagnose pain_points·pain_note 백엔드 처리"
  - 같은 디렉토리의 다른 untracked는 절대 add 하지 말 것 (목록은 PM 작업지시서 §10)

(1) GET endpoint 추가 — app/api/keeper-out/lead/[id]/route.ts
  - 기존 PATCH는 그대로 둠
  - GET 함수 추가, params는 Promise (await 필수)
  - PostgREST embed 한 번에:
    select=*,
      agent_1_data:keeper_outbound_agents!agent_1(id,name,phone,role),
      agent_2_data:keeper_outbound_agents!agent_2(id,name,phone,role),
      agent_3_data:keeper_outbound_agents!agent_3(id,name,phone,role),
      payouts:keeper_outbound_payouts(*),
      events:keeper_outbound_funnel_events(*)
    &id=eq.{id}
    &events.order=created_at.asc
    &payouts.order=created_at.asc
  - lib/keeper-out/supabase.ts의 sbSelect 사용 (service_role 자동 적용)
  - FK hint 형식 두 가지 시도: !agent_1 (컬럼명) 또는 !keeper_outbound_leads_agent_1_fkey
    → 첫 번째 실패 시 두 번째로 fallback. 동작 확인 후 채택한 것 코드에 박기
  - 404 처리 (rows[0] 없으면)
  - try/catch + NextResponse.json 패턴

(2) lib/keeper-out/types.ts — LeadDetail 인터페이스 추가
  export interface LeadDetail extends Lead {
    agent_1_data: Agent | null;
    agent_2_data: Agent | null;
    agent_3_data: Agent | null;
    payouts: Payout[];
    events: FunnelEvent[];
  }

(3) 신규 파일 — app/keeper-out/admin/leads/[id]/page.tsx
  - RSC. params Promise await
  - LeadDetailClient에 id props 전달
  - 상단에 "← 대시보드" 링크 (/keeper-out/admin)

(4) 신규 파일 — app/keeper-out/admin/leads/[id]/LeadDetailClient.tsx
  - "use client"
  - useEffect로 GET /api/keeper-out/lead/{id} fetch
  - 8섹션 렌더 (아래)
  - 색깔: 키퍼 오렌지 #ff5c00 / hover #cc4900 / light #fff6f1 / border #e8e4db
  - 페이지 bg #ffffff (라이트 톤 — 기존 admin 일관성)
  - 모바일 우선: max-w-3xl mx-auto

  섹션 1 Hero: store_name (큰 글씨) / business_no / scenario badge (A 72만 / B 92만 통신결합) /
              expected_benefit / status pipeline 4단계 (PipelineRow의 STAGES 재사용 가능 형태)
  섹션 2 고객: ceo_name / phone (tel: 링크) / naver_place_id (있으면 네이버 플레이스 링크 새탭) /
              marketing_consent_at (relative time)
  섹션 3 현재 보안: current_security / device_age / camera_count / monthly_fee (₩ format) /
                combined_isp — 5개 stat 카드 그리드
  섹션 4 ★ Pain Points: pain_points.map → chip + 매핑 사전 표시. pain_note는 인용 박스(blockquote)
              매핑 사전 (그대로 박기):
              const PAIN_TALKING = {
                "출동지연": "출동시간 SLA 보증 강조",
                "요금부담": "결합 영업 / 가격 비교",
                "약정부담": "위약금 대납 시나리오",
                "사각지대": "카메라 위치/대수 보강 제안",
                "AS불만": "커뮤니케이션 가치 강조",
                "앱불편": "모던 UX 차별화",
              };
  섹션 5 사진: photo_urls.map → <img> 그리드 (next/image 안 써도 OK).
              클릭 시 새 탭 열기 (target="_blank").
              빈 배열이면 "사진 없음" 표시.
  섹션 6 영업자: agent_1_data / agent_2_data / agent_3_data 각 카드.
              null이면 "미배정" 카드 (배정 폼은 P2).
  섹션 7 지급: payouts.map → type별 그룹 (lead_30k / install_50k_per_unit / naver_pay_10k)
              각 행: amount(₩ format) / status badge (pending=yellow, sent=green, failed=red) /
              recipient_phone or recipient_agent_id / paid_at
              합계 카드: 총 지급액 / 대기 금액
  섹션 8 이벤트 타임라인: events.map (시간 오래된→최근).
              event_type 한글 매핑:
              "diagnose_submitted"="진단 제출",
              "naver_pay_sent_auto"="네이버페이 자동발송",
              "naver_pay_failed"="네이버페이 실패",
              "contacted"="콜매칭",
              "installed"="설치완료",
              그 외는 raw 키 그대로
              payload는 JSON.stringify 짧게 또는 주요 필드만

(5) app/keeper-out/admin/components/PipelineRow.tsx 수정
  - 매장명 영역(상단 좌측 div)을 <Link href={`/keeper-out/admin/leads/${lead.id}`}>로 감싸기
  - 4단계 진행 버튼들은 e.stopPropagation() 추가 — 클릭이 라우팅으로 가지 않게
  - "💸 N페이 송금 대기" 버튼도 stopPropagation

[가드레일 — 절대 하지 말 것]
- 청록 #009090 / #00b4b4 / #006B6B / #007575 / #e6f7f7 사용 X (이건 /diagnose 전용)
- app/diagnose/* 디렉토리 수정 X
- globals.css의 keeper.ceo scraped 클래스 (.eiTgBE 등) 수정 X
- "한화" / "키퍼" / "하나비전" 단어를 페이지에 박지 말 것
- next.config 수정 X
- package.json 수정 X (의존성 추가 금지 — 기존 framer-motion / next는 OK)
- main 브랜치 push X
- --no-verify / --no-gpg-sign 등 hook bypass X
- supabase/migrations 새 파일 추가 X (이번 작업은 컬럼 추가 없음)

[검증 체크리스트 — 보고 전 자가 점검 필수]
1. npx tsc --noEmit → 0 출력
2. npx eslint <변경 파일들> → 0 출력
3. npm run build → 성공
4. git status → 의도한 파일만 staged
5. (가능하면) npm run dev 띄우고 실제 lead 1건의 detail URL 접속 확인

[보고 형식 — 작업 끝나면 이 템플릿으로 회신]

## P1 작업 결과 보고

### 1. 커밋 목록
- <sha> feat(keeper-out): /diagnose pain_points·pain_note 백엔드 처리
- <sha> feat(keeper-out/admin): lead GET endpoint with PostgREST embed
- <sha> feat(keeper-out/admin): lead detail view P1 (8섹션)

### 2. 변경 파일
신규:
- app/keeper-out/admin/leads/[id]/page.tsx
- app/keeper-out/admin/leads/[id]/LeadDetailClient.tsx
수정:
- app/api/keeper-out/lead/[id]/route.ts (GET 추가)
- app/keeper-out/admin/components/PipelineRow.tsx (Link wrap)
- lib/keeper-out/types.ts (LeadDetail 인터페이스)

### 3. 검증 결과
- tsc: PASS / FAIL (출력 그대로 붙여넣기)
- eslint: PASS / FAIL
- build: PASS / FAIL (실패 시 마지막 30줄)
- 실제 lead URL 열어본 결과: (있으면) 어떤 데이터 떴는지 / 깨진 섹션 있는지

### 4. 결정 사항 (코덱스 자체 판단한 것 명시)
- FK hint 어떤 형식 채택 (!agent_1 vs !..._fkey)
- agent JOIN 결과 null 표시 방식
- (그 외 자체 판단한 것)

### 5. 막힌 곳 / 의문
(있으면 명시. 없으면 "없음")

### 6. P2 후보 / 발견 (선택)
작업 중 눈에 띈 후속 개선 아이디어 (있으면)

[시작]
지금 작업 시작. 8섹션 모두 박고, tsc/eslint/build 모두 PASS인 상태로 3 commit 박은 뒤 위 보고 형식대로 회신할 것.

12.코덱스 회신 받은 뒤 PM 처리 흐름

  1. 자동님이 코덱스 결과(보고 6 섹션)를 PM(저)에게 전달
  2. PM이 검증:
    • 커밋 sha 3개 git show로 실제 변경 검증
    • tsc/eslint/build 결과 정합성
    • 잘못 add된 파일 (untracked 룰 위반) 있는지
    • 색깔 룰 위반(grep -rln "#009090" app/keeper-out/admin/) 0건인지
  3. 이상 시 fix 작업지시서 작성 → 다음 라운드 디스패치
  4. 정상 시 자동님께 production 배포 또는 P2 진입 결정 요청 (다음 보고서)

참조 문서