diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3dbcfb4 --- /dev/null +++ b/CLAUDE.md @@ -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 `. 페이로드 `{ 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: '한국어 메시지' }, )` 형식. 성공은 `{ 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`). 상황 이미지는 `<>` 태그 또는 키워드로 트리거. + +## 개발 워크플로 + +```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. 사용자 노출 문자열은 한국어로 작성.