All files / src/components/pages/create-group/fields/images-field index.tsx

0% Statements 0/118
0% Branches 0/1
0% Functions 0/1
0% Lines 0/118

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119                                                                                                                                                                                                                                             
'use client';

import Image from 'next/image';

import { useRef } from 'react';

import { AnyFieldApi } from '@tanstack/react-form';

import { Icon } from '@/components/icon';
import { useUploadGroupImages } from '@/hooks/use-group/use-group-upload-images';
import { cn } from '@/lib/utils';
import { ALLOWED_IMAGE_TYPES } from '@/types/service/common';
import { PreUploadGroupImageResponse } from '@/types/service/group';

interface Props {
  field: AnyFieldApi;
}

export const GroupImagesField = ({ field }: Props) => {
  const { mutateAsync } = useUploadGroupImages();

  const onUploadImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const maxAllowed = 3 - field.state.value.length;
    const files = e.target.files;

    if (!files || files.length === 0) return;
    if (files.length > maxAllowed) return;

    const fileArray = Array.from(files);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const invalidFile = fileArray.find((file) => !ALLOWED_IMAGE_TYPES.includes(file.type as any));

    if (invalidFile) {
      alert('jpg, png, webp 파일만 업로드 가능합니다.');
      e.target.value = '';
      return;
    }

    const response = await mutateAsync({
      images: fileArray,
    });

    field.handleChange([...field.state.value, ...response.images]);
  };

  const onUploadImageButtonClick = () => {
    if (!inputRef.current) return;
    inputRef.current.click();
  };

  const onRemoveImageClick = (removeIdx: number) => {
    const removedArray = field.state.value.filter(({}, idx: number) => idx !== removeIdx);
    field.handleChange(removedArray);
  };

  const inputRef = useRef<HTMLInputElement | null>(null);

  return (
    <div className='space-y-1'>
      <div className='mt-6 flex flex-row gap-2'>
        <button
          className={cn(
            'flex-center bg-mono-white group aspect-square w-full max-w-20 cursor-pointer rounded-2xl border-1 border-gray-300', // 기본 스타일
            'hover:bg-gray-50', // hover 스타일
            'transition-all duration-300', // animation 스타일
          )}
          aria-label='이미지 선택 버튼'
          type='button'
          onClick={onUploadImageButtonClick}
        >
          <Icon
            id='plus'
            className={cn(
              'size-6 text-gray-600', // 기본 스타일
              'group-hover:scale-120', // hover 스타일
              'transition-all duration-300', // animation 스타일
            )}
          />
          <input
            ref={inputRef}
            className='hidden'
            accept='image/*'
            multiple
            type='file'
            onChange={(e) => onUploadImage(e)}
          />
        </button>
        {field.state.value.map(
          ({ imageUrl100x100 }: PreUploadGroupImageResponse['images'][0], idx: number) => (
            <div key={imageUrl100x100} className='relative aspect-square w-full max-w-20'>
              <Image
                className='border-mono-black/5 h-full w-full rounded-2xl border-1 object-cover'
                alt='썸네일 이미지'
                fill
                src={imageUrl100x100}
              />

              <button
                className={cn(
                  'flex-center bg-mono-white/80 group absolute top-1.5 right-2 size-4 cursor-pointer rounded-full', // 기본 스타일
                  'hover:bg-mono-white hover:scale-110', // hover 스타일
                  'transition-all duration-300', // animation 스타일
                )}
                aria-label='이미지 삭제 버튼'
                type='button'
                onClick={() => onRemoveImageClick(idx)}
              >
                <Icon id='small-x-1' className='size-1.5 text-gray-700' />
              </button>
            </div>
          ),
        )}
      </div>
      <p className='text-text-sm-medium px-2 text-gray-500'>최대 3개까지 업로드할 수 있어요.</p>
    </div>
  );
};