diff --git a/public/images/kakao-login.png b/public/images/kakao-login.png new file mode 100644 index 00000000..898db903 Binary files /dev/null and b/public/images/kakao-login.png differ diff --git a/src/apis/apis.ts b/src/apis/apis.ts index 33e39c8e..327c9445 100644 --- a/src/apis/apis.ts +++ b/src/apis/apis.ts @@ -4,6 +4,7 @@ import { PoseFeedResponse, PosePickResponse, PoseTalkResponse, + RegisterResponse, } from '.'; import publicApi from './config/publicApi'; @@ -31,3 +32,6 @@ export const getPoseFeed = async ( }); export const getFilterTag = () => publicApi.get('/pose/tags'); + +export const getRegister = (code: string) => + publicApi.get(`/users/login/oauth/kakao?code=${code}`); diff --git a/src/apis/config/privateApi.ts b/src/apis/config/privateApi.ts new file mode 100644 index 00000000..f78f092a --- /dev/null +++ b/src/apis/config/privateApi.ts @@ -0,0 +1,30 @@ +import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; + +import { BASE_API_URL } from '@/constants'; +import { getCookie } from '@/utils/cookieController'; + +import type { CustomInstance } from './type'; + +const privateApi: CustomInstance = axios.create({ + baseURL: `${BASE_API_URL}/api`, + withCredentials: true, +}); + +privateApi.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + try { + const accessToken = getCookie('accessToken'); + config.headers.Authorization = `Bearer ${accessToken}`; + return config; + } catch (error) { + return Promise.reject(error); + } + }, + (error: AxiosError) => { + Promise.reject(error); + } +); + +privateApi.interceptors.response.use((response) => response.data); + +export default privateApi; diff --git a/src/apis/queries.ts b/src/apis/queries.ts index 34102ceb..45bb5a32 100644 --- a/src/apis/queries.ts +++ b/src/apis/queries.ts @@ -5,11 +5,13 @@ import { PoseFeedResponse, PosePickResponse, PoseTalkResponse, + RegisterResponse, getFilterTag, getPoseDetail, getPoseFeed, getPosePick, getPoseTalk, + getRegister, } from '.'; import { FilterState } from '@/hooks/useFilterState'; @@ -50,3 +52,6 @@ export const usePoseFeedQuery = ( export const useFilterTagQuery = (options?: UseQueryOptions) => useSuspenseQuery(['filterTag'], getFilterTag, { ...options }); + +export const useRegisterQuery = (code: string) => + useSuspenseQuery(['register'], () => getRegister(code), {}); diff --git a/src/apis/type.ts b/src/apis/type.ts index c3818218..c15ed499 100644 --- a/src/apis/type.ts +++ b/src/apis/type.ts @@ -78,3 +78,17 @@ export interface PoseTalkResponse { wordId: number; }; } + +export interface Token { + accessToken: string; + refreshToken: string; + grantType: string; + expiresIn: number; +} + +export interface RegisterResponse { + id: number; + nickname: string; + email: string; + token: Token; +} diff --git a/src/app/(Sub)/api/users/login/oauth/kakao/components/LoginSection.tsx b/src/app/(Sub)/api/users/login/oauth/kakao/components/LoginSection.tsx new file mode 100644 index 00000000..07df1cd3 --- /dev/null +++ b/src/app/(Sub)/api/users/login/oauth/kakao/components/LoginSection.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { useRegisterQuery } from '@/apis'; +import { Loading } from '@/components/Loading'; +import { setCookie } from '@/utils/cookieController'; + +interface LoginSectionProps { + code: string; +} +export default function LoginSection({ code }: LoginSectionProps) { + const router = useRouter(); + const { data } = useRegisterQuery(code); + console.log(data); + const { token, email, nickname } = data; + const { accessToken, refreshToken, expiresIn } = token; + setCookie('accessToken', accessToken); + setCookie('refreshToken', refreshToken); + router.replace('/menu'); + + return ; +} diff --git a/src/app/(Sub)/api/users/login/oauth/kakao/page.tsx b/src/app/(Sub)/api/users/login/oauth/kakao/page.tsx new file mode 100644 index 00000000..a4d9c886 --- /dev/null +++ b/src/app/(Sub)/api/users/login/oauth/kakao/page.tsx @@ -0,0 +1,25 @@ +import { QueryAsyncBoundary } from '@suspensive/react-query'; + +import LoginSection from './components/LoginSection'; +import { getRegister } from '@/apis'; +import { RejectedFallback } from '@/components/ErrorBoundary'; +import { Loading } from '@/components/Loading'; +import { HydrationProvider } from '@/components/Provider/HydrationProvider'; + +interface PageProps { + searchParams: { + code: string; + }; +} + +export default function Page({ searchParams }: PageProps) { + const { code } = searchParams; + + return ( + }> + getRegister(code)}> + + + + ); +} diff --git a/src/app/(Sub)/menu/components/LoginModal.tsx b/src/app/(Sub)/menu/components/LoginModal.tsx new file mode 100644 index 00000000..27527522 --- /dev/null +++ b/src/app/(Sub)/menu/components/LoginModal.tsx @@ -0,0 +1,38 @@ +import Image from 'next/image'; + +import { Modal } from '@/components/Modal'; +import { Spacing } from '@/components/Spacing'; + +interface LoginModalProps { + onClose: () => void; +} +export default function LoginModal({ onClose }: LoginModalProps) { + const link = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_SERVER_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_SITE_URL}/api/users/login/oauth/kakao&response_type=code`; + + const handleLogin = () => { + window.location.href = link; + }; + + return ( + + +

간편 로그인

+ +

+ 로그인해야 북마크를 쓸 수 있어요. +
+ 카카오로 3초만에 가입할 수 있어요! +

+ + kakao-login + +
+ ); +} diff --git a/src/app/(Sub)/menu/components/LoginSection.tsx b/src/app/(Sub)/menu/components/LoginSection.tsx index 34decd74..49113dfe 100644 --- a/src/app/(Sub)/menu/components/LoginSection.tsx +++ b/src/app/(Sub)/menu/components/LoginSection.tsx @@ -1,7 +1,7 @@ 'use client'; +import LoginModal from './LoginModal'; import { Icon } from '@/components/Icon'; -import { PreparingModal } from '@/components/Modal'; import { useOverlay } from '@/components/Overlay/useOverlay'; import { Spacing } from '@/components/Spacing'; @@ -14,17 +14,16 @@ function DefaultProfile() { } export default function LoginSection() { - const { open } = useOverlay(); + const { open, exit } = useOverlay(); return (
-
open(({ exit }) => )} - > +
-
로그인하기
+
open(() => )}> + 로그인하기 +
); diff --git a/src/app/(Sub)/menu/components/MakerSection.tsx b/src/app/(Sub)/menu/components/MakerSection.tsx index cc70fae3..2fab67ab 100644 --- a/src/app/(Sub)/menu/components/MakerSection.tsx +++ b/src/app/(Sub)/menu/components/MakerSection.tsx @@ -1,5 +1,4 @@ 'use client'; -import Image from 'next/image'; import BottomFixedDiv from '@/components/BottomFixedDiv'; import { Icon } from '@/components/Icon'; diff --git a/src/utils/cookieController.ts b/src/utils/cookieController.ts new file mode 100644 index 00000000..335275bd --- /dev/null +++ b/src/utils/cookieController.ts @@ -0,0 +1,43 @@ +const isServer = typeof window === 'undefined'; + +export const setCookie = async (key: string, value: string, options?: { expires?: Date }) => { + // 서버 측 쿠키 + if (isServer) { + const { cookies } = await import('next/headers'); + cookies().set(key, value, { + expires: options?.expires, + }); + return; + } + + // 클라이언트 측 쿠키 + document.cookie = `${key}=${value}; path=/; ${ + options?.expires ? `expires=${options.expires.toUTCString()}` : '' + }`; +}; + +export const getCookie = async (key: string) => { + // 서버측 쿠키 + if (isServer) { + const { cookies } = await import('next/headers'); + return cookies().get(key); + } + + // 클라이언트 측 쿠키 + const value = `; ${document.cookie}`; + const parts = value.split(`; ${key}=`); + + return parts.pop()?.split(';').shift(); +}; + +export const removeCookie = async (key: string) => { + // 서버측 쿠키 + if (isServer) { + const { cookies } = await import('next/headers'); + cookies().set(key, ''); + return; + } + + // 클라이언트 측 쿠키 + document.cookie = `${key}=; expires=Thu, 01 Jan 1999 00:00:10 GMT;`; +};