Skip to content
Merged
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
15 changes: 12 additions & 3 deletions src/apis/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
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;
return { message: isError(error) ? error.message : String(error) };
}
};

export const putPassword = async (putPasswordFormData: PutPasswordFormData): PutPasswordResponse => {
await axiosClientHelper.put('/auth/password', putPasswordFormData);
export const putPassword = async (putPasswordFormData: PutPasswordFormData) => {
await axiosClientHelper.put<void>('/auth/password', putPasswordFormData);
};

export const logout = async () => {
await axiosClientHelper.post<void>('/auth/logout');
};
29 changes: 17 additions & 12 deletions src/apis/auth/types.ts
Original file line number Diff line number Diff line change
@@ -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),
Expand All @@ -13,12 +8,24 @@ export const loginSchema = z.object({

export type LoginFormData = z.infer<typeof loginSchema>;

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<typeof loginResponseSchema>;

export type LoginFailResponse = FailResponse;
export const loginFailSchema = z.object({
message: z.string(),
});

export type LoginFailResponse = z.infer<typeof loginFailSchema>;

export type LoginResponse = Promise<LoginSuccessResponse | LoginFailResponse>;

Expand All @@ -34,5 +41,3 @@ export const passwordSchema = z
});

export type PutPasswordFormData = z.infer<typeof passwordSchema>;

export type PutPasswordResponse = Promise<void>;
17 changes: 13 additions & 4 deletions src/apis/users/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<User>('/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<User>('/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<CreateProfileImageSuccessResponse>('/users/me/image', createProfileImageForm, {
const response = await axiosClientHelper.post<ProfileImageUrlResponse>('/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;
};
32 changes: 17 additions & 15 deletions src/apis/users/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,23 @@ export const signupSchema = z

export type SignupFormData = z.infer<typeof signupSchema>;

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<typeof userSchema>;

export type SignupSuccessResponse = User;
export type SignupSuccessResponse = z.infer<typeof userSchema>;

export type SignupFailResponse = FailResponse;

export type SignupResponse = Promise<SignupSuccessResponse | SignupFailResponse>;

export type GetUserResponse = Promise<{ user: User }>;

type ProfileImageUrl = string | URL | null;

export const updateUserFormSchema = z.object({
Expand All @@ -52,7 +52,7 @@ export type UpdateUserForm = Omit<z.infer<typeof updateUserFormSchema>, '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: '지원되지 않는 이미지 파일입니다.',
});

Expand All @@ -64,6 +64,8 @@ export interface CreateProfileImageForm {
image: File;
}

export type CreateProfileImageSuccessResponse = {
profileImageUrl: Exclude<ProfileImageUrl, null>;
};
export const profileImageUrlResponseSchema = z.object({
profileImageUrl: z.union([z.string(), z.instanceof(URL)]),
});

export type ProfileImageUrlResponse = z.infer<typeof profileImageUrlResponseSchema>;
2 changes: 1 addition & 1 deletion src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export default function Layout({
}: Readonly<{
children: React.ReactNode;
}>) {
return <div className='mx-3 flex min-h-dvh flex-col items-center justify-center gap-9 bg-gray-10'>{children}</div>;
return <div className='flex min-h-dvh flex-col items-center justify-center gap-3 bg-gray-10 px-3 py-6'>{children}</div>;
}
2 changes: 0 additions & 2 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import Link from 'next/link';
import LoginForm from '@/components/auth/LoginForm';
import Header from '@/components/auth/Header';
Expand Down
2 changes: 0 additions & 2 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import Link from 'next/link';
import SignupForm from '@/components/auth/SignupForm';
import Header from '@/components/auth/Header';
Expand Down
8 changes: 8 additions & 0 deletions src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 1 addition & 1 deletion src/components/auth/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Checkbox({ errorMessage, register }: { errorMessage?: st
/>
<span className='text-gray-70'>이용약관에 동의합니다.</span>
</label>
{errorMessage && <span className='text-md text-red'>{errorMessage}</span>}
{<span className='h-6 text-md text-red'>{errorMessage}</span>}
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/auth/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ const Field = ({ label, type, placeholder = '', register, errorMessage }: FieldP
<input
type={inputType}
placeholder={placeholder}
className={`rounded-lg border p-4 text-gray-70 ${!isEmpty(errorMessage) ? 'border-2 border-red outline-red' : 'outline-violet-20'}`}
className={`rounded-lg border p-4 text-gray-70 ${!isEmpty(errorMessage) ? 'border-1 border-red outline-red' : 'outline-violet-20'}`}
{...register}
/>
{type === 'password' && (
<Image src={passwordState.icon} alt='비밀번호 숨김 아이콘' width={24} height={24} onClick={togglePasswordVisibility} className='absolute right-4 top-12 cursor-pointer' />
)}
</label>
{!isEmpty(errorMessage) && <span className='text-md text-red'>{errorMessage}</span>}
{<span className='h-6 text-md text-red'>{errorMessage}</span>}
</div>
);
};
Expand Down
9 changes: 4 additions & 5 deletions src/components/auth/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className='flex flex-col items-center gap-[10px]'>
<Link href={'/'} className='flex flex-col gap-[30px]'>
<Image src={LogoCi} alt='로고 CI 이미지' width={200} height={200} />
<Image src={LogoBi} alt='로고 BI 이미지' width={200} height={56} />
<Link href={'/'} className='flex flex-col items-center gap-4'>
<Image src={LogoCi} alt='로고 CI 이미지' width={200} height={200} className='h-24 w-24 md:h-48 md:w-48' />
<span className='font-mont text-4xl font-bold text-violet-20 md:text-6xl'>Taskify</span>
</Link>
<p className='text-xl font-medium text-gray-70'>{children}</p>
<p className='font-medium text-gray-70 md:text-xl'>{children}</p>
</header>
);
}
11 changes: 4 additions & 7 deletions src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 (
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-6'>
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-1'>
<Field label='이메일' type='email' placeholder={LOGIN_FORM_PLACEHOLDER.EMAIL} register={register('email')} errorMessage={errors.email?.message} />
<Field label='비밀번호' type='password' placeholder={LOGIN_FORM_PLACEHOLDER.PASSWORD} register={register('password')} errorMessage={errors.password?.message} />
<SubmitButton text='로그인' isValid={isValid} isSubmitting={isSubmitting} />
Expand Down
11 changes: 8 additions & 3 deletions src/components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 (
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-6'>
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-1'>
<Field label='이메일' type='email' placeholder={SIGNUP_FORM_PLACEHOLDER.EMAIL} register={register('email')} errorMessage={errors.email?.message} />
<Field label='닉네임' type='text' placeholder={SIGNUP_FORM_PLACEHOLDER.NICKNAME} register={register('nickname')} errorMessage={errors.nickname?.message} />
<Field
Expand Down
13 changes: 0 additions & 13 deletions src/hooks/useAuthStore.tsx

This file was deleted.

24 changes: 0 additions & 24 deletions src/stores/authStore.ts

This file was deleted.

25 changes: 0 additions & 25 deletions src/utils/network/axiosHelper.ts

This file was deleted.