# task-1409.1 완료 보고서: Threads API 카드뉴스 업로드 실패 원인분석 및 수정

## S - Situation
task-1398.1 카드뉴스 5장 발행 시 Instagram은 성공(Post ID: 18346557121215868), Threads는 HTTP 400 Bad Request(error_subcode 2207052)로 실패. 4/3에는 성공했으나 4/4에 실패하여 간헐적 문제로 보고됨.

## C - Complication
Threads `api/client.py`의 `post_carousel()` 메서드에서 `is_carousel_item=True` (Python boolean)을 전달하며, httpx가 form data로 직렬화할 때 `True` → `"True"` (대문자 T)로 변환됨. Threads API는 `"true"` (소문자)를 요구하므로 error_subcode 2207052(미디어 URI 요건 불충족) 발생. Instagram client는 `is_carousel_item="true"` (string)을 올바르게 사용하여 정상 동작. 추가로, 에러 응답 body가 exception에 포함되지 않아 디버깅이 어려웠음.

## Q - Question
`is_carousel_item` 파라미터 타입을 수정하고 에러 로깅을 개선하면 Threads 캐러셀 업로드가 안정적으로 동작하는가?

## A - Answer
`is_carousel_item=True`(boolean)을 `"true"`(string)으로 수정하여 httpx 직렬화 시 소문자 `"true"`가 전송되도록 변경. 에러 발생 시 응답 body를 RuntimeError에 포함하여 향후 디버깅 용이하도록 개선. pytest 59건 전체 통과, `urlencode` 직렬화 검증 PASS.

---

## 원인 분석 상세

### 근본 원인: is_carousel_item 파라미터 타입 불일치

```python
# 문제 코드 (client.py:89)
is_carousel_item=True  # Python boolean → urlencode → "True" (대문자 T)

# 정상 코드 (instagram_client.py:42)
is_carousel_item="true"  # Python string → urlencode → "true" (소문자)
```

검증: `urlencode({"is_carousel_item": True})` → `is_carousel_item=True` (대문자 T)
검증: `urlencode({"is_carousel_item": "true"})` → `is_carousel_item=true` (소문자)

### 4/3 성공 → 4/4 실패 원인
코드 변경 없음(git log 확인), 이미지 크기 유사(60-97KB). Meta API 서버측 파라미터 검증 강화로 추정. 이전에는 대소문자 무시했으나 strict validation 적용된 것으로 판단.

### 부수 원인: 에러 로깅 미흡
기존 `response.raise_for_status()`는 httpx 기본 메시지만 전달하여 `error_subcode` 등 상세 정보 확인 불가. RuntimeError로 변경하여 응답 body 포함.

---

## 수정 내용

### 수정 파일 목록
- `/home/jay/projects/ThreadAuto/.worktrees/task-1409.1-dev3/api/client.py`
- `/home/jay/projects/ThreadAuto/.worktrees/task-1409.1-dev3/tests/test_carousel_api.py`

### 수정 1: is_carousel_item 타입 수정 (client.py:91)
- `is_carousel_item=True` → `is_carousel_item="true"`

### 수정 2: 에러 로깅 개선 (client.py:211-223)
- `response.raise_for_status()` → `RuntimeError(f"Threads API error (HTTP {status}): {body}")` 
- 응답 body(최대 500자)를 exception 메시지에 포함

### 수정 3: 테스트 업데이트 (test_carousel_api.py:108-110)
- `c.kwargs.get("is_carousel_item") is True` → `== "true"` (string 검증)

---

## 테스트 결과

| 테스트 파일 | 테스트 수 | 결과 |
|---|---|---|
| tests/test_client.py | 11 | PASS |
| tests/test_carousel_api.py | 8 | PASS |
| tests/test_publisher.py | 39 | PASS |
| urlencode 직렬화 검증 | 1 | PASS |
| **합계** | **59** | **전체 PASS** |

---

## 발견 이슈 및 해결

### 자체 해결 (3건)
1. **is_carousel_item boolean→string 타입 불일치** — `client.py:91`에서 `True` → `"true"` 수정
2. **에러 응답 body 미포함** — `client.py:211-223`에서 RuntimeError로 body 포함하도록 개선
3. **테스트-코드 정합성 불일치** — `test_carousel_api.py:108-110`에서 boolean → string 검증 업데이트

### 범위 외 미해결 (0건)
없음.

---

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: task/task-1409.1-dev3
- **워크트리 경로**: /home/jay/projects/ThreadAuto/.worktrees/task-1409.1-dev3
- **머지 의견**: pytest 59건 전체 통과, 수정 범위 최소(2파일 14줄), Instagram client와 동일한 패턴 적용. 충돌 가���성 낮음. 수정 후 실제 Threads API 테스트 업로드(dry-run)는 수행하지 않았으나 직렬화 검증으로 정확성 확인.

---

## 셀프 QC 체크리스트
- [x] 1. 영향 파일: client.py (ThreadsClient.post_carousel, _request), test_carousel_api.py
- [x] 2. 엣지 케이스: boolean False도 동일 이슈 가능하나 현재 코드에서 False 사용 없음
- [x] 3. 작업 지시와 정확히 일치함 (원인분석 + 코드 수정 + 테스트)
- [x] 4. 에러 처리: RuntimeError로 개선, 보안 이슈 없음 (토큰은 기존과 동일하게 처리)
- [x] 5. 테스트 커버리지: 기존 테스트 + 타입 변경 테스트 모두 통과 (59건)
- [x] 6. 발견 이슈 3건 모두 직접 해결
- [x] 7. 코드 아키텍처: Instagram client와 동일 패턴, SOLID/DRY 위반 없음
- [x] 8. 인터페이스 변경: _request()의 에러 발생 타입 변경 (HTTPStatusError→RuntimeError), 호출부 영향 분석 완료 — threads_publisher.py에서 Exception으로 catch하므로 문제 없음

---

## 모델 사용 기록
- 팀원: 루(Lugh) / 작업: client.py 수정 + 테스트 업데이트 / 사용 모델: sonnet / 정당성: -
- 팀원: 모리건(Morrigan) / 작업: 테스트 검증 실행 / 사용 모델: sonnet / 정당성: -

---

## 산출물 파일
- `/home/jay/projects/ThreadAuto/.worktrees/task-1409.1-dev3/api/client.py`
- `/home/jay/projects/ThreadAuto/.worktrees/task-1409.1-dev3/tests/test_carousel_api.py`
- `/home/jay/workspace/memory/reports/task-1409.1.md`
