# 세 장의 질문: Cloudflare AI 타로 웹앱 실습

질문 입력, 카드 세 장 추첨, 그림별 뜻풀이, AI 해석과 후속 대화를 연결한 예제다. GPU와 Python은 필요하지 않다.

**완성 예제 체험:** https://hufs-tarot-lab.juho-2b6.workers.dev/

질문을 적고 카드를 뽑은 뒤 그림을 누르면 방향별 기본 의미가 나온다. ‘이 카드로 이야기 시작’을 누르면 대화창에서 첫 해석을 읽고 후속 질문을 다섯 번까지 이어갈 수 있다. ‘새 질문 시작’은 카드와 대화를 초기화한다. 체험 사이트는 로그인 없이 열 수 있으며, 자신의 앱을 수정·배포할 때는 아래 준비를 따른다.

## 1. 준비

- Node.js **22 이상**과 자신의 Cloudflare 계정이 필요하다. `node --version`으로 확인한다.
- ZIP을 압축 해제하고 `package.json`이 있는 `tarot-web` 폴더를 코드 편집기에서 연다.
- AI 코딩 도우미를 사용한다면 `PROMPT.md`를 붙여 넣고 이 폴더를 함께 전달한다.
- `wrangler.jsonc`의 `name`을 자신의 영문 소문자 앱 이름으로 바꾼다. 예: `tarot-minji-2026`.

## 2. 실행과 배포

```sh
npm install
npx wrangler login
npx wrangler whoami
npm run dev
```

로그인 후 `whoami`에서 자신의 계정을 확인한다. 계정이 여러 개라면 자신의 계정 ID를 `wrangler.jsonc`의 `account_id`에 추가한다. Account ID는 API 토큰이 아니다. 수업 예제에는 교수자 계정 ID나 비밀값이 들어 있지 않다.

터미널에 나온 로컬 주소(보통 `http://localhost:8787`)를 연다. 카드를 뽑고 질문을 적은 다음 AI 해석을 확인한다. HTML을 더블클릭하거나 정적 파일 서버로만 열면 AI API가 동작하지 않는다. AI 바인딩은 로컬 개발 중에도 Cloudflare에서 실행되므로 인터넷 연결과 로그인이 필요하고 해당 계정의 Workers AI 사용량에 포함된다.

확인을 마치면 `Ctrl+C`로 개발 서버를 종료하고 배포한다.

```sh
npm run check
npm run build:check
npm run deploy
```

터미널에 표시되는 `https://앱이름.본인서브도메인.workers.dev` 주소가 공개 URL이다. Workers 하위 도메인을 처음 만드는 계정은 안내에 따라 설정한다. 배포한 URL을 휴대전화에서도 열어 본다. 이후 파일을 고친 뒤 `npm run deploy`를 다시 실행하면 갱신된다. 이것은 **Worker와 정적 자산을 함께 배포하는 프로젝트**다. Pages에 public 폴더만 업로드하면 AI 기능이 빠진다.

## 3. 파일 구조와 바꿀 곳

```text
public/index.html      질문·카드·뜻풀이와 대화창
public/style.css       색·배치·글꼴
public/app.js          추첨, 카드 상세 보기, 대화 이력과 API 호출
public/cards.json      카드 22장, 이름, 방향별 키워드·설명, 그림 경로
public/cards/          SVG 23장 + PNG 23장 (메이저 22장과 뒷면)
src/worker.js          입력 검증, SYSTEM_PROMPT, Workers AI 호출
wrangler.jsonc         앱 이름, AI 바인딩, 모델, 호출 빈도 제한
PROMPT.md              AI 코딩 도우미에게 줄 제작 요청
tests/worker.test.js   API의 정상·오류 경로 검증
```

그림을 바꾸려면 같은 파일명의 SVG를 교체하거나 PNG를 넣고 cards.json의 `image`를 바꾼다. 이미지 URL을 붙일 수도 있지만 파일을 public/cards에 넣으면 외부 주소 장애의 영향을 받지 않는다. 카드 ID 0~21과 파일명 연결을 유지한다. 수정 후 다시 배포한다. 예제는 모델의 이미지 인식 기능을 사용하지 않으며, 그림을 바꿔도 해석은 JSON의 이름·키워드를 따른다. 의미를 바꾸려면 JSON도 함께 바꾼다.

## 4. API 흐름

브라우저 → 같은 앱의 `/api/reading` → Worker의 `env.AI.run()` → Workers AI → 한국어 답변.

기본 모델은 **Gemma 4 26B A4B** (`@cf/google/gemma-4-26b-a4b-it`)다. `max_completion_tokens: 1200`, `temperature: 0.65`, `chat_template_kwargs: { enable_thinking: false }`로 호출한다. 결과는 `choices[0].message.content`에서 읽으며, 빈 답변이나 길이 제한으로 잘린 답변은 재시도 안내로 처리한다. 추론 내용은 화면에 표시하지 않는다.

**Qwen 3.8 비교 실습:** `wrangler.jsonc`의 `MODEL`을 `@cf/qwen/qwen3.8-27b`로 바꾼 뒤 다시 배포한다. 예제 코드는 이 모델에 `reasoning_effort: "low"`와 `max_completion_tokens: 2000`을 적용한다. 질문과 카드, 프롬프트를 고정해 한국어 표현·응답 시간·카드 맥락 유지·사용량을 비교한다. 다른 모델로 바꿀 때는 해당 모델의 입력 옵션과 응답 형식을 확인한다.

`ai.binding = "AI"`는 Worker가 Cloudflare의 모델 API를 호출하도록 연결하는 설정이다. API 토큰은 프런트엔드에 넣지 않으며 기본 실습에서는 별도 토큰을 발급할 필요가 없다. Wrangler 로그인으로 배포 권한을 얻고 실행할 때는 AI 바인딩을 쓴다. 다른 서버에서 직접 REST API를 호출하려면 Workers AI 전용 토큰을 **그 서버의 비밀 환경변수**에만 둔다.

후속 질문에서는 같은 카드와 원래 질문에 `history`를 더해 보낸다. 형식은 이전 AI 답변(assistant), 후속 질문(user)의 반복이며 마지막 항목은 현재 질문이다. 서버는 역할·순서·길이를 확인한 뒤 시스템 지침, 카드 데이터, 앞선 대화를 순서대로 LLM에 전달한다. 한 카드 뽑기당 후속 질문은 다섯 번까지이며 요청 본문은 64KB 이내다.

질문과 선택한 카드 정보, 이전 대화는 답변 생성을 위해 Cloudflare로 전송된다. 이 예제는 질문·해석을 DB나 브라우저 저장소에 저장하지 않는다. 페이지를 새로고침하거나 새 질문을 시작하면 대화가 사라진다. 개인정보는 질문에 적지 않는다.

## 5. 실습과 확인

- **목표:** 사용자 입력과 카드 데이터를 프롬프트로 묶고, API 결과를 자신의 웹앱에서 보여 준다.
- **조작:** 카드 세 장과 질문을 고정하고 `SYSTEM_PROMPT`의 역할·답변 형식·말투 중 한 항목을 바꾼다. 같은 조건에서도 생성 답변은 달라질 수 있으므로 여러 답변에서 반복되는 차이를 본다.
- **관찰:** 세 카드 이름과 방향이 답변에 반영되는지, 답변이 질문과 연결되는지, 사실처럼 단정하는 표현이 줄었는지 확인한다.
- **결과물:** 배포 URL, 모바일 화면, 수정한 코드나 그림, 프롬프트 수정 전후의 답변 비교를 정리한다.

## 6. 오류 해결과 사용량

| 현상 | 확인할 곳 |
|---|---|
| `npm`을 찾지 못함 | Node.js 22 이상 설치 후 터미널 다시 열기 |
| 로그인이 안 됨 | 브라우저 로그인 계정과 `npx wrangler whoami` |
| AI 바인딩 또는 502 오류 | wrangler.jsonc의 AI 설정, 모델 이름, Workers AI 대시보드 사용량 |
| 429 응답 | 1분 뒤 재시도. 같은 IP의 요청은 합산됨 |
| 그림이 안 보임 | cards.json의 image 경로, 대소문자, public/cards 파일 |
| AI 해석만 동작하지 않음 | 정적 호스팅 대신 `npm run dev` 또는 Workers 배포 주소 사용 |

기본 예제는 IP당 60초에 30회, Gemma 4 출력 최대 1,200토큰(Qwen 3.8은 추론을 포함해 최대 2,000토큰)으로 제한한다. Cloudflare의 호출 빈도 제한은 위치별 근사 제한이며 전체 비용 상한은 아니다. 유료 계정은 무료 할당량을 넘으면 과금될 수 있다. 계정의 Workers AI 대시보드에서 사용량을 확인한다. Free 플랜의 일일 무료 할당량은 2026-09-28 확인 기준 10,000 Neurons이며 무료 할당량을 넘으면 요청이 실패한다. 수업에서 유료 업그레이드는 필수가 아니다.

## 출처와 사용 허락

- [Workers AI 바인딩](https://developers.cloudflare.com/workers-ai/configuration/bindings/)
- [Gemma 4 모델과 파라미터](https://developers.cloudflare.com/workers-ai/models/gemma-4-26b-a4b-it/)
- [Qwen 3.8 모델과 파라미터](https://developers.cloudflare.com/workers-ai/models/qwen3.8-27b/)
- [정적 자산과 Worker 라우팅](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/)
- [요금과 무료 할당량](https://developers.cloudflare.com/workers-ai/platform/pricing/)
- [호출 빈도 제한](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/)

그림은 수업용으로 직접 구성한 단순 상징이며 기존 상업용 타로 덱 이미지를 가져오지 않았다. 이 패키지의 직접 작성 코드·그림·카드 데이터는 CC0 1.0으로 제공한다. 자유롭게 복사·수정·배포할 수 있다. 외부 서비스와 의존 패키지는 각각의 약관·라이선스를 따른다. `LICENSE.txt` 참고.
