Skip to content

Commit 767b73f

Browse files
committed
feat: 이미지 드래그 앤 드롭 기능 추가
1 parent aad04b2 commit 767b73f

File tree

4 files changed

+102
-50
lines changed

4 files changed

+102
-50
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@tanstack/react-query-devtools": "^5.59.20",
6161
"@types/node": "^20",
6262
"@types/react": "^18.3.11",
63+
"@types/react-beautiful-dnd": "^13.1.8",
6364
"@types/react-datepicker": "^6.2.0",
6465
"@types/react-dom": "^18",
6566
"@typescript-eslint/eslint-plugin": "^8.9.0",

src/app/(pages)/(workform)/addform/section/RecruitContentSection.tsx

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,36 +31,48 @@ export default function RecruitContentSection() {
3131
const imageUrlsData: string[] = watch("imageUrls");
3232

3333
// 이미지 파일 change핸들러
34-
const handleChangeImages = async (files: File[]) => {
35-
let uploadedUrls: string[] = [];
36-
//파일 선택 시 업로드 api 요청
37-
try {
38-
uploadedUrls = await uploadImages(files);
39-
} catch (err) {
40-
console.log("이미지 파일 체인지 핸들러 - 이미지 업로드 실패");
41-
console.error(err);
42-
}
43-
// 선택한 이미지 업데이트
44-
const updatedImageList =
45-
uploadedUrls.map((url) => ({
34+
const handleChangeImages = async (files: File[] | string[]) => {
35+
// files가 File[] 타입인 경우 (새로운 이미지 업로드)
36+
if (files.length > 0 && files[0] instanceof File) {
37+
let uploadedUrls: string[] = [];
38+
try {
39+
uploadedUrls = await uploadImages(files as File[]);
40+
} catch (err) {
41+
console.log("이미지 파일 체인지 핸들러 - 이미지 업로드 실패");
42+
console.error(err);
43+
}
44+
45+
// 선택한 이미지 업데이트
46+
const updatedImageList = uploadedUrls.map((url) => ({
4647
url,
4748
id: crypto.randomUUID(),
48-
})) || [];
49+
}));
4950

50-
// 기존 이미지 포함하기
51-
const originalImageList =
52-
imageUrlsData.map((url) => ({
51+
// 기존 이미지 포함하기
52+
const originalImageList = imageUrlsData.map((url) => ({
5353
url,
5454
id: crypto.randomUUID(),
55-
})) || [];
55+
}));
5656

57-
const allImageList = [...originalImageList, ...updatedImageList];
58-
const submitImageList = [...imageUrlsData, ...uploadedUrls];
57+
const allImageList = [...originalImageList, ...updatedImageList];
58+
const submitImageList = [...imageUrlsData, ...uploadedUrls];
5959

60-
// prop으로 전달
61-
setInitialImageList(allImageList);
62-
// 훅폼 데이터에 세팅
63-
setValue("imageUrls", submitImageList, { shouldDirty: true });
60+
// prop으로 전달
61+
setInitialImageList(allImageList);
62+
// 훅폼 데이터에 세팅
63+
setValue("imageUrls", submitImageList, { shouldDirty: true });
64+
}
65+
// files가 string[] 타입인 경우 (드래그 앤 드롭으로 순서 변경)
66+
else {
67+
const urls = files as string[];
68+
const newImageList = urls.map((url) => ({
69+
url,
70+
id: crypto.randomUUID(),
71+
}));
72+
73+
setInitialImageList(newImageList);
74+
setValue("imageUrls", urls, { shouldDirty: true });
75+
}
6476
};
6577

6678
const handleDeleteImage = (url: string) => {
@@ -142,7 +154,7 @@ export default function RecruitContentSection() {
142154
<div className="relative">
143155
<ImageInput
144156
{...register("imageUrls")}
145-
onChange={(files: File[]) => {
157+
onChange={(files: File[] | string[]) => {
146158
handleChangeImages(files);
147159
}}
148160
onDelete={(id) => handleDeleteImage(id)}

src/app/components/input/file/ImageInput/ImageInput.tsx

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import { HiUpload } from "react-icons/hi";
33
import { forwardRef, useState, useEffect } from "react";
44
import { toast } from "react-hot-toast";
5+
import { DragDropContext, Droppable, Draggable, DropResult } from "react-beautiful-dnd";
56
import PreviewItem from "./PreviewItem";
67
import { cn } from "@/lib/tailwindUtil";
78
import { ImageInputType } from "@/types/addform";
89

910
interface ImageInputProps {
1011
name: string;
11-
onChange?: (files: File[]) => void;
12+
onChange?: (files: File[] | string[]) => void;
1213
onDelete?: (id: string) => void;
1314
initialImageList: ImageInputType[];
1415
}
@@ -66,34 +67,61 @@ const ImageInput = forwardRef<HTMLInputElement, ImageInputProps>((props, ref) =>
6667
innerHoverColor: "hover:bg-background-300",
6768
};
6869

70+
const handleDragEnd = (result: DropResult) => {
71+
if (!result.destination) return;
72+
73+
const items = Array.from(imageList);
74+
const [reorderedItem] = items.splice(result.source.index, 1);
75+
items.splice(result.destination.index, 0, reorderedItem);
76+
77+
setImageList(items);
78+
// 상위 컴포넌트에 변경된 이미지 URL 배열 전달
79+
props.onChange?.(items.map((item) => item.url));
80+
};
81+
6982
return (
70-
<div className="flex gap-5 lg:gap-6">
71-
<div
72-
onClick={handleOpenFileSelector}
73-
className={cn(
74-
"relative size-20 cursor-pointer rounded-lg lg:size-[116px]",
75-
colorStyle.bgColor,
76-
colorStyle.borderColor,
77-
colorStyle.hoverColor
78-
)}
79-
>
80-
<input
81-
ref={ref}
82-
type="file"
83-
name={props.name}
84-
accept="image/*"
85-
onChange={(e) => handleFileChange(e.target.files)}
86-
className="hidden"
87-
multiple
88-
/>
89-
<div className="pointer-events-none absolute top-0 z-10 p-7 lg:p-10">
90-
<HiUpload className="text-[24px] text-grayscale-400 lg:text-[36px]" />
83+
<DragDropContext onDragEnd={handleDragEnd}>
84+
<div className="flex gap-5 lg:gap-6">
85+
<div
86+
onClick={handleOpenFileSelector}
87+
className={cn(
88+
"relative size-20 cursor-pointer rounded-lg lg:size-[116px]",
89+
colorStyle.bgColor,
90+
colorStyle.borderColor,
91+
colorStyle.hoverColor
92+
)}
93+
>
94+
<input
95+
ref={ref}
96+
type="file"
97+
name={props.name}
98+
accept="image/*"
99+
onChange={(e) => handleFileChange(e.target.files)}
100+
className="hidden"
101+
multiple
102+
/>
103+
<div className="pointer-events-none absolute top-0 z-10 p-7 lg:p-10">
104+
<HiUpload className="text-[24px] text-grayscale-400 lg:text-[36px]" />
105+
</div>
91106
</div>
107+
<Droppable droppableId="image-list" direction="horizontal">
108+
{(provided) => (
109+
<div {...provided.droppableProps} ref={provided.innerRef} className="flex gap-5 lg:gap-6">
110+
{imageList.map((image, index) => (
111+
<Draggable key={image.id} draggableId={image.id} index={index}>
112+
{(provided) => (
113+
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
114+
<PreviewItem image={image} handleDeleteImage={handleDeleteImage} placeholder={false} />
115+
</div>
116+
)}
117+
</Draggable>
118+
))}
119+
{provided.placeholder}
120+
</div>
121+
)}
122+
</Droppable>
92123
</div>
93-
{imageList.map((image) => (
94-
<PreviewItem key={image.id} image={image} handleDeleteImage={handleDeleteImage} placeholder={false} />
95-
))}
96-
</div>
124+
</DragDropContext>
97125
);
98126
});
99127

0 commit comments

Comments
 (0)