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
18 changes: 18 additions & 0 deletions src/apis/users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axiosHelper from '@/utils/network/axiosHelper';
import { SignupFormData } from './types';
import { isAxiosError } from 'axios';
import { SignupResponse } from './types';
import { isError } from 'es-toolkit/compat';

export const signup = async (signupFormData: SignupFormData): SignupResponse => {
try {
const response = await axiosHelper.post('/users', signupFormData);
return response.data;
} catch (error) {
if (isAxiosError(error)) return error.response?.data;

return {
message: isError(error) ? error.message : String(error),
};
}
};
36 changes: 36 additions & 0 deletions src/apis/users/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from 'zod';
import { SINGUP_FORM_VALID_LENGTH, SIGNUP_FORM_ERROR_MESSAGE } from '@/constants/auth';

export const signupSchema = z
.object({
email: z.string().min(SINGUP_FORM_VALID_LENGTH.EMAIL.MIN, SIGNUP_FORM_ERROR_MESSAGE.EMAIL.MIN).email(SIGNUP_FORM_ERROR_MESSAGE.EMAIL.NOT_FORM),
nickname: z.string().min(SINGUP_FORM_VALID_LENGTH.NICKNAME.MIN, SIGNUP_FORM_ERROR_MESSAGE.NICKNAME.MIN).max(SINGUP_FORM_VALID_LENGTH.NICKNAME.MAX, SIGNUP_FORM_ERROR_MESSAGE.NICKNAME.MAX),
password: z.string().min(SINGUP_FORM_VALID_LENGTH.PASSWORD.MIN, SIGNUP_FORM_ERROR_MESSAGE.PASSWORD.MIN),
passwordConfirm: z.string(),
terms: z.boolean(),
})
.refine((check) => check.password === check.passwordConfirm, {
message: SIGNUP_FORM_ERROR_MESSAGE.PASSWORD_CONFIRM.NOT_MATCH,
path: ['passwordConfirm'],
})
.refine((check) => check.terms, {
message: SIGNUP_FORM_ERROR_MESSAGE.TERMS.NOT_TOS,
path: ['terms'],
});

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

export interface SignupSuccessResponse {
id: number;
email: string;
nickname: string;
profileImageUrl: string | null;
createdAt: string | Date;
updatedAt: string | Date;
}

export interface SignupFailResponse {
message: string;
}

export type SignupResponse = Promise<SignupSuccessResponse | SignupFailResponse>;
17 changes: 17 additions & 0 deletions src/app/(auth)/_components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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>
<p className='text-xl font-medium text-gray-70'>{children}</p>
</header>
);
}
12 changes: 1 addition & 11 deletions src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
function Header() {
//TODO: Header 레이아웃 작업
return <header></header>;
}

export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<Header />
{children}
</>
);
return <div className='mx-3 flex min-h-dvh flex-col items-center justify-center gap-9 bg-gray-10'>{children}</div>;
}
4 changes: 2 additions & 2 deletions src/app/(auth)/login/_components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm({
resolver: zodResolver(loginSchema),
mode: 'onBlur',
Expand All @@ -37,7 +37,7 @@ export default function LoginForm() {
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-6'>
<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} />
<SubmitButton text='로그인' isValid={isValid} isSubmitting={isSubmitting} />
</form>
);
}
18 changes: 14 additions & 4 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import Link from 'next/link';
import LoginForm from './_components/LoginForm';
//TODO: 로그인 페이지 작업
import Header from '../_components/Header';
export default function Login() {
return (
<main>
<LoginForm />
</main>
<>
<Header>오늘도 만나서 반가워요!</Header>
<main className='w-full max-w-[520px]'>
<LoginForm />
<nav className='mt-9 flex items-center justify-center gap-2'>
<span>회원이 아니신가요?</span>
<Link href={'/signup'} className='text-violet-20 underline'>
회원가입하기
</Link>
</nav>
</main>
</>
);
}
40 changes: 13 additions & 27 deletions src/app/(auth)/signup/_components/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,20 @@
'use client';

import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import Field from '@/components/auth/Field';
import SubmitButton from '@/components/auth/SubmitButton';
import Checkbox from './Checkbox';
import { SIGNUP_FORM_PLACEHOLDER, SINGUP_FORM_VALID_LENGTH, SIGNUP_FORM_ERROR_MESSAGE } from '@/constants/auth';

//TODO: API 함수 구현 후 스키마와 타입 정의 옮길 예정
const signupSchema = z
.object({
email: z.string().min(SINGUP_FORM_VALID_LENGTH.EMAIL.MIN, SIGNUP_FORM_ERROR_MESSAGE.EMAIL.MIN).email(SIGNUP_FORM_ERROR_MESSAGE.EMAIL.NOT_FORM),
nickname: z.string().min(SINGUP_FORM_VALID_LENGTH.NICKNAME.MIN, SIGNUP_FORM_ERROR_MESSAGE.NICKNAME.MIN).max(SINGUP_FORM_VALID_LENGTH.NICKNAME.MAX, SIGNUP_FORM_ERROR_MESSAGE.NICKNAME.MAX),
password: z.string().min(SINGUP_FORM_VALID_LENGTH.PASSWORD.MIN, SIGNUP_FORM_ERROR_MESSAGE.PASSWORD.MIN),
passwordConfirm: z.string(),
terms: z.boolean(),
})
.refine((check) => check.password === check.passwordConfirm, {
message: SIGNUP_FORM_ERROR_MESSAGE.PASSWORD_CONFIRM.NOT_MATCH,
path: ['passwordConfirm'],
})
.refine((check) => check.terms, {
message: SIGNUP_FORM_ERROR_MESSAGE.TERMS.NOT_TOS,
path: ['terms'],
});

type SignupFormData = z.infer<typeof signupSchema>;
import { SIGNUP_FORM_PLACEHOLDER } from '@/constants/auth';
import { signupSchema, SignupFormData } from '@/apis/users/types';
import { signup } from '@/apis/users';

export default function SignupForm() {
const {
register,
trigger,
handleSubmit,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm({
resolver: zodResolver(signupSchema),
mode: 'onBlur',
Expand All @@ -45,9 +26,14 @@ export default function SignupForm() {
terms: false,
},
});
const onSubmit = (signupFormData: SignupFormData) => {
// TODO : 디버깅 용으로 남겼습니다. API 함수 구현이 완료되면 로직 수정 예정입니다.
console.log(signupFormData);
const onSubmit = async (signupFormData: SignupFormData) => {
const response = await signup(signupFormData);
// TODO: 디버깅 용으로 alert로 구현했습니다. 모달 기능 구현 후 로직 수정 예정입니다.
if ('message' in response) {
alert(response.message);
} else {
alert('가입이 완료되었습니다!');
}
};

return (
Expand All @@ -70,7 +56,7 @@ export default function SignupForm() {
})}
errorMessage={errors.terms?.message}
/>
<SubmitButton text='가입하기' isValid={isValid} />
<SubmitButton text='가입하기' isValid={isValid} isSubmitting={isSubmitting} />
</form>
);
}
18 changes: 14 additions & 4 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import Link from 'next/link';
import SignupForm from './_components/SignupForm';
//TODO: 회원가입 페이지 작업
import Header from '../_components/Header';
export default function Signup() {
return (
<main>
<SignupForm />
</main>
<>
<Header>첫 방문을 환영합니다!</Header>
<main className='w-full max-w-[520px]'>
<SignupForm />
<nav className='mt-9 flex items-center justify-center gap-2'>
<span>이미 회원이신가요?</span>
<Link href={'/login'} className='text-violet-20 underline'>
로그인하기
</Link>
</nav>
</main>
</>
);
}
20 changes: 17 additions & 3 deletions src/components/auth/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
interface SubmitButtonProps {
isValid: boolean;
text: string;
isSubmitting: boolean;
}

export default function SubmitButton({ isValid, text }: SubmitButtonProps) {
function BouncingLoader() {
return (
<button disabled={!isValid} className={`rounded-lg ${isValid ? 'cursor-pointer bg-violet-20' : 'cursor-not-allowed bg-gray-40'} py-3 text-2lg font-medium text-white`}>
{text}
<div className='flex h-[26px] items-center justify-center space-x-2'>
<div className='h-2 w-2 animate-bounce rounded-full bg-violet-20'></div>
<div className='h-2 w-2 animate-bounce rounded-full bg-violet-20 [animation-delay:0.2s]'></div>
<div className='h-2 w-2 animate-bounce rounded-full bg-violet-20 [animation-delay:0.4s]'></div>
</div>
);
}

export default function SubmitButton({ isValid, text, isSubmitting }: SubmitButtonProps) {
return (
<button
disabled={!isValid || isSubmitting}
className={`rounded-lg ${isValid && !isSubmitting ? 'cursor-pointer bg-violet-20' : 'cursor-not-allowed bg-gray-40'} py-3 text-2lg font-medium text-white`}
>
{!isSubmitting ? text : <BouncingLoader />}
</button>
);
}
7 changes: 7 additions & 0 deletions src/utils/network/axiosHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axios from 'axios';

const axiosHelper = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});

export default axiosHelper;