Skip to content

Commit

Permalink
refactor: 상품 등록 폼 컴포넌트 분리
Browse files Browse the repository at this point in the history
  • Loading branch information
heonq committed Dec 12, 2024
1 parent 1d31dcb commit 7a70eaf
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 101 deletions.
31 changes: 31 additions & 0 deletions src/components/items/registration/productDescriptionInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import cn from '@/lib/cn';
import { ProductInputProps } from './types';

export default function ProductDescriptionInput({
register,
errors,
}: ProductInputProps) {
return (
<div className='input-section'>
<label className='input-label'>상품 소개</label>
<textarea
className={cn('input-base', errors.description && 'error-input')}
placeholder='상품 소개를 입력해주세요'
{...register('description', {
required: '상품 소개를 입력해주세요',
minLength: {
value: 10,
message: '10자 이상 입력해주세요',
},
maxLength: {
value: 100,
message: '100자 이하로 입력해주세요',
},
})}
/>
{errors.description && (
<span className='error-message'>{errors.description.message}</span>
)}
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/items/registration/productNameInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import cn from '@/lib/cn';
import { ProductInputProps } from './types';

export default function ProductNameInput({
register,
errors,
}: ProductInputProps) {
return (
<div className='input-section'>
<label className='input-label'>상품명</label>
<input
className={cn('input-base', errors.name && 'error-input')}
placeholder='상품명을 입력해주세요'
{...register('name', {
required: '상품명을 입력해주세요',
maxLength: {
value: 10,
message: '10자 이내로 입력해주세요',
},
})}
/>
{errors.name && (
<span className='error-message'>{errors.name.message}</span>
)}
</div>
);
}
31 changes: 31 additions & 0 deletions src/components/items/registration/productPriceInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import cn from '@/lib/cn';
import { ProductInputProps } from './types';

export default function ProductPriceInput({
register,
errors,
}: ProductInputProps) {
return (
<div className='input-section'>
<label className='input-label'>판매가격</label>
<input
type='number'
className={cn('input-base', errors.price && 'error-input')}
placeholder='판매 가격을 입력해주세요'
{...register('price', {
valueAsNumber: true,
validate: {
isNumber: (value) => !Number.isNaN(value) || '숫자로 입력해주세요',
isInteger: (value) =>
(Number.isInteger(value) && value >= 0) ||
'양의 정수를 입력해주세요',
},
required: '판매 가격을 입력해주세요',
})}
/>
{errors.price && (
<span className='error-message'>{errors.price.message}</span>
)}
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/items/registration/productTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import cn from '@/lib/cn';
import { ProductTagInputProps } from './types';

export default function ProductTagInput({
tagInput,
handleAddTag,
setTagInput,
tagError,
}: ProductTagInputProps) {
return (
<div className='input-section'>
<label className='input-label'>태그</label>
<input
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleAddTag}
placeholder='태그를 입력해주세요.'
className={cn('input-base', tagError.length && 'error-input')}
/>
{tagError && <span className='error-message'>{tagError}</span>}
</div>
);
}
133 changes: 32 additions & 101 deletions src/components/items/registration/registrationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
'use client';

import CommonBtn from '@/components/common/commonBtn/commonBtn';
import cn from '@/lib/cn';
import { useForm } from 'react-hook-form';
import { ProductRegistrationFormData } from './types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faX } from '@fortawesome/free-solid-svg-icons/faX';
import { createProduct } from '@/services/api/product';
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import useTagInput from '@/hooks/useTagInput';
import ProductNameInput from './productNameInput';
import ProductDescriptionInput from './productDescriptionInput';
import ProductPriceInput from './productPriceInput';
import ProductTagInput from './productTagInput';
import TagsContainer from './tagsContainer';

const InputSectionStyle = 'flex flex-col gap-4 mb-8';
const InputStyle = 'bg-bg-input px-6 py-4 rounded-[12px]';
const LabelStyle = 'text-[18px] font-bold';
const errorMessageStyle = 'text-text-red font-semibold text-[14px]';
const errorInputStyle = 'border border-border-input-error';
export const InputSectionStyle = 'flex flex-col gap-4 mb-8';
export const InputStyle = 'bg-bg-input px-6 py-4 rounded-[12px]';
export const LabelStyle = 'text-[18px] font-bold';
export const errorMessageStyle = 'text-text-red font-semibold text-[14px]';
export const errorInputStyle = 'border border-border-input-error';

export default function ProductRegistrationForm({
defaultValue,
Expand Down Expand Up @@ -81,99 +83,28 @@ export default function ProductRegistrationForm({
등록
</CommonBtn>
</div>
<div className={cn(InputSectionStyle)}>
<label className={LabelStyle}>상품명</label>
<input
className={cn(InputStyle, errors.name && errorInputStyle)}
placeholder='상품명을 입력해주세요'
{...register('name', {
required: '상품명을 입력해주세요',
maxLength: {
value: 10,
message: '10자 이내로 입력해주세요',
},
})}
/>
{errors.name && (
<span className={errorMessageStyle}>{errors.name.message}</span>
)}
</div>
<div className={cn(InputSectionStyle)}>
<label className={LabelStyle}>상품 소개</label>
<textarea
className={cn(InputStyle, errors.description && errorInputStyle)}
placeholder='상품 소개를 입력해주세요'
{...register('description', {
required: '상품 소개를 입력해주세요',
minLength: {
value: 10,
message: '10자 이상 입력해주세요',
},
maxLength: {
value: 100,
message: '100자 이하로 입력해주세요',
},
})}
/>
{errors.description && (
<span className={errorMessageStyle}>
{errors.description.message}
</span>
)}
</div>
<div className={cn(InputSectionStyle)}>
<label className={LabelStyle}>판매가격</label>
<input
type='number'
className={cn(InputStyle, errors.price && errorInputStyle)}
placeholder='판매 가격을 입력해주세요'
{...register('price', {
valueAsNumber: true,
validate: {
isNumber: (value) =>
!Number.isNaN(value) || '숫자로 입력해주세요',
isInteger: (value) =>
(Number.isInteger(value) && value >= 0) ||
'양의 정수를 입력해주세요',
},
required: '판매 가격을 입력해주세요',
})}
/>
{errors.price && (
<span className={errorMessageStyle}>{errors.price.message}</span>
)}
</div>
<div className={cn(InputSectionStyle)}>
<label className={LabelStyle}>태그</label>
<input
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleAddTag}
placeholder='태그를 입력해주세요.'
className={InputStyle}
/>
{tagError && <span className={errorMessageStyle}>{tagError}</span>}
</div>
<div className='flex gap-3'>
{tags.map((tag, index) => (
<span
className='bg-bg-tag flex items-center justify-between px-3 py-[6px] rounded-[26px] gap-2'
key={tag}
>
#{tag}
<button
type='button'
className='w-5 h-5 bg-bg-close-button rounded-full flex items-center justify-center text-text-white-secondary'
onClick={() => handleRemoveTag(index)}
>
<FontAwesomeIcon
icon={faX}
className='w-2'
/>
</button>
</span>
))}
</div>
<ProductNameInput
register={register}
errors={errors}
/>
<ProductDescriptionInput
register={register}
errors={errors}
/>
<ProductPriceInput
register={register}
errors={errors}
/>
<ProductTagInput
tagInput={tagInput}
setTagInput={setTagInput}
handleAddTag={handleAddTag}
tagError={tagError}
/>
<TagsContainer
tags={tags}
handleRemoveTag={handleRemoveTag}
/>
</form>
</div>
);
Expand Down
31 changes: 31 additions & 0 deletions src/components/items/registration/tagsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { TagsContainerProps } from './types';

export default function TagsContainer({
tags,
handleRemoveTag,
}: TagsContainerProps) {
return (
<div className='flex gap-3'>
{tags.map((tag, index) => (
<span
className='bg-bg-tag flex items-center justify-between px-3 py-[6px] rounded-[26px] gap-2'
key={tag}
>
#{tag}
<button
type='button'
className='w-5 h-5 bg-bg-close-button rounded-full flex items-center justify-center text-text-white-secondary'
onClick={() => handleRemoveTag(index)}
>
<FontAwesomeIcon
icon={faX}
className='w-2'
/>
</button>
</span>
))}
</div>
);
}
19 changes: 19 additions & 0 deletions src/components/items/registration/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import { FieldErrors, UseFormRegister } from 'react-hook-form';

export interface ProductRegistrationFormData {
name: string;
description: string;
price: number;
tags: string[];
}

export interface ProductInputProps {
register: UseFormRegister<ProductRegistrationFormData>;
errors: FieldErrors<ProductRegistrationFormData>;
}

export interface ProductTagInputProps {
tagInput: string;
handleAddTag: (e: React.KeyboardEvent<HTMLInputElement>) => void;
setTagInput: (value: string) => void;
tagError: string;
}

export interface TagsContainerProps {
tags: string[];
handleRemoveTag: (index: number) => void;
}

0 comments on commit 7a70eaf

Please sign in to comment.