-
-
-
💊
-
-
+
@@ -416,6 +421,87 @@
diff --git a/docs/ai-upselling-crm.md b/docs/ai-upselling-crm.md
new file mode 100644
index 0000000..c16c4c7
--- /dev/null
+++ b/docs/ai-upselling-crm.md
@@ -0,0 +1,173 @@
+# AI 업셀링 CRM — 마이페이지 맞춤 추천 시스템
+
+## 개요
+키오스크 적립 시 고객 구매이력을 AI가 분석하여 맞춤 제품을 추천.
+고객이 알림톡 → 마이페이지 접속 시 바텀시트 팝업으로 자연스럽게 표시.
+
+## 기술 스택
+- **AI 엔진**: Clawdbot Gateway (Claude Max 구독 재활용, 추가 비용 없음)
+- **통신**: WebSocket (`ws://127.0.0.1:18789`) — JSON-RPC 프로토콜
+- **저장소**: SQLite `ai_recommendations` 테이블
+- **프론트**: 바텀시트 UI (드래그 닫기 지원)
+
+## 전체 흐름
+
+```
+키오스크 적립 (POST /api/kiosk/claim)
+ │
+ ├─ 1. 적립 처리 (기존)
+ ├─ 2. 알림톡 발송 (기존)
+ └─ 3. AI 추천 생성 (fire-and-forget)
+ │
+ ├─ 최근 구매 이력 수집 (SQLite + MSSQL SALE_SUB)
+ ├─ Clawdbot Gateway → Claude 호출
+ ├─ 추천 결과 → ai_recommendations 저장
+ └─ 실패 시 무시 (추천은 부가 기능)
+
+고객: 알림톡 버튼 클릭 → /my-page
+ │
+ ├─ 1.5초 후 GET /api/recommendation/{user_id}
+ │
+ ├─ 추천 있음 → 바텀시트 슬라이드업
+ │ ├─ 아래로 드래그 → 닫기
+ │ ├─ "다음에요" → dismiss
+ │ └─ "관심있어요!" → dismiss + 기록
+ │
+ └─ 추천 없음 → 아무것도 안 뜸
+```
+
+## 핵심 파일
+
+### `backend/services/clawdbot_client.py`
+Clawdbot Gateway Python 클라이언트.
+
+**Gateway WebSocket 프로토콜 (v3):**
+1. WS 연결 → `ws://127.0.0.1:{port}`
+2. 서버 → `connect.challenge` 이벤트 (nonce 전달)
+3. 클라이언트 → `connect` 요청 (token + client info)
+4. 서버 → connect 응답 (ok)
+5. 클라이언트 → `agent` 요청 (message + systemPrompt)
+6. 서버 → `accepted` ack → 최종 응답 (`payloads[].text`)
+
+**주요 함수:**
+| 함수 | 설명 |
+|------|------|
+| `_load_gateway_config()` | `~/.clawdbot/clawdbot.json`에서 port, token 읽기 |
+| `_ask_gateway(message, ...)` | async WebSocket 통신 |
+| `ask_clawdbot(message, ...)` | 동기 래퍼 (Flask에서 호출) |
+| `generate_upsell(user_name, current_items, recent_products)` | 업셀 프롬프트 구성 + 호출 + JSON 파싱 |
+| `_parse_upsell_response(text)` | AI 응답에서 JSON 추출 |
+
+**Gateway 설정:**
+- 설정 파일: `~/.clawdbot/clawdbot.json`
+- Client ID: `gateway-client` (허용된 상수 중 하나)
+- Protocol: v3 (minProtocol=3, maxProtocol=3)
+
+### `backend/db/mileage_schema.sql` — ai_recommendations 테이블
+```sql
+CREATE TABLE IF NOT EXISTS ai_recommendations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ transaction_id VARCHAR(20),
+ recommended_product TEXT NOT NULL, -- "고려은단 비타민C 1000"
+ recommendation_message TEXT NOT NULL, -- 고객에게 보여줄 메시지
+ recommendation_reason TEXT, -- 내부용 추천 이유
+ trigger_products TEXT, -- JSON: 트리거된 구매 품목
+ ai_raw_response TEXT, -- AI 원본 응답
+ status VARCHAR(20) DEFAULT 'active', -- active/dismissed
+ displayed_count INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ expires_at DATETIME, -- 7일 후 만료
+ displayed_at DATETIME,
+ dismissed_at DATETIME,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+```
+
+### `backend/app.py` — API 엔드포인트
+
+| 엔드포인트 | 메서드 | 설명 |
+|-----------|--------|------|
+| `/api/recommendation/
` | GET | 최신 active 추천 조회 (마이페이지용) |
+| `/api/recommendation//dismiss` | POST | 추천 닫기 (status→dismissed) |
+
+**추천 생성 위치**: `api_kiosk_claim()` 함수 끝부분, `_generate_upsell_recommendation()` 호출
+
+### `backend/templates/my_page.html` — 바텀시트 UI
+
+**기능:**
+- 페이지 로드 1.5초 후 추천 API fetch
+- 💊 아이콘 + AI 메시지 + 제품명 배지 (보라색 그라디언트)
+- **터치 드래그 닫기**: 아래로 80px 이상 드래그하면 dismiss
+- 배경 탭 닫기, "다음에요"/"관심있어요!" 버튼
+- 슬라이드업/다운 CSS 애니메이션
+
+## AI 프롬프트
+
+**시스템 프롬프트:**
+```
+당신은 동네 약국(청춘약국)의 친절한 약사입니다.
+고객의 구매 이력을 보고, 자연스럽고 따뜻한 톤으로 약 하나를 추천합니다.
+반드시 JSON 형식으로만 응답하세요.
+```
+
+**유저 프롬프트 구조:**
+```
+고객 이름: {name}
+오늘 구매한 약: {current_items}
+최근 구매 이력: {recent_products}
+
+규칙:
+1. 함께 먹으면 좋은 약 1가지만 추천 (일반의약품/건강기능식품)
+2. 메시지 2문장 이내, 따뜻한 톤
+3. JSON: {"product": "...", "reason": "...", "message": "..."}
+```
+
+**응답 예시:**
+```json
+{
+ "product": "고려은단 비타민C 1000",
+ "reason": "감기약 구매로 면역력 보충 필요",
+ "message": "김영빈님, 감기약 드시는 동안 비타민C도 함께 챙겨드시면 회복에 도움이 돼요."
+}
+```
+
+## Fallback 정책
+
+| 상황 | 동작 |
+|------|------|
+| Gateway 꺼져있음 | 추천 생성 스킵, 로그만 남김 |
+| AI 응답 파싱 실패 | 저장 안 함 |
+| 추천 없을 때 마이페이지 방문 | 바텀시트 안 뜸 |
+| 7일 경과 | `expires_at` 만료, 조회 안 됨 |
+| dismiss 후 재방문 | 같은 추천 안 뜸 (새 적립 시 새 추천 생성) |
+
+## 테스트
+
+```bash
+# 1. Gateway 연결 테스트
+PYTHONIOENCODING=utf-8 python -c "
+from services.clawdbot_client import ask_clawdbot
+print(ask_clawdbot('안녕'))
+"
+
+# 2. 업셀 생성 테스트
+PYTHONIOENCODING=utf-8 python -c "
+import json
+from services.clawdbot_client import generate_upsell
+result = generate_upsell('홍길동', '타이레놀, 챔프시럽', '비타민C, 소화제')
+print(json.dumps(result, ensure_ascii=False, indent=2))
+"
+
+# 3. API 테스트
+curl https://mile.0bin.in/api/recommendation/1
+
+# 4. DB 확인
+python -c "
+import sqlite3, json
+conn = sqlite3.connect('db/mileage.db')
+conn.row_factory = sqlite3.Row
+for r in conn.execute('SELECT * FROM ai_recommendations ORDER BY id DESC LIMIT 5'):
+ print(json.dumps(dict(r), ensure_ascii=False))
+"
+```
diff --git a/docs/windows-utf8-encoding.md b/docs/windows-utf8-encoding.md
new file mode 100644
index 0000000..ad5208a
--- /dev/null
+++ b/docs/windows-utf8-encoding.md
@@ -0,0 +1,74 @@
+# Windows 콘솔 한글 인코딩 (UTF-8) 가이드
+
+## 문제
+Windows 콘솔 기본 인코딩이 `cp949`여서 Python에서 한글 출력 시 깨짐 발생.
+Claude Code bash 터미널, cmd, PowerShell 모두 동일 증상.
+
+```
+# 깨진 출력 예시
+{"product": "������ ���", "message": "�迵���, ..."}
+```
+
+## 해결: 3단계 방어
+
+### 1단계: Python 파일 상단 — sys.stdout UTF-8 래핑
+```python
+import sys
+import os
+
+if sys.platform == 'win32':
+ import io
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+ os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
+```
+
+**적용 위치**: `app.py`, `clawdbot_client.py` 등 진입점 파일 맨 위 (import 전)
+
+> 모듈로 import되는 파일은 `hasattr(sys.stdout, 'buffer')` 체크 추가:
+> ```python
+> if sys.platform == 'win32':
+> import io
+> if hasattr(sys.stdout, 'buffer'):
+> sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+> sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+> ```
+
+### 2단계: 환경변수 — PYTHONIOENCODING
+```bash
+# ~/.bashrc (Claude Code bash 세션)
+export PYTHONIOENCODING=utf-8
+```
+
+또는 실행 시:
+```bash
+PYTHONIOENCODING=utf-8 python backend/app.py
+```
+
+### 3단계: json.dumps — ensure_ascii=False
+```python
+import json
+data = {"product": "비타민C", "message": "추천드려요"}
+print(json.dumps(data, ensure_ascii=False, indent=2))
+```
+`ensure_ascii=False` 없으면 `\uBE44\uD0C0\uBBFCC` 같은 유니코드 이스케이프로 출력됨.
+
+## 프로젝트 내 적용 현황
+
+| 파일 | 방식 |
+|------|------|
+| `backend/app.py` | sys.stdout 래핑 + PYTHONIOENCODING |
+| `backend/services/clawdbot_client.py` | sys.stdout 래핑 (buffer 체크) |
+| `backend/ai_tag_products.py` | sys.stdout 래핑 |
+| `backend/view_products.py` | sys.stdout 래핑 |
+| `backend/import_il1beta_foods.py` | sys.stdout 래핑 |
+| `backend/import_products_from_mssql.py` | sys.stdout 래핑 |
+| `backend/update_product_category.py` | sys.stdout 래핑 |
+| `backend/gui/check_cash.py` | `sys.stdout.reconfigure(encoding='utf-8')` |
+| `backend/gui/check_sunab.py` | `sys.stdout.reconfigure(encoding='utf-8')` |
+| `~/.bashrc` | `export PYTHONIOENCODING=utf-8` |
+
+## 주의사항
+- Flask 로거(`logging.info()` 등)도 stderr로 출력하므로 **stderr도 반드시 래핑**
+- `io.TextIOWrapper`는 이미 래핑된 스트림에 중복 적용하면 에러남 → `hasattr(sys.stdout, 'buffer')` 체크
+- PyQt GUI에서는 stdout이 다를 수 있음 → `hasattr` 가드 필수