diff --git a/package-lock.json b/package-lock.json
index af615c0a..7720c097 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"react-datepicker": "^4.25.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
+ "react-hook-form": "^7.49.3",
"react-loading-skeleton": "^3.3.1",
"react-paginate": "^8.2.0",
"react-router-dom": "^6.21.1",
@@ -6588,6 +6589,22 @@
"react": ">=16.13.1"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.49.3",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz",
+ "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==",
+ "engines": {
+ "node": ">=18",
+ "pnpm": "8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
diff --git a/package.json b/package.json
index 4b76cf3f..18b8e9e4 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"react-datepicker": "^4.25.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
+ "react-hook-form": "^7.49.3",
"react-loading-skeleton": "^3.3.1",
"react-paginate": "^8.2.0",
"react-router-dom": "^6.21.1",
diff --git a/src/components/common/Auth/AuthButton/index.tsx b/src/components/Auth/AuthButton/index.tsx
similarity index 89%
rename from src/components/common/Auth/AuthButton/index.tsx
rename to src/components/Auth/AuthButton/index.tsx
index 3fbe96e8..59f0e3d3 100644
--- a/src/components/common/Auth/AuthButton/index.tsx
+++ b/src/components/Auth/AuthButton/index.tsx
@@ -2,12 +2,19 @@ import styled from '@emotion/styled';
import { AuthButton, AuthButtonStyleProps } from '@/types/auth';
-const AuthButton = ({ size, variant, text, buttonFunc }: AuthButton) => {
+const AuthButton = ({
+ size,
+ variant,
+ text,
+ disabled,
+ buttonFunc
+}: AuthButton) => {
return (
{text}
diff --git a/src/components/common/Auth/AuthInputNormal/index.tsx b/src/components/Auth/AuthInputNormal/index.tsx
similarity index 54%
rename from src/components/common/Auth/AuthInputNormal/index.tsx
rename to src/components/Auth/AuthInputNormal/index.tsx
index c0ee44c7..f8490e93 100644
--- a/src/components/common/Auth/AuthInputNormal/index.tsx
+++ b/src/components/Auth/AuthInputNormal/index.tsx
@@ -1,51 +1,55 @@
-import { ChangeEvent, useState } from 'react';
import styled from '@emotion/styled';
+import { useFormContext } from 'react-hook-form';
-import { AuthInputNormal } from '@/types/auth';
+import { AuthInputNormal, AuthInputStyleProps } from '@/types/auth';
import closeIcon from '@assets/icons/ic-login-close.svg';
import checkInvalid from '@assets/icons/ic-signup-check-invalid.svg';
import checkValid from '@assets/icons/ic-signup-check-valid.svg';
+import { getInputOptions } from '@utils/index';
const AuthInputNormal = ({
type,
id,
placeholder,
usedFor,
- isInvalid
+ isError
}: AuthInputNormal) => {
- const [text, setText] = useState('');
+ const { register, watch, resetField, getFieldState, formState } =
+ useFormContext();
+ const inputValue = watch(id);
+ const handleReset = () => resetField(id);
- const handleChange = (event: ChangeEvent) => {
- setText(event.target.value);
- };
-
- const handleReset = (event: React.MouseEvent) => {
- event.preventDefault();
- setText('');
- };
-
- // TODO : react-hook-form 적용 후 유효성 검사 로직 예정
+ const isInputTouched = getFieldState(id, formState).isTouched;
+ const isValid = isInputTouched ? !isError : false;
return (
-
+
- {usedFor === 'login' && text.length > 0 && (
-
);
@@ -53,18 +57,20 @@ const AuthInputNormal = ({
export default AuthInputNormal;
-const Container = styled.div`
+const Container = styled.div`
position: relative;
- width: 524px;
+ width: ${props =>
+ props.$usedFor === 'signup' && props.$type === 'email' ? '358px' : '524px'};
height: 79px;
display: flex;
align-items: center;
`;
-const Input = styled.input`
- width: 524px;
+const Input = styled.input`
+ width: ${props =>
+ props.$usedFor === 'signup' && props.$type === 'email' ? '358px' : '524px'};
height: 79px;
border-radius: 16px;
diff --git a/src/components/common/Auth/AuthInputPassword/index.tsx b/src/components/Auth/AuthInputPassword/index.tsx
similarity index 75%
rename from src/components/common/Auth/AuthInputPassword/index.tsx
rename to src/components/Auth/AuthInputPassword/index.tsx
index 1749c801..b0343353 100644
--- a/src/components/common/Auth/AuthInputPassword/index.tsx
+++ b/src/components/Auth/AuthInputPassword/index.tsx
@@ -1,5 +1,6 @@
-import { ChangeEvent, useState } from 'react';
+import { useState } from 'react';
import styled from '@emotion/styled';
+import { useFormContext } from 'react-hook-form';
import { AuthInput } from '@/types/auth';
import eyeOn from '@assets/icons/ic-login-eye-on.svg';
@@ -7,24 +8,25 @@ import eyeOff from '@assets/icons/ic-login-eye-off.svg';
import closeIcon from '@assets/icons/ic-login-close.svg';
import checkInvalid from '@assets/icons/ic-signup-check-invalid.svg';
import checkValid from '@assets/icons/ic-signup-check-valid.svg';
+import { getInputOptions } from '@utils/index';
const AuthInputPassword = ({
id,
placeholder,
usedFor,
- isInvalid
+ isError
}: AuthInput) => {
- const [text, setText] = useState('');
- const [showPW, setShowPW] = useState(false);
+ const { register, watch, resetField, getFieldState, formState } =
+ useFormContext();
- const handleChange = (event: ChangeEvent) => {
- setText(event.target.value);
- };
+ const inputValue = watch(id);
+ const handleReset = () => resetField(id);
- const handleReset = (event: React.MouseEvent) => {
- event.preventDefault();
- setText('');
- };
+ const passwordValue = watch('user_password');
+ const isInputTouched = getFieldState(id, formState).isTouched;
+ const isValid = isInputTouched ? !isError : false;
+
+ const [showPW, setShowPW] = useState(false);
const handleShowPW = (event: React.MouseEvent) => {
event.preventDefault();
@@ -37,11 +39,10 @@ const AuthInputPassword = ({
type={showPW ? 'text' : 'password'}
id={id}
placeholder={placeholder}
- value={text}
- onChange={handleChange}
+ {...register(id, getInputOptions(id, passwordValue))}
/>
- {text.length > 0 && (
+ {!!inputValue && (
)}
- {usedFor === 'login' && text.length > 0 && (
+ {usedFor === 'login' && !!inputValue && (
)}
- {usedFor === 'signup' &&
- (isInvalid ? : )}
+ {usedFor === 'signup' && (
+
+ )}
);
diff --git a/src/components/common/Auth/index.tsx b/src/components/Auth/index.tsx
similarity index 100%
rename from src/components/common/Auth/index.tsx
rename to src/components/Auth/index.tsx
diff --git a/src/components/Login/LoginForm/index.tsx b/src/components/Login/LoginForm/index.tsx
index 02b6ceb2..eec62c84 100644
--- a/src/components/Login/LoginForm/index.tsx
+++ b/src/components/Login/LoginForm/index.tsx
@@ -1,12 +1,19 @@
import { useLocation, useNavigate } from 'react-router-dom';
+import { AxiosError } from 'axios';
import styled from '@emotion/styled';
+import {
+ FormProvider,
+ useForm,
+ SubmitHandler,
+ FieldValues
+} from 'react-hook-form';
import { InputValidation } from '@/types/login';
import {
AuthButton,
AuthInputNormal,
AuthInputPassword
-} from '@components/common/Auth';
+} from '@components/Auth';
import { LoginData } from '@/types/auth';
import { postLogin } from 'src/api';
import { setCookies } from '@utils/lib/cookies';
@@ -15,81 +22,99 @@ const LoginForm = () => {
const navigate = useNavigate();
const { state } = useLocation();
- // HACK : 추후 useState로 입력 데이터 관리할 예쩡
- const formData: LoginData = {
- email: 'juhwanTest@gmail.com',
- password: 'juhwanTest'
- };
-
- // HACK: 유효성 검사 기능 구현 후 유효성 메세지 노출 여부 결정
- const isInvalid = true;
+ const methods = useForm({
+ mode: 'onBlur'
+ });
+ const {
+ formState: { errors, isValid },
+ handleSubmit
+ } = methods;
+ const isError = !!errors?.user_id || !!errors?.user_password ? true : false;
const movetoSignUp = (event: React.MouseEvent) => {
event.preventDefault();
navigate('/signup');
};
- const handleLoginSubmit = async (
- event: React.FormEvent
- ) => {
- event.preventDefault();
- const response = await postLogin(formData);
- setCookies('userName', response.name, response.expires_in);
- setCookies('userEmail', response.email, response.expires_in);
- setCookies('accessToken', response.access_token, response.expires_in);
- setCookies('refreshToken', response.refresh_token, response.expires_in);
-
- if (state) {
- navigate(state);
- } else {
- navigate('/');
+ const onSubmit: SubmitHandler = async data => {
+ const formData: LoginData = {
+ email: data.user_id,
+ password: data.user_password
+ };
+ try {
+ const response = await postLogin(formData);
+ setCookies('userName', response.name, response.expires_in);
+ setCookies('userEmail', response.email, response.expires_in);
+ setCookies('accessToken', response.access_token, response.expires_in);
+ setCookies('refreshToken', response.refresh_token, response.expires_in);
+
+ if (state) {
+ navigate(state);
+ } else {
+ navigate('/');
+ }
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ console.log(error);
+ // TODO : 에러코드에 따라 모달 표시 예정
+ // error.response?.data.code;
+ // error.response?.data.message;
+ }
}
};
return (
-
+
+
+
);
};
export default LoginForm;
const Inputs = styled.div`
- margin-bottom: ${props => (props.$isInvalid ? '10px' : '65px')};
+ margin-bottom: ${props => (props.$isValid ? '65px' : '10px')};
display: flex;
flex-direction: column;
@@ -101,12 +126,8 @@ const ValidationText = styled.p`
color: #da1e28;
font-size: 15px;
- font-weight: 500;
- line-height: 32px;
-`;
-
-const ValidationBoldText = styled.span`
font-weight: 700;
+ line-height: 32px;
`;
const Buttons = styled.div`
@@ -114,5 +135,5 @@ const Buttons = styled.div`
flex-direction: column;
gap: 13px;
- ${props => props.$isInvalid && 'margin-top: 23px'};
+ margin-top: ${props => (props.$isValid ? 0 : '23px')};
`;
diff --git a/src/components/SignUp/SignUpForm/index.tsx b/src/components/SignUp/SignUpForm/index.tsx
index 35ec971b..b1f0aa2a 100644
--- a/src/components/SignUp/SignUpForm/index.tsx
+++ b/src/components/SignUp/SignUpForm/index.tsx
@@ -1,91 +1,111 @@
import styled from '@emotion/styled';
+import { FormProvider, useForm } from 'react-hook-form';
import { SignUpInputValidation } from '@/types/signUp';
import {
AuthButton,
AuthInputNormal,
AuthInputPassword
-} from '@components/common/Auth';
+} from '@components/Auth';
const SignUpForm = () => {
- const isInvalid = true;
- const isEmailValidationVisible = false;
+ const methods = useForm({
+ mode: 'all'
+ });
+ const { getFieldState, formState } = methods;
+ const { errors, isValid } = formState;
+
+ const isEmailTouched = getFieldState('user_email', formState).isTouched;
+ const isEmailValid = isEmailTouched ? !errors?.user_email : false;
return (
-
+
+
);
};
@@ -113,49 +133,16 @@ const Label = styled.label`
line-height: 32px;
`;
-const Input = styled.input`
- width: 524px;
- height: 79px;
-
- border-radius: 16px;
- border: 2px solid #757676;
- padding: 23px 20px;
-
- display: flex;
- align-items: center;
-
- color: #1a2849;
- font-size: 18px;
- font-weight: 500;
- line-height: 32px;
-
- &::placeholder {
- color: #979c9e;
- font-size: 18px;
- font-weight: 500;
- line-height: 32px;
- }
-
- &:focus {
- outline: 2px solid #1a2849;
- border-color: #1a2849;
- }
-`;
-
const EmailInputWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
-const EmailInput = styled(Input)`
- width: 358px;
-`;
-
const ValidationText = styled.p`
margin-top: 2px;
margin-left: 12px;
- color: ${props => (props.$isInvalid ? '#DA1E28' : '#1a2849')};
+ color: ${props => (props.$isValid ? '#1a2849' : '#DA1E28')};
font-size: 15px;
font-weight: 700;
line-height: 32px;
diff --git a/src/types/auth.ts b/src/types/auth.ts
index 49861aba..80b9c785 100644
--- a/src/types/auth.ts
+++ b/src/types/auth.ts
@@ -1,8 +1,13 @@
+export type AuthInputStyleProps = {
+ $usedFor: string;
+ $type: string;
+};
+
export type AuthInput = {
id: string;
placeholder: string;
usedFor: string;
- isInvalid: boolean;
+ isError?: boolean;
};
export type AuthInputNormal = AuthInput & {
@@ -21,6 +26,7 @@ export type AuthButton = {
size: string;
variant: string;
text: string;
+ disabled?: boolean;
buttonFunc:
| LoginAPIButton
| SignUpAPIButton
diff --git a/src/types/login.ts b/src/types/login.ts
index f529da27..78ad6373 100644
--- a/src/types/login.ts
+++ b/src/types/login.ts
@@ -1,5 +1,5 @@
export type LoginFormStyleProps = {
- $isInvalid: boolean;
+ $isValid: boolean;
};
-export type InputValidation = Pick;
+export type InputValidation = Pick;
diff --git a/src/types/signUp.ts b/src/types/signUp.ts
index c07f069b..a0fd1e23 100644
--- a/src/types/signUp.ts
+++ b/src/types/signUp.ts
@@ -1,5 +1,5 @@
export type SignUpFormStyleProps = {
- $isInvalid: boolean;
+ $isValid?: boolean;
};
-export type SignUpInputValidation = Pick;
+export type SignUpInputValidation = Pick;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index d2a5ded9..7d76d2a6 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -8,3 +8,4 @@ export {
} from './lib/report';
export { getUpdatedDate } from './lib/calculation';
export { getStatusToLocaleString } from './lib/dashboard';
+export { getInputOptions } from './lib/auth';
diff --git a/src/utils/lib/auth.ts b/src/utils/lib/auth.ts
new file mode 100644
index 00000000..15a0697a
--- /dev/null
+++ b/src/utils/lib/auth.ts
@@ -0,0 +1,72 @@
+const nameRegex = /^[가-힣]{2,20}$/i;
+const emailRegex =
+ /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
+const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,20}$/g;
+
+export const getInputOptions = (inputName: string, password?: string) => {
+ switch (inputName) {
+ case 'user_name':
+ return {
+ required: '이름을 입력해주세요.',
+ minLength: { value: 2, message: '이름은 최소 2자 이상 입력해주세요.' },
+ maxLength: { value: 20, message: '이름은 최대 20자까지 입력해주세요.' },
+ pattern: {
+ value: nameRegex,
+ message: '2-20자의 한글로 입력해주세요.'
+ }
+ };
+ case 'user_email':
+ return {
+ required: '이메일을 입력해주세요.',
+ pattern: {
+ value: emailRegex,
+ message: '이메일 형식에 맞게 입력해주세요.'
+ }
+ };
+ case 'user_password':
+ return {
+ required: '비밀번호를 입력해주세요.',
+ minLength: {
+ value: 8,
+ message: '비밀번호는 최소 8자 이상 입력해주세요.'
+ },
+ maxLength: {
+ value: 20,
+ message: '비밀번호는 최대 20자까지 입력해주세요.'
+ },
+ pattern: {
+ value: passwordRegex,
+ message: '8~20자의 영문, 숫자, 특수문자를 모두 포함하여 입력해주세요.'
+ }
+ };
+ case 'user_password_confirm':
+ return {
+ required: '비밀번호를 입력해주세요.',
+ minLength: {
+ value: 8,
+ message: '비밀번호는 최소 8자 이상 입력해주세요.'
+ },
+ maxLength: {
+ value: 20,
+ message: '비밀번호는 최대 20자까지 입력해주세요.'
+ },
+ pattern: {
+ value: passwordRegex,
+ message: '8~20자의 영문, 숫자, 특수문자를 모두 포함하여 입력해주세요.'
+ },
+ validate: {
+ matchPassword: (value: string) => {
+ return password === value || '비밀번호가 일치하지 않습니다';
+ }
+ }
+ };
+ case 'user_id':
+ return {
+ required: '아이디를 입력해주세요.',
+ pattern: {
+ value: emailRegex,
+ message: '이메일 형식에 맞게 입력해주세요.'
+ }
+ };
+ }
+};