Merge branch 'facebook' of https://gitea.o2o.kr/castad/o2o-castad-backend into facebook

마지막 main 머지를 facebook 브렌치로 백업, 저장한 버전
facebook
Dohyun Lim 2026-03-13 15:49:38 +09:00
commit c6fc164c41
1 changed files with 207 additions and 0 deletions

207
front_plan.md Normal file
View File

@ -0,0 +1,207 @@
# Facebook 로그인 연동 - 프론트엔드 개발계획서
## 1. 개요
CastAD 서비스에 Facebook 계정 연동 기능을 구현한다. 백엔드에 OAuth 2.0 Authorization Code Flow가 이미 구현되어 있으며, 프론트엔드는 이 플로우의 시작(연동 요청)과 끝(결과 수신)을 담당한다.
기술 스택: React 기반 SPA를 전제로 한다.
## 2. OAuth 2.0 전체 플로우
Facebook OAuth 연동은 총 8단계로 이루어진다. 프론트엔드가 직접 관여하는 단계는 1, 2, 8번이며, 3~7번은 Facebook과 백엔드 사이에서 자동으로 처리된다.
**1단계 (프론트 → 백엔드)**: 프론트엔드가 `GET /sns/facebook/connect`를 호출한다. Authorization 헤더에 사용자의 Bearer 토큰을 포함해야 한다. 백엔드는 `auth_url`(Facebook 인증 페이지 URL)과 `state`(CSRF 방지 토큰)를 JSON으로 응답한다.
**2단계 (프론트 → Facebook)**: 프론트엔드가 응답받은 `auth_url`을 팝업 윈도우로 연다. 사용자는 이 팝업에서 Facebook 로그인과 권한 승인을 수행한다.
**3단계 (Facebook → 백엔드)**: 사용자가 승인을 완료하면 Facebook이 백엔드의 `redirect_uri`(`/sns/facebook/callback`)로 리다이렉트한다. 이때 query parameter로 `code`(인가 코드)와 `state`를 전달한다. 프론트엔드는 이 단계에 관여하지 않는다.
**4~6단계 (백엔드 ↔ Facebook)**: 백엔드가 `code`를 Facebook API에 전달하여 access token을 획득하고, 장기 토큰으로 교환한 뒤, 사용자 정보를 조회하여 DB에 저장한다. 프론트엔드는 이 단계에 관여하지 않는다.
**7단계 (백엔드 → 프론트)**: 백엔드가 처리를 완료한 뒤 302 리다이렉트로 프론트엔드 URL로 보낸다. 성공 시 `/social/connect/success`로, 실패 시 `/social/connect/error`로 리다이렉트하며 결과 정보를 query parameter에 포함한다.
**8단계 (프론트)**: 리다이렉트된 페이지(팝업 내부)에서 query parameter를 파싱하고, `window.opener.postMessage()`로 부모 창에 결과를 전달한 뒤 팝업을 닫는다. 부모 창은 메시지를 수신하여 UI를 업데이트한다.
## 3. 백엔드 API 명세
### 3-1. Facebook 연동 시작
- 엔드포인트: `GET /sns/facebook/connect`
- 인증: 필수. `Authorization: Bearer <access_token>` 헤더를 포함해야 한다.
- 성공 응답 (200): `auth_url` 필드에 Facebook 인증 페이지 URL, `state` 필드에 CSRF 방지 토큰이 포함된 JSON 객체를 반환한다.
```json
{
"auth_url": "https://www.facebook.com/v21.0/dialog/oauth?client_id=...&redirect_uri=...&state=...&scope=...&response_type=code",
"state": "5PcHtcvJdQskHw5aG-MIOmv5CCX3L9v3eqQOqyZlKsc"
}
```
- 에러 응답: 401 (인증 실패, 토큰 없음 또는 만료)
### 3-2. Facebook OAuth 콜백
- 엔드포인트: `GET /sns/facebook/callback`
- 인증: 불필요. 이 엔드포인트는 Facebook이 직접 호출하며, 프론트엔드가 직접 호출하지 않는다.
- query parameter: `code` (Facebook 인가 코드), `state` (CSRF 토큰), `error` (선택, OAuth 에러 코드), `error_description` (선택, 에러 설명)
- 응답: JSON이 아닌 302 리다이렉트로 응답한다. 프론트엔드의 성공 또는 에러 페이지로 리다이렉트한다.
성공 시 리다이렉트 URL: `{OAUTH_FRONTEND_URL}/social/connect/success?platform=facebook&account_id={id}&channel_name={name}`
실패 또는 취소 시 리다이렉트 URL: `{OAUTH_FRONTEND_URL}/social/connect/error?platform=facebook&error={message}&cancelled=true|false`
`OAUTH_FRONTEND_URL`은 백엔드 환경변수로 설정되며, 개발 환경 기본값은 `http://localhost:3000`이다. 프론트엔드 배포 도메인과 일치해야 하므로 백엔드 팀과 사전 확인이 필요하다.
### 3-3. Facebook 연동 해제
- 엔드포인트: `DELETE /sns/facebook/disconnect`
- 인증: 필수. `Authorization: Bearer <access_token>` 헤더를 포함해야 한다.
- 성공 응답 (200): `{"success": true, "message": "Facebook 계정 연동이 해제되었습니다."}`
- 에러 응답: 401 (인증 실패), 404 (연동된 Facebook 계정 없음, 에러 코드 `FACEBOOK_ACCOUNT_NOT_FOUND`)
## 4. 프론트엔드 구현 항목
### 4-1. 라우트 등록
React Router에 다음 2개의 라우트를 추가해야 한다. 이 페이지들은 백엔드 콜백의 302 리다이렉트 대상이므로 반드시 존재해야 한다.
- `/social/connect/success`: OAuth 연동 성공 처리 페이지
- `/social/connect/error`: OAuth 연동 실패/취소 처리 페이지
이 두 페이지는 팝업 윈도우 안에서 렌더링되며, 독립적으로 접근 가능해야 한다(SPA 라우팅이 아닌 직접 URL 접근으로도 동작해야 한다).
### 4-2. Facebook 연동 버튼 컴포넌트
소셜 계정 관리 페이지 또는 설정 페이지에 배치한다.
동작 흐름:
1. 사용자가 "Facebook 연동하기" 버튼을 클릭한다.
2. 버튼을 로딩(비활성) 상태로 전환한다.
3. `GET /sns/facebook/connect`를 호출하여 `auth_url``state`를 받는다.
4. `window.open(auth_url, 'facebook-oauth', 'width=600,height=700,scrollbars=yes')`로 팝업을 연다.
5. `window.open()`의 반환값이 `null`이면 브라우저가 팝업을 차단한 것이다. 이 경우 `window.location.href = auth_url`로 현재 창에서 리다이렉트하는 폴백을 적용한다.
6. 팝업에서 `postMessage`를 수신할 `message` 이벤트 리스너를 등록한다.
팝업 방식을 권장하는 이유: 현재 페이지의 상태(폼 입력값, 스크롤 위치 등)를 유지하면서 OAuth 인증을 진행할 수 있다.
### 4-3. OAuth 성공 페이지 (`/social/connect/success`)
이 페이지는 팝업 윈도우 안에서 렌더링된다.
동작 흐름:
1. `URLSearchParams`로 query parameter를 파싱한다. 사용 가능한 파라미터는 `platform`("facebook"), `account_id`(연동된 계정 ID, 숫자), `channel_name`(Facebook 사용자 이름)이다.
2. `window.opener`가 존재하는지 확인한다(팝업으로 열렸는지 판별).
3. 팝업인 경우: `window.opener.postMessage()`로 부모 창에 성공 결과를 전달하고, `window.close()`로 팝업을 닫는다. postMessage의 `targetOrigin`은 보안을 위해 `window.location.origin`으로 지정한다.
4. 팝업이 아닌 경우(폴백 리다이렉트): 소셜 계정 관리 페이지로 `navigate()`한다.
postMessage 데이터 구조:
```json
{
"type": "FACEBOOK_OAUTH_RESULT",
"success": true,
"platform": "facebook",
"accountId": "연동된 계정 ID",
"channelName": "Facebook 사용자 이름"
}
```
### 4-4. OAuth 에러 페이지 (`/social/connect/error`)
이 페이지도 팝업 윈도우 안에서 렌더링된다.
동작 흐름:
1. `URLSearchParams`로 query parameter를 파싱한다. 사용 가능한 파라미터는 `platform`("facebook"), `error`(에러 메시지 문자열), `cancelled`("true" 또는 "false")이다.
2. `cancelled`가 "true"이면 사용자가 Facebook에서 직접 취소한 것이다. "false"이면 시스템 에러이다.
3. 팝업인 경우: `window.opener.postMessage()`로 부모 창에 에러 결과를 전달하고, `window.close()`로 팝업을 닫는다.
4. 팝업이 아닌 경우: 에러 메시지를 화면에 표시하고 "다시 시도" 버튼과 소셜 관리 페이지로 돌아가는 링크를 제공한다.
postMessage 데이터 구조:
```json
{
"type": "FACEBOOK_OAUTH_RESULT",
"success": false,
"platform": "facebook",
"error": "에러 메시지",
"cancelled": true
}
```
### 4-5. 부모 창 메시지 수신 처리
연동 버튼이 있는 컴포넌트에서 `window.addEventListener('message', handler)`로 메시지를 수신한다.
처리 로직:
1. `event.origin`이 현재 페이지의 origin과 일치하는지 검증한다(보안).
2. `event.data.type``'FACEBOOK_OAUTH_RESULT'`인지 확인한다.
3. `event.data.success``true`이면 연동 성공 UI를 표시한다(토스트 메시지, 연동된 계정 정보 갱신 등).
4. `event.data.success``false`이면 `event.data.cancelled` 여부에 따라 취소 메시지 또는 에러 메시지를 표시한다.
5. 컴포넌트 언마운트 시 이벤트 리스너를 반드시 해제한다.
### 4-6. 연동 해제 기능
동작 흐름:
1. 연동된 계정 정보 옆에 "연동 해제" 버튼을 배치한다.
2. 클릭 시 확인 다이얼로그를 표시한다("Facebook 연동을 해제하시겠습니까?").
3. 확인 시 `DELETE /sns/facebook/disconnect`를 호출한다. Authorization 헤더를 포함해야 한다.
4. 성공 응답(200) 수신 시 UI를 미연동 상태로 업데이트한다.
5. 404 응답 시 이미 연동 해제된 상태이므로 UI를 미연동 상태로 업데이트한다.
### 4-7. 연동 상태 표시
연동 전 상태: Facebook 아이콘과 "연동하기" 버튼을 표시한다.
연동 후 상태: Facebook 아이콘, 연동된 사용자 이름(channel_name), "연동 해제" 버튼을 표시한다.
참고: 현재 백엔드에 사용자의 Facebook 연동 상태를 조회하는 전용 API가 없다. 페이지 진입 시 연동 여부를 표시하려면, 기존 사용자 프로필 API에 소셜 연동 정보가 포함되어 있는지 확인하거나, 백엔드에 상태 조회 API 추가를 요청해야 한다. 연동 직후에는 postMessage로 받은 `accountId``channelName`을 컴포넌트 상태에 저장하여 표시할 수 있다.
## 5. 에러 처리
프론트엔드에서 처리해야 하는 에러 시나리오와 대응 방법은 다음과 같다.
**사용자 취소 (`cancelled=true`)**: 사용자가 Facebook 인증 화면에서 "취소"를 눌렀다. "Facebook 연동이 취소되었습니다." 메시지를 표시한다. 재시도 가능하므로 연동 버튼을 다시 활성화한다.
**state 만료 (`FACEBOOK_STATE_EXPIRED`)**: 사용자가 Facebook 로그인을 너무 오래 지체하여 백엔드의 state 토큰이 만료되었다(기본 TTL: 300초, 약 5분). "인증 세션이 만료되었습니다. 다시 시도해주세요." 메시지를 표시한다.
**토큰 교환 실패 (`FACEBOOK_AUTH_FAILED`)**: Facebook에서 받은 인가 코드로 access token 교환이 실패했다. "Facebook 인증에 실패했습니다. 다시 시도해주세요." 메시지를 표시한다.
**Facebook API 오류 (`FACEBOOK_API_ERROR`)**: Facebook Graph API 호출 중 서버 오류가 발생했다. "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요." 메시지를 표시한다.
**연동 해제 시 계정 없음 (`FACEBOOK_ACCOUNT_NOT_FOUND`)**: 이미 연동 해제된 상태에서 다시 해제를 시도했다. UI를 미연동 상태로 갱신한다.
**인증 실패 (401)**: 서비스 로그인 토큰이 만료되었다. 로그인 페이지로 이동시킨다.
## 6. 폴백 처리 (팝업 차단 시)
브라우저가 팝업을 차단하는 경우 `window.open()``null`을 반환한다. 이때는 현재 창에서 `window.location.href = auth_url`로 직접 이동시키는 폴백을 적용한다.
폴백 모드에서는 OAuth 완료 후 `/social/connect/success` 또는 `/social/connect/error` 페이지로 돌아왔을 때 `window.opener``null`이다. 이 경우 postMessage 대신 다음과 같이 처리한다:
1. query parameter에서 결과를 파싱한다.
2. 결과를 `sessionStorage`에 저장한다(키 예시: `facebook_oauth_result`).
3. 소셜 계정 관리 페이지로 `navigate()`한다.
4. 소셜 계정 관리 페이지 마운트 시 `sessionStorage`에서 결과를 확인하고, 있으면 UI에 반영한 뒤 `sessionStorage`에서 삭제한다.
이 폴백이 필요한 이유: 현재 창 리다이렉트 방식에서는 OAuth 전에 있던 페이지 상태가 사라지므로, 결과를 임시 저장하여 원래 페이지로 복귀 후 처리해야 한다.
## 7. 환경 설정 확인 사항
프론트엔드 개발 시작 전에 백엔드 팀과 다음 값들을 확인해야 한다.
1. `OAUTH_FRONTEND_URL`: 백엔드가 콜백 처리 후 프론트엔드로 302 리다이렉트할 때 사용하는 URL이다. 개발 환경에서는 `http://localhost:3000`, 운영 환경에서는 실제 프론트엔드 도메인이 설정되어야 한다. 이 값이 잘못되면 OAuth 완료 후 엉뚱한 URL로 리다이렉트된다.
2. `OAUTH_SUCCESS_PATH`: 성공 시 리다이렉트 경로. 기본값은 `/social/connect/success`이다.
3. `OAUTH_ERROR_PATH`: 실패 시 리다이렉트 경로. 기본값은 `/social/connect/error`이다.
4. `FACEBOOK_REDIRECT_URI`: Facebook이 인증 완료 후 호출하는 백엔드 콜백 URL이다. 프론트엔드가 변경할 수 없으며, Facebook 개발자 콘솔에 등록된 URI와 정확히 일치해야 한다. 현재 설정값은 `http://dev.castad.net/sns/facebook/callback`이다.
## 8. 구현 순서
다음 순서로 구현을 권장한다.
1. React Router에 `/social/connect/success``/social/connect/error` 라우트를 등록한다.
2. 성공 페이지를 구현한다. query parameter 파싱과 postMessage 전송 로직을 작성한다.
3. 에러 페이지를 구현한다. 취소와 에러를 분기 처리하고 postMessage 전송 로직을 작성한다.
4. Facebook 연동 버튼 컴포넌트를 구현한다. API 호출, 팝업 열기, postMessage 수신을 작성한다.
5. 팝업 차단 시 폴백 로직을 구현한다(sessionStorage 기반).
6. 연동 해제 기능을 구현한다.
7. 연동 상태 표시 UI를 구현한다(상태 조회 API 확인 후).