| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- 'use client';
- import { useEffect, useRef, useState } from 'react';
- import { useRouter, useSearchParams } from 'next/navigation';
- import { fetchApi } from '@/lib/utils/client';
- import { SnsConnectResponse } from '@/types/sns';
- import './style.scss';
- const STATE_STORAGE_KEY = 'antooza-sns-google-state';
- /**
- * Google OAuth Authorization Code 콜백.
- *
- * 1. URL ?code= ?state= ?error= 수신
- * 2. state 검증 (sessionStorage 저장값과 비교)
- * 3. POST /api/mypage/sns/google/connect (code, redirectUri)
- * 4. 성공 → /sns-link?connected=google
- * 5. 실패 → /sns-link?error={message}
- */
- export default function GoogleSnsCallbackPage()
- {
- const router = useRouter();
- const searchParams = useSearchParams();
- const [phase, setPhase] = useState<'verifying' | 'success' | 'error'>('verifying');
- const [errorMessage, setErrorMessage] = useState<string|null>(null);
- const handledRef = useRef(false);
- useEffect(() => {
- // React StrictMode 등에서 effect 가 두 번 호출돼 code 가 재사용될 수 있음 → ref 가드
- if (handledRef.current) {
- return;
- }
- handledRef.current = true;
- const code = searchParams.get('code');
- const state = searchParams.get('state');
- const oauthError = searchParams.get('error');
- const finishWithError = (message: string) => {
- setErrorMessage(message);
- setPhase('error');
- window.setTimeout(() => {
- router.replace(`/sns-link?error=${encodeURIComponent(message)}`);
- }, 1200);
- };
- if (oauthError) {
- finishWithError(oauthError);
- return;
- }
- if (!code) {
- finishWithError('Authorization code 가 없습니다.');
- return;
- }
- // state 검증
- let savedState: string|null = null;
- try {
- savedState = window.sessionStorage.getItem(STATE_STORAGE_KEY);
- window.sessionStorage.removeItem(STATE_STORAGE_KEY);
- } catch {
- // silent
- }
- if (!state || !savedState || state !== savedState) {
- finishWithError('state 가 일치하지 않습니다. 다시 시도해주세요.');
- return;
- }
- const redirectUri = `${window.location.origin}/auth/sns/google/callback`;
- (async () => {
- try {
- const res = await fetchApi<SnsConnectResponse>('/api/mypage/sns/google/connect', {
- method: 'POST',
- body: { code, redirectUri },
- silent: true
- });
- if (res.success) {
- setPhase('success');
- window.setTimeout(() => {
- router.replace('/sns-link?connected=google');
- }, 600);
- } else {
- finishWithError(res.message || '연동에 실패했습니다.');
- }
- } catch {
- finishWithError('네트워크 오류로 연동에 실패했습니다.');
- }
- })();
- }, [router, searchParams]);
- return (
- <div className="sns-callback">
- <div className="sns-callback__panel" role="status" aria-live="polite">
- {phase === 'verifying' && (
- <>
- <div className="sns-callback__spinner" aria-hidden="true" />
- <p className="sns-callback__title">Google 계정 연동 처리 중...</p>
- <p className="sns-callback__desc">잠시만 기다려주세요.</p>
- </>
- )}
- {phase === 'success' && (
- <>
- <p className="sns-callback__title">연동이 완료되었습니다!</p>
- <p className="sns-callback__desc">SNS 연결 관리 페이지로 이동합니다.</p>
- </>
- )}
- {phase === 'error' && (
- <>
- <p className="sns-callback__title">연동에 실패했습니다</p>
- <p className="sns-callback__desc">{errorMessage}</p>
- </>
- )}
- </div>
- </div>
- );
- }
|