Repository: themoment-team/imi-frontend
Pull request #36: [feat]회원가입
Author: login: LeeSangHyeok0731
Created: 2025-04-28T09:02:16Z | Merged: 2025-04-29T09:00:11Z

## 개요 💡

회원가입 개발

## 작업내용 ⌨️

회원가입 관련 개발을 진행하였습니다

로그인 기능 개발이 전부 완성되면 회원가입 성공시 바로 로그인되게 하는 기능역시 추가하겠습니다



Diff:
diff --git a/src/app/provider.tsx b/src/app/provider.tsx
index 846065b..cc2a77a 100644
--- a/src/app/provider.tsx
+++ b/src/app/provider.tsx
@@ -1,6 +1,6 @@
 'use client';
 
-import { LoginProvider } from '@/context';
+import { LoginProvider } from '@/contexts';
 
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
diff --git a/src/components/MainPage/Section1/index.tsx b/src/components/MainPage/Section1/index.tsx
index 2fc743e..bb84ed0 100644
--- a/src/components/MainPage/Section1/index.tsx
+++ b/src/components/MainPage/Section1/index.tsx
@@ -3,10 +3,12 @@
 import Link from 'next/link';
 
 import { DownArrow } from '@/asset';
+import { useAuth } from '@/hooks';
 
 import * as S from './section1.css';
 
 const Section1 = ({ scrollToSection2 }: { scrollToSection2: () => void }) => {
+  const { isLogged } = useAuth();
   return (
     <section className={S.SectionContainer}>
       <div className={S.ContentContainer}>
@@ -16,12 +18,15 @@ const Section1 = ({ scrollToSection2 }: { scrollToSection2: () => void }) => {
             당신의 열정과 재능을 보여줄 기회입니다.
           </p>
         </div>
-        <Link href="/signin">
-          <button className={S.ActionButton}>
-            로그인
-            {/* 로그인 여부에 따라 바로가기 or 로그인 버튼 표시 필요 */}
-          </button>
-        </Link>
+        {isLogged ? (
+          <Link href="/write">
+            <button className={S.ActionButton}>바로가기</button>
+          </Link>
+        ) : (
+          <Link href="/signin">
+            <button className={S.ActionButton}>로그인</button>
+          </Link>
+        )}
       </div>
       <div className={S.AnimatedArrow} onClick={scrollToSection2}>
         <DownArrow />
diff --git a/src/context/contextLogin.tsx b/src/contexts/contextLogin.tsx
similarity index 100%
rename from src/context/contextLogin.tsx
rename to src/contexts/contextLogin.tsx
diff --git a/src/context/index.ts b/src/contexts/index.ts
similarity index 100%
rename from src/context/index.ts
rename to src/contexts/index.ts
diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx
index 3087473..e0aa18a 100644
--- a/src/hooks/useAuth.tsx
+++ b/src/hooks/useAuth.tsx
@@ -1,4 +1,4 @@
-import { AuthContext } from '@/context';
+import { AuthContext } from '@/contexts';
 
 import { useContext } from 'react';
 
diff --git a/src/pageContainer/SignInPage/index.tsx b/src/pageContainer/SignInPage/index.tsx
index 259a3c4..0e83ef1 100644
--- a/src/pageContainer/SignInPage/index.tsx
+++ b/src/pageContainer/SignInPage/index.tsx
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
 import { CloseEyes, ImiLogo, OpenEyes } from '@/asset';
 import { useAuth } from '@/hooks';
 import { axiosInstance } from '@/libs';
+import { LoginResponse } from '@/types';
 
 import { useState } from 'react';
 import { FieldErrors, useForm } from 'react-hook-form';
@@ -19,13 +20,6 @@ type FormValues = {
 
 type FormName = 'email' | 'password';
 
-type LoginResponse = {
-  accessToken: string;
-  expiresIn: number;
-  issuedAt: number;
-  refreshToken: string;
-};
-
 const SignInPage = () => {
   const router = useRouter();
   const [isOpen, setIsOpen] = useState(false);
diff --git a/src/pageContainer/SignUpPage/index.tsx b/src/pageContainer/SignUpPage/index.tsx
index a0e609d..bd5f682 100644
--- a/src/pageContainer/SignUpPage/index.tsx
+++ b/src/pageContainer/SignUpPage/index.tsx
@@ -7,7 +7,10 @@ import SignUpTwoPage from './stepTwo';
 
 export default function SignUpPage() {
   const [step, setStep] = useState<number>(1);
-  const [formData, setFormData] = useState({ email: '', password: '' });
+  const [formData, setFormData] = useState({
+    email: '',
+    password: '',
+  });
 
   return step === 1 ? (
     <SignUpOnePage
diff --git a/src/pageContainer/SignUpPage/stepOne/index.tsx b/src/pageContainer/SignUpPage/stepOne/index.tsx
index 2d17aae..bf4fee2 100644
--- a/src/pageContainer/SignUpPage/stepOne/index.tsx
+++ b/src/pageContainer/SignUpPage/stepOne/index.tsx
@@ -3,16 +3,18 @@
 import { useRouter } from 'next/navigation';
 
 import { CloseEyes, ImiLogo, OpenEyes } from '@/asset';
+import { axiosInstance } from '@/libs';
 
-import { useRef, useState } from 'react';
+import { useState } from 'react';
 import { FieldErrors, useForm } from 'react-hook-form';
+import { toast } from 'react-toastify';
 
 import * as S from './signUpOne.css';
 
 type FormValues = {
   email: string;
   password: string;
-  repassword: string;
+  repassword?: string;
 };
 
 type FormName = 'email' | 'password' | 'repassword';
@@ -53,20 +55,34 @@ const SignUpOnePage = ({
     },
   });
 
-  const onSubmit = (data: FormValues) => {
+  const onSubmit = async (data: FormValues) => {
     if (data.email.length === 6) {
       data.email = data.email + '@gsm.hs.kr';
     }
-    setFormData(data);
 
-    //api
-    if (false) {
-      setError('email', {
-        type: 'server',
-        message: '이미 존재하는 이메일입니다',
+    delete data.repassword;
+
+    try {
+      const response = await axiosInstance.post('/user/check-email', {
+        email: data.email,
       });
-    } else {
-      onNext();
+
+      if (response) {
+        setError('email', {
+          type: 'server',
+          message: '이미 존재하는 이메일입니다',
+        });
+        toast.error('이미 존재하는 이메일입니다');
+      } else {
+        setFormData(data);
+        onNext();
+      }
+    } catch (error: any) {
+      if (error.response?.state === 404 && error.response?.state === 503) {
+        toast.error('에러가 발생했습니다');
+      } else {
+        toast.error('서버통신중 에러가 발생했습니다');
+      }
     }
   };
 
@@ -181,6 +197,9 @@ const SignUpOnePage = ({
               className={S.InputBox}
               {...register('repassword', {
                 validate: (value) => {
+                  if (!value) {
+                    return '';
+                  }
                   if (value.length === 0) {
                     return undefined;
                   }
diff --git a/src/pageContainer/SignUpPage/stepTwo/index.tsx b/src/pageContainer/SignUpPage/stepTwo/index.tsx
index 691c257..e5cdce0 100644
--- a/src/pageContainer/SignUpPage/stepTwo/index.tsx
+++ b/src/pageContainer/SignUpPage/stepTwo/index.tsx
@@ -1,19 +1,25 @@
 'use client';
 
+import { AxiosError } from 'axios';
+import Error from 'next/error';
 import { useRouter } from 'next/navigation';
 
 import { ImiLogo } from '@/asset';
+import { useAuth } from '@/hooks';
+import { axiosInstance } from '@/libs';
+import { LoginResponse } from '@/types';
 
 import { FieldErrors, useForm } from 'react-hook-form';
+import { toast } from 'react-toastify';
 
 import * as S from './signUpTwo.css';
 
 type FormValues = {
   name: string;
-  gradeNumber: number;
+  studentId: number;
 };
 
-type FormName = 'name' | 'gradeNumber';
+type FormName = 'name' | 'studentId';
 
 type SignUpTwoPageProps = {
   formData: { email: string; password: string };
@@ -23,6 +29,8 @@ type SignUpTwoPageProps = {
 const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
   const router = useRouter();
 
+  const { setIsLogged } = useAuth();
+
   const {
     register,
     handleSubmit,
@@ -35,10 +43,29 @@ const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
     reValidateMode: 'onBlur',
   });
 
-  const onSubmit = (data: FormValues) => {
-    console.log(data, formData);
+  const onSubmit = async (data: FormValues) => {
+    const Data = { ...data, ...formData };
+
+    try {
+      await axiosInstance.post('/user/join', Data);
+
+      const response: LoginResponse = await axiosInstance.post('/auth/login', {
+        email: Data.email,
+        password: Data.password,
+      });
 
-    router.push('/');
+      document.cookie = `accessToken=${response.accessToken}; path=/;`;
+      document.cookie = `refreshToken=${response.refreshToken}; path=/;`;
+
+      setIsLogged(true);
+
+      toast.success('회원가입에 성공했습니다');
+      router.push('/');
+    } catch (error) {
+      if (error instanceof AxiosError) {
+        toast.error(error?.response?.data.message);
+      }
+    }
   };
 
   const handleFocus = (id: FormName) => {
@@ -53,7 +80,7 @@ const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
 
   const ClearError = () => {
     clearErrors('name');
-    clearErrors('gradeNumber');
+    clearErrors('studentId');
   };
 
   const GoPrev = () => {
@@ -105,22 +132,23 @@ const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
             )}
           </div>
         </div>
-        <div className={S.InputGradeNumberContainer}>
+        <div className={S.InputStudentIdContainer}>
           <p className={S.Text}>Grade</p>
           <div
-            key={'gradeNumber'}
+            key={'studentId'}
             className={
-              errors.gradeNumber
-                ? S.inputGradeNumberVariants.error
-                : S.inputGradeNumberVariants.default
+              errors.studentId
+                ? S.inputStudentIdVariants.error
+                : S.inputStudentIdVariants.default
             }
-            onClick={() => handleFocus('gradeNumber')}
+            onClick={() => handleFocus('studentId')}
           >
             <input
               placeholder="학번을 입력해주세요. ex) 2215"
               className={S.InputBox}
               onClick={() => ClearError()}
-              {...register('gradeNumber', {
+              {...register('studentId', {
+                valueAsNumber: true,
                 validate: (value) => {
                   if (!value) {
                     return undefined;
@@ -138,8 +166,8 @@ const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
           </div>
           <div className={S.ErrorBox}>
             <div></div>
-            {errors.gradeNumber && (
-              <p className={S.ErrorText}>{errors.gradeNumber.message}</p>
+            {errors.studentId && (
+              <p className={S.ErrorText}>{errors.studentId.message}</p>
             )}
           </div>
         </div>
diff --git a/src/pageContainer/SignUpPage/stepTwo/signUpTwo.css.ts b/src/pageContainer/SignUpPage/stepTwo/signUpTwo.css.ts
index 352465b..4410143 100644
--- a/src/pageContainer/SignUpPage/stepTwo/signUpTwo.css.ts
+++ b/src/pageContainer/SignUpPage/stepTwo/signUpTwo.css.ts
@@ -55,7 +55,7 @@ export const InputNameContainer = style({
   },
 });
 
-export const InputGradeNumberContainer = style({
+export const InputStudentIdContainer = style({
   display: 'flex',
   width: '30.125rem',
   flexDirection: 'column',
@@ -71,7 +71,7 @@ export const InputGradeNumberContainer = style({
   },
 });
 
-export const InputGradeNumber = style({
+export const InputStudentId = style({
   display: 'flex',
   alignItems: 'center',
   border: `1px solid ${vars.color.primary}`,
@@ -90,7 +90,7 @@ export const InputGradeNumber = style({
   },
 });
 
-export const inputGradeNumberError = style({
+export const inputStudentIdError = style({
   display: 'flex',
   alignItems: 'center',
   borderRadius: '8px',
@@ -109,9 +109,9 @@ export const inputGradeNumberError = style({
   },
 });
 
-export const inputGradeNumberVariants = styleVariants({
-  default: [InputGradeNumber],
-  error: [inputGradeNumberError],
+export const inputStudentIdVariants = styleVariants({
+  default: [InputStudentId],
+  error: [inputStudentIdError],
 });
 
 export const InputName = style({
diff --git a/src/types/index.ts b/src/types/index.ts
index fb14eb6..3b6d4e9 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,2 +1,3 @@
 export * from './clubResponse';
 export * from './profileResponse';
+export * from './loginResponse';
diff --git a/src/types/loginResponse.ts b/src/types/loginResponse.ts
new file mode 100644
index 0000000..8897d70
--- /dev/null
+++ b/src/types/loginResponse.ts
@@ -0,0 +1,6 @@
+export type LoginResponse = {
+  accessToken: string;
+  expiresIn: number;
+  issuedAt: number;
+  refreshToken: string;
+};


Reviews:
user: login: junjuny0227
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:45:20Z
user: login: LeeSangHyeok0731
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:48:44Z
user: login: junjuny0227
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:54:30Z
user: login: LeeSangHyeok0731
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:57:01Z
user: login: junjuny0227
body: 고생하셨습니다. 다음부터는 역할에 맞는 브랜치에서 작업해주시면 좋을 것 같아요.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T08:59:01Z

Review comments:
diff_hunk: @@ -19,7 +19,7 @@ type FormValues = {
 
 type FormName = 'email' | 'password';
 
-type LoginResponse = {
+export type LoginResponse = {
path: src/pageContainer/SignInPage/index.tsx
user: login: junjuny0227
body: 다른 곳에서 사용하려고 export를 하신거면 types 폴더로 따로 빼시는 게 좋을 것 같습니다
created_at: 2025-04-29T08:45:16Z
author_association: MEMBER
diff_hunk: @@ -19,7 +19,7 @@ type FormValues = {
 
 type FormName = 'email' | 'password';
 
-type LoginResponse = {
+export type LoginResponse = {
path: src/pageContainer/SignInPage/index.tsx
user: login: LeeSangHyeok0731
body: 넵 알겠습니다
created_at: 2025-04-29T08:48:44Z
author_association: MEMBER
diff_hunk: @@ -35,10 +41,27 @@ const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
     reValidateMode: 'onBlur',
   });
 
-  const onSubmit = (data: FormValues) => {
-    console.log(data, formData);
+  const onSubmit = async (data: FormValues) => {
+    const Data = { ...data, ...formData };
+
+    try {
+      await axiosInstance.post('/user/join', Data);
+
+      const response: LoginResponse = await axiosInstance.post('/auth/login', {
+        email: Data.email,
+        password: Data.password,
+      });
 
-    router.push('/');
+      document.cookie = `accessToken=${response.accessToken}; path=/;`;
+      document.cookie = `refreshToken=${response.refreshToken}; path=/;`;
+
+      setIsLogged(true);
+
+      toast.success('회원가입에 성공했습니다');
+      router.push('/');
+    } catch (error: any) {
path: src/pageContainer/SignUpPage/stepTwo/index.tsx
user: login: junjuny0227
body: any 타입 사용하신 이유가 있을까요?
created_at: 2025-04-29T08:54:27Z
author_association: MEMBER
diff_hunk: @@ -35,10 +41,27 @@ const SignUpTwoPage = ({ formData, onPrev }: SignUpTwoPageProps) => {
     reValidateMode: 'onBlur',
   });
 
-  const onSubmit = (data: FormValues) => {
-    console.log(data, formData);
+  const onSubmit = async (data: FormValues) => {
+    const Data = { ...data, ...formData };
+
+    try {
+      await axiosInstance.post('/user/join', Data);
+
+      const response: LoginResponse = await axiosInstance.post('/auth/login', {
+        email: Data.email,
+        password: Data.password,
+      });
 
-    router.push('/');
+      document.cookie = `accessToken=${response.accessToken}; path=/;`;
+      document.cookie = `refreshToken=${response.refreshToken}; path=/;`;
+
+      setIsLogged(true);
+
+      toast.success('회원가입에 성공했습니다');
+      router.push('/');
+    } catch (error: any) {
path: src/pageContainer/SignUpPage/stepTwo/index.tsx
user: login: LeeSangHyeok0731
body: 바꾸는걸 깜빡했습니다
수정했습니다
created_at: 2025-04-29T08:57:01Z
author_association: MEMBER

PR comments:
user: login: LeeSangHyeok0731
created_at: 2025-04-29T08:43:23Z
body: 회원가입시 즉시 로그인 기능 만들었습니다
author_association: MEMBER
user: login: LeeSangHyeok0731
created_at: 2025-04-29T08:59:47Z
body: > 고생하셨습니다. 다음부터는 역할에 맞는 브랜치에서 작업해주시면 좋을 것 같아요.

네 명심하겠습니다
감사합니다
author_association: MEMBER