Codex 작업지시서 #2 · 같은 세션 이어서

2026-05-05 23:00 · P1 PASS 직후 · 동일 코덱스 세션 · 3단: P1.5 layout 분리 → P2 4섹션 → main 머지

3단 순서 (한 세션에 연속)

  1. P1.5 — admin layout 분리 (1 commit, ~15분)
  2. P2 — 4섹션 추가 (3 commits, ~90분)
  3. main 머지 + push (Vercel production 자동 배포)

이미 알고 있는 컨텍스트: 코덱스 같은 세션이므로 P1 작업지시서의 룰 (색깔=오렌지, /diagnose 격리, untracked 보존, sbSelect/PostgREST 패턴, params Promise, Next.js 15) 모두 살아있음. 이 문서는 새 스코프만 명시.

01.P1.5 — admin layout 분리

현재 상태

// app/keeper-out/layout.tsx (현재 — root keeper-out 공통)
export const metadata = {
  title: "매장보안 진단 | Keeper",   // 사장님 진단용 카피
  description: "10초 진단 + N페이 1만원 즉시지급",
};

export default function KeeperOutLayout({ children }) {
  return (
    <div className="min-h-screen bg-white text-[#1a1a1a]">
      <AppHeader />     // ← cashon 로고 + "매장 지원금 진단" 뱃지
      {children}
    </div>
  );
}

이 layout이 /keeper-out/admin/*에도 자동 적용 → 영업원 대시보드에 마케팅 헤더 끼어듦.

해야 할 것

신규app/keeper-out/admin/layout.tsxadmin 전용 layout — Next.js nested layout으로 root override

스펙

// app/keeper-out/admin/layout.tsx (신규)
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Keeper Out Dashboard",
  description: "키퍼 아웃바운드 영업 워크벤치",
};

export default function AdminLayout({
  children,
}: { children: React.ReactNode }) {
  // admin은 키퍼 오렌지 톤 영업원 컨텍스트.
  // AppHeader (cashon 마케팅 헤더) 끼우지 않음 — root layout의 헤더 격리.
  return <>{children}</>;
}
★ Next.js App Router nested layout 동작

커밋 단위

커밋 4: refactor(keeper-out/admin): root layout에서 마케팅 헤더 격리

02.P2 — 4섹션 추가

2.1 GET endpoint 확장 (1 commit)

기존 LEAD_DETAIL_SELECT 배열에 추가 (이미 const 분리되어있음 — push만):

const LEAD_DETAIL_SELECT = [
  // ... 기존 6 항목 유지
  // 신규 3 항목
  "visit_logs:keeper_outbound_visit_logs(*)",
  "place:keeper_outbound_places!place_id(*)",
  "owner:keeper_outbound_owners!owner_id(id,phone,name,total_payback)",
].join(",");

// 추가 query 파라미터
const query = new URLSearchParams({
  select: LEAD_DETAIL_SELECT,
  id: `eq.${id}`,
  "events.order": "created_at.asc",
  "payouts.order": "created_at.asc",
  "visit_logs.order": "visited_at.desc",    // 최근 방문 위로
});

2.2 owner 다른 매장 별도 fetch

owner.id로 다른 leads를 PostgREST self-reference 하면 복잡. 별도 query로 단순하게:

// owner_id가 있을 때만 추가 fetch
let other_leads: Lead[] = [];
if (rows[0].owner_id) {
  const otherQuery = new URLSearchParams({
    select: "id,store_name,status,scenario,expected_benefit,created_at",
    owner_id: `eq.${rows[0].owner_id}`,
    id: `neq.${id}`,                                // 자기 자신 제외
    order: "created_at.desc",
    limit: "10",
  });
  other_leads = await sbSelect<Lead>("keeper_outbound_leads", otherQuery.toString());
}
return NextResponse.json({ ok: true, lead: { ...rows[0], other_leads } });

2.3 LeadDetail types 확장

export interface LeadDetail extends Lead {
  // 기존 5 필드 유지
  agent_1_data: Agent | null;
  agent_2_data: Agent | null;
  agent_3_data: Agent | null;
  payouts: Payout[];
  events: FunnelEvent[];
  // 신규 4 필드
  visit_logs: VisitLog[];
  place: Place | null;
  owner: Owner | null;
  other_leads: Lead[];
}

VisitLog · Place · Owner는 이미 types.ts에 정의되어있음 (P1 작업 시 미사용). import만 추가.

2.4 4섹션 UI

#섹션데이터비고
9 방문 로그 (visit_logs) visited_at(최근→오래된) / promoter_id (agent JOIN 안 됨, ID만 표시) / step / outcome / notes / photo_urls 그리드 / audio_url 링크 / GPS 빈 배열이면 "방문 기록 없음" + 메모 추가 폼만 표시
10 점주 다른 매장 (other_leads) owner.name + owner.phone + total_payback / 다른 leads 카드 그리드 (store_name, status badge, scenario 혜택, created_at) owner 자체가 null이면 섹션 hide. other_leads 빈 배열이면 "다른 매장 없음"
11 매장 마스터 (place) address / road_address / district / closure_rate_pct (반경500m 폐업률) / priority_score / area_sqm / ceiling_height_m / business_reg_year place null이면 섹션 hide. 핵심: closure_rate_pct + priority_score는 강조 표시 (영업 anchoring)
13 방문 메모 추가 폼 step 드롭다운 (STEP1~4) / outcome 드롭다운 (completed/rejected/pending) / notes textarea / submit 방문 로그 섹션 안에 박기. POST 후 GET refetch
메모 추가 폼 — POST endpoint 재사용

이미 app/api/keeper-out/visits/route.ts에 POST 존재 — body는 { lead_id, step, outcome, notes, photo_urls? }. 새 endpoint 만들지 말고 이걸 그대로 사용. P1 단계에선 photo_urls / audio_url / GPS는 안 받음 (P3). 단순 텍스트 메모만.

커밋 단위 (3개)

  1. 커밋 5: feat(keeper-out/admin): GET endpoint에 visit_logs/place/owner embed + other_leads
  2. 커밋 6: feat(keeper-out/admin): lead detail P2 — 방문/점주/매장마스터 3섹션
  3. 커밋 7: feat(keeper-out/admin): lead detail 메모 추가 폼 (visits POST 재사용)

03.main 머지 + push

머지 자가 점검 — 마지막에 한 번 더

✗ 머지 시 절대 금지

04.3단계 stage 카드

STAGE 1 · ~15분 · 1 commit
P1.5 admin layout 분리
root keeper-out layout의 AppHeader를 admin path에서 격리. 방법 A 또는 B 코덱스 선택. 완료 후 /keeper-out/admin 직접 열어서 마케팅 헤더 사라진 것 시각 확인.
STAGE 2 · ~90분 · 3 commits
P2 4섹션 (방문/점주/매장마스터/메모폼)
GET endpoint 확장 → 4섹션 UI → 메모 추가 폼. 각 단계 후 tsc/build 자가 점검. 실제 lead URL 열어서 신규 섹션 빈 데이터 + 데이터 있는 케이스 둘 다 확인.
STAGE 3 · ~5분 · 머지
staging push → main fast-forward → production 배포
build PASS · preview 200 자가 확인 후 main 머지. 충돌 시 즉시 stop. 완료 후 www.keeper-direct.ceo/keeper-out/admin 200 확인.

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

📋 CODEX PROMPT — 같은 세션에 붙여넣기 (아래 통째)
P1 PASS 받았다. 이어서 3단 진행:

[STAGE 1 — P1.5 admin layout 분리, 1 commit, ~15분]

목표: /keeper-out/admin/* 에 root keeper-out layout의 AppHeader (cashon 마케팅 헤더)
가 끼어드는 걸 격리한다. /keeper-out/diagnose 등 사장님 진단 영역엔 영향 없게.

방법 둘 중 선택:
(A) Route group 분리 — /keeper-out/(public)/ 로 기존 page들 이동, admin은 sibling
(B) AppHeader를 client component로 만들어 usePathname()으로 admin 경로면 null 반환

→ B가 파일 이동 없어 안전. B 우선 시도. 단 AppHeader가 이미 "use client"인지 확인 후 결정.
  추가로 app/keeper-out/admin/layout.tsx도 신설:
  - metadata title="Keeper Out Dashboard", description="키퍼 아웃바운드 영업 워크벤치"
  - return <>{children}

검증: /keeper-out/admin → 마케팅 헤더 사라짐 / /keeper-out/diagnose → 헤더 그대로

커밋: refactor(keeper-out/admin): root layout에서 마케팅 헤더 격리

[STAGE 2 — P2 4섹션, 3 commits, ~90분]

(2.1) GET endpoint 확장 — app/api/keeper-out/lead/[id]/route.ts
LEAD_DETAIL_SELECT 배열에 push:
  "visit_logs:keeper_outbound_visit_logs(*)",
  "place:keeper_outbound_places!place_id(*)",
  "owner:keeper_outbound_owners!owner_id(id,phone,name,total_payback)",

URLSearchParams에 추가:
  "visit_logs.order": "visited_at.desc"

owner_id가 있을 때만 다른 매장 별도 fetch:
  if (rows[0].owner_id) {
    const otherQuery = new URLSearchParams({
      select: "id,store_name,status,scenario,expected_benefit,created_at",
      owner_id: `eq.${rows[0].owner_id}`,
      id: `neq.${id}`,
      order: "created_at.desc",
      limit: "10",
    });
    const other_leads = await sbSelect("keeper_outbound_leads", otherQuery.toString());
    return NextResponse.json({ ok: true, lead: { ...rows[0], other_leads } });
  }

types.ts LeadDetail에 추가 (VisitLog/Place/Owner는 이미 정의돼있음, import만 추가):
  visit_logs: VisitLog[];
  place: Place | null;
  owner: Owner | null;
  other_leads: Lead[];

커밋: feat(keeper-out/admin): GET endpoint에 visit_logs/place/owner embed + other_leads

(2.2) 3섹션 추가 — app/keeper-out/admin/leads/[id]/LeadDetailClient.tsx

섹션 9 방문 로그:
  - visit_logs.map (visited_at 최근→오래된 — 이미 서버 정렬됨)
  - 각 항목: visited_at relative / promoter_id (있으면 단축 표시) / step badge / outcome badge
            / notes 인용 / photo_urls 그리드 (있으면) / audio_url 다운로드 링크 / GPS (있으면 좌표)
  - 빈 배열이면 "방문 기록 없음" 텍스트 + 메모 추가 폼 영역만

섹션 10 점주 다른 매장:
  - lead.owner null이면 섹션 hide
  - lead.owner 카드: name (없으면 phone 일부 마스킹) / phone (tel:) / total_payback (₩ format)
  - lead.other_leads.map 카드 그리드:
    "→ {store_name}" (Link href={`/keeper-out/admin/leads/${id}`}) / status badge / scenario 혜택 / created_at relative
    other_leads 빈 배열이면 "다른 매장 없음"

섹션 11 매장 마스터:
  - lead.place null이면 섹션 hide
  - 강조: closure_rate_pct (반경500m 폐업률) + priority_score (영업 우선순위) — 큰 stat 카드
  - 보조: address / road_address / district / area_sqm / ceiling_height_m / business_reg_year

색깔: 키퍼 오렌지 #ff5c00 / hover #cc4900 / light #fff6f1 / border #e8e4db. 청록 절대 X.

커밋: feat(keeper-out/admin): lead detail P2 — 방문/점주/매장마스터 3섹션

(2.3) 섹션 13 메모 추가 폼

위치: 섹션 9 방문 로그 영역 안 (또는 그 직후)
폼 필드:
  - step 드롭다운 (옵션: STEP1, STEP2, STEP3, STEP4)
  - outcome 드롭다운 (옵션: pending, completed, rejected)
  - notes textarea (max 500자, required)
  - submit 버튼 (busy 상태 표시)

POST 호출:
  fetch("/api/keeper-out/visits", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      lead_id: lead.id,
      step,
      outcome,
      notes,
      // promoter_id는 P3
    }),
  });

성공 시: 폼 초기화 + GET 재호출 (visit_logs 갱신). 실패 시 alert.

커밋: feat(keeper-out/admin): lead detail 메모 추가 폼 (visits POST 재사용)

[STAGE 3 — main 머지, ~5분]

순서 (절대 어기지 말 것):
1. git status — staging clean (의도한 untracked만 남음) 확인
2. npm run build — PASS
3. git push origin staging
4. (대기 ~2분) Vercel preview URL 확인 — 가장 최신 deployment 가져와서 200 응답 확인
   curl -s -o /dev/null -w "%{http_code}\n" /keeper-out/admin
5. git checkout main
6. git pull origin main (최신 동기화)
7. git merge staging --ff-only  ← fast-forward만
   - 충돌 시 즉시 stop, PM에게 보고. 임의 해결 금지.
8. git push origin main
9. (대기 ~2분) https://www.keeper-direct.ceo/keeper-out/admin 200 확인
10. git checkout staging (작업 위치 복귀)

[가드레일 재확인]
- 청록 #009090 / #00b4b4 / #006B6B / #007575 / #e6f7f7 사용 X
- /diagnose / globals.css scraped 클래스 / "한화"·"키퍼"·"하나비전" 텍스트 X
- next.config / package.json 수정 X
- main 강제 푸시 / --no-verify / --no-ff X
- untracked 룰: P1 때 보존한 13개 파일/디렉토리 그대로 둘 것
  (CLAUDE.md, lib/keeper-out/catchrank.ts, app/landing/, context/,
   data/, scripts/, docs/, public/assets/, app/api/keeper-out/catchrank/sync-places/)

[검증 — 각 stage 끝마다]
1. npx tsc --noEmit → 0
2. npx eslint <변경 파일들> → 0
3. npm run build → 성공
4. (가능 시) 실제 lead URL 열어서 신규 섹션 데이터 확인

[보고 형식 — STAGE 3 끝나면 한 번에]

## P1.5 + P2 + 머지 작업 결과 보고

### 1. 커밋 목록 (sha + 메시지, 4개)
-  refactor(keeper-out/admin): root layout에서 마케팅 헤더 격리
-  feat(keeper-out/admin): GET endpoint에 visit_logs/place/owner embed + other_leads
-  feat(keeper-out/admin): lead detail P2 — 방문/점주/매장마스터 3섹션
-  feat(keeper-out/admin): lead detail 메모 추가 폼 (visits POST 재사용)

### 2. layout 분리 — 어느 방법(A/B) 채택, 이유

### 3. 변경 파일 (신규/수정 분리)

### 4. 검증 결과 — 각 stage별
- STAGE 1 끝: tsc PASS / build PASS / 시각 확인
- STAGE 2 끝: tsc PASS / build PASS / 실제 lead URL에서 신규 섹션 보이는지
- STAGE 3 끝: staging push 후 preview URL 200 / main fast-forward 성공 /
              production URL 200 (https://www.keeper-direct.ceo/keeper-out/admin)

### 5. 결정 사항 (자체 판단한 것 명시)

### 6. 막힌 곳 / 의문 (있으면)

### 7. 다음 후속 후보 (있으면)

[시작]
지금 STAGE 1부터 진행. 각 STAGE 완료 후 다음으로. 충돌 발생 시 즉시 stop.

06.PM 검증 흐름 (코덱스 회신 받은 뒤)

  1. 4 commit sha + git show로 실제 변경 검증
  2. 색깔 룰 grep — admin 영역 청록 0건
  3. main HEAD가 staging과 일치하는지 (git log origin/main -1)
  4. production URL 응답 — www.keeper-direct.ceo/keeper-out/admin/leads/<id> 8섹션 → 12섹션 확장됐는지
  5. 이상 시 fix 디스패치 / 정상 시 P3 (operator 배정 폼 / RBAC / etc.) 결정 보고서

참조 문서 (전체 흐름)

  1. 2026-05-05 pain API — pain_points 백엔드 닫기
  2. 2026-05-05 P1 Plan — 13섹션 설계
  3. 2026-05-05 Codex Brief P1 — 1차 작업지시서
  4. 2026-05-05 PM 리뷰 P1 PASS — 1차 검증
  5. 이 문서 — 2차 작업지시서 (P1.5 + P2 + 머지)