diff --git a/src/hook/useInput.ts b/src/hook/useInput.ts new file mode 100644 index 00000000..9b99ee6b --- /dev/null +++ b/src/hook/useInput.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; + +type ValidatorFn = (value: string) => string | null; + +function useInput(validator: ValidatorFn, initialValue ='') { + const [value, setValue] = useState(initialValue); + const [error, setError] = useState(null); + const [touched, setTouched] = useState(false); + + const onChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + if (touched) { + const errorMsg = validator(e.target.value); + setError(errorMsg); + } + }; + + const onBlur = () => { + setTouched(true); + const errorMsg = validator(value); + setError(errorMsg); + }; + + const isValid = !error && touched; + + return { value, onChange, onBlur, error, isValid }; +} + +export default useInput; diff --git a/src/page/Login.styled.ts b/src/page/Login.styled.ts new file mode 100644 index 00000000..4b471edb --- /dev/null +++ b/src/page/Login.styled.ts @@ -0,0 +1,85 @@ + +import theme from '@/styles/theme'; +import styled from '@emotion/styled'; + +export const MyDiv = styled.div` + max-width: 720px; + width: 100%; + min-height: 100vh; + height: 100%; + background-color: rgb(255, 255, 255); + padding-top: 2.75rem; +`; + +export const LoginMain = styled.main` + width: 100%; + height: calc(-2.75rem + 100vh); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +export const KakaoLogo = styled.img` + width: 5.5rem; + color: rgb(42, 48, 56); +`; +export const LoginSection = styled.section` + width: 100%; + max-width: 26.25rem; + padding: 16px; +`; + +export const InputSection = styled.input<{ hasError?: boolean }>` + width: 100%; + box-sizing: border-box; + color: rgb(42, 48, 56); + transition: border-color 200ms; + border-style: solid; + min-height: 2.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.375rem; + padding: 8px 0px; + border-width: 0px 0px 1px; + border-color: ${({ hasError }) => (hasError ? 'red' : 'rgb(220, 222, 227)')}; + &:focus { + outline: none; + border-color: ${({ hasError }) => hasError ? 'red' : 'rgb(42, 48, 56)'}; + } + +`; + +export const LoginButton = styled.button<{ notVaild?: boolean }>` + width: 100%; + height: 2.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.1875rem; + color: rgb(42, 48, 56); + background-color: ${theme.colors.kakaoYellow}; + border-radius: 4px; + border: none; + cursor: ${({notVaild}) => (notVaild? 'not-allowed' :'pointer' ) }; + opacity: ${({notVaild}) => (notVaild? '0.5' :'1' ) }; + transition: background-color 200ms; +`; + +export const EmptyDiv16h= styled.div` + width: 100%; + height: 16px; + background-color: transparent; +`; + +export const EmptyDiv48h = styled.div` + width: 100%; + height: 48px; + background-color: transparent; +`; + +export const ErrorMessage = styled.div` + color: red; + font-size: 0.75rem; + margin-top: 4px; +`; + diff --git a/src/page/Login.tsx b/src/page/Login.tsx index f8263683..0972f041 100644 --- a/src/page/Login.tsx +++ b/src/page/Login.tsx @@ -1,78 +1,19 @@ -import styled from '@emotion/styled'; -import { useNavigate } from 'react-router-dom'; - -const MyDiv = styled.div` - max-width: 720px; - width: 100%; - min-height: 100vh; - height: 100%; - background-color: rgb(255, 255, 255); - padding-top: 2.75rem; -`; - -const LoginMain = styled.main` - width: 100%; - height: calc(-2.75rem + 100vh); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -`; - -const KakaoLogo = styled.img` - width: 5.5rem; - color: rgb(42, 48, 56); -`; -const LoginSection = styled.section` - width: 100%; - max-width: 26.25rem; - padding: 16px; -`; -const InputSection = styled.input` - width: 100%; - box-sizing: border-box; - color: rgb(42, 48, 56); - transition: border-color 200ms; - border-style: solid; - min-height: 2.75rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.375rem; - padding: 8px 0px; - border-width: 0px 0px 1px; - border-color: rgb(220, 222, 227); -`; - -const LoginButton = styled.button` - width: 100%; - height: 2.75rem; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.1875rem; - color: rgb(42, 48, 56); - background-color: rgb(254, 229, 0); - border-radius: 4px; - border: none; - cursor: pointer; - transition: background-color 200ms; -`; +import useInput from '@/hook/useInput'; +import { validateEmail, validatePassword } from '@/utils/validateInput'; +import { useNavigate } from 'react-router-dom'; +import { EmptyDiv16h, EmptyDiv48h, ErrorMessage, InputSection, KakaoLogo, LoginButton, LoginMain, LoginSection, MyDiv } from './Login.styled'; -const EmptyDiv1 = styled.div` - width: 100%; - height: 16px; - background-color: transparent; -`; -const EmptyDiv2 = styled.div` - width: 100%; - height: 48px; - background-color: transparent; -`; const Login = () => { const navigate = useNavigate(); + const id = useInput(validateEmail); + const pw = useInput(validatePassword); + + const canSubmit = id.isValid && pw.isValid; + const handleLoginClick = () =>{canSubmit && navigate('/')}; return ( @@ -84,14 +25,33 @@ const Login = () => {
- + + {id.error && {id.error}}
- +
- + + {pw.error && {pw.error}}
- - navigate('/')}>로그인 + + 로그인
diff --git a/src/utils/validateInput.tsx b/src/utils/validateInput.tsx new file mode 100644 index 00000000..9ded11e0 --- /dev/null +++ b/src/utils/validateInput.tsx @@ -0,0 +1,12 @@ +export const validateEmail = (value: string): string | null => { + if (!value) return 'ID를 입력해주세요.'; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) return 'ID는 이메일 형식으로 입력해주세요.'; + return null; +}; + +export const validatePassword = (value: string): string | null => { + if (!value) return 'PW를 입력해주세요.'; + if (value.length < 8) return 'PW는 최소 8글자 이상이어야 합니다.'; + return null; +};