# 네이버 톡톡 프라임애드 견적 챗봇

## 프로젝트 개요
네이버 톡톡 Webhook API를 이용한 자동 견적 챗봇.
고객이 톡톡으로 문의하면 봇이 상품 카테고리를 먼저 선택받고, 단계별로 사이즈/옵션을 물어보고 자동으로 견적을 계산해준다.
마지막에 네이버 스토어 링크로 유도하여 스토어 트래픽/판매량을 유지한다. (PAY 버튼 사용 안 함)

## 개발 범위 (단계적 확장)
- **1단계 (현재)**: 현수막 견적만 완성
- **2단계 (추후)**: PET배너, 점착시트 등 추가
- 현수막 외 카테고리는 일단 "상담원 연결" 처리

## 배포 환경
- 서버: primead.kr (유료 서버, HTTPS 인증서 있음)
- 배포 경로: `/naverStore/talkbot/`
- 언어: PHP
- 배포 방법: SSH 직접 편집 (VS Code + Claude Code)

## 네이버 톡톡 API 정보
- Webhook URL: `https://primead.kr/naverStore/talkbot/webhook.php`
- Authorization (보내기 API 키): `sender.php` 내 `TALKTALK_AUTH_KEY` 상수
- 보내기 API 엔드포인트: `https://gw.talk.naver.com/chatbot/v1/event`
- 사용 이벤트: `send`, `open`, `leave`
- 테스트: 별도 테스트 톡톡 계정 생성 후 Webhook 등록 → 본계정 적용 시 API 키만 교체

## 파일 구조
```
/naverStore/talkbot/
├── webhook.php          # Webhook 수신 + 글로벌 명령 + 카테고리 라우팅
├── sender.php           # 톡톡 보내기 API 호출 (공식 스펙 준수, 100ms 딜레이 rate limit 방어)
├── session.php          # 파일 기반 세션 관리 (30분 만료, flock 동시성)
├── sessions/            # 세션 데이터 저장 폴더 (chmod 777)
├── banner/              # 현수막 견적 대화 플로우
│   ├── handler.php      # step 라우터 + resendBannerStep + goBackBannerStep
│   ├── size_input.php   # TYPE/SIZE/QTY/MULTIPLE 핸들러
│   ├── design_input.php # DESIGN/BG_REMOVE 핸들러
│   ├── finish_input.php # FINISH_CUTTING/FINISH/EYELET_COUNT/POLE_DIRECTION + 그룹규칙
│   ├── finish_split.php # FINISH_APPLY/FINISH_APPLY_QTY (후가공 수량 나누기)
│   ├── extra_input.php  # EXTRA/EXTRA_COUNT (추가상품 선택)
│   ├── calculator.php   # showEstimate (견적 결과 출력 + 견적목록/나누기 분기)
│   ├── calc_engine.php  # 순수 계산 엔진 (calculateEstimate + calc* 함수들)
│   └── cart.php         # 견적목록 기능 (addToCart, CART_ASK, CART_SUMMARY)
└── CLAUDE.md            # 이 파일
```

### 의존성 흐름
```
webhook.php
├── require sender.php     (전역: sendText, sendComposite, passToPartner, takeFromPartner 등)
├── require session.php    (전역: loadSession, saveSession 등)
└── require banner/handler.php  (category=banner일 때)
    ├── require finish_input.php   → 후가공 그룹 유틸 + 선택 핸들러
    ├── require finish_split.php   → 수량 나누기 핸들러
    ├── require extra_input.php    → 추가상품 핸들러
    ├── require design_input.php   → 디자인 선택 핸들러
    ├── require size_input.php     → 사이즈/수량 핸들러
    ├── require calculator.php     → 견적 결과 출력
    │   └── require calc_engine.php  → 순수 계산 함수
    │       └── (내부) require shared/db_connect.php
    └── require cart.php           → 견적목록 기능

admin/api/calc_estimate.php
├── require calc_engine.php   (calculateEstimate 직접 사용)
└── require finish_input.php  (getSelectedGroup 등 유틸 사용)
```

### 소재별 폴더 확장 패턴
새 소재 추가 시: `pet_banner/` 또는 `adhesive_sheet/` 폴더를 만들고 동일 구조로 handler.php 등 구현.
webhook.php의 카테고리 라우팅에 `elseif ($category === 'pet_banner')` 분기 추가.

---

## 대화 플로우 (현재 구현 완료)

```
[open] → 인사말 + 퀵버튼: [현수막 견적][PET배너 견적][점착시트 견적][기타 문의]
  ↓
[CATEGORY] → 현수막 외 → "상담원이 안내해드리겠습니다" / 현수막 → TYPE
  ↓
[TYPE] → 퀵버튼: [일반현수막(normal)] [UV현수막(uv)]
  ↓
[SIZE] → 텍스트 입력 파싱 "300 90" (정규식: /(\d+)\s*[xX×,\s]\s*(\d+)/)
  ↓
[QTY] → 숫자 입력
  ↓ (수량 ≥ 2일 때만)
[MULTIPLE] → 퀵버튼: [데이터 보유] [동일내용] [배경동일 문구수정] [서로다름]
  ↓ (수량 1일 때 또는 MULTIPLE 선택 후)
[DESIGN] → 퀵버튼: [데이터 있음] [간단하게] [단색바탕+단색글씨] [기본바탕 예쁘게] [전문가에게 맡김]
  ※ "데이터 보유" 선택 시 → design=size_adjusted 고정, DESIGN 단계 건너뜀
  ↓
  → (qty≥2일 때) 안내 메시지: "후가공 선택 후 수량을 나눠 적용할 수 있습니다"
  ↓
[FINISH_CUTTING] (일반) / [FINISH] (UV) → 후가공 선택 (그룹 규칙 적용, 아래 상세)
  ├→ 타공아일렛 등 선택 시 → [EYELET_COUNT] 개수 입력
  ├→ 봉미싱 선택 시 → [POLE_DIRECTION] 방향 선택 [좌우/상하]
  └→ 견적보기/필요없음 선택 시 ↓
  ↓
  → (qty≥2일 때)
  [FINISH_APPLY] → "N장 모두에 적용하시나요?" [예][아니요]
    → 예: [EXTRA] → showEstimate → [CART_ASK] (기존과 동일)
    → 아니요:
      [FINISH_APPLY_QTY] → "N장 중 몇 장에 적용?" → 숫자 입력
        → extras 없이 showEstimate → 견적목록 추가
        → "나머지 M장 후가공을 선택해주세요"
        → FINISH_CUTTING/FINISH로 복귀 (같은 사이즈, qty=M)
        → 반복...
  → (qty=1일 때) FINISH_APPLY 건너뛰고 바로 [EXTRA]
  ↓
[EXTRA] → 추가상품 선택 (실리콘양면테잎, 로프)
  ↓
showEstimate → 견적 결과 카드 + 견적목록 추가
  ↓
[CART_ASK] → "추가하실 현수막이 있으신가요?" [예][아니요]
  → 예: TYPE로 복귀 (새 현수막)
  → 아니요: [CART_SUMMARY] → 상세 카드 + [최종완료] → [DONE]
```

### "처음으로" / "다시하기" 입력 → 어디서든 처음으로 복귀

---

## 후가공 그룹 규칙 (⚠️ 핵심 — 실수하면 안 되는 부분)

### 그룹 정의

| 그룹 | 옵션들 | 그룹 내 규칙 |
|------|--------|-------------|
| **A (cutting)** | 열재단, 사방미싱 | **1개만** 선택 가능 |
| **B (hole)** | 타공아일렛 | **1개만** 선택 가능 |
| **C (pole)** | 봉미싱, 각목+로프, 사방로프미싱 | **1개만** 선택 가능 |

### 그룹 간 상호배제

```
A + B → 함께 선택 가능
C → A, B와 함께 선택 불가 (C는 단독)
```

- **C그룹 선택 시**: A, B그룹 버튼이 사라짐
- **A 또는 B그룹 선택 시**: C그룹 버튼이 사라짐

### 기본 포함 규칙

- **일반현수막**: 열재단이 **기본 포함** (A,B그룹 사용 시 자동 추가)
- **UV현수막**: 사방미싱이 **기본 포함** (비용 0원)
- **C그룹 선택 시**: 기본 후가공(열재단/사방미싱) **자동 추가하지 않음**

### 개별 옵션 제약

| 옵션 | 제약 |
|------|------|
| **각목+로프** | 짧은 길이(min(width,height)) **90cm 이하**만 선택 가능 |
| **봉미싱** | 방향 선택 필수 (좌우/상하), 방향에 따라 가격 상이 |
| **타공아일렛** | 개수 입력 필수 (현수막 1장 기준) |
| **열재단** | 원단 리스트에 짧은 길이가 정확히 매칭되면 **비용 0원** |
| **사방미싱** | UV현수막에서는 **비용 0원** |

---

## 복수구매 옵션 (수량 ≥ 2일 때 필수)

| 옵션 | 편집비 계산 |
|------|------------|
| **데이터 보유** (`has_data`) | 편집비 0원, 디자인 단계 건너뜀 |
| **동일내용** (`same_content`) | 디자인비 1회만 |
| **배경동일 문구수정** (`same_bg`) | 기본 1회 + simple단가 × (수량-1) |
| **서로다름** (`different`) | 디자인비 × 수량 |

---

## 견적 계산 파이프라인 (calc_engine.php)

DB 저장 없이 순수 계산. 단가는 `overall_price` 테이블에서 조회.
기존 5개 Calculator 클래스(waterbase_Placard/classes/) 로직을 이식.
결과 출력은 `calculator.php`의 `showEstimate()`가 담당.

```
1. 소재비  ← MaterialPriceCalculator 로직
   - threshold: 일반=180cm, UV=200cm
   - 양쪽 50cm 이하 → length=1.0
   - threshold 초과 시 2등분→3등분→... 순차 분할
2. 편집비  ← EditingPriceCalculator 로직
   - multiple 옵션에 따라 계산 방식 분기
   - editing_qty 파라미터: 지정 시 편집비만 해당 수량으로 계산 (split 모드용)
3. 후가공비 ← PostProcessingCalculator 로직
   - totalLength, totalPerimeter에 이미 quantity가 곱해져 있음 (⚠️)
   - 열재단: isExactCategory이면 0원
   - 봉미싱: 방향별 길이 계산 (좌우=height×2, 상하=width×2)
   - 200 이하 기본가, 초과시 ceil(길이/100)×100 기준 배수 적용
4. 단가 합산 = 소재비 + 편집비 + 후가공비 + 기본제작비(basic_basic_fee)
5. 할인 3종
   ① 기본제작비 할인: 소재비 ≥ 10,000원 → discount/basic_amount
   ② 면적 할인: 전체 면적(m²) 기준 최대 할인율 매칭 → 소재비 × rate/100
   ③ 천원 절삭: (합계 - 할인① - 할인②) % 1000
```

### ⚠️ 계산 시 주의사항

- `totalLength`(=remainder_size)와 `totalPerimeter`에는 **이미 quantity가 곱해져 있음**
  → 열재단/사방미싱 계산 시 `× 1`로 전달 (다시 quantity를 곱하면 안 됨)
- 타공아일렛: `단가 × 옵션수량 × 주문수량` (quantity를 곱해야 함)
- 봉미싱/각목: `단가(또는 배수) × 주문수량`

### Split 모드 편집비 중복 방지 (calculator.php)

수량 나누기(split) 시 각 그룹마다 `calculateEstimate`가 독립 호출되므로,
편집비가 그룹 수만큼 중복 청구되는 문제를 방지하는 로직:

- **첫 번째 그룹**: `editing_qty = split_total` (원래 총수량 기준 편집비 1회 계산)
- **2번째+ 그룹**: `multiple = 'has_data'` 로 덮어씀 → 편집비 0원
- 세션 변수 `split_editing_done`으로 첫 그룹 여부 추적, split 종료 시 정리

---

## 톡톡 API 참고 자료
- 공식 GitHub: https://github.com/navertalk/chatbot-api
- compositeContent 스크린샷: https://raw.githubusercontent.com/navertalk/chatbot-api/master/images/composite_message.jpg
- 퀵버튼 스크린샷: https://raw.githubusercontent.com/navertalk/chatbot-api/master/images/btn_quick.jpg
- 이미지 업로드 API: https://github.com/navertalk/chatbot-api/blob/master/imageupload_api_v1.md

## 톡톡 API 메시지 구조 (공식 스펙)

### 퀵버튼 (quickReply)
```json
{
  "quickReply": {
    "buttonList": [
      {"type": "TEXT", "data": {"title": "표시명", "code": "전송값"}}
    ]
  }
}
```
- `title`: 최대 10자
- `quickReply`는 `compositeContent`와 같은 레벨에 위치

### compositeContent 버튼
```json
{
  "buttonList": [
    {"type": "TEXT", "data": {"title": "버튼명", "code": "코드"}},
    {"type": "LINK", "data": {"title": "링크", "url": "...", "mobileUrl": "..."}}
  ]
}
```

### 이미지
```json
{
  "image": {"imageUrl": "https://primead.kr/naverStore/images/..."}
}
```
- HTTPS 필수, 권장 530×290px, JPG/PNG/GIF
- compositeList에 여러 개 → **좌우 스와이프 캐로셀** (최대 10개)

---

## 세션 데이터 구조
```json
{
  "step": "TYPE|SIZE|QTY|MULTIPLE|DESIGN|FINISH_CUTTING|FINISH|EYELET_COUNT|POLE_DIRECTION|FINISH_APPLY|FINISH_APPLY_QTY|EXTRA|EXTRA_COUNT|CART_ASK|CART_SUMMARY|DONE",
  "category": "banner",
  "type": "normal|uv",
  "width": 300,
  "height": 90,
  "qty": 2,
  "multiple": "0|has_data|same_content|same_bg|different",
  "design": "size_adjusted|simple|mono_theme|standard_layout|expert_choice",
  "finish": [
    {"name": "heat_cutting"},
    {"name": "eyelet_hole", "quantity": 4},
    {"name": "pole_stitching", "direction": "horizontal"}
  ],
  "extras": [
    {"name": "siliconetape", "quantity": 4},
    {"name": "rope", "quantity": 5}
  ],
  "cart": [ "...견적목록 항목들..." ],
  "split_total": 5,
  "split_remaining": 3,
  "split_editing_done": true,
  "result": { "...계산 결과..." },
  "updated_at": 1234567890
}
```

### 수량 나누기 세션 변수
- `split_total`: 원래 총 수량 (예: 5). 설정되어 있으면 "나누기 모드" 진행 중
- `split_remaining`: 현재 그룹 확정 후 남은 수량. showEstimate에서 분기 판단에 사용
- `split_editing_done`: 첫 그룹 견적 완료 시 true 설정 → 이후 그룹 편집비 0원 처리
- 마지막 그룹 견적 완료 시 `split_total`, `split_remaining`, `split_editing_done` 모두 삭제

## 세션 관리
- 파일 기반: `sessions/{md5(user)}.json`
- 30분 미활동 시 자동 초기화
- `flock(LOCK_EX)` 으로 동시성 보호
- `leave` 이벤트 시 세션 삭제

---

## Webhook 처리 흐름 (webhook.php)

1. **즉시 HTTP 200 응답** (`fastcgi_finish_request`)
2. JSON 파싱 → 이벤트 분기
3. `echo` → 무시 (무한루프 방지)
4. **`standby` 자동 복구** → 의도치 않은 standby 상태 감지 시 `takeFromPartner()` 호출하여 챗봇 제어권 회수
   - 명시적 상담원 연결(`counselor_mode`)이 아닌 경우 → 즉시 복구
   - 명시적 상담원 연결이었지만 10분 경과 → 자동 복구
   - 10분 이내 → 상담 중으로 판단하여 무시
5. `leave` → 세션 삭제
6. `handover` → 상담원이 상담완료 눌렀을 때 제어권 복귀
7. `open` → 세션 초기화 + 인사말
8. `send` → 세션 로드 → 단계별 핸들러 호출 → 보내기 API 호출

---

## 주의사항

- Webhook 응답은 반드시 **5초 이내** (네이버 타임아웃)
- `echo` 이벤트 수신 시 **절대 응답하면 안 됨** (무한루프)
- sessions/ 폴더에 **웹서버 쓰기 권한** 필요 (chmod 777)
- 테스트↔본계정 전환 시 `sender.php`의 **API 키만 교체**
- calculator.php는 **DB 저장 없이 순수 계산만** 수행 (기존 Calculator 클래스와 독립)
- 후가공 그룹 상호배제 규칙을 **반드시 준수** (위 표 참조)

---

## Rate Limit 방어 (봇 멈춤 방지)

네이버 톡톡 API는 **3초에 30개 이상 요청 시 차단** (API 키 단위, 규칙은 인프라별 상이).
다수 고객이 동시에 봇을 이용하면 rate limit에 걸려 전체 사용자에게 봇이 먹통이 될 수 있음.

### 3중 방어 구조

| 레이어 | 위치 | 내용 |
|--------|------|------|
| **1. API 호출 딜레이** | `sender.php` `sendToTalkTalk()` | 모든 API 호출 직전 `usleep(100000)` (100ms) 대기 |
| **2. Standby 자동 복구** | `webhook.php` | standby 플래그 감지 시 `takeFromPartner()` 로 제어권 회수 |
| **3. 메시지 버스트 후 방어** | `calculator.php` `showEstimate()` | split/견적 출력 등 연속 전송 구간 끝에 `takeFromPartner()` 호출 |

### 메시지 수 최적화
- split 플로우(수량 나누기): 기존 4~5개 → **3개로 축소**
- 견적 결과 출력: 텍스트 + 카드를 합쳐서 전송 최소화
