From 406ffc98b06b7159b66a699e06016c607ea594cb Mon Sep 17 00:00:00 2001 From: Tokyun02 Date: Tue, 4 Feb 2025 21:55:48 +0900 Subject: [PATCH 1/6] =?UTF-8?q?:recycle:=20refactor=20:=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8F=BC=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88,=20=ED=83=80=EC=9E=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/users/types.ts | 21 ++++++++++++++++ .../(auth)/signup/_components/SignupForm.tsx | 24 ++----------------- 2 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 src/apis/users/types.ts diff --git a/src/apis/users/types.ts b/src/apis/users/types.ts new file mode 100644 index 0000000..8315324 --- /dev/null +++ b/src/apis/users/types.ts @@ -0,0 +1,21 @@ +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; diff --git a/src/app/(auth)/signup/_components/SignupForm.tsx b/src/app/(auth)/signup/_components/SignupForm.tsx index d39526d..60b66ef 100644 --- a/src/app/(auth)/signup/_components/SignupForm.tsx +++ b/src/app/(auth)/signup/_components/SignupForm.tsx @@ -1,32 +1,12 @@ '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; +import { SIGNUP_FORM_PLACEHOLDER } from '@/constants/auth'; +import { signupSchema, SignupFormData } from '@/apis/users/types'; export default function SignupForm() { const { From e4986e094defbd80711bc5433db78e49fcf82ed4 Mon Sep 17 00:00:00 2001 From: Tokyun02 Date: Wed, 5 Feb 2025 15:34:38 +0900 Subject: [PATCH 2/6] =?UTF-8?q?:sparkles:=20feat=20:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=ED=8F=BC=20=EC=A0=9C=EC=B6=9C=20API=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 회원가입 POST 요청에 따른 타입 작성 2. API 함수 호출 파일 작성 3. 제출 로딩 상태에 따른 Submit Button 로딩 처리 --- src/apis/users/index.ts | 20 +++++++++++++++++++ src/apis/users/types.ts | 15 ++++++++++++++ .../(auth)/login/_components/LoginForm.tsx | 4 ++-- .../(auth)/signup/_components/SignupForm.tsx | 16 ++++++++++----- src/components/auth/SubmitButton.tsx | 20 ++++++++++++++++--- src/utils/network/axiosHelper.ts | 7 +++++++ src/utils/network/isResponseSuccess.ts | 4 ++++ 7 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 src/apis/users/index.ts create mode 100644 src/utils/network/axiosHelper.ts create mode 100644 src/utils/network/isResponseSuccess.ts diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts new file mode 100644 index 0000000..916dae8 --- /dev/null +++ b/src/apis/users/index.ts @@ -0,0 +1,20 @@ +import axiosHelper from '@/utils/network/axiosHelper'; +import { SignupFormData } from './types'; +import isResponseSuccess from '@/utils/network/isResponseSuccess'; +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); + if (!isResponseSuccess(response.status)) throw new Error(`${response.status} ${response.statusText}`); + return response.data; + } catch (error) { + if (isAxiosError(error)) return error.response?.data; + + return { + message: isError(error) ? error.message : String(error), + }; + } +}; diff --git a/src/apis/users/types.ts b/src/apis/users/types.ts index 8315324..8b52f9e 100644 --- a/src/apis/users/types.ts +++ b/src/apis/users/types.ts @@ -19,3 +19,18 @@ export const signupSchema = z }); export type SignupFormData = z.infer; + +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; diff --git a/src/app/(auth)/login/_components/LoginForm.tsx b/src/app/(auth)/login/_components/LoginForm.tsx index efa08cb..144f548 100644 --- a/src/app/(auth)/login/_components/LoginForm.tsx +++ b/src/app/(auth)/login/_components/LoginForm.tsx @@ -19,7 +19,7 @@ export default function LoginForm() { const { register, handleSubmit, - formState: { errors, isValid }, + formState: { errors, isValid, isSubmitting }, } = useForm({ resolver: zodResolver(loginSchema), mode: 'onBlur', @@ -37,7 +37,7 @@ export default function LoginForm() {
- + ); } diff --git a/src/app/(auth)/signup/_components/SignupForm.tsx b/src/app/(auth)/signup/_components/SignupForm.tsx index 60b66ef..2c9817d 100644 --- a/src/app/(auth)/signup/_components/SignupForm.tsx +++ b/src/app/(auth)/signup/_components/SignupForm.tsx @@ -7,13 +7,14 @@ import SubmitButton from '@/components/auth/SubmitButton'; import Checkbox from './Checkbox'; 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', @@ -25,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 ( @@ -50,7 +56,7 @@ export default function SignupForm() { })} errorMessage={errors.terms?.message} /> - + ); } diff --git a/src/components/auth/SubmitButton.tsx b/src/components/auth/SubmitButton.tsx index 5c38f54..a838db8 100644 --- a/src/components/auth/SubmitButton.tsx +++ b/src/components/auth/SubmitButton.tsx @@ -1,12 +1,26 @@ interface SubmitButtonProps { isValid: boolean; text: string; + isSubmitting: boolean; } -export default function SubmitButton({ isValid, text }: SubmitButtonProps) { +function BouncingLoader() { return ( - ); } diff --git a/src/utils/network/axiosHelper.ts b/src/utils/network/axiosHelper.ts new file mode 100644 index 0000000..bdf7d85 --- /dev/null +++ b/src/utils/network/axiosHelper.ts @@ -0,0 +1,7 @@ +import axios from 'axios'; + +const axiosHelper = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, +}); + +export default axiosHelper; diff --git a/src/utils/network/isResponseSuccess.ts b/src/utils/network/isResponseSuccess.ts new file mode 100644 index 0000000..42323aa --- /dev/null +++ b/src/utils/network/isResponseSuccess.ts @@ -0,0 +1,4 @@ +export default function isResponseSuccess(status: number) { + if (status >= 200 && status < 300) return true; + return false; +} From 2652a929ffeed568f702eb4ec533ffb76f38f3f2 Mon Sep 17 00:00:00 2001 From: Tokyun02 Date: Wed, 5 Feb 2025 15:47:24 +0900 Subject: [PATCH 3/6] =?UTF-8?q?:lipstick:=20feat=20:=20Auth=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20Header=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/_components/Header.tsx | 16 ++++++++++++++++ src/app/(auth)/layout.tsx | 5 +---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 src/app/(auth)/_components/Header.tsx diff --git a/src/app/(auth)/_components/Header.tsx b/src/app/(auth)/_components/Header.tsx new file mode 100644 index 0000000..66e095a --- /dev/null +++ b/src/app/(auth)/_components/Header.tsx @@ -0,0 +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'; + +export default function Header() { + return ( +
+ + 로고 CI 이미지 + 로고 BI 이미지 + +

오늘도 만나서 반가워요!

+
+ ); +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 70922cb..99dc609 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,7 +1,4 @@ -function Header() { - //TODO: Header 레이아웃 작업 - return
; -} +import Header from './_components/Header'; export default function Layout({ children, From 46c45eb22f9e43d07be2a3d95c4956a132db130c Mon Sep 17 00:00:00 2001 From: Tokyun02 Date: Wed, 5 Feb 2025 16:32:11 +0900 Subject: [PATCH 4/6] =?UTF-8?q?:lipstick:=20feat=20:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/_components/Header.tsx | 5 +++-- src/app/(auth)/layout.tsx | 9 +-------- src/app/(auth)/login/page.tsx | 20 +++++++++++++++----- src/app/(auth)/signup/page.tsx | 18 ++++++++++++++---- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/app/(auth)/_components/Header.tsx b/src/app/(auth)/_components/Header.tsx index 66e095a..88a0828 100644 --- a/src/app/(auth)/_components/Header.tsx +++ b/src/app/(auth)/_components/Header.tsx @@ -2,15 +2,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() { +export default function Header({ children }: { children: ReactNode }) { return (
로고 CI 이미지 로고 BI 이미지 -

오늘도 만나서 반가워요!

+

{children}

); } diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 99dc609..a981957 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,14 +1,7 @@ -import Header from './_components/Header'; - export default function Layout({ children, }: 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 b3db7d7..5dc301f 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,9 +1,19 @@ +import Link from 'next/link'; import LoginForm from './_components/LoginForm'; -//TODO: 로그인 페이지 작업 -export default function Login() { +import Header from '../_components/Header'; +export default function Signup() { return ( -
- -
+ <> +
오늘도 만나서 반가워요!
+
+ + +
+ ); } diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 16aa449..89c536f 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -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 ( -
- -
+ <> +
첫 방문을 환영합니다!
+
+ + +
+ ); } From 5d19a7eef8d9960bf8716d19aec8b8c2e8028aec Mon Sep 17 00:00:00 2001 From: Tokyun02 Date: Wed, 5 Feb 2025 16:35:02 +0900 Subject: [PATCH 5/6] =?UTF-8?q?:truck:=20chore=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/login/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 5dc301f..5088777 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import LoginForm from './_components/LoginForm'; import Header from '../_components/Header'; -export default function Signup() { +export default function Login() { return ( <>
오늘도 만나서 반가워요!
From 7e1f65592e63e1c222b8cf68f65f246186c497a0 Mon Sep 17 00:00:00 2001 From: Tokyun02 Date: Wed, 5 Feb 2025 18:51:29 +0900 Subject: [PATCH 6/6] =?UTF-8?q?:recycle:=20refactor=20:=20axios=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/users/index.ts | 2 -- src/utils/network/isResponseSuccess.ts | 4 ---- 2 files changed, 6 deletions(-) delete mode 100644 src/utils/network/isResponseSuccess.ts diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts index 916dae8..012f68f 100644 --- a/src/apis/users/index.ts +++ b/src/apis/users/index.ts @@ -1,6 +1,5 @@ import axiosHelper from '@/utils/network/axiosHelper'; import { SignupFormData } from './types'; -import isResponseSuccess from '@/utils/network/isResponseSuccess'; import { isAxiosError } from 'axios'; import { SignupResponse } from './types'; import { isError } from 'es-toolkit/compat'; @@ -8,7 +7,6 @@ import { isError } from 'es-toolkit/compat'; export const signup = async (signupFormData: SignupFormData): SignupResponse => { try { const response = await axiosHelper.post('/users', signupFormData); - if (!isResponseSuccess(response.status)) throw new Error(`${response.status} ${response.statusText}`); return response.data; } catch (error) { if (isAxiosError(error)) return error.response?.data; diff --git a/src/utils/network/isResponseSuccess.ts b/src/utils/network/isResponseSuccess.ts deleted file mode 100644 index 42323aa..0000000 --- a/src/utils/network/isResponseSuccess.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default function isResponseSuccess(status: number) { - if (status >= 200 && status < 300) return true; - return false; -}