Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 2 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Input
className=""
name="name"
value={text}
placeholder="test"
onChange={(e) => setTest(e.target.value)}
/>
<MyForm />
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 25 additions & 7 deletions src/components/atoms/Input.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement> {
name: string;
id: string;
placeholder?: string;
type?: string;
register: UseFormRegisterReturn;
onChange?: ChangeEventHandler<HTMLInputElement>;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => 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 <input className={inputClass} {...props} autoComplete="off" />;
console.log(isInvalid, props.value);
console.log(register.name);

return (
<div className="input-wrapper">
<input
className={inputClass}
{...props}
aria-invalid={isInvalid}
{...register}
autoComplete="off"
/>
{isValid && <img src={checkIcon} alt="check icon" />}
</div>
);
};

export default Input;
10 changes: 10 additions & 0 deletions src/components/atoms/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
interface LabelProps {
name: string;
text: string;
}

const Label = ({ name, text }: LabelProps) => {
return <label htmlFor={name}>{text}</label>;
};

export default Label;
55 changes: 55 additions & 0 deletions src/components/form/FormGroup.tsx
Original file line number Diff line number Diff line change
@@ -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<FormValues>({ mode: 'onChange', defaultValues: { name: '', email: '' } });

const onSubmit = (data: FormValues) => {
console.log(data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-group">
<Label name="name" text="이름" />
<Input
id="name"
value={watch('name', '')}
aria-invalid={errors.name ? 'true' : 'false'}
placeholder="홍길동"
register={register('name', {
required: true,
minLength: {
value: 10,
message: '10자 이상 입력해주세요.',
},
maxLength: {
value: 20,
message: '20자 이하로 입력해주세요.',
},
pattern: {
value: USERNAME_PATTERN,
message: '유효하지 않은 이름입니다.',
},
})}
/>
{errors.name && <span>{errors.name.message}</span>}
</div>
<input type="submit" value="submit" />
</form>
);
};

export default FormGroup;
8 changes: 8 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -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,
};
6 changes: 6 additions & 0 deletions src/styles/abstracts/_variables.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$light-gray: #f2f2f2;
$blue: #2273ed;
$red-400: #fc4c70;
$white: #ffffff;
$black: #333333;
$gray: #7f7f7f;
55 changes: 44 additions & 11 deletions src/styles/base/_base.scss
Original file line number Diff line number Diff line change
@@ -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;
}