diff --git a/src/apis/auth/index.ts b/src/apis/auth/index.ts index 2ea9e25..fed079b 100644 --- a/src/apis/auth/index.ts +++ b/src/apis/auth/index.ts @@ -1,11 +1,16 @@ import axiosClientHelper from '@/utils/network/axiosClientHelper'; -import { LoginFormData, LoginResponse, PutPasswordFormData, PutPasswordResponse } from './types'; +import { LoginFormData, LoginResponse, loginResponseSchema, PutPasswordFormData } from './types'; import { isAxiosError } from 'axios'; import { isError } from 'es-toolkit/compat'; export const login = async (loginFormData: LoginFormData): LoginResponse => { try { const response = await axiosClientHelper.post('/auth/login', loginFormData); + + const result = loginResponseSchema.safeParse(response.data); + if (!result.success) { + throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); + } return response.data; } catch (error) { if (isAxiosError(error)) return error.response?.data; @@ -13,6 +18,10 @@ export const login = async (loginFormData: LoginFormData): LoginResponse => { } }; -export const putPassword = async (putPasswordFormData: PutPasswordFormData): PutPasswordResponse => { - await axiosClientHelper.put('/auth/password', putPasswordFormData); +export const putPassword = async (putPasswordFormData: PutPasswordFormData) => { + await axiosClientHelper.put('/auth/password', putPasswordFormData); +}; + +export const logout = async () => { + await axiosClientHelper.post('/auth/logout'); }; diff --git a/src/apis/auth/types.ts b/src/apis/auth/types.ts index a661e6b..c976010 100644 --- a/src/apis/auth/types.ts +++ b/src/apis/auth/types.ts @@ -1,10 +1,5 @@ import { z } from 'zod'; import { LOGIN_FORM_VALID_LENGTH, LOGIN_FORM_ERROR_MESSAGE, PASSWORD_PUT_FORM_VALID_LENGTH, PASSWORD_PUT_FORM_ERROR_MESSAGE } from '@/constants/auth'; -import { User } from '@/apis/users/types'; - -interface FailResponse { - message: string; -} export const loginSchema = z.object({ email: z.string().min(LOGIN_FORM_VALID_LENGTH.EMAIL.MIN, LOGIN_FORM_ERROR_MESSAGE.EMAIL.MIN).email(LOGIN_FORM_ERROR_MESSAGE.EMAIL.NOT_FORM), @@ -13,12 +8,24 @@ export const loginSchema = z.object({ export type LoginFormData = z.infer; -export interface LoginSuccessResponse { - user: User; - accessToken: string; -} +export const loginResponseSchema = z.object({ + user: z.object({ + id: z.number(), + email: z.string(), + nickname: z.string(), + profileImageUrl: z.union([z.string(), z.instanceof(URL), z.null()]), + createdAt: z.union([z.string(), z.date()]), + updatedAt: z.union([z.string(), z.date()]), + }), +}); + +export type LoginSuccessResponse = z.infer; -export type LoginFailResponse = FailResponse; +export const loginFailSchema = z.object({ + message: z.string(), +}); + +export type LoginFailResponse = z.infer; export type LoginResponse = Promise; @@ -34,5 +41,3 @@ export const passwordSchema = z }); export type PutPasswordFormData = z.infer; - -export type PutPasswordResponse = Promise; diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts index 2bdf764..a644404 100644 --- a/src/apis/users/index.ts +++ b/src/apis/users/index.ts @@ -1,11 +1,13 @@ import axiosClientHelper from '@/utils/network/axiosClientHelper'; -import { CreateProfileImageForm, CreateProfileImageSuccessResponse, GetUserResponse, SignupFormData, SignupResponse, UpdateUserForm, User } from './types'; +import { CreateProfileImageForm, ProfileImageUrlResponse, profileImageUrlResponseSchema, SignupFormData, SignupResponse, UpdateUserForm, User, userSchema } from './types'; import { isAxiosError } from 'axios'; import { isError } from 'es-toolkit/compat'; export const signup = async (signupFormData: SignupFormData): SignupResponse => { try { const response = await axiosClientHelper.post('/users', signupFormData); + const result = userSchema.safeParse(response.data); + if (!result.success) throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); return response.data; } catch (error) { if (isAxiosError(error)) return error.response?.data; @@ -15,21 +17,28 @@ export const signup = async (signupFormData: SignupFormData): SignupResponse => } }; -export const getUser = async (): GetUserResponse => { - const response = await axiosClientHelper.get('/users/me'); +export const getUser = async () => { + const response = await axiosClientHelper.get('/users/me'); + const result = userSchema.safeParse(response.data); + if (!result.success) throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); return response.data; }; export const updateUser = async (updateUserForm: UpdateUserForm) => { const response = await axiosClientHelper.put('/users/me', updateUserForm); + const result = userSchema.safeParse(response.data); + if (!result.success) throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); return response.data; }; export const createProfileImage = async (createProfileImageForm: CreateProfileImageForm) => { - const response = await axiosClientHelper.post('/users/me/image', createProfileImageForm, { + const response = await axiosClientHelper.post('/users/me/image', createProfileImageForm, { headers: { 'Content-Type': 'multipart/form-data', }, }); + + const result = profileImageUrlResponseSchema.safeParse(response.data); + if (!result.success) throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); return response.data; }; diff --git a/src/apis/users/types.ts b/src/apis/users/types.ts index 6548065..57c61ab 100644 --- a/src/apis/users/types.ts +++ b/src/apis/users/types.ts @@ -24,23 +24,23 @@ export const signupSchema = z export type SignupFormData = z.infer; -export interface User { - id: number; - email: string; - nickname: string; - profileImageUrl: string | null | URL; - createdAt: string | Date; - updatedAt: string | Date; -} +export const userSchema = z.object({ + id: z.number(), + email: z.string().email(), + nickname: z.string(), + profileImageUrl: z.union([z.string().url(), z.null()]), + createdAt: z.union([z.string(), z.date()]), + updatedAt: z.union([z.string(), z.date()]), +}); + +export type User = z.infer; -export type SignupSuccessResponse = User; +export type SignupSuccessResponse = z.infer; export type SignupFailResponse = FailResponse; export type SignupResponse = Promise; -export type GetUserResponse = Promise<{ user: User }>; - type ProfileImageUrl = string | URL | null; export const updateUserFormSchema = z.object({ @@ -52,7 +52,7 @@ export type UpdateUserForm = Omit, 'profile profileImageUrl: ProfileImageUrl; }; -const profileImageUrlSchema = z.instanceof(File).refine((file) => ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml'].includes(file.type), { +const profileImageUrlSchema = z.instanceof(File).refine((file) => ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml', 'image/ico'].includes(file.type), { message: '지원되지 않는 이미지 파일입니다.', }); @@ -64,6 +64,8 @@ export interface CreateProfileImageForm { image: File; } -export type CreateProfileImageSuccessResponse = { - profileImageUrl: Exclude; -}; +export const profileImageUrlResponseSchema = z.object({ + profileImageUrl: z.union([z.string(), z.instanceof(URL)]), +}); + +export type ProfileImageUrlResponse = z.infer; diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index a981957..f80da4b 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -3,5 +3,5 @@ export default function Layout({ }: Readonly<{ children: React.ReactNode; }>) { - return
{children}
; + return
{children}
; } diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index f7b9516..02c5ecf 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,5 +1,3 @@ -'use client'; - import Link from 'next/link'; import LoginForm from '@/components/auth/LoginForm'; import Header from '@/components/auth/Header'; diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 9008973..43cfd21 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,5 +1,3 @@ -'use client'; - import Link from 'next/link'; import SignupForm from '@/components/auth/SignupForm'; import Header from '@/components/auth/Header'; diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..272d631 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; + +export const POST = async () => { + const response = new NextResponse(null, { status: 204 }); + + response.cookies.delete('accessToken'); + return response; +}; diff --git a/src/components/auth/Checkbox.tsx b/src/components/auth/Checkbox.tsx index bfd060a..cce3a30 100644 --- a/src/components/auth/Checkbox.tsx +++ b/src/components/auth/Checkbox.tsx @@ -11,7 +11,7 @@ export default function Checkbox({ errorMessage, register }: { errorMessage?: st /> 이용약관에 동의합니다. - {errorMessage && {errorMessage}} + {{errorMessage}} ); } diff --git a/src/components/auth/Field.tsx b/src/components/auth/Field.tsx index a9f7669..432fb0c 100644 --- a/src/components/auth/Field.tsx +++ b/src/components/auth/Field.tsx @@ -42,14 +42,14 @@ const Field = ({ label, type, placeholder = '', register, errorMessage }: FieldP {type === 'password' && ( 비밀번호 숨김 아이콘 )} - {!isEmpty(errorMessage) && {errorMessage}} + {{errorMessage}} ); }; diff --git a/src/components/auth/Header.tsx b/src/components/auth/Header.tsx index 88a0828..55e1600 100644 --- a/src/components/auth/Header.tsx +++ b/src/components/auth/Header.tsx @@ -1,17 +1,16 @@ import Image from 'next/image'; import Link from 'next/link'; import LogoCi from '@/assets/images/logo_ci.png'; -import LogoBi from '@/assets/images/logo_bi.png'; import { ReactNode } from 'react'; export default function Header({ children }: { children: ReactNode }) { return (
- - 로고 CI 이미지 - 로고 BI 이미지 + + 로고 CI 이미지 + Taskify -

{children}

+

{children}

); } diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index ed156c6..8aa033b 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -8,7 +8,7 @@ import { LOGIN_FORM_PLACEHOLDER } from '@/constants/auth'; import { loginSchema, LoginFormData } from '@/apis/auth/types'; import { login } from '@/apis/auth'; import { useRouter } from 'next/navigation'; -import useAuthStore from '@/hooks/useAuthStore'; +import useAlert from '@/hooks/useAlert'; export default function LoginForm() { const { @@ -25,22 +25,19 @@ export default function LoginForm() { }); const router = useRouter(); - const { setAccessToken } = useAuthStore(); + const alert = useAlert(); const onSubmit = async (loginFormData: LoginFormData) => { const response = await login(loginFormData); - // TODO: 디버깅 용으로 alert로 구현했습니다. 모달 기능 구현 후 로직 수정 예정입니다. if ('message' in response) { alert(response.message); } else { - alert('로그인이 완료되었습니다!'); - setAccessToken(response.accessToken); - router.replace('/mydashboard'); + alert('로그인이 완료되었습니다!', () => router.replace('/mydashboard')); } }; return ( -
+ diff --git a/src/components/auth/SignupForm.tsx b/src/components/auth/SignupForm.tsx index 2c9817d..2940674 100644 --- a/src/components/auth/SignupForm.tsx +++ b/src/components/auth/SignupForm.tsx @@ -8,6 +8,8 @@ import Checkbox from './Checkbox'; import { SIGNUP_FORM_PLACEHOLDER } from '@/constants/auth'; import { signupSchema, SignupFormData } from '@/apis/users/types'; import { signup } from '@/apis/users'; +import useAlert from '@/hooks/useAlert'; +import { useRouter } from 'next/navigation'; export default function SignupForm() { const { @@ -26,18 +28,21 @@ export default function SignupForm() { terms: false, }, }); + + const alert = useAlert(); + const router = useRouter(); + const onSubmit = async (signupFormData: SignupFormData) => { const response = await signup(signupFormData); - // TODO: 디버깅 용으로 alert로 구현했습니다. 모달 기능 구현 후 로직 수정 예정입니다. if ('message' in response) { alert(response.message); } else { - alert('가입이 완료되었습니다!'); + alert('가입이 완료되었습니다!', () => router.replace('/login')); } }; return ( - + { - setIsHydrated(true); - }, []); - - return isHydrated ? store : { accessToken: null, setAccessToken: () => {}, clearAccessToken: () => {} }; -} diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts deleted file mode 100644 index 458ffbe..0000000 --- a/src/stores/authStore.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; - -interface AuthState { - accessToken: string | null; - setAccessToken: (accessToken: string) => void; - clearAccessToken: () => void; -} - -const authStore = create()( - persist( - (set) => ({ - accessToken: null, - setAccessToken: (accessToken) => set({ accessToken }), - clearAccessToken: () => set({ accessToken: null }), - }), - { - name: 'auth-storage', - storage: createJSONStorage(() => localStorage), - }, - ), -); - -export default authStore; diff --git a/src/utils/network/axiosHelper.ts b/src/utils/network/axiosHelper.ts deleted file mode 100644 index aabe064..0000000 --- a/src/utils/network/axiosHelper.ts +++ /dev/null @@ -1,25 +0,0 @@ -import axios from 'axios'; -import authStore from '@/stores/authStore'; - -const axiosHelper = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, -}); - -const getAccessToken = () => { - if (!window) return null; - const accessToken = authStore.getState().accessToken; - return accessToken; -}; - -axiosHelper.interceptors.request.use( - (config) => { - const token = getAccessToken(); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error), -); - -export default axiosHelper;