diff --git a/CLAUDE.md b/CLAUDE.md index 68b3c9c..a9feefe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Read the shared project context first: - `docs/setup-guide.md` +- 크로스레포(웹↔서버↔AI) API 계약·ADR은 `../docs/` 참조 — 진입점 `../CLAUDE.md` ## Project Overview diff --git a/apps/todos/ai_client.py b/apps/todos/ai_client.py index 46f6f8f..b0e1fdc 100644 --- a/apps/todos/ai_client.py +++ b/apps/todos/ai_client.py @@ -154,7 +154,6 @@ def _request( } if self.api_key: headers["X-API-Key"] = self.api_key - headers["X-Internal-Service-Token"] = self.api_key try: response = httpx.request( diff --git a/docs/api-character-generation.md b/docs/api-character-generation.md index f7a6600..9a0c5ed 100644 --- a/docs/api-character-generation.md +++ b/docs/api-character-generation.md @@ -1,316 +1,11 @@ -# 캐릭터 생성 API 명세서 - -> 캐릭터(주민) 생성·등록·조회 전 구간 API 명세. 프론트(mongle-web) ↔ Django(mongle-server) ↔ AI(mongle-ai) 연동 포함. -> 작성: 2026-06-14 · 기준 브랜치: `fix/character-gen-api` - -## 1. 개요 - -캐릭터 생성은 **이미지 생성에 수십 초~수 분**이 걸리므로 **비동기 잡(Job) + 폴링** 구조다. 흐름은 4단계: - -1. (사진 있을 때) S3 업로드용 presigned URL 발급 → 프론트가 S3에 직접 업로드 -2. 생성 잡 등록 (Celery 큐) → 즉시 `job_id` 반환 -3. 잡 상태 폴링 → 완료 시 생성 결과 수신 -4. 결과를 확정해 캐릭터 등록 - -- **인증**: 모든 엔드포인트 JWT Bearer 필요 (`Authorization: Bearer `). 미인증/만료 시 공통 `401`. -- **Base URL**: `/api/v1/characters` -- **응답 포맷**: flat JSON (envelope 없음). 에러는 `{"error": ""}` (검증 실패는 `{"error": <필드별 메시지>}`) -- **제약**: 활성 캐릭터 최대 **10개**, 하루 이미지 생성 **3회** - -## 2. 전체 흐름 (시퀀스) - -```mermaid -sequenceDiagram - participant FE as 프론트(mongle-web) - participant BE as Django(mongle-server) - participant CW as Celery Worker - participant AI as mongle-ai(/v1/character) - participant S3 - - Note over FE,S3: (사진 첨부 시에만 1~2단계) - FE->>BE: 1. POST /characters/source-images - BE-->>FE: 201 {source_img_id, upload:{url, headers}} - FE->>S3: 2. PUT upload.url (headers 그대로) + 파일 바이트 - - FE->>BE: 3. POST /characters/generation-jobs {name, persona, keywords, source_img_id?} - BE->>CW: enqueue(job_id, name, persona) - BE-->>FE: 202 {job_id, status:QUEUED, estimated_seconds} - - CW->>AI: POST /v1/character (X-API-Key) {user_id,name,persona,keywords,source_image_url} - AI-->>CW: {status:done, result:{image_url, ...}} - CW->>BE: job.gen_img_url 저장, status=SUCCEEDED - - loop 폴링 (2~4초 간격) - FE->>BE: 4. GET /characters/generation-jobs/:id - BE-->>FE: 200 {status, result?} - end - - FE->>BE: 5. POST /characters {gen_job_id, name, persona} - BE-->>FE: 201 {character_id, name, gen_img_url, persona} -``` - -## 3. 엔드포인트 목록 - -| # | 요구사항 ID | Method | Path | 설명 | 뷰 | -| --- | ----------- | ------ | -------------------------------------------- | -------------------- | ---------------------------- | -| 1 | CHAR-001 | POST | `/api/v1/characters/source-images` | presigned URL 발급 | `SourceImageCreateView` | -| 2 | CHAR-002 | POST | `/api/v1/characters/generation-jobs` | 생성 잡 등록(비동기) | `GenerationJobCreateView` | -| 3 | CHAR-003 | GET | `/api/v1/characters/generation-jobs/:job_id` | 잡 상태·결과 조회 | `GenerationJobDetailView` | -| 4 | CHAR-004 | POST | `/api/v1/characters` | 캐릭터 확정 등록 | `CharacterListView.post` | -| 5 | CHAR-005 | GET | `/api/v1/characters` | 활성 캐릭터 목록 | `CharacterListView.get` | -| 6 | — | GET | `/api/v1/characters/:character_id` | 캐릭터 상세 | `CharacterDetailView.get` | -| 7 | — | DELETE | `/api/v1/characters/:character_id` | 캐릭터 삭제(soft) | `CharacterDetailView.delete` | -| 8 | — | GET | `/api/v1/characters/:character_id/quests` | 퀘스트 목록 | `QuestListView` | - ---- - -## 4. 엔드포인트 상세 - -### 4.1. CHAR-001 — presigned URL 발급 - -`POST /api/v1/characters/source-images` - -사진을 첨부하는 경우에만 호출. 발급받은 `upload` 정보로 프론트가 **S3에 직접 PUT** 업로드한다. - -**Request Body** - -| 필드 | 타입 | 필수 | 설명 | -| ---------------- | ------------ | ---- | ----------------------------- | -| `file_name` | string(≤255) | ✅ | 원본 파일명 | -| `content_type` | enum | ✅ | `image/jpeg` 또는 `image/png` | -| `content_length` | int | ✅ | 1 ~ 5,242,880 (5MB) | - -**비즈니스 로직 (서버 처리 순서)** - -1. 입력 검증 (위 표). 위반 시 `400 {"error": <필드 오류>}`. -2. `content_type` → 확장자 매핑 (`image/jpeg`→`jpg`, `image/png`→`png`). -3. `object_key` 생성: `{AWS_S3_PREFIX}/source-images/{user_id}/{uuid}.{ext}` (기본 prefix `mongle-village`). AI 워커와 동일 네임스페이스로 모아 IAM `mongle-village/*` 와 일치시킨다. -4. `SourceImage` 레코드 생성: `status=PENDING_UPLOAD`, `expires_at = now + 600초`. -5. S3 `put_object` presigned URL 발급 — **`ContentType`만 서명에 묶인다**(클라이언트가 같은 `Content-Type`으로 PUT해야 검증 통과). `Content-Length`는 서명하지 않는다(브라우저 금지 헤더). -6. `201` 응답. - -**Response `201 Created`** - -```json -{ - "source_img_id": "uuid", - "object_key": "mongle-village/source-images//.png", - "upload": { - "url": "https://...s3...?X-Amz-Algorithm=...&X-Amz-Signature=...", - "headers": { "Content-Type": "image/png" } - }, - "expires_at": "2026-06-14T10:00:00+00:00" -} -``` - -- ⚠️ `upload.headers`는 **POST 폼 `fields`가 아니라 PUT 요청 헤더**다. 프론트는 S3 PUT 시 `upload.headers`를 **그대로** 실어야 한다(다르면 `403 SignatureDoesNotMatch`). -- `Content-Length`는 서명 헤더에 포함하지 않는다(`SignedHeaders=content-type;host`). 브라우저 Fetch API가 금지 헤더로 취급해 자동 설정하므로, 서명에 넣으면 `403 SignatureDoesNotMatch`가 난다. -- presigned URL 만료: 600초. - -**에러** - -| 상황 | HTTP | 응답 | -| --------------------------------------------------------------------------------------------- | ----- | -------------------------- | -| 입력 검증 실패(`content_type`이 jpeg/png 아님, `content_length` 범위 밖, `file_name` 누락 등) | `400` | `{"error": <필드별 오류>}` | -| S3 스토리지 미설정(`AWS_S3_BUCKET` 비어 있음) | `503` | `{"error": "STORAGE_NOT_CONFIGURED"}` | -| presigned 발급 실패(AWS 자격증명/네트워크) | `500` | DRF 기본(별도 핸들링 없음) | - -> CHAR-001은 다른 엔드포인트와 달리 머신 코드(`*_EXCEEDED`)가 아니라 **DRF 검증 오류 형태**를 반환한다(에러 포맷 표준화는 비범위). - ---- - -### 4.2. CHAR-002 — 생성 잡 등록 - -`POST /api/v1/characters/generation-jobs` - -**Request Body** - -| 필드 | 타입 | 필수 | 설명 | -| ---------------------- | -------------- | ---- | --------------------------------------- | -| `name` | string(1~8) | ✅ | 캐릭터 이름 | -| `persona` | string | ✅ | 페르소나 설명 | -| `personality_keywords` | string[] (1~3) | ✅ | 성격 키워드 | -| `source_img_id` | uuid | ❌ | CHAR-001에서 받은 ID (사진 없으면 생략) | -| `custom_prompt` | string(≤200) | ❌ | 외형 묘사 (등록 시 `Character.visual`에 저장) | - -**비즈니스 로직 (서버 처리 순서)** - -1. 입력 검증. 위반 시 `400`. -2. **활성 캐릭터 수 제한**: `Character(user, is_active=True).count() >= 10` → `422 CHARACTER_LIMIT_EXCEEDED`. -3. **일일 생성 제한**: 오늘 `ImgGenLog(user).count() >= 3` → `429 DAILY_GENERATION_LIMIT_EXCEEDED`. -4. `source_img_id`가 있으면: - - `SourceImage` 조회(본인 소유). 없으면 `404 SOURCE_IMAGE_NOT_FOUND`. - - `status==UPLOAD_EXPIRED` 또는 `now > expires_at` → `410 SOURCE_IMAGE_UPLOAD_EXPIRED`. - - `check_object_exists()`로 실제 S3 업로드 확인 → 있으면 `status=UPLOAD_COMPLETED`. -5. `CharacterGenerationJob` 생성: `status=QUEUED`, `personality_keywords`, `custom_prompt`, `source_image`. -6. `ImgGenLog` 생성(일일 카운트 +1). -7. **Celery 큐 등록**: `process_character_generation_job.delay(job_id, name, persona)`. - - name·persona는 잡 모델에 저장하지 않고 **태스크 인자로 전달**(마이그레이션 회피). -8. `202` 응답. - -**Response `202 Accepted`** - -```json -{ "job_id": "uuid", "status": "QUEUED", "estimated_seconds": 60 } -``` - -**에러**: `CHARACTER_LIMIT_EXCEEDED`(422), `DAILY_GENERATION_LIMIT_EXCEEDED`(429), `SOURCE_IMAGE_NOT_FOUND`(404), `SOURCE_IMAGE_UPLOAD_EXPIRED`(410). - ---- - -### 4.3. CHAR-003 — 잡 상태 조회(폴링) - -`GET /api/v1/characters/generation-jobs/:job_id` - -**비즈니스 로직** - -1. `CharacterGenerationJob` 조회(`job_id`, 본인 소유). 없으면 `404 NOT_FOUND`. -2. 직렬화: `result`는 `status==SUCCEEDED`일 때만 `{gen_img_url, persona}`, 그 외 `null`. -3. `200` 응답. 프론트는 2~4초 간격 폴링. - -**Response `200 OK`** - -```json -{ - "job_id": "uuid", - "status": "QUEUED | IN_PROGRESS | SUCCEEDED | FAILED | CONSUMED", - "result": { "gen_img_url": "https://cdn/...png", "persona": "..." }, - "created_at": "...", - "updated_at": "..." -} -``` - ---- - -### 4.4. CHAR-004 — 캐릭터 확정 등록 - -`POST /api/v1/characters` - -**Request Body** - -| 필드 | 타입 | 필수 | 설명 | -| ------------ | ----------- | ---- | ------------ | -| `gen_job_id` | uuid | ✅ | 성공한 잡 ID | -| `name` | string(1~8) | ✅ | 캐릭터 이름 | -| `persona` | string | ✅ | 페르소나 | - -**비즈니스 로직 (서버 처리 순서)** - -1. 입력 검증. 위반 시 `400`. -2. **활성 캐릭터 수 제한**: `>= 10` → `422 CHARACTER_LIMIT_EXCEEDED`. -3. `CharacterGenerationJob` 조회(`gen_job_id`, 본인 소유). 없으면 `404 JOB_NOT_FOUND`. -4. `status==CONSUMED` → `409 JOB_ALREADY_CONSUMED` (한 잡은 한 번만 등록 가능). -5. `status!=SUCCEEDED` → `400 JOB_NOT_SUCCEEDED`. -6. `Character` 생성: `generation_job=job`, `character_name=name`, `gen_img_url=job.gen_img_url`, `persona=job.persona`, `visual=job.appearance`, `appearance_payload=job.appearance_payload`. - - 생성 완료와 확정 등록은 서로 다른 시점이므로, 구조화 외형 데이터는 먼저 Job에 보관했다가 이 단계에서 Character로 복사한다. -7. `job.status = CONSUMED`. -8. `201` 응답 + (권장) `Location: /api/v1/characters/:id`. - -**Response `201 Created`** - -```json -{ - "character_id": "uuid", - "name": "몽글", - "gen_img_url": "https://cdn/...png", - "persona": "착한 곰", - "created_at": "..." -} -``` - -**에러**: `CHARACTER_LIMIT_EXCEEDED`(422), `JOB_NOT_FOUND`(404), `JOB_ALREADY_CONSUMED`(409), `JOB_NOT_SUCCEEDED`(400). - ---- - -### 4.5. CHAR-005 — 캐릭터 목록 / 4.6 상세 / 4.7 삭제 / 4.8 퀘스트 - -- **GET `/characters?limit=20`** — 본인 **활성** 캐릭터(`is_active=True`)를 cursor 기반 조회. 응답은 캐릭터 요약 배열. -- **GET `/characters/:id`** — 캐릭터 상세(`name`(=character_name), `gen_img_url`, `persona`, 활성 퀘스트 등). 본인 소유 아니면 `404`. -- **DELETE `/characters/:id`** — **soft delete**(`is_active=False`). 마지막 활성 캐릭터 삭제는 `LAST_CHARACTER` guard로 차단. 성공 `200/204`. -- **GET `/characters/:id/quests`** — 해당 캐릭터의 퀘스트 목록. - ---- - -## 5. 내부 연동: Django → mongle-ai (Celery 태스크) - -`process_character_generation_job(job_id, name, persona)`가 AI 서비스를 호출한다. **mongle-ai 계약이 정본**이며 Django가 거기에 맞춘다. - -**비즈니스 로직 (Celery 워커)** - -1. `job` 조회(없으면 종료). `status = IN_PROGRESS`. -2. `job.source_image`가 있으면 S3 URL 구성: - `https://{BUCKET}.s3.{REGION}.amazonaws.com/{object_key}` → `source_image_url`. 없으면 `""`. -3. `POST {AI_SERVICE_URL}/v1/character` 호출: - - 헤더 `X-API-Key: ` (⚠️ Bearer 아님) - - Body `{user_id, name, persona, personality_keywords, source_image_url}` - - `timeout = 120초` -4. 완료 응답 envelope에서 **`result`** 키 파싱(⚠️ `data` 아님): - - `result.image_url` → `job.gen_img_url` - - `result.gen_img_object_key` → `job.gen_img_object_key` - - `result.appearance` → `job.appearance` - - `result.appearance_payload` → `job.appearance_payload` -5. `status = SUCCEEDED` 저장. 사용자가 나중에 확정 등록할 때 Job의 외형 데이터를 Character로 복사한다. -6. 예외 발생 시 `status = FAILED`, `error_code/error_message` 저장. - -**mongle-ai 응답 예시** - -```json -{ - "status": "done", - "result": { - "image_url": "https://...", - "persona": "...", - "personality": "...", - "speech_style": "...", - "background": "...", - "appearance": "round brown bear with a red ribbon", - "appearance_payload": { - "character_type": "bear", - "main_colors": ["brown"], - "accessories": ["red ribbon"] - } - }, - "error": null -} -``` - -- ⚠️ 개인화 필드(`personality`/`speech_style`/`background`)는 현재 **저장하지 않는다**(향후 챗봇 메모리 기능에서 도입 예정). -- `appearance_payload`는 프론트 표시용이 아니라 피드 이미지에서 동일 캐릭터를 재현하기 위한 내부 데이터다. 캐릭터 확정 후 `Character`에 보관하며, 이후 피드 생성 태스크가 `POST /v1/feed/generate`의 `character.appearance_payload`로 전달한다. -- ⚠️ **운영 주의**: 콜드스타트 시 AI 응답이 `timeout(120초)`을 초과할 수 있다(RunPod LLM·이미지 워커 동시 cold). 첫 요청/유휴 후 첫 요청에서 `FAILED` 위험 → 워커 웜 유지 또는 timeout 상향 검토 필요. - ---- - -## 6. 상태 전이 (CharacterGenerationJob) - -``` -QUEUED → IN_PROGRESS → SUCCEEDED → CONSUMED(등록 완료) - ↘ FAILED -``` - -| 상태 | 의미 | -| ------------- | ------------------------------------------ | -| `QUEUED` | 큐 등록됨(Celery 대기) | -| `IN_PROGRESS` | Celery가 AI 호출 중 | -| `SUCCEEDED` | 이미지·결과 저장 완료 (등록 가능) | -| `FAILED` | AI 호출 실패(`error_code`/`error_message`) | -| `CONSUMED` | 캐릭터로 등록 완료(재사용 불가) | - -## 7. 에러 코드 요약 - -| 코드 | HTTP | 발생 위치 | -| --------------------------------- | ---- | -------------------------------- | -| `STORAGE_NOT_CONFIGURED` | 503 | CHAR-001 | -| `CHARACTER_LIMIT_EXCEEDED` | 422 | CHAR-002, CHAR-004 | -| `DAILY_GENERATION_LIMIT_EXCEEDED` | 429 | CHAR-002 | -| `SOURCE_IMAGE_NOT_FOUND` | 404 | CHAR-002 | -| `SOURCE_IMAGE_UPLOAD_EXPIRED` | 410 | CHAR-002 | -| `JOB_NOT_FOUND` / `NOT_FOUND` | 404 | CHAR-003, CHAR-004 | -| `JOB_ALREADY_CONSUMED` | 409 | CHAR-004 | -| `JOB_NOT_SUCCEEDED` | 400 | CHAR-004 | -| `LAST_CHARACTER` | 4xx | DELETE (마지막 캐릭터 삭제 차단) | - -## 8. 참고 - -- 설계 배경: `docs/superpowers/specs/2026-06-14-character-generation-api-unification-design.md` (mong-studio 루트) -- 구현 코드: `apps/characters/{urls,views,serializers,tasks}.py`, `infrastructure/storage/s3.py` -- 프론트 호출: `mongle-web/src/features/character/characterApi.ts` +# 캐릭터 생성 API 명세서 (→ 정본으로 이동) + +> **이 문서의 정본은 스튜디오 루트 `../docs/api/character-generation-api.md` 다** +> (mong-studio 작업 공간 기준 `docs/api/character-generation-api.md`). +> +> 두 벌이 각자 갱신되며 갈라졌던 것을 2026-07-09 정본으로 병합했다 — 이 사본에만 있던 +> presigned PUT `headers` 함정(403 SignatureDoesNotMatch), `STORAGE_NOT_CONFIGURED`(503), +> `appearance_payload` 매핑은 모두 정본에 반영됨. cancel/quota 엔드포인트도 정본에만 있다. +> +> **여기에 내용을 다시 추가하지 말 것.** 계약이 바뀌면 정본을 고치고, +> 구현 코드는 `apps/characters/{urls,views,serializers,tasks}.py` 를 본다. diff --git a/docs/backend-api-flow.md b/docs/backend-api-flow.md index fed8c3b..693595a 100644 --- a/docs/backend-api-flow.md +++ b/docs/backend-api-flow.md @@ -7,7 +7,8 @@ - 브라우저는 내부 서비스 토큰을 알면 안 된다. - 프론트엔드가 호출하는 Django API는 `Authorization: Bearer {access_token}` 기반으로 인증한다. - Django가 AI 서버를 호출할 때는 내부 헤더를 붙인다. -- AI 공통 규칙상 내부 API는 `X-Internal-Service-Token`을 검증해야 한다. +- AI 내부 인증은 **`X-API-Key` 단독**이다 (mongle-ai `require_api_key`, ADR 0005). 과거 문서의 + `X-Internal-Service-Token`은 AI가 읽지 않는 헤더였고 2026-07-09 코드에서도 제거됐다. - DB 저장은 Django가 담당한다. ## 공통 헤더 @@ -25,9 +26,9 @@ | Header | 설명 | | --- | --- | -| `X-Internal-Service-Token` | 내부 서비스 토큰 | -| `X-API-Key` | 기존 AI 구현 호환용 API key | +| `X-API-Key` | AI 내부 인증 (`MONGLE_API_KEY`) — 유일한 인증 헤더 | | `X-Request-Id` | 요청 추적 ID | +| `User-Agent: mongle-server/1.0` | RunPod 프록시(Cloudflare)의 기본 urllib UA 차단(1010) 우회 | | `Content-Type: application/json` | JSON 요청 | ## TODO 플래너 흐름 @@ -41,7 +42,6 @@ mongle-web mongle-server POST {MONGLE_AI_API_BASE}/v1/todo/chat - X-Internal-Service-Token: {MONGLE_AI_API_KEY} X-API-Key: {MONGLE_AI_API_KEY} mongle-ai @@ -53,7 +53,6 @@ mongle-web mongle-server GET {MONGLE_AI_API_BASE}/v1/todo/chat/{job_id} - X-Internal-Service-Token: {MONGLE_AI_API_KEY} X-API-Key: {MONGLE_AI_API_KEY} mongle-ai @@ -75,7 +74,6 @@ mongle-server mongle-ai POST /v1/quest/generate - X-Internal-Service-Token: {MONGLE_AI_API_KEY} X-API-Key: {MONGLE_AI_API_KEY} ``` @@ -88,20 +86,12 @@ mongle-ai | `due_date == 오늘` | `Todo` | | `due_date != 오늘` | `Schedule` | -### 3. AI 내부 commit +### 3. (제거됨) AI 내부 commit — `/todos/commit/` -```text -internal caller - POST /api/v1/todos/commit/ - X-Internal-Service-Token: {MONGLE_AI_API_KEY} - -mongle-server - 내부 토큰 검증 후 Todo/Schedule 저장 -``` - -`/todos/commit/`은 AI/internal 계약을 위해 남겨둔 endpoint다. 프론트에서 직접 호출하지 않는다. - -현재 Django 구현에서는 인증 사용자 없이 내부 토큰만으로 들어오면 `demo@mongle.local` 사용자로 저장하는 fallback이 있다. 실제 사용자별 AI callback 저장을 사용하려면 요청 payload에 `user_id`를 포함하거나 `thread_id -> user` 매핑을 통해 저장 대상을 명확히 해야 한다. +과거 문서는 AI/internal이 `POST /api/v1/todos/commit/`로 저장을 콜백하는 계약을 기술했으나, +**이 라우트는 현재 존재하지 않는다** (`apps/todos/urls.py`에 없음 — 미정의 뷰와 함께 제거됨). +저장은 전부 브라우저 인증 경로(`/todos/planner-confirm/`, `/todos/confirm/`)로 이뤄진다. +AI의 `/v1/todo/commit`은 별개 엔드포인트이며 Django가 현재 호출하지 않는다(아래 표 참조). ## TODO 관련 endpoint 구분 @@ -112,7 +102,6 @@ mongle-server | `GET /api/v1/todos/chat/{job_id}/` | Web | `Authorization` | 멀티턴 플래너 대화 job 상태/결과 조회 | | `POST /api/v1/todos/planner-confirm/` | Web | `Authorization` | 플래너 후보를 현재 사용자 Todo/Schedule로 저장 | | `POST /api/v1/todos/confirm/` | Web | `Authorization` | 일반 TODO 후보 확정 저장 | -| `POST /api/v1/todos/commit/` | Internal/AI | `X-Internal-Service-Token` | 내부 commit 계약 | ## AI 서버 endpoint 구분 @@ -126,7 +115,6 @@ mongle-server ## 주의사항 -- `/todos/commit/`과 AI의 `/v1/todo/commit`은 이름이 비슷하지만 호출 방향과 책임을 반드시 확인해야 한다. -- 프론트는 `/todos/commit/`을 직접 호출하지 않고 `/todos/planner-confirm/`을 사용한다. -- AI 공통 규칙의 primary 내부 인증 헤더는 `X-Internal-Service-Token`이다. -- 기존 구현 호환을 위해 Django `TodoAIClient`는 `X-API-Key`도 함께 보낸다. +- Django `/todos/commit/` 라우트는 제거됐다. AI의 `/v1/todo/commit`과 혼동하지 말 것(후자도 현재 Django가 호출하지 않음). +- 저장은 `/todos/planner-confirm/`(플래너) 또는 `/todos/confirm/`(일반)을 사용한다. +- AI 내부 인증 헤더는 `X-API-Key` 하나다. `X-Internal-Service-Token`은 AI가 읽지 않아 2026-07-09 코드·문서에서 제거됐다. diff --git a/tests/test_todos_ai.py b/tests/test_todos_ai.py index 11123f0..91f6d16 100644 --- a/tests/test_todos_ai.py +++ b/tests/test_todos_ai.py @@ -467,7 +467,7 @@ def test_todo_ai_client_request_uses_httpx_with_internal_headers() -> None: assert kwargs["json"] == {"hello": "world"} assert kwargs["timeout"] == 7 assert kwargs["headers"]["X-API-Key"] == "token" - assert kwargs["headers"]["X-Internal-Service-Token"] == "token" + assert "X-Internal-Service-Token" not in kwargs["headers"] def test_todo_ai_client_request_maps_http_status_errors() -> None: