Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import LoadingSvg from 'components/loading/loading-svg';

import { useTikTokOAuth } from '../hooks/useTikTokOAuth';

export default function TikTokOAuthLoading() {
useTikTokOAuth({});

return (
<div className="flex h-screen w-full items-center justify-center">
<div className="text-center">
<LoadingSvg />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect } from 'react';

import { useRouter, useSearchParams } from 'next/navigation';

import { useQueryClient } from '@tanstack/react-query';
import { CONNECT_SNS_KEYS } from 'constants/query-key';

interface UseTikTokOAuthOptions {
onSuccess?: () => void;
onError?: (error: string) => void;
}

export const useTikTokOAuth = ({
onSuccess,
onError,
}: UseTikTokOAuthOptions) => {
const router = useRouter();
const searchParams = useSearchParams();
const queryClient = useQueryClient();

useEffect(() => {
const success = searchParams.get('success');
const error = searchParams.get('error');
const returnTo = searchParams.get('return_to');

const returnPath = returnTo || '/my-page/connect-sns';

const timer = setTimeout(() => {
if (success) {
queryClient.invalidateQueries({
queryKey: CONNECT_SNS_KEYS.CONNECT_SNS(),
});

onSuccess?.();
router.replace(`${returnPath}?success=true`);
} else if (error) {
onError?.(error);
router.replace(`${returnPath}?error=${error}`);
} else {
router.replace(returnPath);
}
}, 1000);
return () => clearTimeout(timer);
}, [searchParams, router, queryClient, onSuccess, onError]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import TikTokOAuthLoading from '../components/tiktok-oauth-loading';

export default function TikTokOAuthLoadingPage() {
return <TikTokOAuthLoading />;
}
53 changes: 53 additions & 0 deletions apps/web/app/api/auth/sns/tiktok/connect/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';

import { getServerCookie } from 'utils/action/cookie';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const returnTo = searchParams.get('return_to');

// 사용자 인증 확인
const accessToken = await getServerCookie('AccessToken');
if (!accessToken) {
return NextResponse.redirect(new URL('/login', request.url));
}

// 백엔드 API 서버로 fetch 요청 (인증 헤더 포함)
const backendUrl = process.env.NEXT_PUBLIC_API_SERVER_URL;
if (!backendUrl) {
throw new Error('API 서버 URL이 설정되지 않았습니다.');
}

const connectUrl = new URL(`${backendUrl}/api/auth/sns/tiktok/connect`);
if (returnTo) {
connectUrl.searchParams.set('return_to', returnTo);
}

// 백엔드로 fetch 요청 (Authorization 헤더 포함)
const response = await fetch(connectUrl.toString(), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
redirect: 'manual', // 수동 리다이렉트 처리
});

if (response.status === 302 || response.status === 307) {
// 리다이렉트 응답인 경우 Location 헤더에서 URL 추출
const location = response.headers.get('location');
if (location) {
return NextResponse.redirect(location);
}
}

// 다른 응답인 경우 에러 처리
throw new Error(`백엔드 요청 실패: ${response.status}`);
} catch {
return NextResponse.json(
{ error: 'TikTok 연결 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
100 changes: 100 additions & 0 deletions apps/web/app/api/auth/tiktok/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server';

import { setCookie } from 'utils/action/cookie';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
const returnTo = searchParams.get('return_to');

if (error) {
const loadingUrl = new URL('/oauth/tiktok/loading', request.url);
loadingUrl.searchParams.set('error', error);
if (returnTo) {
loadingUrl.searchParams.set('return_to', returnTo);
}
return NextResponse.redirect(loadingUrl);
}

if (!code) {
const loadingUrl = new URL('/oauth/tiktok/loading', request.url);
loadingUrl.searchParams.set('error', 'no_code');
if (returnTo) {
loadingUrl.searchParams.set('return_to', returnTo);
}
return NextResponse.redirect(loadingUrl);
}

const backendUrl = process.env.NEXT_PUBLIC_API_SERVER_URL;
if (!backendUrl) {
throw new Error('API 서버 URL이 설정되지 않았습니다.');
}

const callbackUrl = new URL(`${backendUrl}/api/auth/tiktok/callback`);
callbackUrl.searchParams.set('code', code);
if (state) callbackUrl.searchParams.set('state', state);
if (returnTo) callbackUrl.searchParams.set('return_to', returnTo);

const response = await fetch(callbackUrl.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code,
state,
return_to: returnTo,
frontend_callback_url: request.url,
}),
});

if (!response.ok) {
throw new Error(`백엔드 콜백 처리 실패: ${response.status}`);
}

const result = await response.json();

if (result.data?.redirectUrl) {
return NextResponse.redirect(result.data.redirectUrl);
}

if (result.success) {
// 토큰 저장 (필요한 경우)
if (result.data?.accessToken) {
await setCookie('AccessToken', result.data.accessToken);
}

// 로딩 페이지로 리다이렉트 (원래 페이지 정보 포함)
const loadingUrl = new URL('/oauth/tiktok/loading', request.url);
loadingUrl.searchParams.set('success', 'true');
if (returnTo) {
loadingUrl.searchParams.set('return_to', returnTo);
}

return NextResponse.redirect(loadingUrl);
} else {
// 에러 시에도 로딩 페이지로 리다이렉트
const loadingUrl = new URL('/oauth/tiktok/loading', request.url);
loadingUrl.searchParams.set('error', result.message || 'callback_failed');
if (returnTo) {
loadingUrl.searchParams.set('return_to', returnTo);
}

return NextResponse.redirect(loadingUrl);
}
} catch {
const returnTo = new URL(request.url).searchParams.get('return_to');

// 에러 시에도 로딩 페이지로 리다이렉트
const loadingUrl = new URL('/oauth/tiktok/loading', request.url);
loadingUrl.searchParams.set('error', 'callback_failed');
if (returnTo) {
loadingUrl.searchParams.set('return_to', returnTo);
}

return NextResponse.redirect(loadingUrl);
}
}
Loading
Loading