---
name: "PICKIT: Init"
description: 새 폴더에서 PICKIT 프로젝트 전체를 처음부터 생성합니다. 디렉토리 구조, Rules, 백엔드(FastAPI+Gemini), 클라이언트(pickit.js)를 순서대로 만들고 바로 실행 가능한 상태로 만듭니다.
category: PICKIT Workflow
tags: [pickit, init, bootstrap]
---

# PICKIT Init

빈 폴더에서 PICKIT MVP를 처음부터 완전히 세팅한다.  
질문 없이 순서대로 실행하고, 단계마다 완료 여부를 확인한다.

---

## 실행 순서

### 1단계 — 디렉토리 구조 생성

다음 폴더와 파일을 생성:

```
pickit_mvp/
├── client/
│   └── pickit.js          ← 카페24에 삽입되는 클라이언트
├── server/
│   ├── __init__.py
│   ├── main.py            ← FastAPI 서버 (pickit-llm-cache 스킬로 생성)
│   ├── llm.py             ← Gemini API + 캐싱 (pickit-llm-cache 스킬로 생성)
│   └── llm_prompts.py     ← Rule별 LLM 프롬프트 (pickit-rules-init으로 생성)
├── rules/
│   ├── rules.json         ← 75개 Rule 정의 (pickit-rules-init으로 생성)
│   └── nudge_copy.json    ← LLM 실패 시 폴백 카피 (pickit-rules-init으로 생성)
├── data/
│   └── bandit_state.json  ← Bandit 학습 데이터 (자동 생성)
├── .env.example
├── .gitignore
└── requirements.txt
```

`server/__init__.py`는 빈 파일로 생성.

### 2단계 — `.gitignore` 생성

```
.env
data/bandit_state.json
__pycache__/
*.pyc
*.pyo
.DS_Store
```

### 3단계 — `.env.example` 생성

```
# Gemini API 키 — https://aistudio.google.com/app/apikey 에서 발급
GEMINI_API_KEY=your_gemini_api_key_here

# Redis (선택사항 — 없어도 동작)
REDIS_HOST=localhost
REDIS_PORT=6379
```

### 4단계 — Rules 전체 생성 (pickit-rules-init 스킬 실행)

`/pickit:rules-init` 커맨드의 절차를 그대로 실행해 다음 세 파일을 생성:
- `rules/rules.json` — T01~T10 × 세그먼트 × 가격대 × 리뷰 조합 75개 Rule
- `rules/nudge_copy.json` — 전체 Rule 폴백 카피
- `server/llm_prompts.py` — Rule별 Gemini 프롬프트 딕셔너리

### 5단계 — 백엔드 생성 (pickit-llm-cache 스킬 실행)

`pickit-llm-cache` 스킬의 절차를 그대로 실행해 다음 파일을 생성:
- `server/llm.py` — Gemini API 호출 + Redis 선택적 캐싱
- `server/main.py` — FastAPI 서버 전체
- `requirements.txt`

`data/bandit_state.json`을 `{}`로 초기화.

### 6단계 — `client/pickit.js` 생성

카페24 쇼핑몰에 삽입될 클라이언트 스크립트. 아래 구조로 생성:

```javascript
(function () {
  'use strict';

  // ── 설정 ──────────────────────────────────────────────
  const PICKIT_CONFIG = window.__PICKIT__ || {};
  const SHOP_ID = PICKIT_CONFIG.shopId || 'unknown';
  const API_BASE = PICKIT_CONFIG.apiBase || 'http://localhost:8000';

  // ── 상태 ──────────────────────────────────────────────
  const state = {
    productNo: null,
    productPrice: 0,
    stockNumber: 999,
    reviewCount: 0,
    hasOption: false,
    isLoggedIn: false,
    memberIdCrypt: null,
    productViewCount: 1,
    timeOnPage: 0,
    nudgeShown: false,
    triggeredRules: new Set(),
  };

  // ── 상품 컨텍스트 읽기 ────────────────────────────────
  function readProductContext() {
    state.productNo = (window.link_product_detail || '').match(/\/(\d+)\//)?.[1] || null;
    state.productPrice = parseInt(window.product_price || '0', 10);
    state.stockNumber = parseInt(window.stock_number || '999', 10);
    state.hasOption = window.has_option === 'T';
    state.isLoggedIn = !!document.cookie.match(/iscache=F/);
    state.memberIdCrypt = window.CAFE24?.FRONT_EXTERNAL_SCRIPT_VARIABLE_DATA?.common_member_id_crypt || null;
    state.reviewCount = parseInt(
      document.querySelector('.detail_tab a[href="#review"] span')?.textContent || '0', 10
    );
    // 방문 횟수 (sessionStorage)
    const key = 'pk_vc_' + state.productNo;
    state.productViewCount = parseInt(sessionStorage.getItem(key) || '0', 10) + 1;
    sessionStorage.setItem(key, state.productViewCount);
  }

  // ── 넛지 요청 ─────────────────────────────────────────
  async function requestNudge(triggerId, extra) {
    if (state.nudgeShown) return;
    if (!state.productNo) return;

    const body = {
      shop_id: SHOP_ID,
      product_no: state.productNo,
      trigger_id: triggerId,
      product_price: state.productPrice,
      stock_number: state.stockNumber,
      review_count: state.reviewCount,
      is_logged_in: state.isLoggedIn,
      product_view_count: state.productViewCount,
      member_id_crypt: state.memberIdCrypt,
      ...extra,
    };

    try {
      const res = await fetch(API_BASE + '/nudge', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!res.ok) return;
      const nudge = await res.json();
      renderNudge(nudge);
    } catch (e) {
      // 네트워크 오류 → 조용히 실패
    }
  }

  // ── 넛지 렌더링 (pickit-nudge-design 스킬 기준) ────────
  function renderNudge(nudge) {
    if (state.nudgeShown) return;
    const anchor = document.querySelector('button#actionCart')
      || document.querySelector('.xans-product-action');
    if (!anchor) return;

    injectStyle();

    const widget = document.createElement('div');
    widget.className = 'pickit-widget';

    // 좌측 요소
    const EMOJI_MAP = {
      review_summary: '⭐', social_proof: '👥', size_guide: '📏',
      shipping: '🚚', quality: '✅', personalized: '👤',
    };
    const left = document.createElement('div');
    left.className = 'pickit-left';
    if (nudge.metric) {
      left.classList.add('pickit-left--metric');
      const val = document.createElement('span');
      val.className = 'pickit-metric-value';
      val.textContent = nudge.metric + (nudge.metric_unit || '');
      left.appendChild(val);
    } else {
      const emoji = EMOJI_MAP[nudge.nudge_type];
      if (emoji) {
        left.classList.add('pickit-left--emoji');
        left.textContent = emoji;
      }
    }
    if (left.textContent || left.children.length) widget.appendChild(left);

    // 본문
    const body = document.createElement('p');
    body.className = 'pickit-body';
    if (nudge.body_emphasis && nudge.body && nudge.body.includes(nudge.body_emphasis)) {
      const parts = nudge.body.split(nudge.body_emphasis);
      if (parts[0]) body.appendChild(document.createTextNode(parts[0]));
      const strong = document.createElement('strong');
      strong.textContent = nudge.body_emphasis;
      body.appendChild(strong);
      if (parts[1]) body.appendChild(document.createTextNode(parts[1]));
    } else {
      body.textContent = nudge.body || nudge.title || '';
    }
    widget.appendChild(body);

    // CTA
    const cta = document.createElement('button');
    cta.className = 'pickit-cta';
    cta.setAttribute('type', 'button');
    cta.textContent = (nudge.cta || '자세히 보기') + ' >>';
    cta.addEventListener('click', function () {
      const target = document.querySelector(nudge.cta_target || '#actionCart');
      if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
      navigator.sendBeacon(
        API_BASE + '/nudge/' + nudge.rule_id + '/feedback',
        JSON.stringify({ shop_id: SHOP_ID, rule_id: nudge.rule_id, nudge_type: nudge.nudge_type, clicked: true })
      );
    });
    widget.appendChild(cta);

    anchor.parentNode.insertBefore(widget, anchor);
    state.nudgeShown = true;

    // 노출 이벤트
    navigator.sendBeacon(
      API_BASE + '/collect',
      JSON.stringify({ shop_id: SHOP_ID, event: 'nudge_shown', product_no: state.productNo, payload: { rule_id: nudge.rule_id } })
    );
  }

  function injectStyle() {
    if (document.getElementById('pickit-style')) return;
    const style = document.createElement('style');
    style.id = 'pickit-style';
    style.textContent = `
      @keyframes pickit-slide-up {
        from { opacity: 0; transform: translateY(10px); }
        to   { opacity: 1; transform: translateY(0); }
      }
      .pickit-widget {
        display: flex; align-items: center; gap: 12px;
        position: relative; overflow: hidden;
        background: #fff; border: 1px solid #E0E0E0;
        border-radius: 14px; box-shadow: 0 2px 12px rgba(0,0,0,.08);
        padding: 14px 16px; margin-bottom: 12px;
        font-family: inherit; box-sizing: border-box;
        animation: pickit-slide-up 260ms ease;
      }
      .pickit-widget::after {
        content: ''; position: absolute; top: -14px; right: -14px;
        width: 48px; height: 48px; background: #7B68EE; border-radius: 50%;
      }
      .pickit-left { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
      .pickit-left--metric { min-width: 64px; }
      .pickit-left--emoji { font-size: 28px; line-height: 1; }
      .pickit-metric-value { font-size: 36px; font-weight: 900; color: #1A1A1A; line-height: 1; }
      .pickit-body { flex: 1; font-size: 14px; color: #1A1A1A; line-height: 1.4; word-break: keep-all; margin: 0; }
      .pickit-body strong { font-weight: 700; }
      .pickit-cta {
        flex-shrink: 0; white-space: nowrap;
        background: #F5F5F5; border: 1px solid #C8C8C8;
        border-radius: 8px; padding: 8px 12px;
        font-size: 13px; color: #1A1A1A; cursor: pointer; z-index: 1;
      }
      .pickit-cta:hover { background: #EBEBEB; }
      @media (max-width: 480px) {
        .pickit-widget { flex-wrap: wrap; }
        .pickit-cta { width: 100%; justify-content: center; }
      }
    `;
    document.head.appendChild(style);
  }

  // ── 트리거 감지 ───────────────────────────────────────

  // T01: 장시간 체류 (120초)
  let _timer = setInterval(function () {
    state.timeOnPage += 10;
    if (state.timeOnPage >= 120) {
      clearInterval(_timer);
      requestNudge('T01');
    }
  }, 10000);

  // T02: 반복 방문 (3회 이상)
  if (state.productViewCount >= 3) {
    requestNudge('T02');
  }

  // T03: 리뷰 탭 클릭
  document.querySelector('a[href="#review"]')?.addEventListener('click', function () {
    requestNudge('T03');
  });

  // T04: 옵션 미선택 장바구니 시도
  document.querySelector('button#actionCart')?.addEventListener('click', function () {
    if (state.hasOption) {
      const optionSelected = document.querySelector('.btnSubmit.sizeS.on, .opt_list .selected');
      if (!optionSelected) requestNudge('T04');
    }
  });

  // T05: 품절 근접 재고
  if (state.stockNumber > 0 && state.stockNumber <= 5) {
    requestNudge('T05');
  }

  // T06: 사이즈 가이드 클릭
  document.querySelector('.size_guide_info')?.addEventListener('click', function () {
    requestNudge('T06');
  });

  // T07: 이탈 의도 (PC: 마우스 상단 이탈)
  document.addEventListener('mouseleave', function (e) {
    if (e.clientY < 5) requestNudge('T07', { exit_intent: true });
  });

  // T08: 수량 증가
  document.querySelector('.QuantityUp')?.addEventListener('click', function () {
    const qty = parseInt(document.querySelector('input#quantity')?.value || '1', 10);
    if (qty >= 2) requestNudge('T08');
  });

  // T10: 상세페이지 스크롤 완료
  const prdDetail = document.querySelector('#prdDetail');
  if (prdDetail) {
    const observer = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        if (entry.isIntersecting) {
          requestNudge('T10');
          observer.disconnect();
        }
      });
    }, { threshold: 0.9 });
    observer.observe(prdDetail);
  }

  // ── 초기화 ────────────────────────────────────────────
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', readProductContext);
  } else {
    readProductContext();
  }

})();
```

### 7단계 — `requirements.txt` 최종 확인

`pickit-llm-cache` 스킬에서 이미 생성됐으면 확인만, 없으면 생성:

```
fastapi
uvicorn[standard]
slowapi
pydantic
google-genai
redis
```

### 8단계 — import 오류 없는지 확인

```bash
python -c "
from server.llm import generate_nudge
from server.main import app
import json, pathlib
print('✅ server OK')
rules = json.loads(pathlib.Path('rules/rules.json').read_text())
print(f'✅ rules OK — {len(rules)}개')
"
```

오류가 있으면 수정 후 재확인.

### 9단계 — 완료 안내

```
✅ PICKIT MVP 초기화 완료
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
생성된 파일:
  client/pickit.js          카페24 삽입 클라이언트 (10개 트리거)
  server/main.py            FastAPI 서버
  server/llm.py             Gemini API + 캐싱
  server/llm_prompts.py     75개 Rule LLM 프롬프트
  rules/rules.json          75개 Rule 정의
  rules/nudge_copy.json     폴백 카피
  data/bandit_state.json    Bandit 학습 데이터
  requirements.txt
  .env.example
  .gitignore

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
지금 해야 할 것 (직접):

  1. Gemini API 키 발급
     https://aistudio.google.com/app/apikey

  2. .env 파일 생성
     cp .env.example .env
     # .env 열어서 GEMINI_API_KEY= 뒤에 키 입력

  3. 패키지 설치
     pip install -r requirements.txt

  4. 서버 실행
     uvicorn server.main:app --reload

  5. 동작 확인
     curl http://localhost:8000/health

  6. 카페24에 넛지 삽입 (테스트)
     카페24 관리자 → 디자인 → HTML 편집
     </body> 바로 위에 추가:
     <script>
       window.__PICKIT__ = { shopId: "YOUR_SHOP_ID", apiBase: "https://your-server.com" };
     </script>
     <script src="https://your-server.com/static/pickit.js"></script>

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
이후 기능 추가:
  /pickit:build <기능명>   새 기능 구현
  /pickit:debug            Playwright QA 테스트
  /pickit:nudge-gen        특정 카테고리 Rule 추가
  /pickit:design           위젯 디자인 커스터마이징
```
