From b18347a104620642a54b4a01bc4675c442edf10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=9D=80=EC=A7=80?= Date: Fri, 16 Jan 2026 09:34:53 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 12 ++++ src/app/(auth)/login/page.tsx | 6 +- src/config/client.ts | 132 ++++++++++++++++++++++++++++------ src/types/auth.ts | 10 +++ 4 files changed, 133 insertions(+), 27 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index 3907e62..e391946 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -2,9 +2,21 @@ import { LoginRequest, LoginResponse } from '../types/auth'; import { apiFetch } from '@/config/client'; +// 로그인-POST /{teamId}/auth/login export function login(payload: LoginRequest) { return apiFetch('/auth/login', { method: 'POST', body: payload, }); } + +// 토큰 리프레시 - POST /{teamId}/auth/tokens +export function refreshTokens(refreshToken: string) { + return apiFetch<{ accessToken: string; refreshToken: string }>( + '/auth/tokens', + { + method: 'POST', + body: { refreshToken }, + } + ); +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index a412894..a4c32f0 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -24,7 +24,6 @@ export default function LoginPage() { if (savedEmail) setEmail(savedEmail); - // 사용 후 제거 sessionStorage.removeItem('signupEmail'); }, []); @@ -48,13 +47,10 @@ export default function LoginPage() { setErrors(newErrors); if (Object.values(newErrors).some(Boolean)) return; + try { const result = await login({ email, password }); - // TODO: 보안 강화를 위해 - // refreshToken → httpOnly cookie - // accessToken → memory 관리 방식으로 변경 예정 - // 토큰 저장 localStorage.setItem('accessToken', result.accessToken); localStorage.setItem('refreshToken', result.refreshToken); diff --git a/src/config/client.ts b/src/config/client.ts index 3249394..4f5818b 100644 --- a/src/config/client.ts +++ b/src/config/client.ts @@ -1,27 +1,73 @@ +import { refreshTokens } from '@/api/auth'; import { API_BASE_URL } from '@/config/api'; type Params = Record; -type ApiFetchOptions

= Omit & { - params?: P; - body?: unknown; -}; - /** * apiFetch에서 사용하는 옵션 타입 * * @param params URL query string (?key=value)으로 변환될 값 * @param body POST / PATCH 요청 시 전달할 payload * @param headers 추가로 병합할 HTTP 헤더 + * @param skipAuthRefresh 토큰 리프레시 로직 스킵 여부 (기본값: false) * @param options fetch의 RequestInit 옵션(method, cache 등) */ +type ApiFetchOptions

= Omit & { + params?: P; + body?: unknown; + skipAuthRefresh?: boolean; +}; + +let isRefreshing = false; +let refreshPromise: Promise | null = null; + +/** + * 토큰 리프레시 함수 + */ +async function refreshAccessToken(): Promise { + const refreshToken = localStorage.getItem('refreshToken'); + + if (!refreshToken) { + return false; + } + + try { + const data = await refreshTokens(refreshToken); + + // 새 토큰 저장 + localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('refreshToken', data.refreshToken); + + return true; + } catch (error) { + // 리프레시 실패 시 로그아웃 처리 + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + + // 로그인 페이지로 리다이렉트 + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + + return false; + } +} + +/** + * apiFetch 함수 (토큰 리프레시 로직 포함) + */ export async function apiFetch( endpoint: string, - { params, body, headers, ...options }: ApiFetchOptions

= {} + { + params, + body, + headers, + skipAuthRefresh = false, + ...options + }: ApiFetchOptions

= {} ): Promise { const searchParams = new URLSearchParams(); - // query string 생성 if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { @@ -30,30 +76,72 @@ export async function apiFetch( }); } - // 최종 요청 URL const url = `${API_BASE_URL}${endpoint}${ searchParams.toString() ? `?${searchParams.toString()}` : '' }`; - const accessToken = - typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null; - // 공통 헤더, body JSON 설정 - const isFormData = body instanceof FormData; - const res = await fetch(url, { - ...options, - headers: { - ...(isFormData ? {} : { 'Content-Type': 'application/json' }), - ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), // 토큰 추가 - ...headers, - }, - body: isFormData ? body : body ? JSON.stringify(body) : undefined, - }); + const makeRequest = async (): Promise => { + const token = + typeof window !== 'undefined' + ? localStorage.getItem('accessToken') + : null; + + const isFormData = body instanceof FormData; + + return fetch(url, { + ...options, + headers: { + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...headers, + }, + body: isFormData ? body : body ? JSON.stringify(body) : undefined, + }); + }; + + let res = await makeRequest(); + + // 401 에러 && 리프레시 스킵하지 않는 경우 → 토큰 리프레시 시도 + if (res.status === 401 && !skipAuthRefresh) { + // 이미 리프레시 중인 경우 대기 + if (isRefreshing && refreshPromise) { + await refreshPromise; + // 리프레시 완료 후 재요청 + res = await makeRequest(); + } else { + // 새로운 리프레시 시작 + isRefreshing = true; + refreshPromise = refreshAccessToken().then((success) => { + isRefreshing = false; + refreshPromise = null; + return success; + }); + + const refreshSuccess = await refreshPromise; + + if (refreshSuccess) { + // 리프레시 성공 → 원래 요청 재시도 + res = await makeRequest(); + } else { + // 리프레시 실패 → 에러 던지기 + throw new ApiError( + '인증이 만료되었습니다. 다시 로그인해주세요.', + 401, + null + ); + } + } + } // 공통 에러 처리 if (!res.ok) { const errorBody = await res.json().catch(() => null); if (res.status === 401) { + const accessToken = + typeof window !== 'undefined' + ? localStorage.getItem('accessToken') + : null; console.error('[apiFetch] Unauthorized', { url, accessToken, @@ -82,6 +170,6 @@ export class ApiError extends Error { public readonly body: any ) { super(message); - this.name = ''; + this.name = 'ApiError'; } } diff --git a/src/types/auth.ts b/src/types/auth.ts index bed7a5b..864e423 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -26,3 +26,13 @@ export interface LoginResponse { // 공통 사용자 정보 export type UserResponse = SignupResponse; + +// 토큰 리프레시 요청/응답 타입 추가 +export interface RefreshTokenRequest { + refreshToken: string; +} + +export interface RefreshTokenResponse { + accessToken: string; + refreshToken: string; +} From 73382cd82674036221a3cd066017e4ff520fa434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=9D=80=EC=A7=80?= Date: Fri, 16 Jan 2026 09:54:30 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EA=B8=B0=EB=8A=A5=20-=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=EB=A3=A8=ED=94=84=20=EC=9C=84=ED=97=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/auth.ts b/src/api/auth.ts index e391946..98814b4 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -17,6 +17,7 @@ export function refreshTokens(refreshToken: string) { { method: 'POST', body: { refreshToken }, + skipAuthRefresh: true, } ); } From c5e14b970548665989072b1bc1f773c9284a0cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=9D=80=EC=A7=80?= Date: Fri, 16 Jan 2026 18:58:14 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EA=B0=84=ED=8E=B8=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/oauth.ts | 15 +++++++++++++ src/app/(auth)/signup/page.tsx | 18 ++++++++++++++- src/app/oauth/kakao/page.tsx | 41 ++++++++++++++++++++++++++++++++++ src/config/oauth.ts | 7 ++++++ src/types/oauth.ts | 20 ++++++++++++----- 5 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/api/oauth.ts create mode 100644 src/app/oauth/kakao/page.tsx create mode 100644 src/config/oauth.ts diff --git a/src/api/oauth.ts b/src/api/oauth.ts new file mode 100644 index 0000000..758d87c --- /dev/null +++ b/src/api/oauth.ts @@ -0,0 +1,15 @@ +// src/api/oauth.ts +import { apiFetch } from '@/config/client'; +import { OAuthSignupResponse } from '@/types/oauth'; + +export function kakaoSignUp(params: { + nickname: string; + redirectUri: string; + token: string; +}) { + return apiFetch('/oauth/sign-up/kakao', { + method: 'POST', + body: params, + skipAuthRefresh: true, + }); +} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 0f25ba1..ae15e9b 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -9,6 +9,7 @@ import { signup } from '@/api/users'; import kakaoLogo from '@/assets/icons/auth/ic-kakao.svg'; import Button from '@/components/Button'; import { TextInput, PasswordInput } from '@/components/Input'; +import { KAKAO_REST_API_KEY, KAKAO_REDIRECT_URI } from '@/config/oauth'; import { useGuestOnly } from '@/hooks/useGuestOnly'; import { validateEmail, @@ -36,6 +37,16 @@ export default function SignupPage() { const router = useRouter(); + const handleKakaoSignup = () => { + const url = + 'https://kauth.kakao.com/oauth/authorize' + + `?client_id=${KAKAO_REST_API_KEY}` + + `&redirect_uri=${encodeURIComponent(KAKAO_REDIRECT_URI)}` + + '&response_type=code'; + + window.location.href = url; + }; + const handleChange = (key: keyof typeof form) => (value: string) => { setForm((prev) => ({ ...prev, @@ -199,7 +210,12 @@ export default function SignupPage() {

- diff --git a/src/app/oauth/kakao/page.tsx b/src/app/oauth/kakao/page.tsx new file mode 100644 index 0000000..0e35b3d --- /dev/null +++ b/src/app/oauth/kakao/page.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect, useRef } from 'react'; + +import { kakaoSignUp } from '@/api/oauth'; +import { KAKAO_REDIRECT_URI } from '@/config/oauth'; + +export default function KakaoSignupPage() { + const router = useRouter(); + const hasRequested = useRef(false); + + useEffect(() => { + if (hasRequested.current) return; + hasRequested.current = true; + + const code = new URLSearchParams(window.location.search).get('code'); + if (!code) return; + + const signup = async () => { + try { + const res = await kakaoSignUp({ + nickname: '카카오유저', + redirectUri: KAKAO_REDIRECT_URI, + token: code, + }); + + localStorage.setItem('accessToken', res.accessToken); + localStorage.setItem('refreshToken', res.refreshToken); + + router.replace('/'); + } catch (e) { + console.error('카카오 회원가입 실패', e); + } + }; + + signup(); + }, [router]); + + return

카카오 회원가입 처리 중...

; +} diff --git a/src/config/oauth.ts b/src/config/oauth.ts new file mode 100644 index 0000000..e1c94cf --- /dev/null +++ b/src/config/oauth.ts @@ -0,0 +1,7 @@ +// src/config/oauth.ts +export const KAKAO_REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; + +export const KAKAO_REDIRECT_URI = + process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI || + 'http://localhost:3000/oauth/kakao'; +//redirectUri는 카카오 디벨로퍼 + 백엔드에 등록한 값과 동일해야 함 diff --git a/src/types/oauth.ts b/src/types/oauth.ts index f603116..e561636 100644 --- a/src/types/oauth.ts +++ b/src/types/oauth.ts @@ -1,7 +1,15 @@ -/** ====================== - * Request Types - ======================= */ +// src/types/oauth.ts +export interface OAuthUser { + id: number; + email: string; + nickname: string; + profileImageUrl: string; + createdAt: string; + updatedAt: string; +} -/** ====================== - * Response Types - ======================= */ +export interface OAuthSignupResponse { + user: OAuthUser; + accessToken: string; + refreshToken: string; +} From 9849cb9ba84aa6b86324bd459d9b43a62c40ba6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=9D=80=EC=A7=80?= Date: Fri, 16 Jan 2026 21:59:33 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EA=B0=84=ED=8E=B8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/oauth.ts | 55 ++++++++++++++++++++++++--- src/app/(auth)/login/page.tsx | 19 +++++++++- src/app/(auth)/signup/page.tsx | 3 +- src/app/oauth/kakao/page.tsx | 69 ++++++++++++++++++++++++++-------- src/config/oauth.ts | 5 +-- src/types/oauth.ts | 2 +- 6 files changed, 126 insertions(+), 27 deletions(-) diff --git a/src/api/oauth.ts b/src/api/oauth.ts index 758d87c..8c651b5 100644 --- a/src/api/oauth.ts +++ b/src/api/oauth.ts @@ -1,15 +1,58 @@ // src/api/oauth.ts import { apiFetch } from '@/config/client'; -import { OAuthSignupResponse } from '@/types/oauth'; +import { OAuthAuthResponse } from '@/types/oauth'; -export function kakaoSignUp(params: { - nickname: string; +type OAuthProvider = 'kakao'; + +type OAuthSignInParams = { + provider: OAuthProvider; + redirectUri: string; + token: string; +}; + +type OAuthSignUpParams = { + provider: OAuthProvider; redirectUri: string; token: string; -}) { - return apiFetch('/oauth/sign-up/kakao', { + nickname: string; +}; + +/** + * OAuth 로그인 + * POST /oauth/sign-in/{provider} + */ +export function oauthSignIn({ + provider, + redirectUri, + token, +}: OAuthSignInParams) { + return apiFetch(`/oauth/sign-in/${provider}`, { + method: 'POST', + body: { + redirectUri, + token, + }, + skipAuthRefresh: true, + }); +} + +/** + * OAuth 회원가입 + * POST /oauth/sign-up/{provider} + */ +export function oauthSignUp({ + provider, + redirectUri, + token, + nickname, +}: OAuthSignUpParams) { + return apiFetch(`/oauth/sign-up/${provider}`, { method: 'POST', - body: params, + body: { + redirectUri, + token, + nickname, + }, skipAuthRefresh: true, }); } diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index a4c32f0..cb8c586 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -9,6 +9,7 @@ import { login } from '@/api/auth'; import kakaoLogo from '@/assets/icons/auth/ic-kakao.svg'; import Button from '@/components/Button'; import { TextInput, PasswordInput } from '@/components/Input'; +import { KAKAO_REST_API_KEY, KAKAO_REDIRECT_URI } from '@/config/oauth'; import { useGuestOnly } from '@/hooks/useGuestOnly'; import { validateEmail, validatePassword } from '@/util/validations'; @@ -19,6 +20,17 @@ export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const handleKakaoLogin = () => { + const url = + 'https://kauth.kakao.com/oauth/authorize' + + `?client_id=${KAKAO_REST_API_KEY}` + + `&redirect_uri=${encodeURIComponent(KAKAO_REDIRECT_URI)}` + + '&response_type=code' + + '&state=login'; + + window.location.href = url; + }; + useEffect(() => { const savedEmail = sessionStorage.getItem('signupEmail'); @@ -126,7 +138,12 @@ export default function LoginPage() {
- diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index ae15e9b..878ef9d 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -42,7 +42,8 @@ export default function SignupPage() { 'https://kauth.kakao.com/oauth/authorize' + `?client_id=${KAKAO_REST_API_KEY}` + `&redirect_uri=${encodeURIComponent(KAKAO_REDIRECT_URI)}` + - '&response_type=code'; + '&response_type=code' + + '&state=signup'; window.location.href = url; }; diff --git a/src/app/oauth/kakao/page.tsx b/src/app/oauth/kakao/page.tsx index 0e35b3d..8c737ef 100644 --- a/src/app/oauth/kakao/page.tsx +++ b/src/app/oauth/kakao/page.tsx @@ -1,41 +1,80 @@ +// src/app/oauth/kakao/page.tsx 'use client'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useRef } from 'react'; -import { kakaoSignUp } from '@/api/oauth'; +import { oauthSignIn, oauthSignUp } from '@/api/oauth'; import { KAKAO_REDIRECT_URI } from '@/config/oauth'; -export default function KakaoSignupPage() { +export default function KakaoCallbackPage() { const router = useRouter(); + const searchParams = useSearchParams(); const hasRequested = useRef(false); useEffect(() => { + const code = searchParams.get('code'); + const state = searchParams.get('state'); + + if (!code || !state) { + router.replace('/login'); + return; + } + if (hasRequested.current) return; hasRequested.current = true; - const code = new URLSearchParams(window.location.search).get('code'); - if (!code) return; + window.history.replaceState({}, '', '/oauth/kakao'); - const signup = async () => { + const processOAuth = async () => { try { - const res = await kakaoSignUp({ - nickname: '카카오유저', - redirectUri: KAKAO_REDIRECT_URI, - token: code, - }); + let res; + + if (state === 'signup') { + res = await oauthSignUp({ + provider: 'kakao', + nickname: '카카오유저', + redirectUri: KAKAO_REDIRECT_URI, + token: code, + }); + } else { + res = await oauthSignIn({ + provider: 'kakao', + redirectUri: KAKAO_REDIRECT_URI, + token: code, + }); + } localStorage.setItem('accessToken', res.accessToken); localStorage.setItem('refreshToken', res.refreshToken); router.replace('/'); } catch (e) { - console.error('카카오 회원가입 실패', e); + console.error('카카오 인증 실패', e); + + // 에러 처리 + if (e instanceof Error) { + if (state === 'signup' && e.message.includes('이미 존재')) { + alert('이미 가입된 계정입니다. 로그인 페이지로 이동합니다.'); + router.replace('/login'); + } else if (state === 'login' && e.message.includes('존재하지 않')) { + alert('가입되지 않은 계정입니다. 회원가입 페이지로 이동합니다.'); + router.replace('/signup'); + } else { + alert('인증에 실패했습니다. 다시 시도해주세요.'); + router.replace(state === 'signup' ? '/signup' : '/login'); + } + } } }; - signup(); - }, [router]); + processOAuth(); + }, [router, searchParams]); - return

카카오 회원가입 처리 중...

; + return ( +
+

카카오 인증 처리 중...

+

잠시만 기다려주세요

+
+ ); } diff --git a/src/config/oauth.ts b/src/config/oauth.ts index e1c94cf..3657894 100644 --- a/src/config/oauth.ts +++ b/src/config/oauth.ts @@ -1,7 +1,6 @@ // src/config/oauth.ts export const KAKAO_REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; -export const KAKAO_REDIRECT_URI = - process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI || - 'http://localhost:3000/oauth/kakao'; +export const KAKAO_REDIRECT_URI = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!; + //redirectUri는 카카오 디벨로퍼 + 백엔드에 등록한 값과 동일해야 함 diff --git a/src/types/oauth.ts b/src/types/oauth.ts index e561636..9bdb9e9 100644 --- a/src/types/oauth.ts +++ b/src/types/oauth.ts @@ -8,7 +8,7 @@ export interface OAuthUser { updatedAt: string; } -export interface OAuthSignupResponse { +export interface OAuthAuthResponse { user: OAuthUser; accessToken: string; refreshToken: string; From 32a984978943fe21adab599bac691e617a08b0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=9D=80=EC=A7=80?= Date: Fri, 16 Jan 2026 22:34:44 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=96=BC?= =?UTF-8?q?=EB=9E=8F=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/signup/page.tsx | 11 +++++++---- .../(common)/(mypage)/mypage/hooks/useMyPageForm.ts | 10 ++++++---- src/app/oauth/kakao/page.tsx | 10 +++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 878ef9d..b965917 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -9,6 +9,7 @@ import { signup } from '@/api/users'; import kakaoLogo from '@/assets/icons/auth/ic-kakao.svg'; import Button from '@/components/Button'; import { TextInput, PasswordInput } from '@/components/Input'; +import { useToast } from '@/components/toast/useToast'; import { KAKAO_REST_API_KEY, KAKAO_REDIRECT_URI } from '@/config/oauth'; import { useGuestOnly } from '@/hooks/useGuestOnly'; import { @@ -36,6 +37,7 @@ export default function SignupPage() { }); const router = useRouter(); + const toast = useToast(); const handleKakaoSignup = () => { const url = @@ -83,29 +85,29 @@ export default function SignupPage() { password: form.password, nickname: form.nickname, }); - //TODO toast 팝업으로 교체 - alert('회원가입이 완료되었습니다!'); + toast.success('회원가입이 완료되었습니다!'); sessionStorage.setItem('signupEmail', form.email); - router.push('/login'); } catch (error) { - //TODO 에러처리 if (error instanceof Error) { if (error.message.includes('이메일')) { setErrors((prev) => ({ ...prev, email: error.message, })); + toast.error(error.message); } else if (error.message.includes('닉네임')) { setErrors((prev) => ({ ...prev, nickname: error.message, })); + toast.error(error.message); } else { setErrors((prev) => ({ ...prev, password: error.message, })); + toast.error(error.message); } } else { // 예상치 못한 에러 @@ -113,6 +115,7 @@ export default function SignupPage() { ...prev, password: '알 수 없는 오류가 발생했습니다.', })); + toast.error('알 수 없는 오류가 발생했습니다.'); } } }; diff --git a/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts b/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts index 07d624a..235999e 100644 --- a/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts +++ b/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts @@ -6,6 +6,7 @@ import { createUpdatePayload, isUnauthorizedError } from './useMypageFormUtils'; import { validateForm } from './useMyPageFormValidators'; import { useGetMyInfo, useUpdateMyInfo } from './useUser'; +import { useToast } from '@/components/toast/useToast'; import { getApiErrorMessage } from '@/util/error'; /** @@ -16,6 +17,7 @@ import { getApiErrorMessage } from '@/util/error'; */ export function useMyPageForm() { const router = useRouter(); + const toast = useToast(); // React Query: 사용자 정보 조회 const { @@ -59,7 +61,7 @@ export function useMyPageForm() { if (fetchError) { console.error('사용자 정보 로딩 실패:', fetchError); if (isUnauthorizedError(fetchError)) { - alert('로그인이 필요합니다.'); + toast.warning('로그인이 필요합니다.'); router.push('/signin'); } } @@ -102,7 +104,7 @@ export function useMyPageForm() { try { const payload = createUpdatePayload(formData); await updateProfile(payload); - alert('저장되었습니다.'); + toast.success('저장되었습니다!'); // 비밀번호 필드 초기화 setFormData((prev) => ({ @@ -113,12 +115,12 @@ export function useMyPageForm() { } catch (error: unknown) { console.error('저장 실패:', error); if (isUnauthorizedError(error)) { - alert('로그인이 필요합니다.'); + toast.warning('로그인이 필요합니다.'); router.push('/signin'); return; } const errorMessage = getApiErrorMessage(error, '저장에 실패했습니다.'); - alert(errorMessage); + toast.error(errorMessage); } }; diff --git a/src/app/oauth/kakao/page.tsx b/src/app/oauth/kakao/page.tsx index 8c737ef..f061a6a 100644 --- a/src/app/oauth/kakao/page.tsx +++ b/src/app/oauth/kakao/page.tsx @@ -5,12 +5,14 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useRef } from 'react'; import { oauthSignIn, oauthSignUp } from '@/api/oauth'; +import { useToast } from '@/components/toast/useToast'; import { KAKAO_REDIRECT_URI } from '@/config/oauth'; export default function KakaoCallbackPage() { const router = useRouter(); const searchParams = useSearchParams(); const hasRequested = useRef(false); + const toast = useToast(); useEffect(() => { const code = searchParams.get('code'); @@ -55,13 +57,15 @@ export default function KakaoCallbackPage() { // 에러 처리 if (e instanceof Error) { if (state === 'signup' && e.message.includes('이미 존재')) { - alert('이미 가입된 계정입니다. 로그인 페이지로 이동합니다.'); + toast.error('이미 가입된 계정입니다. 로그인 페이지로 이동합니다.'); router.replace('/login'); } else if (state === 'login' && e.message.includes('존재하지 않')) { - alert('가입되지 않은 계정입니다. 회원가입 페이지로 이동합니다.'); + toast.warning( + '가입되지 않은 계정입니다. 회원가입 페이지로 이동합니다.' + ); router.replace('/signup'); } else { - alert('인증에 실패했습니다. 다시 시도해주세요.'); + toast.error('인증에 실패했습니다. 다시 시도해주세요.'); router.replace(state === 'signup' ? '/signup' : '/login'); } } From b3a726a8c37e3366e29a94ddceeb44bac23eb482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=9D=80=EC=A7=80?= Date: Sat, 17 Jan 2026 01:08:16 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EA=B0=84=ED=8E=B8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?-=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A0=84=EC=9A=A9=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/login/page.tsx | 3 ++- src/app/(auth)/signup/page.tsx | 3 ++- .../(mypage)/mypage/hooks/useMyPageForm.ts | 4 ++-- src/app/layout.tsx | 2 ++ src/app/oauth/kakao/page.tsx | 20 ++++++++++--------- src/components/Header/HeaderAuth.tsx | 5 +++++ src/components/SideMenu/SideMenuNav.tsx | 5 +++++ src/types/oauth.ts | 19 ++++++++++++++++++ 8 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index cb8c586..6c6c7e9 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -26,7 +26,8 @@ export default function LoginPage() { `?client_id=${KAKAO_REST_API_KEY}` + `&redirect_uri=${encodeURIComponent(KAKAO_REDIRECT_URI)}` + '&response_type=code' + - '&state=login'; + '&state=login' + + '&prompt=login'; window.location.href = url; }; diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index b965917..3c827fc 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -45,7 +45,8 @@ export default function SignupPage() { `?client_id=${KAKAO_REST_API_KEY}` + `&redirect_uri=${encodeURIComponent(KAKAO_REDIRECT_URI)}` + '&response_type=code' + - '&state=signup'; + '&state=signup' + + '&prompt=login'; window.location.href = url; }; diff --git a/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts b/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts index 235999e..516d57d 100644 --- a/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts +++ b/src/app/(common)/(mypage)/mypage/hooks/useMyPageForm.ts @@ -62,7 +62,7 @@ export function useMyPageForm() { console.error('사용자 정보 로딩 실패:', fetchError); if (isUnauthorizedError(fetchError)) { toast.warning('로그인이 필요합니다.'); - router.push('/signin'); + router.push('/login'); } } }, [fetchError, router]); @@ -116,7 +116,7 @@ export function useMyPageForm() { console.error('저장 실패:', error); if (isUnauthorizedError(error)) { toast.warning('로그인이 필요합니다.'); - router.push('/signin'); + router.push('/login'); return; } const errorMessage = getApiErrorMessage(error, '저장에 실패했습니다.'); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ddb31a2..8d0e39f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -35,6 +35,8 @@ export default function RootLayout({ src={`https://dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_KEY}&autoload=false&libraries=services`} strategy="beforeInteractive" /> + {/* kakao logout SDK */} +