Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#388] Input, Select 컴포넌트 에러 처리 #389

Merged
merged 10 commits into from
Aug 11, 2023
4 changes: 2 additions & 2 deletions public/icons/warning-circle.svg
gxxrxn marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 63 additions & 22 deletions src/stories/Base/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Meta, StoryObj } from '@storybook/react';
import Input from '@/ui/Base/Input';
import Button from '@/ui/Base/Button';
import { SubmitHandler, useForm } from 'react-hook-form';
import { Control, SubmitHandler, useForm, useWatch } from 'react-hook-form';
import ErrorMessage from '@/ui/Base/ErrorMessage';

const meta: Meta<typeof Input> = {
title: 'Base/Input',
Expand All @@ -22,6 +23,7 @@ const InputWithUseForm = () => {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm<DefaultValues>({
mode: 'all',
Expand All @@ -36,23 +38,32 @@ const InputWithUseForm = () => {
onSubmit={handleSubmit(handleSubmitForm)}
className="flex w-[43rem] flex-col gap-[1.6rem]"
>
<Input
placeholder="이름을 입력해주세요."
{...register('name', {
required: '필수 항목입니다.',
minLength: { value: 2, message: '2자 이상 입력해 주세요.' },
maxLength: { value: 10, message: '10자 이하 입력해 주세요.' },
})}
error={errors.name}
/>
<Input
placeholder="나이를 입력해 주세요."
{...register('age', {
pattern: { value: /^[0-9]+$/, message: '숫자만 입력 가능해요' },
min: { value: 0, message: '0살부터 입력 가능해요.' },
})}
error={errors.age}
/>
<div className="flex flex-col gap-[0.5rem]">
<Input
placeholder="이름을 입력해주세요."
{...register('name', {
required: '필수 항목입니다.',
minLength: { value: 2, message: '2자 이상 입력해 주세요.' },
maxLength: { value: 10, message: '10자 이하 입력해 주세요.' },
})}
error={!!errors.name}
/>
<div className="flex flex-row-reverse justify-between gap-[0.4rem]">
<Length control={control} minLength={2} maxLength={10} />
{errors.name && <ErrorMessage>{errors.name.message}</ErrorMessage>}
</div>
</div>
<div className="flex flex-col gap-[0.5rem]">
<Input
placeholder="나이를 입력해 주세요."
{...register('age', {
pattern: { value: /^[0-9]+$/, message: '숫자만 입력 가능해요' },
min: { value: 0, message: '0살부터 입력 가능해요.' },
})}
error={!!errors.age}
/>
{errors.age && <ErrorMessage>{errors.age.message}</ErrorMessage>}
</div>
<Button
size="large"
type="submit"
Expand All @@ -63,6 +74,33 @@ const InputWithUseForm = () => {
</form>
);
};

const Length = ({
control,
minLength,
maxLength,
}: {
control: Control<DefaultValues>;
minLength: number;
maxLength: number;
}) => {
const nickname = useWatch({
control,
name: 'name',
});

const currentLength = nickname ? nickname.length : 0;
const isError = minLength > currentLength || currentLength > maxLength;
const textColor = isError ? 'text-warning-800 ' : 'text-main-900';

return (
<div>
<span className={textColor}>{currentLength}</span>/
{maxLength}
</div>
);
};

export const Default: Story = {
args: {
placeholder: '입력해 주세요.',
Expand All @@ -72,11 +110,14 @@ export const Default: Story = {
export const Invalid: Story = {
args: {
placeholder: '입력해 주세요.',
error: {
type: 'value',
message: '에러 메시지에요.',
},
error: true,
},
render: args => (
<div className="flex flex-col gap-[0.5rem]">
<Input {...args} />
<ErrorMessage>양식을 확인해주세요.</ErrorMessage>
</div>
),
};

export const WithUseForm: Story = {
Expand Down
65 changes: 39 additions & 26 deletions src/stories/Base/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
import Select from '@/ui/Base/Select';
import { SubmitHandler, useForm } from 'react-hook-form';
import Button from '@/ui/Base/Button';
import ErrorMessage from '@/ui/Base/ErrorMessage';

const meta: Meta<typeof Select> = {
title: 'Base/Select',
Expand Down Expand Up @@ -39,30 +40,36 @@ const SelectWithUseForm = () => {
onSubmit={handleSubmit(handleSubmitForm)}
className="flex w-[43rem] flex-col gap-[1.6rem]"
>
<Select
placeholder="숫자를 선택해주세요. (필수)"
{...register('requiredNumber', {
required: '필수 항목입니다.',
})}
errorMessage={errors.requiredNumber?.message}
>
{numbers.map(number => (
<Select.Option key={number} value={number}>
{number}
</Select.Option>
))}
</Select>
<Select
placeholder="숫자를 선택해주세요."
{...register('number')}
errorMessage={errors.number?.message}
>
{numbers.map(number => (
<Select.Option key={number} value={number}>
{number}
</Select.Option>
))}
</Select>
<div className="flex flex-col gap-[0.5rem]">
<Select
placeholder="숫자를 선택해주세요. (필수)"
{...register('requiredNumber', {
required: '필수 항목입니다.',
})}
error={!!errors.requiredNumber}
>
{numbers.map(number => (
<Select.Option key={number} value={number}>
{number}
</Select.Option>
))}
</Select>
{errors.requiredNumber && <ErrorMessage>{errors.requiredNumber.message}</ErrorMessage>}
</div>
<div className="flex flex-col gap-[0.5rem]">
<Select
placeholder="숫자를 선택해주세요."
{...register('number')}
error={!!errors.number}
>
{numbers.map(number => (
<Select.Option key={number} value={number}>
{number}
</Select.Option>
))}
</Select>
{errors.number && <ErrorMessage>{errors.number.message}</ErrorMessage>}
</div>
<Button
size="large"
type="submit"
Expand Down Expand Up @@ -93,9 +100,15 @@ export const Default: Story = {

export const Invalid: Story = {
args: {
placeholder: '선택해 주세요.',
errorMessage: '에러 메시지에요.',
placeholder: '입력해 주세요.',
error: true,
},
render: args => (
<div className="flex flex-col gap-[0.5rem]">
<Select {...args} />
<ErrorMessage>양식을 확인해주세요.</ErrorMessage>
</div>
),
};

export const WithUseForm: Story = {
Expand Down
13 changes: 13 additions & 0 deletions src/ui/Base/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IconWarningCircle } from '@public/icons';
import { ReactNode } from 'react';

const ErrorMessage = ({ children }: { children: ReactNode }) => {
return (
<div className="flex items-center gap-[0.4rem] text-xs text-warning-800">
<IconWarningCircle />
minjongbaek marked this conversation as resolved.
Show resolved Hide resolved
{children}
</div>
);
};

export default ErrorMessage;
13 changes: 7 additions & 6 deletions src/ui/Base/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { ComponentPropsWithoutRef, forwardRef, Ref } from 'react';
import { FieldError } from 'react-hook-form';

interface InputProps extends ComponentPropsWithoutRef<'input'> {
error?: FieldError;
error?: boolean;
}

const Input = ({ error, ...props }: InputProps, ref: Ref<HTMLInputElement>) => {
const Input = (
{ error, children, ...props }: InputProps,
ref: Ref<HTMLInputElement>
) => {
const borderColor = error
? 'border-warning-800 focus:border-warning-800'
: 'border-black-400 focus:border-main-900';
return (
<div className="flex flex-col gap-[0.5rem] text-sm">
<div className="text-sm">
<input
className={`w-full rounded-[0.5rem] border-[0.05rem] px-[1rem] py-[1.3rem] outline-none ${borderColor}`}
{...props}
ref={ref}
/>
{/* TODO: 에러 메시지 컴포넌트로 교체 */}
{error && <div className="text-warning-800">{error.message}</div>}
{children}
</div>
);
};
Expand Down
17 changes: 8 additions & 9 deletions src/ui/Base/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ interface SelectProps
ComponentPropsWithoutRef<'select'>,
'className' | 'defaultValue' | 'required'
> {
errorMessage?: string;
error?: boolean;
}

const Select = (
{ errorMessage, children, placeholder, ...props }: SelectProps,
const _Select = (
{ error, children, placeholder, ...props }: SelectProps,
ref: Ref<HTMLSelectElement>
) => {
const borderColor = errorMessage
const borderColor = error
? 'border-warning-800'
: 'border-black-400 focus:border-main-900';

return (
<div className="flex flex-col gap-[0.5rem] text-sm">
<div className="text-sm">
<select
ref={ref}
defaultValue=""
required
className={`rounded-[0.5rem] border-[0.05rem] px-[1.0rem] py-[1.3rem] outline-none ${borderColor} cursor-pointer appearance-none bg-[url('/icons/select-icon.svg')] bg-[calc(100%-1rem)_center] bg-no-repeat invalid:text-placeholder`}
className={`rounded-[0.5rem] border-[0.05rem] px-[1.0rem] py-[1.3rem] outline-none ${borderColor} w-full cursor-pointer appearance-none bg-[url('/icons/select-icon.svg')] bg-[calc(100%-1rem)_center] bg-no-repeat invalid:text-placeholder`}
minjongbaek marked this conversation as resolved.
Show resolved Hide resolved
{...props}
>
{placeholder && (
Expand All @@ -32,7 +32,6 @@ const Select = (
)}
{children}
</select>
{errorMessage && <div className="text-warning-800">{errorMessage}</div>}
</div>
);
};
Expand All @@ -49,6 +48,6 @@ const Option = ({
);
};

const ExportSelect = Object.assign(forwardRef(Select), { Option });
const Select = Object.assign(forwardRef(_Select), { Option });

export default ExportSelect;
export default Select;