o2o-castad-backend/poc/instagram1-difi/DESIGN.md

23 KiB

Instagram Graph API POC 설계 문서

📋 1. 요구사항 요약

1.1 기능적 요구사항

기능 설명 우선순위
인증 Access Token 관리, Long-lived Token 교환, Token 검증 필수
계정 정보 비즈니스 계정 ID 조회, 프로필 정보 조회 필수
미디어 관리 목록/상세 조회, 이미지/비디오 게시 (Container → Publish) 필수
인사이트 계정/미디어별 인사이트 조회 필수
댓글 관리 댓글 조회, 답글 작성 필수

1.2 비기능적 요구사항

  • 비동기 처리: 모든 API 호출은 async/await 사용
  • Rate Limit 준수: 시간당 200 요청 제한, 429 에러 시 지수 백오프
  • 에러 처리: 체계적인 예외 계층 구조
  • 로깅: 요청/응답 추적 가능
  • 보안: Credentials 환경변수 관리, 민감정보 로깅 방지

📐 2. 설계 개요

2.1 아키텍처 다이어그램

┌─────────────────────────────────────────────────────────────────┐
│                         examples/                                │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐            │
│  │ auth_example │ │media_example │ │insights_example│           │
│  └──────┬───────┘ └──────┬───────┘ └──────┬───────┘            │
└─────────┼────────────────┼────────────────┼─────────────────────┘
          │                │                │
          ▼                ▼                ▼
┌─────────────────────────────────────────────────────────────────┐
│                    InstagramGraphClient                          │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  - _request()          : 공통 HTTP 요청 (재시도, 로깅)    │   │
│  │  - debug_token()       : 토큰 검증                       │   │
│  │  - exchange_token()    : Long-lived 토큰 교환            │   │
│  │  - get_account()       : 계정 정보 조회                  │   │
│  │  - get_media_list()    : 미디어 목록                     │   │
│  │  - get_media()         : 미디어 상세                     │   │
│  │  - publish_image()     : 이미지 게시                     │   │
│  │  - publish_video()     : 비디오 게시                     │   │
│  │  - get_account_insights() : 계정 인사이트                │   │
│  │  - get_media_insights()   : 미디어 인사이트              │   │
│  │  - get_comments()      : 댓글 조회                       │   │
│  │  - reply_comment()     : 댓글 답글                       │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
          │                │                │
          ▼                ▼                ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│    models.py    │ │  exceptions.py  │ │    config.py    │
│  (Pydantic v2)  │ │ (커스텀 예외)    │ │   (Settings)    │
└─────────────────┘ └─────────────────┘ └─────────────────┘

2.2 모듈 의존성 관계

config.py ◄─────────────────────────────────────┐
    │                                            │
    ▼                                            │
exceptions.py ◄─────────────────────┐           │
    │                                │           │
    ▼                                │           │
models.py ◄──────────────┐          │           │
    │                     │          │           │
    ▼                     │          │           │
client.py ────────────────┴──────────┴───────────┘
    │
    ▼
examples/ (사용 예제)

🌐 3. API 설계 (Instagram Graph API 엔드포인트)

3.1 Base URL

https://graph.instagram.com (Instagram Platform API)
https://graph.facebook.com/v21.0 (Facebook Graph API - 일부 기능)

3.2 인증 API

3.2.1 토큰 검증 (Debug Token)

GET /debug_token
?input_token={access_token}
&access_token={app_id}|{app_secret}

Response:
{
  "data": {
    "app_id": "123456789",
    "type": "USER",
    "application": "App Name",
    "expires_at": 1234567890,
    "is_valid": true,
    "scopes": ["instagram_basic", "instagram_content_publish"],
    "user_id": "17841400000000000"
  }
}

3.2.2 Long-lived Token 교환

GET /access_token
?grant_type=ig_exchange_token
&client_secret={app_secret}
&access_token={short_lived_token}

Response:
{
  "access_token": "IGQVJ...",
  "token_type": "bearer",
  "expires_in": 5184000  // 60일 (초)
}

3.2.3 토큰 갱신

GET /refresh_access_token
?grant_type=ig_refresh_token
&access_token={long_lived_token}

Response:
{
  "access_token": "IGQVJ...",
  "token_type": "bearer",
  "expires_in": 5184000
}

3.3 계정 API

3.3.1 계정 정보 조회

GET /me
?fields=id,username,name,account_type,profile_picture_url,followers_count,follows_count,media_count
&access_token={access_token}

Response:
{
  "id": "17841400000000000",
  "username": "example_user",
  "name": "Example User",
  "account_type": "BUSINESS",
  "profile_picture_url": "https://...",
  "followers_count": 1000,
  "follows_count": 500,
  "media_count": 100
}

3.4 미디어 API

3.4.1 미디어 목록 조회

GET /{ig-user-id}/media
?fields=id,media_type,media_url,thumbnail_url,caption,timestamp,permalink,like_count,comments_count
&limit=25
&access_token={access_token}

Response:
{
  "data": [
    {
      "id": "17880000000000000",
      "media_type": "IMAGE",
      "media_url": "https://...",
      "caption": "My photo",
      "timestamp": "2024-01-01T00:00:00+0000",
      "permalink": "https://www.instagram.com/p/...",
      "like_count": 100,
      "comments_count": 10
    }
  ],
  "paging": {
    "cursors": {
      "before": "...",
      "after": "..."
    },
    "next": "https://graph.instagram.com/..."
  }
}

3.4.2 미디어 상세 조회

GET /{ig-media-id}
?fields=id,media_type,media_url,thumbnail_url,caption,timestamp,permalink,like_count,comments_count,children{id,media_type,media_url}
&access_token={access_token}

3.4.3 이미지 게시 (2단계 프로세스)

Step 1: Container 생성

POST /{ig-user-id}/media
?image_url={public_image_url}
&caption={caption_text}
&access_token={access_token}

Response:
{
  "id": "17889000000000000"  // container_id
}

Step 2: Container 상태 확인

GET /{container-id}
?fields=status_code,status
&access_token={access_token}

Response:
{
  "status_code": "FINISHED",  // IN_PROGRESS, FINISHED, ERROR
  "id": "17889000000000000"
}

Step 3: 게시

POST /{ig-user-id}/media_publish
?creation_id={container_id}
&access_token={access_token}

Response:
{
  "id": "17880000000000001"  // 게시된 media_id
}

3.4.4 비디오 게시 (3단계 프로세스)

Step 1: Container 생성

POST /{ig-user-id}/media
?media_type=REELS
&video_url={public_video_url}
&caption={caption_text}
&share_to_feed=true
&access_token={access_token}

Response:
{
  "id": "17889000000000000"
}

Step 2 & 3: 이미지와 동일 (상태 확인 → 게시)

3.5 인사이트 API

3.5.1 계정 인사이트

GET /{ig-user-id}/insights
?metric=impressions,reach,profile_views,accounts_engaged
&period=day
&metric_type=total_value
&access_token={access_token}

Response:
{
  "data": [
    {
      "name": "impressions",
      "period": "day",
      "values": [{"value": 1000}],
      "title": "Impressions",
      "description": "Total number of times..."
    }
  ]
}

3.5.2 미디어 인사이트

GET /{ig-media-id}/insights
?metric=impressions,reach,engagement,saved
&access_token={access_token}

Response:
{
  "data": [
    {
      "name": "impressions",
      "period": "lifetime",
      "values": [{"value": 500}],
      "title": "Impressions"
    }
  ]
}

3.6 댓글 API

3.6.1 댓글 목록 조회

GET /{ig-media-id}/comments
?fields=id,text,username,timestamp,like_count,replies{id,text,username,timestamp}
&access_token={access_token}

Response:
{
  "data": [
    {
      "id": "17890000000000000",
      "text": "Great photo!",
      "username": "commenter",
      "timestamp": "2024-01-01T12:00:00+0000",
      "like_count": 5,
      "replies": {
        "data": [...]
      }
    }
  ]
}

3.6.2 댓글 답글 작성

POST /{ig-comment-id}/replies
?message={reply_text}
&access_token={access_token}

Response:
{
  "id": "17890000000000001"
}

📦 4. 데이터 모델 (Pydantic v2)

4.1 인증 모델

class TokenInfo(BaseModel):
    """토큰 정보"""
    access_token: str
    token_type: str = "bearer"
    expires_in: int  # 초 단위

class TokenDebugData(BaseModel):
    """토큰 디버그 정보"""
    app_id: str
    type: str
    application: str
    expires_at: int  # Unix timestamp
    is_valid: bool
    scopes: list[str]
    user_id: str

class TokenDebugResponse(BaseModel):
    """토큰 디버그 응답"""
    data: TokenDebugData

4.2 계정 모델

class Account(BaseModel):
    """Instagram 비즈니스 계정"""
    id: str
    username: str
    name: Optional[str] = None
    account_type: str  # BUSINESS, CREATOR
    profile_picture_url: Optional[str] = None
    followers_count: int = 0
    follows_count: int = 0
    media_count: int = 0
    biography: Optional[str] = None
    website: Optional[str] = None

4.3 미디어 모델

class MediaType(str, Enum):
    """미디어 타입"""
    IMAGE = "IMAGE"
    VIDEO = "VIDEO"
    CAROUSEL_ALBUM = "CAROUSEL_ALBUM"
    REELS = "REELS"

class Media(BaseModel):
    """미디어 정보"""
    id: str
    media_type: MediaType
    media_url: Optional[str] = None
    thumbnail_url: Optional[str] = None
    caption: Optional[str] = None
    timestamp: datetime
    permalink: str
    like_count: int = 0
    comments_count: int = 0
    children: Optional[list["Media"]] = None  # 캐러셀용

class MediaContainer(BaseModel):
    """미디어 컨테이너 (게시 전 상태)"""
    id: str
    status_code: Optional[str] = None  # IN_PROGRESS, FINISHED, ERROR
    status: Optional[str] = None

class MediaList(BaseModel):
    """미디어 목록 응답"""
    data: list[Media]
    paging: Optional[Paging] = None

class Paging(BaseModel):
    """페이징 정보"""
    cursors: Optional[dict[str, str]] = None
    next: Optional[str] = None
    previous: Optional[str] = None

4.4 인사이트 모델

class InsightValue(BaseModel):
    """인사이트 값"""
    value: int
    end_time: Optional[datetime] = None

class Insight(BaseModel):
    """인사이트 정보"""
    name: str
    period: str  # day, week, days_28, lifetime
    values: list[InsightValue]
    title: str
    description: Optional[str] = None
    id: str

class InsightResponse(BaseModel):
    """인사이트 응답"""
    data: list[Insight]

4.5 댓글 모델

class Comment(BaseModel):
    """댓글 정보"""
    id: str
    text: str
    username: str
    timestamp: datetime
    like_count: int = 0
    replies: Optional["CommentList"] = None

class CommentList(BaseModel):
    """댓글 목록 응답"""
    data: list[Comment]
    paging: Optional[Paging] = None

4.6 에러 모델

class APIError(BaseModel):
    """Instagram API 에러 응답"""
    message: str
    type: str
    code: int
    error_subcode: Optional[int] = None
    fbtrace_id: Optional[str] = None

class ErrorResponse(BaseModel):
    """에러 응답 래퍼"""
    error: APIError

🚨 5. 예외 처리 전략

5.1 예외 계층 구조

class InstagramAPIError(Exception):
    """Instagram API 기본 예외"""
    def __init__(self, message: str, code: int = None, subcode: int = None):
        self.message = message
        self.code = code
        self.subcode = subcode
        super().__init__(self.message)

class AuthenticationError(InstagramAPIError):
    """인증 관련 에러 (토큰 만료, 무효 등)"""
    # code: 190 (Invalid OAuth access token)
    pass

class RateLimitError(InstagramAPIError):
    """Rate Limit 초과 (HTTP 429)"""
    def __init__(self, message: str, retry_after: int = None):
        super().__init__(message, code=4)
        self.retry_after = retry_after

class PermissionError(InstagramAPIError):
    """권한 부족 에러"""
    # code: 10 (Permission denied)
    # code: 200 (Requires business account)
    pass

class MediaPublishError(InstagramAPIError):
    """미디어 게시 실패"""
    # 이미지 URL 접근 불가, 포맷 오류 등
    pass

class InvalidRequestError(InstagramAPIError):
    """잘못된 요청 (파라미터 오류 등)"""
    # code: 100 (Invalid parameter)
    pass

class ResourceNotFoundError(InstagramAPIError):
    """리소스를 찾을 수 없음"""
    # code: 803 (Object does not exist)
    pass

5.2 에러 코드 매핑

API Error Code Subcode Exception Class 설명
4 - RateLimitError Rate limit 초과
10 - PermissionError 권한 부족
100 - InvalidRequestError 잘못된 파라미터
190 458 AuthenticationError 앱 권한 없음
190 463 AuthenticationError 토큰 만료
190 467 AuthenticationError 유효하지 않은 토큰
200 - PermissionError 비즈니스 계정 필요
803 - ResourceNotFoundError 리소스 없음

5.3 재시도 전략

RETRY_CONFIG = {
    "max_retries": 3,
    "base_delay": 1.0,  # 초
    "max_delay": 60.0,  # 초
    "exponential_base": 2,
    "retryable_status_codes": [429, 500, 502, 503, 504],
    "retryable_error_codes": [4, 17, 341],  # Rate limit 관련
}

🔧 6. 클라이언트 인터페이스

6.1 InstagramGraphClient 클래스

class InstagramGraphClient:
    """Instagram Graph API 클라이언트"""

    def __init__(
        self,
        access_token: str,
        app_id: Optional[str] = None,
        app_secret: Optional[str] = None,
        api_version: str = "v21.0",
        timeout: float = 30.0,
    ):
        """
        Args:
            access_token: Instagram 액세스 토큰
            app_id: Facebook 앱 ID (토큰 검증 시 필요)
            app_secret: Facebook 앱 시크릿 (토큰 교환 시 필요)
            api_version: Graph API 버전
            timeout: HTTP 요청 타임아웃 (초)
        """
        pass

    async def __aenter__(self) -> "InstagramGraphClient":
        """비동기 컨텍스트 매니저 진입"""
        pass

    async def __aexit__(self, *args) -> None:
        """비동기 컨텍스트 매니저 종료"""
        pass

    # ==================== 인증 ====================

    async def debug_token(self) -> TokenDebugResponse:
        """현재 토큰 정보 조회 (유효성 검증)"""
        pass

    async def exchange_long_lived_token(self) -> TokenInfo:
        """단기 토큰을 장기 토큰(60일)으로 교환"""
        pass

    async def refresh_token(self) -> TokenInfo:
        """장기 토큰 갱신"""
        pass

    # ==================== 계정 ====================

    async def get_account(self) -> Account:
        """현재 계정 정보 조회"""
        pass

    async def get_account_id(self) -> str:
        """현재 계정 ID만 조회"""
        pass

    # ==================== 미디어 ====================

    async def get_media_list(
        self,
        limit: int = 25,
        after: Optional[str] = None,
    ) -> MediaList:
        """미디어 목록 조회 (페이지네이션 지원)"""
        pass

    async def get_media(self, media_id: str) -> Media:
        """미디어 상세 조회"""
        pass

    async def publish_image(
        self,
        image_url: str,
        caption: Optional[str] = None,
    ) -> Media:
        """이미지 게시 (Container 생성 → 상태 확인 → 게시)"""
        pass

    async def publish_video(
        self,
        video_url: str,
        caption: Optional[str] = None,
        share_to_feed: bool = True,
    ) -> Media:
        """비디오/릴스 게시"""
        pass

    async def publish_carousel(
        self,
        media_urls: list[str],
        caption: Optional[str] = None,
    ) -> Media:
        """캐러셀(멀티 이미지) 게시"""
        pass

    # ==================== 인사이트 ====================

    async def get_account_insights(
        self,
        metrics: list[str],
        period: str = "day",
    ) -> InsightResponse:
        """계정 인사이트 조회

        Args:
            metrics: 조회할 메트릭 목록
                - impressions, reach, profile_views, accounts_engaged 등
            period: 기간 (day, week, days_28)
        """
        pass

    async def get_media_insights(
        self,
        media_id: str,
        metrics: Optional[list[str]] = None,
    ) -> InsightResponse:
        """미디어 인사이트 조회

        Args:
            media_id: 미디어 ID
            metrics: 조회할 메트릭 (기본: impressions, reach, engagement, saved)
        """
        pass

    # ==================== 댓글 ====================

    async def get_comments(
        self,
        media_id: str,
        limit: int = 50,
    ) -> CommentList:
        """미디어의 댓글 목록 조회"""
        pass

    async def reply_comment(
        self,
        comment_id: str,
        message: str,
    ) -> Comment:
        """댓글에 답글 작성"""
        pass

    # ==================== 내부 메서드 ====================

    async def _request(
        self,
        method: str,
        endpoint: str,
        params: Optional[dict] = None,
        data: Optional[dict] = None,
    ) -> dict:
        """
        공통 HTTP 요청 처리
        - Rate Limit 시 지수 백오프 재시도
        - 에러 응답 → 커스텀 예외 변환
        - 요청/응답 로깅
        """
        pass

    async def _wait_for_container(
        self,
        container_id: str,
        timeout: float = 60.0,
        poll_interval: float = 2.0,
    ) -> MediaContainer:
        """컨테이너 상태가 FINISHED가 될 때까지 대기"""
        pass

📁 7. 파일 구조

poc/instagram/
├── __init__.py              # 패키지 초기화 및 public API export
├── config.py                # Settings (환경변수 관리)
├── exceptions.py            # 커스텀 예외 클래스
├── models.py                # Pydantic v2 데이터 모델
├── client.py                # InstagramGraphClient
├── examples/
│   ├── __init__.py
│   ├── auth_example.py      # 토큰 검증, 교환 예제
│   ├── account_example.py   # 계정 정보 조회 예제
│   ├── media_example.py     # 미디어 조회/게시 예제
│   ├── insights_example.py  # 인사이트 조회 예제
│   └── comments_example.py  # 댓글 조회/답글 예제
├── DESIGN.md                # 설계 문서 (본 문서)
└── README.md                # 사용 가이드

7.1 각 파일의 역할

파일 역할 의존성
config.py 환경변수 로드, API 설정 pydantic-settings
exceptions.py 커스텀 예외 정의 -
models.py API 요청/응답 Pydantic 모델 pydantic
client.py Instagram Graph API 클라이언트 httpx, 위 모듈들
examples/*.py 실행 가능한 예제 코드 client.py

📋 8. 구현 순서

개발 에이전트가 따라야 할 순서:

Phase 1: 기반 모듈 (의존성 없음)

  1. config.py - Settings 클래스
  2. exceptions.py - 예외 클래스 계층

Phase 2: 데이터 모델

  1. models.py - Pydantic 모델 (Token, Account, Media, Insight, Comment)

Phase 3: 클라이언트 구현

  1. client.py - InstagramGraphClient
    • 4.1: 기본 구조 및 _request() 메서드
    • 4.2: 인증 메서드 (debug_token, exchange_token, refresh_token)
    • 4.3: 계정 메서드 (get_account, get_account_id)
    • 4.4: 미디어 메서드 (get_media_list, get_media, publish_image, publish_video)
    • 4.5: 인사이트 메서드 (get_account_insights, get_media_insights)
    • 4.6: 댓글 메서드 (get_comments, reply_comment)

Phase 4: 예제 및 문서

  1. examples/auth_example.py
  2. examples/account_example.py
  3. examples/media_example.py
  4. examples/insights_example.py
  5. examples/comments_example.py
  6. README.md

9. 설계 검수 결과

검수 체크리스트

  • 기존 프로젝트 패턴과 일관성 - 3계층 구조, Pydantic v2, 비동기 패턴 적용
  • 비동기 처리 - httpx.AsyncClient, async/await 전체 적용
  • N+1 쿼리 문제 - 해당 없음 (외부 API 호출)
  • 트랜잭션 경계 - 해당 없음 (DB 미사용)
  • 예외 처리 전략 - 계층화된 예외, 에러 코드 매핑, 재시도 로직
  • 확장성 - 새 엔드포인트 추가 용이, 모델 확장 가능
  • 직관적 구조 - 명확한 모듈 분리, 일관된 네이밍
  • SOLID 원칙 - 단일 책임, 개방-폐쇄 원칙 준수

참고 문서


🔄 다음 단계

설계가 완료되었습니다. /develop 명령으로 개발 에이전트를 호출하여 구현을 진행합니다.