app/keeper-out/admin/leads/[id]/ 신규 페이지를 만들어 lead 한 건의
고객·매장·pain·사진·영업자·지급·이벤트 8섹션을 한 화면에 펼친다.
스코프는 어제 박은 보고서 §05의 P1 컬럼만 — visit_logs / owner / place 마스터는 P2로 미룸.
| 상태 | 내용 |
|---|---|
| DB 박힘 | keeper_outbound_leads.pain_points text[] + pain_note text + GIN 인덱스 (Supabase hvcrbdpjxfszhwrtcygq에 적용 완료) |
| untracked | 3 파일 변경 + 1 마이그레이션 신규 — staging에 아직 commit 안 됨. 코덱스가 P1 작업 시작 전 이 변경분을 별도 commit으로 박은 뒤 P1 진행 |
| 컨벤션 살아있음 | 색깔 룰: /keeper-out/admin/*은 키퍼 오렌지 #ff5c00 (청록 금지). 영업원 컨텍스트. |
| 기반 코드 | AdminClient.tsx + PipelineRow + lib/keeper-out/supabase.ts + lib/keeper-out/types.ts |
| 리포 root | /home/tlswkehd/projects/keeper-direct |
| 현재 브랜치 | staging (main push 금지 — AGENTS.md 룰) |
| Supabase 프로젝트 | hvcrbdpjxfszhwrtcygq (URL https://hvcrbdpjxfszhwrtcygq.supabase.co) |
| service_role key | SUPABASE_SERVICE_ROLE_KEY 환경변수 — lib/keeper-out/supabase.ts가 사용 |
| Photo bucket | Supabase Storage keeper-out-photos (public, URL은 photo_urls text[]에 그대로 박힘) |
| Vercel Blob | 로고/브랜드 이미지용 별도 (이번 작업 무관) |
| Next.js | 15+ App Router, RSC + Client Component 혼합. params는 Promise — await params 필수 |
#009090 / #00b4b4 / #006B6B / #007575 / #e6f7f7 청록 토큰 일체 사용 금지 (이건 /diagnose 전용)app/diagnose/* 디렉토리 절대 수정 금지 (격리 영역)globals.css의 .eiTgBE·.iqRQFp·.bPtSge 등 keeper.ceo scraped 클래스 수정 금지| 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 패턴 그대로 차용.
| 종류 | 경로 | 역할 |
|---|---|---|
| 신규 | app/keeper-out/admin/leads/[id]/page.tsx | RSC 셸 — 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.tsx | row를 <Link href="/keeper-out/admin/leads/{id}">로 감싸기 (또는 store_name 클릭만) |
| 수정 | lib/keeper-out/types.ts | LeadDetail 인터페이스 신규 (Lead + agents/payouts/events embedded) |
기존 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 });
}
}
!keeper_outbound_leads_agent_1_fkey 형식의 hint는 PostgREST가 모호한 관계 추론 시
필요합니다. 실제 FK 이름은 Supabase에서 자동 생성되므로 다음 중 하나로 시도:
(1) !agent_1 (컬럼명 hint), (2) !keeper_outbound_leads_agent_1_fkey,
(3) Supabase introspect 후 실 이름 박기. 코덱스가 둘 다 시도하고 동작하는 쪽 채택.
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[];
}
모든 섹션은 한 페이지에 세로 스택. 모바일 우선(max-w-3xl mx-auto) — 영업원이 폰에서 본다.
| # | 섹션 이름 | 표시 데이터 | 특이사항 |
|---|---|---|---|
| 1 | Hero | 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="설치완료" 등. |
const PAIN_TALKING: Record<string, string> = {
"출동지연": "출동시간 SLA 보증 강조",
"요금부담": "결합 영업 / 가격 비교",
"약정부담": "위약금 대납 시나리오",
"사각지대": "카메라 위치/대수 보강 제안",
"AS불만": "커뮤니케이션 가치 강조",
"앱불편": "모던 UX 차별화",
};
PipelineRow의 store_name 영역 (또는 row 통째)을 <Link href={`/keeper-out/admin/leads/${lead.id}`}>로 감싸기. 단, 4 stage 진행 버튼은 e.stopPropagation()로 클릭 전파 차단 (advance 클릭이 라우팅으로 가지 않게).app/keeper-out/admin/leads/[id]/page.tsx 상단에 "← 대시보드" 링크 (/keeper-out/admin으로 복귀).npx tsc --noEmit — 출력 0줄 (에러 없음)npx eslint app/api/keeper-out/lead/[id]/route.ts app/keeper-out/admin/leads/[id]/page.tsx app/keeper-out/admin/leads/[id]/LeadDetailClient.tsx app/keeper-out/admin/components/PipelineRow.tsx lib/keeper-out/types.ts — 출력 0줄npm run build — 성공 (Next.js production build)/keeper-out/admin 접속 → lead row 클릭 → detail 페이지 진입 확인git add supabase/migrations/20260505_keeper_outbound_pain.sql lib/keeper-out/types.ts app/api/keeper-out/diagnose/route.tsfeat(keeper-out): /diagnose pain_points·pain_note 백엔드 처리
git add app/api/keeper-out/lead/[id]/route.ts lib/keeper-out/types.tsfeat(keeper-out/admin): lead GET endpoint with PostgREST embed
git add app/keeper-out/admin/leads/[id]/ app/keeper-out/admin/components/PipelineRow.tsxfeat(keeper-out/admin): lead detail view P1 (8섹션)
app/landing/PLAN.md · app/landing/keeper/ · app/landing/outbound/ (별개 작업)context/ · data/ · scripts/ · docs/ · public/assets/ (데이터/스크립트 덤프)app/api/keeper-out/catchrank/sync-places/ (다른 작업)lib/keeper-out/catchrank.ts 수정분 (다른 작업)CLAUDE.md 수정분 (claude-station auto-update)아래 박스 통째 복사해서 코덱스 세션에 붙여넣으세요:
당신은 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 박은 뒤 위 보고 형식대로 회신할 것.
grep -rln "#009090" app/keeper-out/admin/) 0건인지/home/tlswkehd/projects/keeper-direct/AGENTS.mdfeedback_diagnose_design_isolation.md