# InsuRo 스레드 발행 기능 — Phase 1: 서버 엔드포인트 + PublishPanel

## 작업 레벨: Lv.3

## 프로젝트 시스템 3문서
- DevSystem: `/home/jay/workspace/memory/plans/anu-guide-system/plan.md`

## 프로젝트
- InsuRo: `/home/jay/projects/InsuRo`
- 서버: `/home/jay/projects/InsuRo/server`

## 미팅 기록
- `/home/jay/workspace/memory/meetings/2026-04-28-onestop-posting-ux.md`

## 기능 개요
AI 콘텐츠 작성(/generate) 결과 화면에서 스레드(Threads)에 바로 발행할 수 있는 기능.
ThreadAuto V2의 Threads API 구현을 참고하여 InsuRo 서버에 재현.
사용자별 Threads API 토큰(user_api_keys 테이블)을 사용.

## 참고 코드
- ThreadAuto Threads API 클라이언트: `/home/jay/projects/ThreadAuto/api/client.py`
  - ThreadsClient 클래스: post_text(), post_image(), post_carousel()
  - 인증: OAuth 장기 토큰, access_token 기반
  - API 베이스: `https://graph.threads.net/v1.0`
  - 플로우: 컨테이너 생성 → 폴링/대기 → 발행
- InsuRo 사용자 API 키 저장: `user_api_keys` 테이블 (provider="threads")
- InsuRo 설정 UI: `src/pages/Settings.tsx` (스레드 토큰 입력 이미 구현)

## 수정 사항

### 1. 서버: 스레드 발행 엔드포인트
파일: `server/main.py` (또는 별도 `server/threads_publisher.py` 모듈 분리)

`POST /api/insuro/publish-threads`

```python
class PublishThreadsRequest(BaseModel):
    content_id: str | None = None   # 기존 콘텐츠 ID (contents 테이블)
    text: str                       # 발행할 텍스트
    image_urls: list[str] = []      # 이미지 URL (선택)

@app.post("/api/insuro/publish-threads")
async def publish_threads(req: PublishThreadsRequest, payload: dict = Depends(verify_jwt)):
    user_id = payload["sub"]
    
    # 1. user_api_keys에서 provider="threads" 토큰 조회
    token_row = sb.table("user_api_keys").select("api_key").eq("user_id", user_id).eq("provider", "threads").eq("is_active", True).maybe_single().execute()
    if not token_row.data:
        raise HTTPException(400, "스레드 API 토큰이 설정되지 않았습니다. 개인 설정에서 등록해주세요.")
    
    access_token = token_row.data["api_key"]
    
    # 2. Threads Graph API 호출 (ThreadAuto 패턴)
    # - 텍스트만: post_text
    # - 이미지 1장: post_image  
    # - 이미지 여러장: post_carousel
    
    # 3. 발행 결과 저장 (publish_log 테이블 또는 contents에 published_at 업데이트)
    
    return {"ok": True, "post_id": post_id, "channel": "threads"}
```

핵심 구현 사항:
- Threads Graph API 호출은 httpx 또는 requests 사용
- API 베이스: `https://graph.threads.net/v1.0`
- 컨테이너 생성 → 폴링(5초 간격, 최대 60초) → 발행 순서
- 에러 핸들링: 토큰 만료(401) → "토큰이 만료되었습니다. 설정에서 갱신해주세요" 응답
- Rate limit: 사용자별 일일 발행 횟수 제한 (30회/일)
- 보안: 토큰은 서버사이드에서만 처리, 클라이언트에 노출 안 함

### 2. 프론트: PublishPanel 컴포넌트
파일: `src/components/PublishPanel.tsx` (신규)

Generate.tsx 결과 화면 하단에 표시되는 발행 패널:

```tsx
interface PublishPanelProps {
  content: string;        // 발행할 텍스트
  contentId?: string;     // contents 테이블 ID
  imageUrls?: string[];   // 이미지 URL
}
```

UI 구성:
- 채널 선택 탭 (스레드만 활성, 나머지 3개 "준비 중" 표시)
- 스레드 계정 연동 상태 표시 (설정에서 토큰 등록 여부 확인)
- 미연동 시: "개인 설정에서 스레드 API 토큰을 등록해주세요" + 설정 페이지 링크
- 연동 시: 미리보기 (스레드 형태로 텍스트 표시) + "발행" 버튼
- 발행 상태: idle → pending → success/error (버튼 자체가 상태 표현)
- 실패 시 인라인 에러 + 재시도 버튼

### 3. Generate.tsx에 PublishPanel 통합
파일: `src/pages/Generate.tsx`

결과 표시 영역 하단 (금소법 검증 결과 아래)에 PublishPanel 추가:
- 콘텐츠 생성 완료 후에만 표시
- 프로 이상 플랜만 표시 (무료/베이직은 업그레이드 안내)
- PublishPanel에 result(텍스트)와 contentId 전달

### 4. 스레드 연동 상태 확인 API
파일: `server/main.py`

`GET /api/insuro/publish-status`
- user_api_keys에서 사용자의 연동된 채널 목록 반환
- 프론트에서 채널별 연동 상태 표시에 사용

## affected_files
- `server/main.py` (수정 — publish-threads, publish-status 엔드포인트)
- `server/threads_publisher.py` (신규 — Threads Graph API 클라이언트, ThreadAuto 참고)
- `src/components/PublishPanel.tsx` (신규 — 발행 패널 컴포넌트)
- `src/pages/Generate.tsx` (수정 — PublishPanel 통합)

## 검증 시나리오
1. 스레드 토큰 미등록 상태 → PublishPanel에서 "토큰 등록 필요" 안내 표시
2. 스레드 토큰 등록 상태 → 미리보기 + 발행 버튼 표시
3. 발행 클릭 → 서버 API 호출 → Threads Graph API로 실제 발행 (토큰 유효 시)
4. 토큰 만료 → "토큰 만료, 갱신 필요" 에러 메시지
5. 무료/베이직 플랜 → PublishPanel 대신 업그레이드 안내
6. npm run build 성공
7. 서버 재시작 정상