Skip to content

Commit e5f1f77

Browse files
authored
Merge pull request #188 from prgrms-web-devcourse-final-project/refactor/167-signup
[refactor] 회원가입 리펙토링
2 parents d3807ed + cd5ee0c commit e5f1f77

21 files changed

+631
-523
lines changed

src/components/InputAuthCode.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@ import CountdownTimer from '@/components/CountdownTimer';
33
import React from 'react';
44
import { twMerge } from 'tailwind-merge';
55

6-
type ValidationMessage = {
7-
type: 'success' | 'error' | 'default';
8-
message: string;
9-
};
10-
116
interface InputAuthCodeProps extends React.InputHTMLAttributes<HTMLInputElement> {
127
id: string;
138
label: string;
149
className?: string;
15-
messages: ValidationMessage; // 띄울 메시지
10+
isValid: boolean;
11+
validationMessage: string; // 띄울 메세지
1612
emailSent: boolean;
1713
onTimeout?: () => void; // 시간이 만료되었을 때 실행할 함수
1814
onResendEmail?: () => void; // 재전송 함수
1915
resendCount: number; // 재전송 횟수
2016

2117
// ✅ 버튼 관련 속성 추가
2218
buttonText: string | React.ReactNode;
23-
variant: 'primary' | 'secondary' | 'disabled';
19+
variant: 'primary' | 'disabled';
2420
onClick: () => void;
2521
onButtonClick?: () => void;
2622
}
@@ -32,20 +28,23 @@ function InputAuthCode({
3228
buttonText,
3329
variant,
3430
onClick,
35-
messages,
31+
isValid,
32+
disabled,
33+
value,
34+
validationMessage,
3635
emailSent,
3736
resendCount,
3837
onTimeout,
3938
onResendEmail,
4039
...props
4140
}: InputAuthCodeProps) {
42-
const colorMap = {
43-
success: 'text-functional-success',
44-
error: 'text-functional-danger',
45-
default: 'text-gray-60',
41+
// 메세지 색 선택 로직
42+
const getMessageColor = (value: string, isValid: boolean) => {
43+
if (!value) return 'text-gray-60';
44+
return isValid ? 'text-functional-success' : 'text-functional-danger';
4645
};
4746

48-
const messageColor = colorMap[messages.type];
47+
const messageColor = getMessageColor(value as string, isValid);
4948

5049
return (
5150
<div className="flex flex-col w-full">
@@ -54,10 +53,15 @@ function InputAuthCode({
5453
</label>
5554
<div className="flex gap-2">
5655
<div className="w-full h-[38px] rounded-lg input-shadow outline-0 px-3 caption-m placeholder:text-gray-50 focus-within:ring-1 focus-within:ring-primary-active bg-white flex items-center">
57-
<input id={id} type="text" className="w-full " {...props} />
58-
{emailSent && messages?.type !== 'success' && (
59-
<CountdownTimer key={resendCount} onTimeout={onTimeout} />
60-
)}
56+
<input
57+
id={id}
58+
type="text"
59+
className="w-full "
60+
value={value}
61+
disabled={disabled}
62+
{...props}
63+
/>
64+
{emailSent && !isValid && <CountdownTimer key={resendCount} onTimeout={onTimeout} />}
6165
</div>
6266
{buttonText && (
6367
<Button
@@ -76,11 +80,11 @@ function InputAuthCode({
7680
<p
7781
className={twMerge('text-functional-danger text-[9px]/[18px] ml-[5px]', messageColor)}
7882
>
79-
{messages?.message}
83+
{validationMessage}
8084
</p>
81-
{messages.type !== 'success' && (
85+
{value === '' && (
8286
<div className="flex gap-1 items-center text-gray-60 text-[9px]">
83-
<button className="underline cursor-pointer" onClick={onResendEmail}>
87+
<button className="underline cursor-pointer" onClick={onResendEmail} type="button">
8488
재전송
8589
</button>
8690
<span>({resendCount}/3)</span>

src/components/InputField.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,17 @@ import Button from '@/components/Button';
22
import Input from '@/components/Input';
33
import { twMerge } from 'tailwind-merge';
44

5-
type ValidationMessage = {
6-
type: 'success' | 'error' | '';
7-
message: string;
8-
};
95
//id, label 필수
106
interface InputFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
117
id: string;
128
label: string;
139
className?: string;
1410
isValid?: boolean;
15-
validationMessages?: ValidationMessage; // 띄울 메세지
11+
validationMessage?: string; // 띄울 메세지
1612
}
1713
interface ButtonProps {
1814
buttonText?: string | React.ReactNode;
19-
variant?: 'primary' | 'secondary' | 'disabled';
15+
variant?: 'primary' | 'disabled';
2016
onClick?: () => void;
2117
}
2218

@@ -27,7 +23,8 @@ export default function InputField({
2723
buttonText,
2824
variant,
2925
onClick,
30-
validationMessages,
26+
isValid,
27+
validationMessage,
3128
...props
3229
}: InputFieldProps & ButtonProps) {
3330
return (
@@ -49,16 +46,14 @@ export default function InputField({
4946
)}
5047
</div>
5148
<div className="flex items-center h-5">
52-
{validationMessages?.message && (
49+
{validationMessage !== '' && (
5350
<p
5451
className={twMerge(
5552
'text-functional-danger text-[9px]/[18px] ml-[5px]',
56-
validationMessages?.type === 'error'
57-
? 'text-functional-danger'
58-
: 'text-functional-success',
53+
isValid ? 'text-functional-success' : 'text-functional-danger',
5954
)}
6055
>
61-
{validationMessages?.message}
56+
{validationMessage}
6257
</p>
6358
)}
6459
</div>

src/constants/limits.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export const MIN_NICKNAME_LENGTH = 2; //닉네임 최소 길이
12
export const MAX_NICKNAME_LENGTH = 7; //닉네임 최대 길이
23

34
export const MIN_ID_LENGTH = 5; //아이디 최소 길이

src/constants/validation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
export const ID_REGEX = /^[a-zA-Z0-9]{5,20}$/; // 영문 + 숫자만 허용 (5~20자)
22

3-
export const PASSWORD_REQUIRED_RULES =
4-
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,16}$/;
3+
export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,16}$/;
54
//8~16자, 영문 + 숫자 + 특수문자 포함
65

76
export const NICKNAME_REGEX = /^[A-Za-z-0-9]{2,7}$/; // 영문 + 한글 + 숫자만 허용 2~7자 가능
87

98
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 이메일
9+
10+
export const AUTHCODE_REGEX = /^[a-zA-Z0-9]{6}$/; // 인증코드

src/hooks/useEmailCheck.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { getEmailAvailability } from '@/apis/email';
2+
import { useEmailVerification } from '@/hooks/useEmailVerification';
3+
import { useMutation } from '@tanstack/react-query';
4+
5+
// 이메일 인증 요청 및 이메일 중복 확인 요청
6+
export const useEmailCheck = (
7+
email: string, // 이메일
8+
setEmail: (val: string) => void, // formdata 수정
9+
setValidity: (val: boolean) => void,
10+
setValidationMessage: (val: { success: boolean; message: string }) => void,
11+
setButtonVariant: (val: 'primary' | 'disabled') => void,
12+
) => {
13+
// 이메일 인증 요청 훅 사용
14+
const { requestEmailVerification, isPending } = useEmailVerification(
15+
email,
16+
setValidationMessage,
17+
setValidity,
18+
setButtonVariant,
19+
setEmail,
20+
);
21+
// ✅ 이메일 중복 확인 요청
22+
const emailCheckMutation = useMutation({
23+
mutationFn: () => getEmailAvailability(email),
24+
onSuccess: ({ code }) => {
25+
if (code === 200) {
26+
requestEmailVerification(); // ✅ 이메일 인증 요청 실행
27+
} else if (code === 409) {
28+
setValidationMessage({
29+
success: false,
30+
message: '이 이메일은 이미 사용 중입니다. 다른 이메일을 입력해주세요',
31+
});
32+
}
33+
},
34+
onError: () => {
35+
setValidationMessage({
36+
success: false,
37+
message: '이메일 중복 확인 중 오류가 발생했습니다. 다시 시도해주세요',
38+
});
39+
},
40+
});
41+
42+
return {
43+
emailCheck: emailCheckMutation.mutate, // ✅ 이메일 중복 확인 실행 함수
44+
isChecking: emailCheckMutation.isPending || isPending,
45+
};
46+
};

src/hooks/useEmailVerification.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { postEmailVerificationRequest } from '@/apis/email';
2+
import { useMutation } from '@tanstack/react-query';
3+
4+
// 이메일 인증 요청 훅 (인증 및 재전송)
5+
export const useEmailVerification = (
6+
email: string,
7+
setValidationMessage: (val: { success: boolean; message: string }) => void,
8+
setValidity: (val: boolean) => void,
9+
setButtonVariant: (val: 'primary' | 'disabled') => void,
10+
setEmail: (val: string) => void, // formdata 수정
11+
) => {
12+
const emailVerificationMutation = useMutation({
13+
mutationFn: () => postEmailVerificationRequest(email),
14+
onSuccess: ({ code }) => {
15+
if (code === 200) {
16+
setValidationMessage({
17+
success: true,
18+
message: '이메일 인증 메일이 발송되었습니다. 메일함에서 인증번호를 확인 후 입력해주세요',
19+
});
20+
21+
setValidity(true); // 폼 유효성 확인 업데이트
22+
setButtonVariant('disabled'); // 버튼 막기
23+
setEmail(email); // 이메일 업데이트하기
24+
}
25+
},
26+
onError: () => {
27+
setValidationMessage({
28+
success: false,
29+
message: '이메일 인증 요청 중 오류가 발생했습니다. 다시 시도해주세요',
30+
});
31+
},
32+
});
33+
34+
return {
35+
requestEmailVerification: emailVerificationMutation.mutate, // 이메일 인증 요청 함수
36+
isPending: emailVerificationMutation.isPending, // 진행 중 여부
37+
};
38+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { postEmailVerificationCheck } from '@/apis/email';
2+
import { useMutation } from '@tanstack/react-query';
3+
4+
// 이메일와 인증번호 확인 훅
5+
export const useEmailVerificationCheck = (
6+
email: string, // 이메일
7+
authcode: string, // 인증코드
8+
setValidity: (val: boolean) => void,
9+
setValidationMessage: (val: { success: boolean; message: string }) => void,
10+
setButtonVariant: (val: 'primary' | 'disabled') => void,
11+
) => {
12+
const verificationCheckMutation = useMutation({
13+
mutationFn: () => postEmailVerificationCheck(email, authcode),
14+
onSuccess: ({ code }) => {
15+
if (code === 200) {
16+
setValidationMessage({ success: true, message: '이메일 인증이 완료되었습니다' });
17+
setButtonVariant('disabled'); // 버튼 비활성화
18+
setValidity(true); // 완료 처리
19+
} else {
20+
setValidationMessage({ success: false, message: '인증 코드가 올바르지 않습니다' });
21+
}
22+
},
23+
24+
onError: () => {
25+
setValidationMessage({
26+
success: false,
27+
message: '오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
28+
});
29+
},
30+
});
31+
32+
return {
33+
verifyEmail: verificationCheckMutation.mutate, // 인증 요청 함수
34+
isLoading: verificationCheckMutation.isPending, // 로딩 상태
35+
};
36+
};

src/hooks/useIdAvailability.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getIdAvailability } from '@/apis/user';
2+
import { useMutation } from '@tanstack/react-query';
3+
4+
export const useIdAvailability = (
5+
text: string,
6+
setValue: (val: string) => void,
7+
setValidity: (val: boolean) => void,
8+
setValidation: (val: { success: boolean; message: string }) => void,
9+
) => {
10+
const { mutate, isPending } = useMutation({
11+
mutationFn: () => getIdAvailability(text),
12+
onSuccess: (data) => {
13+
if (data.code === 200) {
14+
setValidation({ success: true, message: '사용 가능한 아이디입니다' });
15+
setValue(text);
16+
setValidity(true);
17+
} else if (data.code === 409) {
18+
setValidation({ success: false, message: '이미 사용 중인 아이디입니다' });
19+
}
20+
},
21+
onError: () => {
22+
setValidation({
23+
success: false,
24+
message: '예기치 않은 오류가 발생했습니다. 다시 시도해주세요',
25+
});
26+
},
27+
});
28+
29+
return { mutate, isPending };
30+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getNicknameAvailability } from '@/apis/user';
2+
import { useMutation } from '@tanstack/react-query';
3+
4+
export const useNicknameAvailability = (
5+
text: string,
6+
setValue: (val: string) => void,
7+
setValidity: (val: boolean) => void,
8+
setValidation: (val: { success: boolean; message: string }) => void,
9+
) => {
10+
const { mutate, isPending } = useMutation({
11+
mutationFn: () => getNicknameAvailability(text),
12+
onSuccess: (data) => {
13+
if (data.code === 200) {
14+
setValidation({ success: true, message: '사용 가능한 닉네임입니다' });
15+
setValue(text);
16+
setValidity(true);
17+
} else if (data.code === 409) {
18+
setValidation({ success: false, message: '이미 사용 중인 닉네임입니다' });
19+
}
20+
},
21+
onError: () => {
22+
setValidation({
23+
success: false,
24+
message: '예기치 않은 오류가 발생했습니다. 다시 시도해주세요',
25+
});
26+
},
27+
});
28+
29+
return { mutate, isPending };
30+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { postEmailVerificationRequest } from '@/apis/email';
2+
import { useMutation } from '@tanstack/react-query';
3+
4+
// 이메일 재전송 훅
5+
export const useResendEmailVerification = (
6+
email: string,
7+
setValidationMessage: (val: { success: boolean; message: string }) => void,
8+
setButtonVariant: (val: 'primary' | 'disabled') => void,
9+
setResendCount: React.Dispatch<React.SetStateAction<number>>,
10+
) => {
11+
const resendEmailMutation = useMutation({
12+
mutationFn: () => postEmailVerificationRequest(email),
13+
onSuccess: ({ code }) => {
14+
if (code === 200) {
15+
setValidationMessage({
16+
success: true,
17+
message: '인증번호가 오지 않았나요?',
18+
});
19+
20+
setResendCount((count) => count + 1);
21+
setButtonVariant('disabled');
22+
}
23+
},
24+
onError: () => {
25+
setValidationMessage({
26+
success: false,
27+
message: '이메일 재전송 중 오류가 발생했습니다. 다시 시도해주세요.',
28+
});
29+
},
30+
});
31+
32+
return {
33+
resendEmailVerification: resendEmailMutation.mutate,
34+
isPending: resendEmailMutation.isPending,
35+
};
36+
};

0 commit comments

Comments
 (0)