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 120 121 122 123 124 125 126 127 128 129 | 'use client'; import { useRef, useState } from 'react'; import { AnyFieldApi } from '@tanstack/react-form'; import { Icon } from '@/components/icon'; import { ImageLoadingBar } from '@/components/pages/create-group/fields/images-field/image-loading-bar'; import { GroupImage } from '@/components/shared'; import { Hint } from '@/components/ui'; import { useUploadGroupImages } from '@/hooks/use-group/use-group-upload-images'; import { cn } from '@/lib/utils'; import { validateImage } from '@/lib/validateImage'; import { PreUploadGroupImageResponse } from '@/types/service/group'; interface Props { field: AnyFieldApi; } export const GroupImagesField = ({ field }: Props) => { const [preUploadError, setPreUploadError] = useState(''); const inputRef = useRef<HTMLInputElement | null>(null); const { mutateAsync, isPending } = useUploadGroupImages(); const onUploadImage = async (e: React.ChangeEvent<HTMLInputElement>) => { if (!e.target.files || !e.target.files.length) return; const fileArray = Array.from(e.target.files); for (const file of fileArray) { const { valid, error } = await validateImage(file); if (!valid && error) { setPreUploadError(error); e.target.value = ''; return; } } const response = await mutateAsync({ images: fileArray, }); field.handleChange([...field.state.value, ...response.images]); setPreUploadError(''); }; 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); }; return ( <div className='space-y-1'> <ul className='mt-6 flex gap-2'> <li className='relative aspect-square w-full max-w-20'> <button className={cn( 'flex-center bg-mono-white group size-full cursor-pointer rounded-2xl border-1 border-gray-300', // 기본 스타일 'hover:bg-gray-50', 'transition-all duration-300', )} aria-label='이미지 선택 버튼' disabled={isPending} type='button' onClick={onUploadImageButtonClick} > {isPending ? ( <ImageLoadingBar /> ) : ( <Icon id='plus' className={cn( 'size-6 text-gray-600', 'group-hover:scale-120', 'transition-all duration-300', )} /> )} <input ref={inputRef} className='hidden' accept='image/*' multiple type='file' onChange={(e) => onUploadImage(e)} /> </button> </li> {field.state.value.map( ({ imageUrl100x100 }: PreUploadGroupImageResponse['images'][0], idx: number) => ( <li key={imageUrl100x100} className='relative aspect-square w-full max-w-20'> <GroupImage className='border-mono-black/5 size-full border-1' size='md' src={imageUrl100x100} /> <button className={cn( 'flex-center bg-mono-white/80 absolute top-1.5 right-2 size-4 cursor-pointer rounded-full', 'hover:bg-mono-white hover:scale-110', 'transition-all duration-300', )} aria-label='이미지 삭제 버튼' type='button' onClick={() => onRemoveImageClick(idx)} > <Icon id='small-x-1' className='size-1.5 text-gray-700' /> </button> </li> ), )} </ul> {preUploadError && <Hint message={preUploadError} />} <p className='text-text-sm-medium px-2 text-gray-500 select-none'> 최대 3개까지 업로드할 수 있어요. </p> </div> ); }; |