ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • react-hook-form typescript로 변환 및 컴포넌트 생성 (antd)
    Language/React 2023. 12. 27. 17:42

    이번 포스팅에서는 react-hook-form을 전체 사이트에 적용하기에 앞서 코드 라인을 줄여서 가독성을 높이고 타입스크립트로 컴포넌트를 제작해 두려고 한다.

     

    현재 javascript 및 typescript가 혼재 되어 있지만.. 시간될때 마다 수정해 나가자! 언젠간.. typescript만 남으리라!!

     

    react-hook-form component 적용

    import { useForm } from 'react-hook-form';
    import { NInput, NInputNumber, NTextArea, NCheckbox, NSelect, NRadioGroup, NTinyMceEditor, NImageSelector } from 'components/FormControls';
    
    
    const TestPop = (props) => {
      ...
      
      const {
        handleSubmit,
        watch,
        formState: { errors },
        control,
        reset,
      } = useForm({
        defaultValues: {},
      });
      
      ...
      
      return (
    
        ...
    
        <NInput
            name="TITLE"
            control={control}
            rules={{ required: '제목은 필수 입력입니다.' }}
            placeholder="제목을 입력해주세요."
            maxLength={50}
        />
    
        <NInputNumber name="WEIGHT" control={control} placeholder="정렬 가중치를 입력해주세요." maxLength={4} />
    
        <NCheckbox label="고방" name="GOBANG_CATEGORY_YN" control={control} checked={watch('GOBANG_CATEGORY_YN')} />
    
        <NRadioGroup name="DISPLAY_TYPE" control={control} rules={{ required: '공개설정은 필수 입력입니다.' }}>
            {Object.keys(DISPLAY_TYPES).map((item, idx) => (
              <Radio key={idx} value={item}>
                {DISPLAY_TYPES[item]}
              </Radio>
            ))}
        </NRadioGroup>
    
        <NSelect
            name="TAG_ITEMS"
            control={control}
            rules={{ required: '태그는 필수 입력입니다.' }}
            mode="tags"
            tokenSeparators={[' ']}
            placeholder="태그 선택"
            options={options}
        />
    
        <NTextArea name="DESCRIPTION" control={control} placeholder="그룹 요약을 입력해주세요." rows={2} showCount={true} maxLength={200} />
    
    
        <NTinyMceEditor
            name="CONTENT"
            control={control}
            rules={{ required: '콘텐츠 내용은 필수 입력입니다.' }}
            // placeholder="aa"
            height="50vh"
        />
    
        <NImageSelector name="TITLE_IMAGE" control={control} rules={{ required: '커버이미지는 필수 입력입니다.' }} type="single" />
    
        <NImageSelector control={control} name="IMAGE1" value={watch('IMAGE1')} rules={{ required: true }} />
    
        {props.target === 'THEME' && (
          <NImageSelector control={control} name="IMAGE2" value={watch('IMAGE2')} rules={{ required: true }} />
        )}
        
        <NDatePicker
          control={control}
          name="RECEIPT_FROM_DT"
          rules={{ required: '접수시작일은(는) 필수 입력입니다.' }}
          placeholder="접수시작일 선택"
          allowClear={true}
          disabledDate={(current) => current && current < moment().subtract(1, 'days').endOf('day')}
          onChange={(date, dateString) => {
            const theFromYmd = date ? date.format('YYYYMMDD') : null;
            const theToYmd = watch('RECEIPT_TO_DT') ? watch('RECEIPT_TO_DT').format('YYYYMMDD') : null;
    
            if (theFromYmd !== null && theToYmd !== null && theFromYmd > theToYmd) {
              // RECEIPT_FROM_DT.isAfter(RECEIPT_TO_DT)
              setValue('RECEIPT_TO_DT', null);
            }
          }}
        />
    	...
      
      );
    }

     

     

    FormControls.tsx

    import React from 'react';
    import { Input, InputNumber, Checkbox, Radio, Select, DatePicker } from 'antd';
    import TinyMceEditor from 'components/TinyMceEditor';
    import ImageSingle from 'components/ImageSingle';
    // import ImageSelector from 'components/ImageSelector';
    // import CorverImageSelector from 'components/CorverImageSelector';
    import moment from 'moment';
    
    import { Controller } from 'react-hook-form';
    import FormErrorMessage from 'components/FormErrorMessage';
    
    interface NProps {
      // type: string;
      // label: string;
      control: any;
      name: string;
      rules: Record<string, any>;
      placeholder: string;
      children: React.ReactNode;
    
      disabled?: boolean;
      showErrorMessage?: boolean;
    }
    
    interface NInputProps extends NProps {
      maxLength: number;
      showCount?: boolean;
    }
    
    export const NInput = ({ placeholder = '', maxLength, showCount = false, ...rest }: NInputProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              {/* type={type} bordered={false} */}
              <Input
                {...field}
                maxLength={maxLength}
                showCount={showCount}
                placeholder={placeholder}
                disabled={rest.disabled}
                className={fieldState.invalid ? 'error' : ''}
              />
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NInputNumberProps extends NProps {
      maxLength: number;
    }
    
    export const NInputNumber = ({ placeholder = '', maxLength, ...rest }: NInputNumberProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <InputNumber
                {...field}
                maxLength={maxLength}
                placeholder={placeholder}
                disabled={rest.disabled}
                className={fieldState.invalid ? 'error' : ''}
              />
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NCheckboxProps extends NProps {
      label: string;
      checked?: boolean;
    }
    export const NCheckbox = ({ label, checked, ...rest }: NCheckboxProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <Checkbox {...field} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''} checked={checked}>
                {label}
              </Checkbox>
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NRadioGroupProps extends NProps {}
    export const NRadioGroup = ({ ...rest }: NRadioGroupProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <div className={'row-box' + (fieldState.invalid ? ' error' : '')}>
                <Radio.Group {...field} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''}>
                  {rest.children}
                </Radio.Group>
              </div>
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NSelectOption {
      label: string | number | null;
      value: string | number | null;
    }
    
    interface NSelectProps extends NProps {
      mode?: 'multiple' | 'tags';
      options?: NSelectOption[];
      tokenSeparators?: string[];
    }
    export const NSelect = ({ mode, placeholder = '', options, tokenSeparators, ...rest }: NSelectProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <Select
                {...field}
                disabled={rest.disabled}
                className={fieldState.invalid ? 'error' : ''}
                placeholder={placeholder}
                mode={mode}
                tokenSeparators={tokenSeparators}
                options={options}
              >
                {rest.children}
              </Select>
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NTextAreaProps extends NProps {
      rows: number;
      maxLength: number;
      showCount?: boolean;
    }
    export const NTextArea = ({ placeholder = '', rows, maxLength, showCount = false, ...rest }: NTextAreaProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <Input.TextArea
                {...field}
                placeholder={placeholder}
                rows={rows}
                showCount={showCount}
                maxLength={maxLength}
                disabled={rest.disabled}
                className={fieldState.invalid ? 'error' : ''}
              />
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NTinyMceEditorProps extends NProps {
      height?: string;
    }
    export const NTinyMceEditor = ({ placeholder = '', height = '30vh', ...rest }: NTinyMceEditorProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <div className={'row-box' + (fieldState.invalid ? ' error' : '')}>
                <TinyMceEditor
                  {...field}
                  height={height}
                  content={field.value}
                  placeholder={placeholder}
                  onChange={(content: string) => {
                    field.onChange(content);
                  }}
                />
              </div>
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    interface NImageSelectorProps extends NProps {
      // type?: string | undefined;
    }
    export const NImageSelector = ({ ...rest }: NImageSelectorProps) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <div className={'row-image-box' + (fieldState.invalid ? ' error' : '')}>
                <ImageSingle
                  {...field}
                  label="사진선택"
                  // file={field.value && { src: field.value }}
                  file={field.value}
                  onChange={(value: { src: any } | null) => {
                    if (value !== null && !Array.isArray(value)) {
                      field.onChange(value);
                    } else {
                      field.onChange(null);
                    }
                  }}
                />
    
                {/* <CorverImageSelector
                  src={field.value && field.value}
                  onChange={(file: { src: any } | null) => {
                    if (file !== null) {
                      field.onChange(file.src);
                    } else {
                      field.onChange(null);
                    }
                  }}
                /> */}
              </div>
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };
    
    type DatePickerOnChange = (date: moment.Moment | null, dateString: string) => void;
    
    interface NDatePickerProps extends NProps {
      disabledDate: (current: any | null) => any;
      allowClear?: boolean;
      value?: moment.Moment | null | undefined;
      onChange?: DatePickerOnChange;
    }
    
    export const NDatePicker: React.FC<NDatePickerProps> = ({ placeholder = '', ...rest }) => {
      return (
        <Controller
          name={rest.name}
          control={rest.control}
          rules={rest.rules}
          render={({ field, fieldState }) => (
            <>
              <DatePicker
                {...field}
                placeholder={placeholder}
                className={fieldState.invalid ? 'error' : ''}
                value={rest.value !== undefined ? rest.value : field.value && moment(field.value, 'YYYYMMDD')}
                // value={field.value && moment(field.value, 'YYYYMMDD')}
                disabled={rest.disabled}
                disabledDate={rest.disabledDate}
                allowClear={rest.allowClear}
                onChange={(date, dateString) => {
                  field.onChange(date); // Trigger the React Hook Form onChange event
                  if (rest.onChange) {
                    rest.onChange(date, dateString); // Call the passed onChange prop
                  }
                }}
              />
    
              {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && (
                <FormErrorMessage message={fieldState.error?.message} />
              )}
            </>
          )}
        />
      );
    };

     

    ImageSingle.tsx

    import React, { useContext } from 'react';
    import { Image } from 'antd';
    import { Request } from 'lib/common';
    import { BaseContext } from 'components/Contexts';
    
    interface ImageSingleProps {
      label?: string | undefined;
      file?: { src: any } | null | undefined;
      className?: string | undefined;
      thumbWith?: number | undefined;
      thumbHeight?: number | undefined;
      onChange: (file: { src: any } | null) => void;
    }
    
    const ImageSingle: React.FC<ImageSingleProps> = ({ label = '사진선택', file, className = '', ...props }) => {
      const context = useContext(BaseContext);
    
      const handleImageChange = async (e: { target: { files: any } }) => {
        const formData = new FormData();
        // formData.append('THUMB_WIDTH', props.thumbWith ? props.thumbWith : 104 * 2);
        // formData.append('THUMB_HEIGHT', props.thumbHeight ? props.thumbHeight : 104 * 2);
        formData.append('THUMB_WIDTH', props.thumbWith ? String(props.thumbWith) : String(104 * 2));
        formData.append('THUMB_HEIGHT', props.thumbHeight ? String(props.thumbHeight) : String(104 * 2));
    
        for (const file of e.target.files) {
          formData.append('file', file);
        }
    
        const response = await Request.upload('/image', formData);
        if (response.status === 200) {
          props.onChange({ src: response.locationList[0] });
        } else {
          context.alert({
            title: (
              <>
                한 번에 업로드 가능한 이미지 용량은 최대 20MB를 초과할 수 없습니다.
                <br />
                다수의 이미지를 등록 하신다면, 하나씩 나눠서 업로드하는 것을 권장합니다.
              </>
            ),
            buttons: [{ text: '확인', type: 'primary', danger: true }],
          });
        }
      };
    
      const handleDelete = (idx: number) => {
        props.onChange(null);
      };
    
      return (
        <div className={'wrap-image-selector ' + className}>
          {file ? (
            <div className="wrap-images">
              <ImageItem idx={0} file={file} onDelete={handleDelete} />
            </div>
          ) : (
            <div className="select-image">
              {label}
              <input
                type="file"
                accept="image/png, image/gif, image/jpeg, image/bmp, image/x-icon, image/webp"
                // multiple={type !== 'single'}
                onChange={handleImageChange}
              />
            </div>
          )}
        </div>
      );
    };
    
    interface ImageItemProps {
      idx: number;
      file?: { src: any } | null | undefined;
      onDelete: (arg0: any) => void;
    }
    
    const ImageItem: React.FC<ImageItemProps> = ({ file, ...props }) => {
      const context = useContext(BaseContext);
    
      return (
        <div className="item">
          <div>
            <Image src={file && file.src} alt="" className="center-cropped" />
    
            <button
              className="btn-delete"
              onClick={(e) => {
                e.stopPropagation();
    
                context.alert({
                  title: '삭제 하시겠습니까?',
                  buttons: [
                    {
                      text: '삭제',
                      type: 'primary',
                      danger: true,
                      onClick: () => {
                        props.onDelete(props.idx);
                      },
                    },
                    { text: '취소' },
                  ],
                });
              }}
            >
              <span>삭제</span>
            </button>
          </div>
        </div>
      );
    };
    
    export default ImageSingle;

     

    버전도 너무 옛날 버전이고 부분부분 바꿀려니 개노가다네 ㅋ

    어거지로 할려니 개빡시당..ㅠ 아자아자!!

     

     

     

    rules - pattern

    <NInput
        control={control}
        name="TITLE"
    
        ...
        rules={{ required: '제목은(는) 필수 입력입니다.' }}
        rules={{ required: '제목은(는) 필수 입력입니다.', pattern: /^\S+@\S+$/i }}
        rules={{ required: { value: true, message: '제목은(는) 필수 입력입니다.' }, pattern: /^\S+@\S+$/i }}
        rules={{
            required: { value: true, message: '제목은(는) 필수 입력입니다.' },
            pattern: { value: /^\S+@\S+$/i, message: '정규식이 맞지 않아...' },
        }}
        rules={{
            pattern: {
                value: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i,
                message: '이메일 형식이 아닙니다.',
            },
            minLength: {
                value: 8,
                message: '비밀번호는 8자 이상이여야 합니다,',
            },
        }}
        ...
    
        placeholder="제목을(를) 입력해주세요."
        maxLength={300}
    />

     

    rules - validate

    <NInputNumber
        control={control}
        name="BIRTH_YEAR"
        rules={{
          required: { value: true, message: '출생연도를 입력해주세요.' },
          validate: (value: string) => {
            const currentYear = new Date().getFullYear();
            const birthYear = parseInt(value);
            const age = currentYear - birthYear - 1; // 만나이
            if (age < 0 || age > 100) {
              return '올바른 출생년도를 입력하세요.';
            } else if (age < 14) {
              return '만 14세 이상만 가입이 가능합니다.';
            }
          },
        }}
        placeholder="출생연도 입력"
        maxLength={4}
    />

     

    rules - pattern & validate

    <NInput
        control={control}
        name="NICKNAME"
        rules={{
          required: { value: true, message: '닉네임을 입력해주세요.' },
          pattern: {
            value: /^[가-힣a-zA-Z0-9]*$/,
            message: '띄어쓰기 없이 한글, 영문, 숫자만 가능합니다.',
            // value: /^[가-힣a-zA-Z0-9]{2,12}$/,
            // message: '2~12내에서 띄어쓰기 없이 한글, 영문, 숫자만 가능합니다.',
          },
          validate: {
            lengthCheck: (value: string) => (value.length >= 2 && value.length <= 12) || '2에서 12자 사이로 입력하세요.',
          },
        }}
        placeholder="닉네임 입력"
        maxLength={12}
    />

     

    상황에 따라 필수여부 변경

    rules={{ required: watch('floatingButtonShowYn') === 'Y' ? '플로팅 랜딩 URL은 필수 입력입니다.' : false }}

     

    댓글

Designed by Tistory.