24 KiB
24 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: 기반 모듈 (의존성 없음)
config.py- Settings 클래스exceptions.py- 예외 클래스 계층
Phase 2: 데이터 모델
models.py- Pydantic 모델 (Token, Account, Media, Insight, Comment)
Phase 3: 클라이언트 구현
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)
- 4.1: 기본 구조 및
Phase 4: 예제 및 문서
examples/auth_example.pyexamples/account_example.pyexamples/media_example.pyexamples/insights_example.pyexamples/comments_example.pyREADME.md
✅ 9. 설계 검수 결과
검수 체크리스트
- 기존 프로젝트 패턴과 일관성 - 3계층 구조, Pydantic v2, 비동기 패턴 적용
- 비동기 처리 - httpx.AsyncClient, async/await 전체 적용
- N+1 쿼리 문제 - 해당 없음 (외부 API 호출)
- 트랜잭션 경계 - 해당 없음 (DB 미사용)
- 예외 처리 전략 - 계층화된 예외, 에러 코드 매핑, 재시도 로직
- 확장성 - 새 엔드포인트 추가 용이, 모델 확장 가능
- 직관적 구조 - 명확한 모듈 분리, 일관된 네이밍
- SOLID 원칙 - 단일 책임, 개방-폐쇄 원칙 준수
참고 문서
🔄 다음 단계
설계가 완료되었습니다. /develop 명령으로 개발 에이전트를 호출하여 구현을 진행합니다.