o2o-castad-backend/WORK_PLAN_FACEBOOK_OAUTH.md

28 KiB

Facebook OAuth 구현 작업 계획서

작성일: 2026-03-03 프로젝트: O2O Castad Backend 대상 모듈: app/sns/ (Facebook OAuth 연동)


1. 개요

Facebook OAuth 2.0 로그인 기능을 구현합니다. Facebook Graph API를 통해 사용자 인증, 토큰 교환, 장기 토큰 발급, 페이지 토큰 조회 기능을 제공합니다.

참조 공식 문서

Facebook OAuth 2.0 핵심 흐름

1. 사용자 → Facebook 인증 페이지 리다이렉트
   URL: https://www.facebook.com/v21.0/dialog/oauth
   Params: client_id, redirect_uri, state, scope, response_type=code

2. Facebook → 콜백 URL로 인가 코드(code) 전달

3. 서버 → 인가 코드를 액세스 토큰으로 교환
   URL: https://graph.facebook.com/v21.0/oauth/access_token
   Params: client_id, client_secret, code, redirect_uri

4. 서버 → 단기 토큰을 장기 토큰으로 교환 (약 60일)
   URL: https://graph.facebook.com/v21.0/oauth/access_token
   Params: grant_type=fb_exchange_token, client_id, client_secret, fb_exchange_token

5. 서버 → 사용자 정보 조회
   URL: https://graph.facebook.com/v21.0/me
   Params: fields=id,name,email,picture

6. 서버 → 페이지 목록 및 페이지 토큰 조회 (선택)
   URL: https://graph.facebook.com/v21.0/{user-id}/accounts

2. 파일 구조 및 역할 분담

app/
├── utils/
│   └── facebook_oauth.py          # [신규] Facebook OAuth 클래스 (API 통신 전담)
├── sns/
│   ├── api/routers/v1/
│   │   └── oauth.py               # [구현] 엔드포인트 정의 (prefix: /sns)
│   ├── services/
│   │   └── facebook.py            # [구현] 비즈니스 로직 (facebook_oauth.py 클래스 호출)
│   ├── models.py                  # [검토/수정] SNSUploadTask 필드 추가 여부 확인
│   ├── dependency.py              # [구현] SNS 전용 Depends 정의
│   └── schemas/
│       └── facebook_schema.py     # [신규] Facebook OAuth 스키마 정의
├── config.py                      # [수정] FacebookSettings 독립 클래스 신설 + 인스턴스 생성
├── .env                           # [수정] FACEBOOK_ 환경변수 추가
└── main.py                        # [수정] 라우터 등록 및 Scalar 문서 업데이트

3. 상세 작업 단계

Phase 1: 설정 및 기반 작업

Step 1.1: config.py - FacebookSettings 독립 클래스 신설

  • 파일: config.py
  • 작업: FacebookSettings 독립 클래스를 신규 생성 (기존 SocialOAuthSettings 내 주석 코드는 삭제)
  • 네이밍 패턴: 기존 KakaoSettings, JWTSettings 등과 동일한 패턴 준수
  • 환경변수: 실제 값은 .env 파일에서 FACEBOOK_ prefix 변수로 관리

config.py에 추가할 클래스 (KakaoSettings 바로 아래에 배치):

class FacebookSettings(BaseSettings):
    """Facebook OAuth 설정

    Facebook Graph API를 통한 OAuth 2.0 인증 설정입니다.
    Meta for Developers (https://developers.facebook.com/)에서 앱을 생성하고
    App ID/Secret을 발급받아야 합니다.
    """

    FACEBOOK_APP_ID: str = Field(
        default="",
        description="Facebook App ID (Meta 개발자 콘솔에서 발급)",
    )
    FACEBOOK_APP_SECRET: str = Field(
        default="",
        description="Facebook App Secret",
    )
    FACEBOOK_REDIRECT_URI: str = Field(
        default="http://localhost:8000/sns/facebook/callback",
        description="Facebook OAuth 콜백 URI",
    )
    FACEBOOK_GRAPH_API_VERSION: str = Field(
        default="v21.0",
        description="Facebook Graph API 버전",
    )
    FACEBOOK_OAUTH_SCOPE: str = Field(
        default="public_profile,email,pages_show_list,pages_read_engagement,pages_manage_posts",
        description="Facebook OAuth 요청 권한 범위 (쉼표 구분)",
    )

    model_config = _base_config

인스턴스 생성 (파일 하단 인스턴스 블록에 추가):

facebook_settings = FacebookSettings()

SocialOAuthSettings 내 기존 Facebook 주석 코드 처리:

  • SocialOAuthSettings 내의 Facebook 관련 주석 처리된 코드 블록(524~530행)은 삭제
  • 해당 책임이 FacebookSettings 클래스로 완전 이관됨

.env 파일에 추가할 환경변수:

# ============================================================
# Facebook OAuth 설정
# ============================================================
FACEBOOK_APP_ID=your-facebook-app-id
FACEBOOK_APP_SECRET=your-facebook-app-secret
FACEBOOK_REDIRECT_URI=http://localhost:8000/sns/facebook/callback
FACEBOOK_GRAPH_API_VERSION=v21.0
FACEBOOK_OAUTH_SCOPE=public_profile,email,pages_show_list,pages_read_engagement,pages_manage_posts
  • 주의: redirect_uri는 sns 프리픽스 기준으로 설정 (/sns/facebook/callback)

Phase 2: OAuth 클라이언트 구현

Step 2.1: app/utils/facebook_oauth.py - FacebookOAuthClient 클래스 구현

  • 파일: app/utils/facebook_oauth.py (신규)
  • 클래스명: FacebookOAuthClient (oauth 단어 포함 필수)
  • 역할: Facebook Graph API와의 HTTP 통신 전담
  • 패턴: KakaoOAuthClient (app/user/services/kakao.py) 패턴 준수
  • HTTP 클라이언트: httpx.AsyncClient (기존 Instagram 패턴 사용)
  • Graph API 버전: v21.0

클래스 구조:

from config import facebook_settings

class FacebookOAuthClient:
    """Facebook OAuth 2.0 API 클라이언트"""

    # URL 템플릿 (API 버전은 facebook_settings에서 동적 로드)
    AUTH_URL_TEMPLATE = "https://www.facebook.com/{version}/dialog/oauth"
    TOKEN_URL_TEMPLATE = "https://graph.facebook.com/{version}/oauth/access_token"
    USER_INFO_URL_TEMPLATE = "https://graph.facebook.com/{version}/me"
    PAGES_URL_TEMPLATE = "https://graph.facebook.com/{version}/{user_id}/accounts"

    def __init__(self):
        # facebook_settings에서 설정값 로드
        self.client_id = facebook_settings.FACEBOOK_APP_ID
        self.client_secret = facebook_settings.FACEBOOK_APP_SECRET
        self.redirect_uri = facebook_settings.FACEBOOK_REDIRECT_URI
        self.api_version = facebook_settings.FACEBOOK_GRAPH_API_VERSION
        self.scope = facebook_settings.FACEBOOK_OAUTH_SCOPE

    def get_authorization_url(self, state: str) -> str:
        # Facebook 인증 페이지 URL 생성
        # scope: facebook_settings.FACEBOOK_OAUTH_SCOPE에서 로드

    async def get_access_token(self, code: str) -> dict:
        # 인가 코드 → 단기 액세스 토큰 교환
        # 반환: {access_token, token_type, expires_in}

    async def exchange_long_lived_token(self, short_lived_token: str) -> dict:
        # 단기 토큰 → 장기 토큰 교환 (약 60일)
        # 반환: {access_token, token_type, expires_in}

    async def get_user_info(self, access_token: str) -> dict:
        # 사용자 프로필 조회
        # fields: id, name, email, picture
        # 반환: {id, name, email, picture}

    async def get_user_pages(self, user_id: str, access_token: str) -> list[dict]:
        # 사용자가 관리하는 Facebook 페이지 목록 조회
        # 반환: [{id, name, access_token, category}, ...]

예외 클래스 (같은 파일 내 정의):

class FacebookOAuthException(HTTPException):
    """Facebook OAuth 기본 예외"""

class FacebookAuthFailedError(FacebookOAuthException):
    """인증 실패 (400)"""

class FacebookAPIError(FacebookOAuthException):
    """API 호출 오류 (500)"""

class FacebookTokenExpiredError(FacebookOAuthException):
    """토큰 만료 (401)"""

필수 사항:

  • 모든 메서드에 logger.debug() 로 입력값, 호출 URL, 응답 상태 기록
  • 중요 함수 호출 부분에 한글 주석 추가
  • 모듈 로거: get_logger(__name__)

Phase 3: 스키마 정의

Step 3.1: app/sns/schemas/facebook_schema.py - Facebook 스키마 구현

  • 파일: app/sns/schemas/facebook_schema.py (신규)
  • 패턴: 기존 sns_schema.py 패턴 준수

스키마 정의:

class FacebookConnectResponse(BaseModel):
    """Facebook OAuth 연동 시작 응답"""
    auth_url: str           # Facebook 인증 페이지 URL
    state: str              # CSRF 방지용 state 토큰

class FacebookCallbackRequest(BaseModel):
    """Facebook OAuth 콜백 파라미터"""
    code: str               # 인가 코드
    state: str              # CSRF state 토큰

class FacebookTokenResponse(BaseModel):
    """Facebook 토큰 교환 응답"""
    access_token: str       # 액세스 토큰
    token_type: str         # Bearer
    expires_in: int         # 만료 시간 (초)

class FacebookUserInfo(BaseModel):
    """Facebook 사용자 정보"""
    id: str                 # Facebook 사용자 ID
    name: str               # 이름
    email: Optional[str]    # 이메일 (선택)
    picture: Optional[dict] # 프로필 사진 (선택)

class FacebookPageInfo(BaseModel):
    """Facebook 페이지 정보"""
    id: str                 # 페이지 ID
    name: str               # 페이지 이름
    access_token: str       # 페이지 액세스 토큰
    category: Optional[str] # 카테고리

class FacebookAccountResponse(BaseModel):
    """Facebook 연동 완료 응답"""
    success: bool
    message: str
    account_id: int                            # SocialAccount.id
    platform_user_id: str                      # Facebook 사용자 ID
    platform_username: str                     # Facebook 사용자 이름
    pages: Optional[list[FacebookPageInfo]]    # 관리 가능한 페이지 목록

Phase 4: 의존성 정의

Step 4.1: app/sns/dependency.py - SNS 전용 Depends 구현

  • 파일: app/sns/dependency.py
  • 역할: SNS 모듈에서만 사용하는 FastAPI 의존성

정의 내용:

async def get_facebook_oauth_client() -> FacebookOAuthClient:
    """FacebookOAuthClient 인스턴스를 제공하는 의존성"""

async def get_facebook_social_account(
    current_user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
) -> SocialAccount:
    """현재 사용자의 활성 Facebook 소셜 계정을 조회하는 의존성"""
    # SocialAccount에서 platform=facebook, is_active=True, is_deleted=False 조회
    # 없으면 SocialAccountNotFoundError 발생

async def validate_oauth_state(state: str) -> str:
    """OAuth state 토큰 유효성 검증 의존성"""
    # Redis에서 state 조회 및 검증
    # 유효하지 않으면 예외 발생

Phase 5: 서비스 레이어 구현

Step 5.1: app/sns/services/facebook.py - 비즈니스 로직 구현

  • 파일: app/sns/services/facebook.py
  • 역할: 엔드포인트와 OAuth 클라이언트 사이의 비즈니스 로직
  • 핵심 원칙: 모든 Facebook API 호출은 FacebookOAuthClient 클래스를 통해서만 수행

서비스 함수 구조:

class FacebookService:
    def __init__(self):
        self.oauth_client = FacebookOAuthClient()

    async def start_connect(self, user_uuid: str) -> FacebookConnectResponse:
        """Facebook 연동 시작"""
        # 1. CSRF state 토큰 생성 (secrets.token_urlsafe)
        # 2. Redis에 state:user_uuid 매핑 저장 (TTL: OAUTH_STATE_TTL_SECONDS)
        # 3. oauth_client.get_authorization_url(state) 호출
        # 4. FacebookConnectResponse 반환

    async def handle_callback(
        self, code: str, state: str, session: AsyncSession
    ) -> FacebookAccountResponse:
        """OAuth 콜백 처리"""
        # 1. Redis에서 state로 user_uuid 조회 및 검증
        # 2. oauth_client.get_access_token(code) → 단기 토큰 획득
        # 3. oauth_client.exchange_long_lived_token() → 장기 토큰 교환
        # 4. oauth_client.get_user_info() → 사용자 정보 조회
        # 5. oauth_client.get_user_pages() → 페이지 목록 조회
        # 6. SocialAccount 생성 또는 업데이트 (DB 저장)
        #    - platform: facebook
        #    - access_token: 장기 토큰
        #    - platform_user_id: Facebook 사용자 ID
        #    - platform_username: Facebook 사용자 이름
        #    - platform_data: {pages: [...], picture_url: "..."}
        #    - token_expires_at: 현재시간 + expires_in
        #    - scope: 요청한 scope 문자열
        # 7. FacebookAccountResponse 반환

    async def disconnect(
        self, user_uuid: str, session: AsyncSession
    ) -> None:
        """Facebook 연동 해제"""
        # SocialAccount 소프트 삭제 (is_deleted=True, is_active=False)

facebook_service = FacebookService()

필수 사항:

  • 모든 주요 단계에서 logger.debug() 호출
  • 주요 함수 호출 부분에 한글 주석 추가
  • 에러 발생 시 logger.error() 로 상세 에러 정보 기록

Phase 6: 라우터 구현

Step 6.1: app/sns/api/routers/v1/oauth.py - 엔드포인트 구현

  • 파일: app/sns/api/routers/v1/oauth.py
  • prefix: /sns (항목 1번 요구사항 - sns 프리픽스 필수)
  • tags: ["SNS OAuth"]

엔드포인트 구조:

router = APIRouter(prefix="/sns", tags=["SNS OAuth"])

@router.get("/facebook/connect")
async def facebook_connect(
    current_user: User = Depends(get_current_user),
) -> FacebookConnectResponse:
    """Facebook OAuth 연동 시작"""
    # facebook_service.start_connect() 호출

@router.get("/facebook/callback")
async def facebook_callback(
    code: str | None = Query(None),
    state: str | None = Query(None),
    error: str | None = Query(None),
    error_description: str | None = Query(None),
    session: AsyncSession = Depends(get_session),
) -> RedirectResponse:
    """Facebook OAuth 콜백 처리"""
    # 에러/취소 처리
    # facebook_service.handle_callback() 호출
    # 성공/실패에 따라 프론트엔드로 리다이렉트

@router.delete("/facebook/disconnect")
async def facebook_disconnect(
    current_user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
) -> dict:
    """Facebook 계정 연동 해제"""
    # facebook_service.disconnect() 호출

Phase 7: 모델 검토 및 수정

Step 7.1: SNSUploadTask 모델 필드 검토

  • 파일: app/sns/models.py
  • 검토 항목: Facebook 토큰 정보 저장을 위한 추가 필드 필요 여부

분석 결과:

  • SNSUploadTask는 업로드 작업 관리용 모델이며, 토큰 저장 역할이 아님

  • Facebook 토큰 정보는 기존 SocialAccount 모델에 이미 적절한 필드가 존재:

    • access_token (Text): 장기 토큰 저장
    • refresh_token (Text, nullable): Facebook은 refresh_token 미지원이므로 NULL
    • token_expires_at (DateTime): 장기 토큰 만료 시간
    • platform_data (JSON): 페이지 토큰, 페이지 ID 등 추가 정보
    • scope (Text): OAuth scope 저장
  • SNSUploadTask에 Facebook 업로드 시 필요한 필드 추가 검토:

    • platform 필드 추가 (String(20)): 어떤 플랫폼에 업로드하는지 구분 (현재 Instagram 전용 구조)
    • platform_post_id 필드 추가 (String(255), nullable): 업로드 후 플랫폼에서 반환한 게시물 ID
    • platform_post_url 필드 추가 (String(2048), nullable): 업로드 후 게시물 URL

Phase 8: main.py 라우터 등록 및 Scalar 문서 업데이트

Step 8.1: main.py 수정

  • 파일: main.py

변경 내용:

  1. 라우터 import 추가:

    from app.sns.api.routers.v1.oauth import router as sns_oauth_router
    
  2. 라우터 등록:

    app.include_router(sns_oauth_router)  # SNS OAuth 라우터 (Facebook)
    
    • 주의: oauth.py 내부 router에 이미 /sns prefix가 있으므로 main.py에서는 prefix 추가 불필요
  3. tags_metadata 업데이트 - SNS 태그 설명에 Facebook 추가:

    {
        "name": "SNS OAuth",
        "description": """SNS OAuth API - Facebook 계정 연동
    
    **인증: 필요** - `Authorization: Bearer {access_token}` 헤더 필수
    
    ## Facebook OAuth 연동 흐름
    
    1. `GET /sns/facebook/connect` - Facebook OAuth 인증 URL 획득
    2. 사용자를 auth_url로 리다이렉트 → Facebook 로그인 및 권한 승인
    3. Facebook에서 `/sns/facebook/callback`으로 인가 코드 전달
    4. 서버에서 토큰 교환, 장기 토큰 발급, 사용자 정보 조회
    5. 연동 완료 후 프론트엔드로 리다이렉트
    
    ## 계정 관리
    
    - `DELETE /sns/facebook/disconnect` - Facebook 계정 연동 해제
    """,
    },
    
  4. 공개 엔드포인트 추가 (인증 불필요):

    public_endpoints = [
        ...
        "/sns/facebook/callback",  # Facebook OAuth 콜백
    ]
    
  5. 기존 SNS 태그 설명 업데이트:

    • SNS 태그 description에 Facebook 업로드 관련 엔드포인트 추가

4. 작업 순서 (실행 순서)

순서 Phase 작업 내용 대상 파일 의존성
1 Phase 1 config.py FacebookSettings 클래스 신설 + .env 변수 추가 config.py, .env 없음
2 Phase 3 Facebook 스키마 정의 app/sns/schemas/facebook_schema.py 없음
3 Phase 2 FacebookOAuthClient 클래스 구현 app/utils/facebook_oauth.py Step 1 (config)
4 Phase 4 SNS 전용 Depends 구현 app/sns/dependency.py Step 3
5 Phase 5 Facebook 서비스 레이어 구현 app/sns/services/facebook.py Step 2, 3, 4
6 Phase 6 OAuth 라우터 엔드포인트 구현 app/sns/api/routers/v1/oauth.py Step 2, 5
7 Phase 7 SNSUploadTask 모델 필드 추가 app/sns/models.py Step 1~6 완료 후 검토
8 Phase 8 main.py 라우터 등록 및 Scalar 문서 main.py Step 6
9 - 코드리뷰 수행 (/review) 전체 변경 파일 Step 1~8 전체
10 - 코드리뷰 결과 기반 개선 해당 파일 Step 9

5. 품질 기준 (전 단계 공통)

5.1 로깅 기준 (항목 7)

  • 모든 엔드포인트 진입점: logger.info() 로 요청 시작 기록
  • 외부 API 호출 전/후: logger.debug() 로 URL, 파라미터, 응답 상태 기록
  • 중요 변수 할당: logger.debug() 로 변수값 확인
  • 에러 발생 시: logger.error() 로 상세 에러 정보 기록
  • 로거 패턴: from app.utils.logger import get_logger; logger = get_logger(__name__)

5.2 주석 기준 (항목 8)

  • 각 메서드의 주요 단계별 한글 주석 (예: # 인가 코드를 액세스 토큰으로 교환)
  • 외부 API 호출 시 호출 대상 명시 (예: # Facebook Graph API - 사용자 정보 조회)
  • 복잡한 분기 로직에 판단 기준 설명

5.3 코드 품질 기준

  • 타입 힌트 필수
  • async/await 패턴 준수
  • Pydantic v2 스키마 사용
  • 서비스 레이어 패턴 (router → service → oauth_client)

6. Scalar 문서 설정 (항목 9)

변경 파일: main.py

  • tags_metadata에 "SNS OAuth" 태그 추가
  • custom_openapi() 함수의 public_endpoints에 Facebook 콜백 경로 추가
  • 기존 "SNS" 태그 description에 Facebook 업로드 안내 추가 (향후 확장)

7. 코드리뷰 수행 (항목 10, 11)

리뷰 범위

모든 구현 완료 후 1회 코드리뷰 수행:

  1. 설계 검증

    • 레이어 분리 적정성 (router → service → oauth_client)
    • 의존성 방향 확인 (순환 의존 없음)
    • 예외 처리 체계 일관성
  2. 코드 정의 검증

    • 타입 힌트 누락 확인
    • async/await 적정 사용
    • 로거 사용 패턴 일관성
    • 주석 존재 여부
  3. 보안 검증

    • CSRF state 토큰 검증 로직
    • 토큰 노출 방지 (로그에 토큰 전체 미출력)
    • redirect_uri 고정 검증
  4. 기능 검증

    • OAuth 흐름 완전성
    • 에러 케이스 처리 (취소, 코드 만료, 토큰 실패)
    • DB 저장 정합성

리뷰 결과 처리 (항목 11)

  • 설계적 오류: 구조 변경 및 수정
  • 코드 정의 오류: 즉시 수정 반영

8. 검수 1차 - 작업 계획 완전성 검증

검수 항목별 준수 여부

# 요구사항 준수 여부 근거
1 oauth.py 라우터에 sns 프리픽스 O router = APIRouter(prefix="/sns") - Phase 6
2 utils/facebook_oauth.py에 단일 클래스, 이름에 oauth 포함 O FacebookOAuthClient 클래스 - Phase 2
3 비즈니스 로직은 facebook.py, facebook_oauth.py 클래스 사용 O FacebookServiceFacebookOAuthClient를 호출 - Phase 5
4 SNSUploadTask 모델 참조 후 필요 필드 추가 O Phase 7에서 platform, platform_post_id, platform_post_url 추가 검토
5 SNS 전용 Depends는 sns/dependency.py O Phase 4에서 구현
6 스키마는 sns/schemas에 정의 O facebook_schema.py - Phase 3
7 중요 입력/호출/변수 logger.debug 출력 O 품질 기준 5.1 적용
8 중요 함수 호출 주석 O 품질 기준 5.2 적용
9 Scalar 문서 설정 업데이트 O Phase 8에서 tags_metadata, public_endpoints 수정
10 1회 코드리뷰 수행 O Step 9에서 /review 수행
11 리뷰 후 설계적/코드 정의 오류 개선 O Step 10에서 수정 반영

1차 검수 발견 사항

발견 1: redirect_uri 불일치 위험

  • config.py의 SocialOAuthSettings 내 기존 주석은 /social/facebook/callback으로 되어 있으나, 본 계획에서는 /sns/facebook/callback으로 설정
  • 해결: FacebookSettings 독립 클래스에서 redirect_uri를 /sns/facebook/callback으로 명확히 설정하고, SocialOAuthSettings 내 Facebook 주석 코드는 삭제

발견 2: Redis 의존성 미확인

  • OAuth state 토큰 저장에 Redis 사용 예정이나, Redis 클라이언트 획득 방법 미명시
  • 해결: app/database/session.py의 Redis 연결 활용 또는 dependency.py에 Redis 의존성 추가 명시

발견 3: 기존 SNS 라우터와의 prefix 충돌 가능성

  • 기존 app/sns/api/routers/v1/sns.py에 이미 prefix="/sns"가 설정되어 있음
  • 새로운 oauth.py에도 prefix="/sns" 설정 시 main.py에서 중복 prefix 발생 가능
  • 해결: main.py에서 기존 sns_router는 prefix 없이 등록 (app.include_router(sns_router)), 새 oauth_router도 prefix 없이 등록하되 router 내부에 /sns prefix 유지

발견 4: schemas 디렉토리 init.py 확인 필요

  • app/sns/schemas/ 디렉토리에 __init__.py가 있는지 확인하여 import 가능하도록 보장

9. 검수 2차 - 설계 개선 검토

설계 관점 검토

검토 1: 레이어 분리 적정성 - 적정

Router (oauth.py)
  ↓ 호출
Service (facebook.py - FacebookService)
  ↓ 호출
OAuth Client (facebook_oauth.py - FacebookOAuthClient)
  ↓ HTTP 요청
Facebook Graph API
  • 각 레이어의 책임이 명확히 분리됨
  • OAuth 클라이언트는 HTTP 통신만, 서비스는 비즈니스 로직만, 라우터는 HTTP 요청/응답만 담당

검토 2: 기존 social 모듈과의 관계 - 개선 필요 없음

  • app/social/ 모듈은 YouTube OAuth 및 범용 업로드 담당
  • app/sns/ 모듈은 Instagram/Facebook 등 SNS 특화 기능 담당
  • 역할이 명확히 분리되어 있으므로 현행 구조 유지

검토 3: 토큰 갱신 전략 - 보완 필요

  • Facebook은 refresh_token을 미지원하며, 장기 토큰도 약 60일 만료
  • 보완: FacebookOAuthClient에 토큰 만료 확인 유틸리티 메서드 추가
    def is_token_expired(self, token_expires_at: datetime) -> bool:
        """토큰 만료 여부 확인 (만료 7일 전부터 True 반환)"""
    
  • 서비스 레이어에서 API 호출 전 토큰 만료 확인 → 만료 시 재연동 안내 예외 발생

검토 4: 페이지 토큰 관리 - 적정

  • 페이지 토큰은 SocialAccount.platform_data JSON 필드에 저장
  • 이 방식은 기존 YouTube의 channel_id 저장 패턴과 일관됨

검토 5: scope 범위 - 해결 완료

  • 기본 scope: public_profile, email (Facebook 앱 리뷰 없이 사용 가능)
  • 페이지 관리 scope: pages_show_list, pages_read_engagement, pages_manage_posts (앱 리뷰 필요)
  • 비디오 업로드 scope: publish_video (향후 확장 시)
  • 해결: FacebookSettings.FACEBOOK_OAUTH_SCOPE 필드에서 .env 변수로 관리
    • 기본값: public_profile,email,pages_show_list,pages_read_engagement,pages_manage_posts
    • 향후 확장 시 .env 파일에서 scope 변경만으로 대응 가능

검토 6: 에러 코드 체계 - 적정

  • Facebook OAuth 예외 클래스가 기존 Kakao 패턴과 동일 구조
  • HTTP 상태 코드 + 내부 에러 코드 + 메시지 3단계 체계

2차 검수 발견 사항

발견 1: state 토큰 저장소 구현 세부사항 보완

  • Redis 키 패턴 명확화 필요: facebook_oauth_state:{state} → value: user_uuid
  • TTL 설정: social_oauth_settings.OAUTH_STATE_TTL_SECONDS (기본 300초)
  • 반영: Phase 5의 start_connect() 구현 시 Redis 키 패턴 명시

발견 2: 기존 SocialAccount 중복 체크 로직

  • 동일 Facebook 사용자가 재연동할 경우, 기존 레코드를 업데이트해야 함
  • 반영: handle_callback()에서 (user_uuid, platform, platform_user_id) 기준으로 UPSERT 로직 구현

발견 3: 향후 확장성 확보

  • 현재 Facebook 전용이지만 TikTok 등 추가 시 패턴 재사용 가능한 구조
  • dependency.pyget_facebook_social_account()는 플랫폼 파라미터화하여 범용화 가능
  • 판단: 현재 단계에서는 Facebook 전용으로 유지 (YAGNI 원칙)

10. 최종 작업 실행 순서 (확정)

1. config.py 수정 (FacebookSettings 독립 클래스 신설 + SocialOAuthSettings 내 Facebook 주석 삭제)
2. .env 파일에 FACEBOOK_ 환경변수 추가
3. app/sns/schemas/facebook_schema.py 생성 (스키마 정의)
4. app/utils/facebook_oauth.py 생성 (FacebookOAuthClient 클래스 - facebook_settings 참조)
5. app/sns/dependency.py 구현 (SNS 전용 Depends)
6. app/sns/services/facebook.py 구현 (비즈니스 로직)
7. app/sns/api/routers/v1/oauth.py 구현 (엔드포인트)
8. app/sns/models.py 검토 및 필드 추가
9. main.py 수정 (라우터 등록 + Scalar 문서 업데이트)
10. 코드리뷰 수행 (/review)
11. 코드리뷰 결과 기반 개선 수행

부록: 참조 소스

기존 코드 패턴 참조 파일

패턴 참조 파일 설명
OAuth 클래스 app/user/services/kakao.py KakaoOAuthClient 구조
HTTP 클라이언트 app/utils/instagram.py httpx.AsyncClient 패턴
SNS 라우터 app/sns/api/routers/v1/sns.py prefix, 예외 처리 패턴
Social OAuth 라우터 app/social/api/routers/v1/oauth.py 콜백, 리다이렉트 패턴
모델 app/user/models.py SocialAccount, Platform enum
스키마 app/sns/schemas/sns_schema.py Pydantic v2 패턴
설정 (독립 클래스) config.py KakaoSettings 패턴 → FacebookSettings
설정 (OAuth 공통) config.py SocialOAuthSettings (state TTL, 프론트엔드 URL 등)
로거 app/utils/logger.py get_logger 패턴