diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a1b7a1cb..76ed77ab 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -26,7 +26,12 @@ module.exports = { }, plugins: ['@typescript-eslint', 'react-hooks', 'react', 'prettier'], rules: { - 'prettier/prettier': 'error', + 'prettier/prettier': [ + 'error', + { + endOfLine: 'auto', + }, + ], 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': [ 'warn', diff --git a/src/App.tsx b/src/App.tsx index b6c93b6f..6d1119e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,9 @@ -import Input from '@components/atoms/Input'; -import { useState } from 'react'; +import MyForm from '@components/form/FormGroup'; function App() { - const [text, setTest] = useState(''); - return ( <> - setTest(e.target.value)} - /> + ); } diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 00000000..ff30aad4 --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/atoms/Input.tsx b/src/components/atoms/Input.tsx index acd7d525..6c26271b 100644 --- a/src/components/atoms/Input.tsx +++ b/src/components/atoms/Input.tsx @@ -1,19 +1,37 @@ -import { ChangeEvent, HTMLAttributes } from 'react'; +import { ChangeEventHandler, HTMLAttributes } from 'react'; import { formatClassName } from '../../utils/classNames'; +import checkIcon from '../../assets/icons/check.svg'; +import { UseFormRegisterReturn } from 'react-hook-form'; interface InputProps extends HTMLAttributes { - name: string; + id: string; placeholder?: string; type?: string; + register: UseFormRegisterReturn; + onChange?: ChangeEventHandler; value?: string; - onChange?: (e: ChangeEvent) => void; - className?: string; } -const Input = ({ className, ...props }: InputProps) => { - const inputClass = formatClassName(className); +const Input = ({ register, 'aria-invalid': isInvalid, ...props }: InputProps) => { + const hasValue = props.value && props.value.length > 0; + const isValid = isInvalid === 'false' && hasValue; + const inputClass = formatClassName(hasValue && !isValid && 'error'); - return ; + console.log(isInvalid, props.value); + console.log(register.name); + + return ( +
+ + {isValid && check icon} +
+ ); }; export default Input; diff --git a/src/components/atoms/Label.tsx b/src/components/atoms/Label.tsx new file mode 100644 index 00000000..d8411422 --- /dev/null +++ b/src/components/atoms/Label.tsx @@ -0,0 +1,10 @@ +interface LabelProps { + name: string; + text: string; +} + +const Label = ({ name, text }: LabelProps) => { + return ; +}; + +export default Label; diff --git a/src/components/form/FormGroup.tsx b/src/components/form/FormGroup.tsx new file mode 100644 index 00000000..c15622b5 --- /dev/null +++ b/src/components/form/FormGroup.tsx @@ -0,0 +1,55 @@ +import Input from '@components/atoms/Input'; +import Label from '@components/atoms/Label'; +import { USERNAME_PATTERN } from '../../constants/constants'; +import { useForm } from 'react-hook-form'; + +interface FormValues { + name: string; + email: string; +} + +const FormGroup = () => { + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ mode: 'onChange', defaultValues: { name: '', email: '' } }); + + const onSubmit = (data: FormValues) => { + console.log(data); + }; + + return ( +
+
+
+ +
+ ); +}; + +export default FormGroup; diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 00000000..9da7fec1 --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1,8 @@ +export const USERNAME_PATTERN = /^(?!.*[ㄱ-ㅎㅏ-ㅣ])[a-z0-9ㄱ-힇]+$/; +export const PASSWORD_PATTERN = + /^(?=(?:[^a-zA-Z]*[a-zA-Z]))(?=(?:\D*\d))(?=(?:[a-zA-Z0-9]*[~!-_@#]))[a-zA-Z0-9~!-_]+$/; + +export const PATTERNS = { + username: USERNAME_PATTERN, + password: PASSWORD_PATTERN, +}; diff --git a/src/styles/abstracts/_variables.scss b/src/styles/abstracts/_variables.scss index e69de29b..085a6e0e 100644 --- a/src/styles/abstracts/_variables.scss +++ b/src/styles/abstracts/_variables.scss @@ -0,0 +1,6 @@ +$light-gray: #f2f2f2; +$blue: #2273ed; +$red-400: #fc4c70; +$white: #ffffff; +$black: #333333; +$gray: #7f7f7f; diff --git a/src/styles/base/_base.scss b/src/styles/base/_base.scss index 18c2b8ae..e99cdb68 100644 --- a/src/styles/base/_base.scss +++ b/src/styles/base/_base.scss @@ -1,17 +1,50 @@ -input { - font-size: 14px; - font-family: 'spoqa Han Sans Neo', 'sans-serif'; +.form-group { + display: grid; + gap: 8px; + + span { + font-size: 12px; + color: $red-400; + } +} + +.input-wrapper { + position: relative; + + img { + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + width: 20px; + height: 20px; + } + + input { + font-size: 14px; + font-family: 'spoqa Han Sans Neo', 'sans-serif'; + color: $black; + + width: 100%; + height: 28px; - width: 100%; - height: 40px; + outline: none; + border: none; + border-bottom: 1px solid $light-gray; - outline: none; - border: none; - border-bottom: 1px solid #f2f2f2; + transition: border-bottom 0.2s ease-in-out; - transition: border-bottom 0.2s ease-in-out; + &:focus-within { + border-bottom: 1px solid $blue; + } - &:focus-within { - border-bottom: 1px solid #fd8da4; + &.error { + border-bottom: 1px solid $red-400; + } } } + +label { + font-size: 14px; + color: $gray; +}