675 lines
28 KiB
Markdown
675 lines
28 KiB
Markdown
# 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 패턴 |
|