Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# CLAUDE.md

이 파일은 이 저장소에서 작업하는 Claude Code (및 기타 AI 어시스턴트)를 위한 안내서입니다.

## 프로젝트 개요

**AI 캐릭터 챗** — 관리자 패널에서 캐릭터를 완전히 변수화하여 운영하는 AI 롤플레이 채팅 서비스.
**Hono + Cloudflare Pages + D1(SQLite)** 기반의 단일 Worker 앱이며, 프론트엔드는 별도 빌드 없이 `src/index.tsx` 안에서 HTML 문자열로 인라인 제공됩니다.

핵심 특징:
- 캐릭터 설정은 DB의 `character_config` 싱글톤 행(id=1)에 저장 → 관리자 패널에서 코드 수정 없이 변경
- 게스트(7일)/회원(30일) 이중 구조, 둘 다 JWT 기반
- LLM API 키 접두사로 Claude(`sk-ant-`) / OpenAI 호환 자동 분기
- 30턴(=15쌍) 초과 시 이전 대화를 LLM으로 요약 압축
- 토스페이먼츠 구독 결제(베이직/프리미엄)

## 기술 스택

| 구분 | 기술 |
|------|------|
| 런타임/프레임워크 | Hono v4 (Cloudflare Workers) |
| 배포 | Cloudflare Pages (`@hono/vite-build/cloudflare-pages`) |
| DB | Cloudflare D1 (SQLite) — 바인딩 이름 `DB` |
| 인증 | JOSE, JWT HS256 |
| 빌드 | Vite 6 |
| 프론트 | 인라인 HTML/CSS/JS + Tailwind CDN + Font Awesome CDN (빌드 파이프라인 없음) |
| 비밀번호 | Web Crypto API PBKDF2 (bcrypt 미사용 — Workers 호환성) |

> **중요**: 의존성은 `hono`와 `jose` 둘뿐입니다. Node 전용 패키지(`bcryptjs`, `fs` 등)는 Workers에서 동작하지 않으므로 추가하지 마세요. 암호화/인코딩은 Web Crypto / `atob` / `btoa`를 사용합니다.

## 디렉터리 구조

```
src/
├── index.tsx # 메인 Hono 앱 진입점 + 전체 프론트엔드 HTML (mainHTML / adminHTML). ~1500줄
├── types.ts # 모든 TypeScript 타입 + PLANS 상수
├── renderer.tsx # hono/jsx-renderer (현재 라우트에서 미사용)
├── lib/
│ ├── db.ts # 모든 D1 쿼리 헬퍼 (여기 외에는 raw SQL 최소화)
│ ├── jwt.ts # JWT 서명/검증 (user/admin), 만료 파서
│ └── llm.ts # LLM 호출, 프롬프트 빌드, 응답 파싱, 기억 압축, 이미지 트리거
├── middleware/
│ └── auth.ts # requireAuth (사용자), requireAdmin (관리자)
└── routes/
├── public.ts # /api/character, /api/guest/init, /api/status (인증 불필요)
├── user.ts # /api/user/* (회원가입/로그인/프로필/me) + 비밀번호 헬퍼
├── chat.ts # /api/chat (전송/히스토리/초기화)
├── admin.ts # /api/admin/* (캐릭터 설정, 키, 이미지 업로드, 통계, 결제 조회)
└── payment.ts # /api/payment/* (토스페이먼츠 결제 흐름)
migrations/
├── 0001_initial_schema.sql # character_config, users, chat_sessions, guest_sessions
├── 0002_payments.sql # payments + users 구독 컬럼 (ALTER)
└── 0003_images.sql # images (Base64 저장)
public/static/style.css # renderer.tsx용 — 실사용 스타일은 index.tsx 인라인
```

## 라우트 등록 (src/index.tsx)

`app.route()`로 마운트되며 **prefix가 라우터 파일 내부 경로와 합쳐집니다**:

```
/api → publicRoutes (예: GET /api/character, GET /api/status)
/api/user → userRoutes
/api/chat → chatRoutes
/api/admin → adminRoutes
/api/payment → paymentRoutes
GET /api/images/:id → index.tsx에 직접 정의 (D1의 Base64 이미지 서빙, 공개)
GET /admin → adminHTML()
GET / 및 * → mainHTML() (SPA 폴백)
```

`/api/*`에 CORS(`origin: *`)가 전역 적용됩니다.

## 인증 모델

- **사용자/게스트 JWT**: `requireAuth` 미들웨어. `Authorization: Bearer <token>`. 페이로드 `{ sub, isGuest }`를 `c.get('jwtPayload')`로 사용. 게스트 7일, 회원 30일.
- **관리자 JWT**: `requireAdmin`. `sub: 'admin'`, 8시간. `ADMIN_PASSWORD`로 로그인하여 발급.
- 모든 JWT는 동일한 `JWT_SECRET`(HS256)으로 서명. 게스트도 정식 JWT(`isGuest: true`)를 받습니다.
- 비밀번호는 `users.password_hash`에 `saltHex:hashHex` 형식(PBKDF2-SHA256, 10만 회)으로 저장.

## 데이터 모델 (D1)

| 테이블 | 설명 |
|--------|------|
| `character_config` | 캐릭터 설정 **싱글톤(id=1)**. JSON 컬럼(`example_dialogues`, `specs`, `hashtags`, `situation_images`)은 문자열로 저장하고 `db.ts`에서 파싱. |
| `users` | 회원. `token_used`, `is_subscribed`, `subscribed_until`, `monthly_token_limit`. |
| `chat_sessions` | 회원별 대화 1행(`user_id` UNIQUE upsert). `history`(JSON), `summary`(압축 요약), `turn_count`. |
| `guest_sessions` | 게스트 임시 세션, `expires_at` 7일 TTL. |
| `payments` | 토스 결제. `toss_order_id` UNIQUE, status `pending/done/failed/canceled`. |
| `images` | 업로드 이미지 Base64 저장. `/api/images/:id`로 서빙. |

> JSON 데이터는 항상 `db.ts`의 헬퍼를 통해 읽고/쓰세요. 직접 `JSON.parse`하지 말고 `parseCharacterRow`/`parseJson` 패턴을 따르세요. 업데이트는 `updateCharacterConfig`의 화이트리스트(`allowed`)를 거칩니다.

## 코드 컨벤션

- **언어**: 주석·에러 메시지·UI 문자열은 모두 **한국어**. 코드 식별자는 영어. 새 코드도 이 패턴을 유지하세요.
- **섹션 주석**: 파일/함수 구분에 `// ===...===` 또는 `// ── ... ──` 박스 주석을 사용. 라우트마다 `// ── METHOD /path — 설명 ──` 헤더를 답니다.
- **DB 접근**: raw SQL은 `lib/db.ts`에 모읍니다. 라우트에서 직접 `c.env.DB.prepare(...)`를 쓰는 경우는 일회성 통계/세션 정리 등 예외에 한정.
- **타입**: 모든 공유 타입은 `types.ts`에 정의. `Bindings`에 환경변수를 추가하면 타입도 함께 갱신.
- **에러 응답**: `c.json({ error: '한국어 메시지' }, <status>)` 형식. 성공은 `{ success: true, ... }`.
- **ID**: `crypto.randomUUID()` 사용. 상황 이미지 id는 `si_${Date.now()}`.
- **LLM 분기**: 새 LLM 호출도 `llm.ts`의 `apiKey.startsWith('sk-ant-')` → Claude, 그 외 → OpenAI 호환 패턴을 따르세요. Claude는 system을 별도 필드, OpenAI는 messages 배열에 포함.
- **응답 포맷**: LLM 응답은 `*상황묘사* "대사"` 패턴으로 파싱(`parseResponse`). 상황 이미지는 `<<IMAGE:id>>` 태그 또는 키워드로 트리거.

## 개발 워크플로

```bash
npm install

# D1 로컬 마이그레이션 (.wrangler/state에 로컬 SQLite 생성)
npm run db:migrate:local

# 빌드 (dist/ 생성 — wrangler pages dev가 dist를 서빙하므로 필수)
npm run build

# 개발 서버 (둘 중 하나)
npm run dev # vite dev 서버
npm run dev:sandbox # wrangler pages dev (D1 로컬, 0.0.0.0:3000)
pm2 start ecosystem.config.cjs # PM2로 위 sandbox 실행

# DB 콘솔 / 리셋
npm run db:console
npm run db:reset # 로컬 D1 삭제 후 재마이그레이션

# 타입 생성
npm run cf-typegen
```

> **주의 (이름 불일치)**: `package.json`의 `db:*` 스크립트와 `dev:sandbox`는 D1 이름으로 `webapp-production`을 사용하지만, `wrangler.jsonc`의 바인딩 `database_name`은 `ai-character-chat-production`입니다. 로컬 마이그레이션/실행 시 이 차이를 인지하고, 프로덕션 D1을 다룰 때는 `wrangler.jsonc` 기준(`ai-character-chat-production`, id `ee234956-...`)을 사용하세요. 통일이 필요하면 양쪽을 함께 맞추세요.

## 마이그레이션 추가

- `migrations/000N_이름.sql` 형식으로 새 파일 추가 (순번 증가).
- SQLite는 컬럼 DROP을 지원하지 않습니다 → 사용 중단 컬럼은 주석으로만 표기(0002의 DomoAI 컬럼 참고).
- 새 컬럼 추가는 `ALTER TABLE ... ADD COLUMN` (기본값 필수). 적용: `npm run db:migrate:local` → 프로덕션 `npm run db:migrate:prod`.
- 스키마 변경 시 `types.ts`와 `db.ts` 헬퍼도 함께 갱신.

## 환경변수

`Bindings`(types.ts)에 정의. 로컬은 `.dev.vars`(gitignore됨), 프로덕션은 `wrangler pages secret put`.

| 변수 | 용도 | 비고 |
|------|------|------|
| `JWT_SECRET` | 모든 JWT 서명 | **필수**, 32자 이상 |
| `ADMIN_PASSWORD` | 관리자 로그인 | 기본 `admin1234` (변경 권장) |
| `GUEST_TOKEN_LIMIT` | 게스트 대화 한도 | 기본 `10` |
| `MEMBER_TOKEN_LIMIT` | 회원 기본 한도 | 기본 `100` (구독 시 `monthly_token_limit`로 대체) |
| `TOSS_SECRET_KEY` | 토스 결제 승인(서버) | 없으면 토스 테스트 키 |
| `TOSS_CLIENT_KEY` | 토스 결제창(공개) | 없으면 토스 테스트 키 |
| LLM API 키 | Claude/OpenAI 키 | **환경변수 아님** — DB `character_config.claude_api_key`에 저장, 관리자 패널에서 런타임 변경 |

## 배포 (Cloudflare Pages)

```bash
npm run deploy # = npm run build && wrangler pages deploy dist
# 시크릿:
npx wrangler pages secret put JWT_SECRET --project-name ai-character-chat
npx wrangler pages secret put ADMIN_PASSWORD --project-name ai-character-chat
```

## 알아둘 점 / 함정

- **프론트엔드는 `index.tsx` 내부 문자열**입니다. UI 변경은 `mainHTML()`/`adminHTML()` 템플릿 리터럴을 수정하세요. JSX 컴포넌트가 아닙니다. (`renderer.tsx`/`public/static/style.css`는 현재 라우트에서 사용되지 않음.)
- **README와의 차이**: README에 "DomoAI 영상 생성"이 완료로 표기되어 있으나 0002 마이그레이션에서 DomoAI는 사용 중단되었습니다. 또한 README의 API 표는 결제/통계/이미지 업로드 라우트를 일부 누락하고 있으니, 실제 라우트는 `src/routes/`를 기준으로 삼으세요.
- **이미지**는 R2가 아니라 D1에 Base64 TEXT로 저장됩니다(5MB 제한). 대용량/대량 업로드에는 부적합.
- **게스트→회원 이어받기**: `register`/`login` 시 `guestToken`을 받아 `guest_sessions` → `chat_sessions`로 복사/병합합니다.
- **토큰 한도 = 대화 횟수**: `token_used`는 실제 토큰이 아니라 대화(턴) 횟수 카운터입니다.
- **결제 검증**: 성공 콜백에서 DB 저장 금액과 토스 전달 금액을 대조해 위변조를 막고, 토스 서버 승인 API 호출 후에야 구독을 적용합니다. 이 검증 흐름을 우회하지 마세요.

## 작업 시 권장 절차

1. 변경 후 `npm run build`로 타입/빌드 에러 확인 (별도 테스트 스위트·린터는 없음).
2. 스키마를 건드렸다면 `migrations/`, `types.ts`, `lib/db.ts`를 한 세트로 갱신.
3. 새 라우트는 해당 `routes/*.ts`에 추가하고, 필요한 경우 `index.tsx`에서 마운트 확인.
4. 사용자 노출 문자열은 한국어로 작성.