Repository: SuimKim/medi-click
Pull request #66: [Feature]#14,#16 회원가입 예외처리 추가
Author: login: sharet9446
Labels: name: 📬 API
description: 서버 API 통신
name: 🐞 BugFix
description: 버그 관련 수정 사항
name: ✨ Feature
description: 기능 개발
name: 🐿️ 현식
description: 담당자 정현식
Created: 2025-03-24T15:01:18Z | Merged: 2025-03-25T08:48:56Z

## ✨ feature(#14,#16): 회원가입 예외처리 추가

<br/>

## 🔎 작업 내용

- 회원가입에서 '역할 선택' 삭제
- 회원가입 예외처리 추가
- 회원가입에 성공하면 users 안에 데이터가 들어가면서 로그인 페이지로 이동
- 회원가입 성공시 자동 로그인되는 버그 수정
- 토스토(toast) 적용

  <br/>

## 🖼️ 작업 내용 미리보기
[Screenshot 2025-03-24 at 23.59.43.webm](https://github.com/user-attachments/assets/cb325635-1257-46bb-85b2-57ccceefb1d5)

<br/>

## ⏰ 예상 리뷰 시간

10분

### ✔️ 이슈 닫기

Closes #16
Closes #14


Linked issue #16: [Feature] 회원가입 페이지 기능 구현
## 어떤 기능인가요?

> 회원가입을 하는 기능을 구현합니다.

## 작업 상세 내용

- [ ] 아직 해결하지 못한 일



Diff:
diff --git a/src/components/features/auth/AuthForm.tsx b/src/components/features/auth/AuthForm.tsx
index 2799b7a3..092b6029 100644
--- a/src/components/features/auth/AuthForm.tsx
+++ b/src/components/features/auth/AuthForm.tsx
@@ -2,16 +2,12 @@
 
 import { useRouter } from 'next/navigation';
 import { useEffect, useState } from 'react';
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from '@/components/ui/select';
+
+import { toast } from 'react-toastify';
 import { MODE } from '@/constants/authMode';
+import { PATH } from '@/constants/routerPath';
 import { cn } from '@/lib/utils';
-import { getSession, login, signUp } from '@/utils/api/auth';
+import { getSession, logIn, logOut, signUp } from '@/utils/api/auth';
 import { Button } from '../../ui/button';
 import { Input } from '../../ui/input';
 
@@ -28,26 +24,42 @@ const AuthForm = ({ mode }: Props) => {
     name: '',
     phone: '',
     birth: '',
-    role: '',
   });
 
   const [isLogin, setIsLogin] = useState(false);
 
+  // 사용자가 입력 필드 값을 변경할 때 호출되는 함수
   const handleAuthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const { name, value } = e.target;
     setFormData((prev) => ({ ...prev, [name]: value }));
   };
 
-  const handleRoleChange = (value: string) => {
-    setFormData((prev) => ({ ...prev, role: value }));
-  };
-
+  // 로그인 또는 회원가입 폼 제출 시 호출되는 함수
   const handleAuthSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
+
     if (mode === MODE.SIGNUP) {
-      signUp(formData);
+      const signUpError = await signUp(formData);
+
+      if (signUpError) {
+        toast.error('이미 사용 중인 이메일입니다.');
+        return;
+      }
+
+      toast.success('회원가입이 완료되었습니다.');
+
+      logOut();
+
+      router.push(PATH.LOGIN);
     } else {
-      login(formData);
+      const { error: loginError } = await logIn(formData);
+
+      if (loginError) {
+        toast.error('아이디나 비밀번호가 틀렸습니다.');
+        return;
+      }
+
+      router.push(PATH.HOME);
     }
   };
 
@@ -55,6 +67,13 @@ const AuthForm = ({ mode }: Props) => {
     getSession(setIsLogin);
   }, []);
 
+  const AuthInputTitle = {
+    email: '이메일 형식이 올바르지 않습니다. 예시: example@domain.com',
+    password: '영문과 숫자를 포함한 6글자 이상을 입력해주세요.',
+    name: '이름을 정확히 입력해주세요. (한글 2~4자)',
+    phone: '전화번호를 정확히 입력해주세요. 예시: 010-1111-1111',
+  };
+
   const AuthInputClassName = cn(
     'h-14 w-full rounded-lg bg-gray02 px-5 text-lg',
   );
@@ -68,20 +87,22 @@ const AuthForm = ({ mode }: Props) => {
         name='email'
         type='text'
         placeholder='이메일 주소'
+        pattern='^[^\s@]+@[^\s@]+\.[^\s@]+$'
+        title={AuthInputTitle.email}
         value={formData.email}
         onChange={handleAuthChange}
         className={AuthInputClassName}
-        required
       />
 
       <Input
         name='password'
         type='password'
         placeholder='비밀번호'
+        pattern='^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*]{6,}$'
+        title={AuthInputTitle.password}
         value={formData.password}
         onChange={handleAuthChange}
         className={AuthInputClassName}
-        required
       />
 
       {mode === MODE.SIGNUP && (
@@ -90,21 +111,22 @@ const AuthForm = ({ mode }: Props) => {
             name='name'
             type='text'
             placeholder='이름'
+            pattern='^[가-힣]{2,4}$'
+            title={AuthInputTitle.name}
             value={formData.name}
             onChange={handleAuthChange}
             className={AuthInputClassName}
-            required
           />
 
           <Input
             name='phone'
             type='tel'
             placeholder='전화번호'
-            pattern='[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}|[0-9]{9,11}'
+            pattern='^(010)-\d{4}-\d{4}$'
+            title={AuthInputTitle.phone}
             value={formData.phone}
             onChange={handleAuthChange}
             className={AuthInputClassName}
-            required
           />
 
           <Input
@@ -114,20 +136,8 @@ const AuthForm = ({ mode }: Props) => {
             value={formData.birth}
             onChange={handleAuthChange}
             className={AuthInputClassName}
-            required
             onClick={(e) => ((e.target as HTMLInputElement).type = 'date')}
           />
-
-          <Select value={formData.role} onValueChange={handleRoleChange}>
-            <SelectTrigger className={AuthInputClassName}>
-              <SelectValue placeholder='역할 선택' />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value='doctor'>의사</SelectItem>
-              <SelectItem value='nurse'>간호사</SelectItem>
-              <SelectItem value='patient'>환자</SelectItem>
-            </SelectContent>
-          </Select>
         </>
       )}
 
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 1e462707..b3277b87 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -14,6 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
         )}
         ref={ref}
         {...props}
+        required
       />
     );
   },
diff --git a/src/utils/api/auth.ts b/src/utils/api/auth.ts
index 7445b125..d55673d2 100644
--- a/src/utils/api/auth.ts
+++ b/src/utils/api/auth.ts
@@ -1,3 +1,4 @@
+import { TABLE } from '@/constants/supabaseTables';
 import { supabase } from '../supabase/supabase';
 
 interface FormData {
@@ -6,38 +7,64 @@ interface FormData {
   name: string;
   phone: string;
   birth: string;
-  role: string;
 }
 
+// 회원가입을 처리하는 함수
 export const signUp = async ({
   email,
   password,
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
-    email,
-    password,
-    options: {
-      data: {
-        name,
-        phone_number: phone,
-        birth,
-        role,
-      },
-    },
-  });
+  try {
+    const {
+      data: { user },
+      error,
+    } = await supabase.auth.signUp({
+      email,
+      password,
+    });
+
+    if (error) {
+      console.error('회원가입 중 오류 발생:', error.message);
+      return error.message;
+    }
+
+    const { error: dbError } = await supabase.from(TABLE.USERS).insert({
+      id: user?.id,
+      email,
+      name,
+      phone_number: phone,
+      birth,
+    });
+
+    if (dbError) {
+      console.error('데이터베이스에 저장 중 오류 발생', dbError.message);
+      return dbError.message;
+    }
+
+    return error;
+  } catch (error) {
+    console.error('회원가입 중 오류 발생:', error);
+    return;
+  }
 };
 
-export const login = async ({ email, password }: FormData) => {
-  await supabase.auth.signInWithPassword({
+// 로그인을 처리하는 함수
+export const logIn = async ({ email, password }: FormData) => {
+  return await supabase.auth.signInWithPassword({
     email,
     password,
   });
 };
 
+// 로그아웃을 처리하는 함수
+export const logOut = async () => {
+  await supabase.auth.signOut();
+};
+
+// 현재 세션을 확인하는 함수
 export const getSession = async (setIsLogin: (value: boolean) => void) => {
   try {
     const {


Reviews:
user: login: lje00220
body: 고생 많으셨습니다! 현식님 그런데 혹시 생년월일을 기존 date 타입에서 text로 변경하신 이유가 있을까요? date로 입력을 하는게 유저입장에서 더 직관적인 것 같아서요!
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T16:13:37Z
user: login: parkminjo
body: 코드 작성하시느라 고생하셨습니다!
지은님이 이미 코맨트를 잘 남겨주셔서 저는 부가적인 내용만 달아두었습니다.
코맨트 확인해주시면 좋을 것 같아요~!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T00:30:33Z
user: login: SuimKim
body: 앞서서 두분이 리뷰를 잘 남겨주셨네요! 조금만 수정을 하면 될 것 같아요 유효성 검사 때문에 고민 많으셨을 텐데 수고하셨어요!
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-25T01:50:48Z
user: login: sharet9446
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T04:28:01Z
user: login: lje00220
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T06:29:22Z
user: login: lje00220
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T06:31:00Z
user: login: parkminjo
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T07:48:14Z

Review comments:
diff_hunk: @@ -38,16 +32,30 @@ const AuthForm = ({ mode }: Props) => {
     setFormData((prev) => ({ ...prev, [name]: value }));
   };
 
-  const handleRoleChange = (value: string) => {
-    setFormData((prev) => ({ ...prev, role: value }));
-  };
-
   const handleAuthSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
+
     if (mode === MODE.SIGNUP) {
-      signUp(formData);
+      const signUpError = await signUp(formData);
+
+      if (signUpError) {
+        toast.error('이미 사용 중인 이메일입니다.', {
+          position: 'top-center',
+          autoClose: 3000,
path: src/components/features/auth/AuthForm.tsx
user: login: lje00220
body: toast 알림 위치는 다같이 통일된 위치에 하는게 좋을 것 같습니다! 함께 얘기 나눠보고 정하면 좋을 것 같네요
created_at: 2025-03-24T15:55:08Z
author_association: COLLABORATOR
diff_hunk: @@ -38,16 +32,30 @@ const AuthForm = ({ mode }: Props) => {
     setFormData((prev) => ({ ...prev, [name]: value }));
   };
 
-  const handleRoleChange = (value: string) => {
-    setFormData((prev) => ({ ...prev, role: value }));
-  };
-
   const handleAuthSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
+
     if (mode === MODE.SIGNUP) {
-      signUp(formData);
+      const signUpError = await signUp(formData);
+
+      if (signUpError) {
+        toast.error('이미 사용 중인 이메일입니다.', {
+          position: 'top-center',
+          autoClose: 3000,
+        });
+        return;
+      }
+
+      toast.success('회원가입이 완료되었습니다.', {
+        position: 'top-center',
+        autoClose: 3000,
+      });
+
+      logOut();
+
+      router.push('login');
path: src/components/features/auth/AuthForm.tsx
user: login: lje00220
body: router 경로는 routerPath에서 가져다 써주세요!!
created_at: 2025-03-24T15:56:35Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
     email,
     password,
-    options: {
-      data: {
-        name,
-        phone_number: phone,
-        birth,
-        role,
-      },
-    },
   });
+
+  await supabase.from('users').insert({
path: src/utils/api/auth.ts
user: login: lje00220
body: 테이블명도 supabaseTables를 보면 이미 있습니다!
created_at: 2025-03-24T16:01:36Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
path: src/utils/api/auth.ts
user: login: lje00220
body: Supabase에서 트리거 설정을 하면 supabase와 한번 더 통신할 필요없이 회원가입을 하면 자동으로 users 테이블에 유저 정보를 넣어줄 수 있습니다! 혹시 트리거 설정은 어려우실까요?
created_at: 2025-03-24T16:03:21Z
author_association: COLLABORATOR
diff_hunk: @@ -117,17 +132,6 @@ const AuthForm = ({ mode }: Props) => {
             required
path: src/components/features/auth/AuthForm.tsx
user: login: lje00220
body: 전에도 말씀드렸지만 required가 모든 input에 필요한거면 컴포넌트 안에 속성을 지정해줘도 좋을 것 같습니다
created_at: 2025-03-24T16:05:51Z
author_association: COLLABORATOR
diff_hunk: @@ -100,7 +114,8 @@ const AuthForm = ({ mode }: Props) => {
             name='phone'
             type='tel'
             placeholder='전화번호'
-            pattern='[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}|[0-9]{9,11}'
+            pattern='^(02|010|031|032|033|041|042|043|051|052|053|054|055|061|062|063|064)-?\d{3,4}-?\d{4}$'
path: src/components/features/auth/AuthForm.tsx
user: login: lje00220
body: 정규식도 다 빼서 객체를 만든다음 불러오는 방식을 사용해도 좋을 것 같습니다
created_at: 2025-03-24T16:07:54Z
author_association: COLLABORATOR
diff_hunk: @@ -100,7 +114,8 @@ const AuthForm = ({ mode }: Props) => {
             name='phone'
             type='tel'
             placeholder='전화번호'
-            pattern='[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}|[0-9]{9,11}'
+            pattern='^(02|010|031|032|033|041|042|043|051|052|053|054|055|061|062|063|064)-?\d{3,4}-?\d{4}$'
+            title='전화번호를 정확히 입력해주세요. 예시: 010-1111-1111 또는 01011111111'
path: src/components/features/auth/AuthForm.tsx
user: login: lje00220
body: 타이틀도 따로 객체로 정리해주는게 좋을 것 같습니다
created_at: 2025-03-24T16:08:27Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
     email,
     password,
-    options: {
-      data: {
-        name,
-        phone_number: phone,
-        birth,
-        role,
-      },
-    },
   });
+
+  await supabase.from('users').insert({
+    id: user?.id,
+    email,
+    name,
+    phone_number: phone,
+    birth,
+  });
+
+  return error;
 };
 
-export const login = async ({ email, password }: FormData) => {
-  await supabase.auth.signInWithPassword({
+export const logIn = async ({ email, password }: FormData) => {
+  const { data, error } = await supabase.auth.signInWithPassword({
     email,
     password,
   });
+
+  if (error) throw error;
path: src/utils/api/auth.ts
user: login: lje00220
body: 로그인은 에러 발생 시 throw error만 하고 유저에게 따로 알려주는 UI는 없는 건가요?
created_at: 2025-03-24T16:10:13Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
     email,
     password,
-    options: {
-      data: {
-        name,
-        phone_number: phone,
-        birth,
-        role,
-      },
-    },
   });
+
+  await supabase.from('users').insert({
+    id: user?.id,
+    email,
+    name,
+    phone_number: phone,
+    birth,
+  });
+
+  return error;
path: src/utils/api/auth.ts
user: login: parkminjo
body: try catch문을 이용해서 에러 처리를 해주시면 코드의 안전성이 높아질 것 같습니다!
created_at: 2025-03-25T00:28:19Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
     email,
     password,
-    options: {
-      data: {
-        name,
-        phone_number: phone,
-        birth,
-        role,
-      },
-    },
   });
+
+  await supabase.from('users').insert({
+    id: user?.id,
+    email,
+    name,
+    phone_number: phone,
+    birth,
+  });
+
+  return error;
 };
 
-export const login = async ({ email, password }: FormData) => {
-  await supabase.auth.signInWithPassword({
+export const logIn = async ({ email, password }: FormData) => {
+  const { data, error } = await supabase.auth.signInWithPassword({
     email,
     password,
   });
+
+  if (error) throw error;
path: src/utils/api/auth.ts
user: login: parkminjo
body: 이부분도 try catch문을 이용해서 에러를 잡아주시면 더 좋을 것 같아요.
지은님이 말씀하신 내용처럼 로그인 에러가 발생했을 때 사용자에게 알려주는 부분도 있으면 좋을 것 같습니다.
created_at: 2025-03-25T00:29:19Z
author_association: COLLABORATOR
diff_hunk: @@ -100,7 +114,8 @@ const AuthForm = ({ mode }: Props) => {
             name='phone'
             type='tel'
             placeholder='전화번호'
-            pattern='[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}|[0-9]{9,11}'
+            pattern='^(02|010|031|032|033|041|042|043|051|052|053|054|055|061|062|063|064)-?\d{3,4}-?\d{4}$'
+            title='전화번호를 정확히 입력해주세요. 예시: 010-1111-1111 또는 01011111111'
path: src/components/features/auth/AuthForm.tsx
user: login: SuimKim
body: 연락처 형식도 얘기해봐야겠군요!
created_at: 2025-03-25T01:49:07Z
author_association: OWNER
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
path: src/utils/api/auth.ts
user: login: sharet9446
body: 이 부분 시도를 해봤는데 잘 안됐습니다 ㅠㅠ 다하고 시간 남으면 시도를 해봐야 할 것 같아요!
created_at: 2025-03-25T04:28:01Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +14,38 @@ export const signUp = async ({
   name,
   phone,
   birth,
-  role,
 }: FormData) => {
-  await supabase.auth.signUp({
+  const {
+    data: { user },
+    error,
+  } = await supabase.auth.signUp({
path: src/utils/api/auth.ts
user: login: lje00220
body: 네네!! 저도 예약 페이지 끝내고나서 한번 찾아보겠습니다!! 고생하셨어요🥹
created_at: 2025-03-25T06:29:22Z
author_association: COLLABORATOR

PR comments:
user: login: sharet9446
created_at: 2025-03-25T03:25:04Z
body: 
@lje00220 
> 고생 많으셨습니다! 현식님 그런데 혹시 생년월일을 기존 date 타입에서 text로 변경하신 이유가 있을까요? date로 입력을 하는게 유저입장에서 더 직관적인 것 같아서요!

text로 했을때 [Screenshot 2025-03-25 at 12.21.47.webm](https://github.com/user-attachments/assets/c2a97c9f-ad49-465d-9e54-20f33fb029a7)
date로 했을때 [Screenshot 2025-03-25 at 12.21.13.webm](https://github.com/user-attachments/assets/7d3eed75-73c3-47ad-8509-c4115e206117)
차이입니다!

author_association: COLLABORATOR
user: login: lje00220
created_at: 2025-03-25T03:46:00Z
body: > text로 했을때 [Screenshot 2025-03-25 at 12.21.47.webm](https://github.com/user-attachments/assets/c2a97c9f-ad49-465d-9e54-20f33fb029a7)
> date로 했을때 [Screenshot 2025-03-25 at 12.21.13.webm](https://github.com/user-attachments/assets/7d3eed75-73c3-47ad-8509-c4115e206117)
> 차이입니다!

오호 혹시 text로 해도 date로 선택한 뒤에 자동변환되는건가요?
author_association: COLLABORATOR
user: login: sharet9446
created_at: 2025-03-25T04:27:08Z
body: 
> 오호 혹시 text로 해도 date로 선택한 뒤에 자동변환되는건가요?

text 상태에서 input 창을 누르면 input 창이 자동으로 date로 변환되고, 날짜를 고르면 다시 text로 변환됩니다!
author_association: COLLABORATOR