|
| 1 | +"use client"; |
| 2 | +import { HiUpload } from "react-icons/hi"; |
| 3 | +import { forwardRef, useState, useEffect } from "react"; |
| 4 | +import { toast } from "react-hot-toast"; |
| 5 | +import { DragDropContext, Droppable, Draggable, DropResult } from "react-beautiful-dnd"; |
| 6 | +import PreviewItem from "./PreviewItem"; |
| 7 | +import { cn } from "@/lib/tailwindUtil"; |
| 8 | +import { ImageInputType } from "@/types/addform"; |
| 9 | +import React from "react"; |
| 10 | + |
| 11 | +interface ImageInputProps { |
| 12 | + name: string; |
| 13 | + onChange?: (files: File[] | string[]) => void; |
| 14 | + onDelete?: (id: string) => void; |
| 15 | + initialImageList: ImageInputType[]; |
| 16 | +} |
| 17 | + |
| 18 | +const ImageInput = forwardRef<HTMLInputElement, ImageInputProps>((props, ref) => { |
| 19 | + const [imageList, setImageList] = useState<ImageInputType[]>(props.initialImageList || []); |
| 20 | + |
| 21 | + useEffect(() => { |
| 22 | + if (props.initialImageList?.length > 0) { |
| 23 | + setImageList(props.initialImageList); |
| 24 | + } |
| 25 | + }, [props.initialImageList]); |
| 26 | + |
| 27 | + const handleFileChange = (selectedFiles: FileList | null) => { |
| 28 | + if (selectedFiles) { |
| 29 | + const filesArray = Array.from(selectedFiles); // FileList를 배열로 변환 |
| 30 | + const validFiles = filesArray.filter((file) => file.type.startsWith("image/")); |
| 31 | + |
| 32 | + if (validFiles.length + imageList.length > 3) { |
| 33 | + toast.error("이미지는 최대 3개까지 업로드할 수 있습니다."); |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + if (validFiles.length === 0) { |
| 38 | + toast.error("이미지 파일만 업로드할 수 있습니다."); |
| 39 | + return; |
| 40 | + } |
| 41 | + |
| 42 | + // 선택된 파일을 상위 컴포넌트로 전달 |
| 43 | + props.onChange?.(validFiles); |
| 44 | + } |
| 45 | + }; |
| 46 | + |
| 47 | + const handleDeleteImage = (targetUrl: string) => { |
| 48 | + const newImageList = imageList.filter((image) => image.url !== targetUrl); |
| 49 | + setImageList(newImageList); |
| 50 | + props.onDelete?.(targetUrl); |
| 51 | + }; |
| 52 | + |
| 53 | + const handleOpenFileSelector = () => { |
| 54 | + if (typeof ref === "function") { |
| 55 | + const fileInput = document.querySelector(`input[name="${props.name}"]`); |
| 56 | + if (fileInput) { |
| 57 | + (fileInput as HTMLInputElement).click(); |
| 58 | + } |
| 59 | + } else if (ref && "current" in ref) { |
| 60 | + ref.current?.click(); |
| 61 | + } |
| 62 | + }; |
| 63 | + |
| 64 | + const colorStyle = { |
| 65 | + bgColor: "bg-background-200", |
| 66 | + borderColor: "border-[0.5px] border-transparent", |
| 67 | + hoverColor: "hover:border-grayscale-200 hover:bg-background-300", |
| 68 | + innerHoverColor: "hover:bg-background-300", |
| 69 | + }; |
| 70 | + |
| 71 | + const handleDragEnd = (result: DropResult) => { |
| 72 | + if (!result.destination) return; |
| 73 | + |
| 74 | + const items = Array.from(imageList); |
| 75 | + const [reorderedItem] = items.splice(result.source.index, 1); |
| 76 | + items.splice(result.destination.index, 0, reorderedItem); |
| 77 | + |
| 78 | + setImageList(items); |
| 79 | + // 상위 컴포넌트에 변경된 이미지 URL 배열 전달 |
| 80 | + props.onChange?.(items.map((item) => item.url)); |
| 81 | + }; |
| 82 | + |
| 83 | + return ( |
| 84 | + <DragDropContext onDragEnd={handleDragEnd}> |
| 85 | + <div className="flex gap-5 lg:gap-6"> |
| 86 | + <div |
| 87 | + onClick={handleOpenFileSelector} |
| 88 | + className={cn( |
| 89 | + "relative size-20 cursor-pointer rounded-lg lg:size-[116px]", |
| 90 | + colorStyle.bgColor, |
| 91 | + colorStyle.borderColor, |
| 92 | + colorStyle.hoverColor |
| 93 | + )} |
| 94 | + > |
| 95 | + <input |
| 96 | + ref={ref} |
| 97 | + type="file" |
| 98 | + name={props.name} |
| 99 | + accept="image/*" |
| 100 | + onChange={(e) => handleFileChange(e.target.files)} |
| 101 | + className="hidden" |
| 102 | + multiple |
| 103 | + /> |
| 104 | + <div className="pointer-events-none absolute top-0 z-10 p-7 lg:p-10"> |
| 105 | + <HiUpload className="text-[24px] text-grayscale-400 lg:text-[36px]" /> |
| 106 | + </div> |
| 107 | + </div> |
| 108 | + <Droppable droppableId="image-list" direction="horizontal"> |
| 109 | + {(provided) => ( |
| 110 | + <div {...provided.droppableProps} ref={provided.innerRef} className="flex gap-5 lg:gap-6"> |
| 111 | + {imageList.map((image, index) => ( |
| 112 | + <Draggable key={image.id} draggableId={image.id} index={index}> |
| 113 | + {(provided) => ( |
| 114 | + <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> |
| 115 | + <PreviewItem image={image} handleDeleteImage={handleDeleteImage} placeholder={false} /> |
| 116 | + </div> |
| 117 | + )} |
| 118 | + </Draggable> |
| 119 | + ))} |
| 120 | + {provided.placeholder} |
| 121 | + </div> |
| 122 | + )} |
| 123 | + </Droppable> |
| 124 | + </div> |
| 125 | + </DragDropContext> |
| 126 | + ); |
| 127 | +}); |
| 128 | + |
| 129 | +ImageInput.displayName = "ImageInput"; |
| 130 | + |
| 131 | +export default ImageInput; |
0 commit comments