# Facebook OAuth 구현 작업 계획서 > 작성일: 2026-03-03 > 프로젝트: O2O Castad Backend > 대상 모듈: `app/sns/` (Facebook OAuth 연동) --- ## 1. 개요 Facebook OAuth 2.0 로그인 기능을 구현합니다. Facebook Graph API를 통해 사용자 인증, 토큰 교환, 장기 토큰 발급, 페이지 토큰 조회 기능을 제공합니다. ### 참조 공식 문서 - Facebook Manual Login Flow: https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow - Facebook Graph API: https://developers.facebook.com/docs/graph-api - Long-Lived Token Exchange: https://developers.facebook.com/docs/facebook-login/guides/access-tokens/get-long-lived ### 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 바로 아래에 배치): ```python 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 ``` **인스턴스 생성** (파일 하단 인스턴스 블록에 추가): ```python facebook_settings = FacebookSettings() ``` **SocialOAuthSettings 내 기존 Facebook 주석 코드 처리**: - `SocialOAuthSettings` 내의 Facebook 관련 주석 처리된 코드 블록(524~530행)은 삭제 - 해당 책임이 `FacebookSettings` 클래스로 완전 이관됨 **.env 파일에 추가할 환경변수**: ```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 **클래스 구조**: ```python 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}, ...] ``` **예외 클래스** (같은 파일 내 정의): ```python 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` 패턴 준수 **스키마 정의**: ```python 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 의존성 **정의 내용**: ```python 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` 클래스를 통해서만 수행 **서비스 함수 구조**: ```python 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"]` **엔드포인트 구조**: ```python 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 추가**: ```python from app.sns.api.routers.v1.oauth import router as sns_oauth_router ``` 2. **라우터 등록**: ```python app.include_router(sns_oauth_router) # SNS OAuth 라우터 (Facebook) ``` - 주의: oauth.py 내부 router에 이미 `/sns` prefix가 있으므로 main.py에서는 prefix 추가 불필요 3. **tags_metadata 업데이트** - SNS 태그 설명에 Facebook 추가: ```python { "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. **공개 엔드포인트 추가** (인증 불필요): ```python 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** | `FacebookService`가 `FacebookOAuthClient`를 호출 - 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`에 토큰 만료 확인 유틸리티 메서드 추가 ```python 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.py`의 `get_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 패턴 |