From 380f5d82a43f5414e021dd5d616bcab04890b347 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 14 Mar 2026 19:45:39 +0900 Subject: [PATCH 1/3] feat(web): support drag and drop image upload in community forms --- .../[postId]/modify/PostModifyForm.tsx | 71 +++++++++++++++++- .../community/[boardCode]/create/PostForm.tsx | 72 ++++++++++++++++++- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx index 81299f47..bc77c860 100644 --- a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx +++ b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx @@ -1,7 +1,7 @@ "use client"; import { useRouter } from "next/navigation"; -import { type ChangeEvent, useEffect, useRef, useState } from "react"; +import { type ChangeEvent, type DragEvent, useEffect, useRef, useState } from "react"; import { useUpdatePost } from "@/apis/community"; import { toast } from "@/lib/zustand/useToastStore"; @@ -32,6 +32,8 @@ const PostModifyForm = ({ const imageUploadRef = useRef(null); const [selectedImage, setSelectedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); + const [isDraggingImage, setIsDraggingImage] = useState(false); + const dragDepthRef = useRef(0); const router = useRouter(); const updatePostMutation = useUpdatePost(); @@ -77,9 +79,61 @@ const PostModifyForm = ({ }; }, [selectedImage]); + const selectImageFile = (file: File | null) => { + if (!file) { + setSelectedImage(null); + return; + } + + if (!file.type.startsWith("image/")) { + toast.error("이미지 파일만 업로드할 수 있습니다."); + return; + } + + setSelectedImage(file); + }; + const handleImageChange = (event: ChangeEvent) => { const file = event.target.files?.[0] ?? null; - setSelectedImage(file); + selectImageFile(file); + }; + + const handleImageDragEnter = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current += 1; + setIsDraggingImage(true); + }; + + const handleImageDragOver = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = "copy"; + }; + + const handleImageDragLeave = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current -= 1; + if (dragDepthRef.current <= 0) { + dragDepthRef.current = 0; + setIsDraggingImage(false); + } + }; + + const handleImageDrop = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current = 0; + setIsDraggingImage(false); + + const file = event.dataTransfer.files?.[0] ?? null; + if (!file) return; + + selectImageFile(file); }; const removeSelectedImage = () => { @@ -128,7 +182,18 @@ const PostModifyForm = ({ return ( <> -
+
+ {isDraggingImage ? ( +
+ 이미지를 놓아 업로드하세요 +
+ ) : null}
{ const [isQuestion, setIsQuestion] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); + const [isDraggingImage, setIsDraggingImage] = useState(false); + const dragDepthRef = useRef(0); const createPostMutation = useCreatePost(); @@ -59,9 +62,61 @@ const PostForm = ({ boardCode }: PostFormProps) => { }; }, [selectedImage]); + const selectImageFile = (file: File | null) => { + if (!file) { + setSelectedImage(null); + return; + } + + if (!file.type.startsWith("image/")) { + toast.error("이미지 파일만 업로드할 수 있습니다."); + return; + } + + setSelectedImage(file); + }; + const handleImageChange = (event: ChangeEvent) => { const file = event.target.files?.[0] ?? null; - setSelectedImage(file); + selectImageFile(file); + }; + + const handleImageDragEnter = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current += 1; + setIsDraggingImage(true); + }; + + const handleImageDragOver = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = "copy"; + }; + + const handleImageDragLeave = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current -= 1; + if (dragDepthRef.current <= 0) { + dragDepthRef.current = 0; + setIsDraggingImage(false); + } + }; + + const handleImageDrop = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current = 0; + setIsDraggingImage(false); + + const file = event.dataTransfer.files?.[0] ?? null; + if (!file) return; + + selectImageFile(file); }; const removeSelectedImage = () => { @@ -109,7 +164,18 @@ const PostForm = ({ boardCode }: PostFormProps) => { } /> -
+
+ {isDraggingImage ? ( +
+ 이미지를 놓아 업로드하세요 +
+ ) : null}
Date: Sat, 14 Mar 2026 19:48:18 +0900 Subject: [PATCH 2/3] fix(web): validate community post form before submit --- .../web/src/apis/community/patchUpdatePost.ts | 1 - apps/web/src/apis/community/postCreatePost.ts | 1 - .../[postId]/modify/PostModifyForm.tsx | 17 ++++++++++---- .../community/[boardCode]/create/PostForm.tsx | 23 +++++++++++++++++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/web/src/apis/community/patchUpdatePost.ts b/apps/web/src/apis/community/patchUpdatePost.ts index bec18c47..fc5bae5c 100644 --- a/apps/web/src/apis/community/patchUpdatePost.ts +++ b/apps/web/src/apis/community/patchUpdatePost.ts @@ -59,7 +59,6 @@ const useUpdatePost = () => { }, onError: (error) => { console.error("게시글 수정 실패:", error); - toast.error("게시글 수정에 실패했습니다."); }, }); }; diff --git a/apps/web/src/apis/community/postCreatePost.ts b/apps/web/src/apis/community/postCreatePost.ts index 8a8d29ab..ce77607f 100644 --- a/apps/web/src/apis/community/postCreatePost.ts +++ b/apps/web/src/apis/community/postCreatePost.ts @@ -52,7 +52,6 @@ const useCreatePost = () => { }, onError: (error) => { console.error("게시글 생성 실패:", error); - toast.error("게시글 등록에 실패했습니다."); }, }); }; diff --git a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx index bc77c860..66e3ac9c 100644 --- a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx +++ b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx @@ -144,16 +144,24 @@ const PostModifyForm = ({ }; const submitPost = async () => { - if (!title.trim()) { + const trimmedTitle = title.trim(); + const trimmedContent = content.trim(); + + if (!trimmedTitle) { toast.error("제목을 입력해주세요."); return; } - if (!content.trim()) { + if (!trimmedContent) { toast.error("내용을 입력해주세요."); return; } + if (trimmedContent.length > 255) { + toast.error("내용은 255자 이하로 입력해주세요."); + return; + } + updatePostMutation.mutate( { postId, @@ -161,8 +169,8 @@ const PostModifyForm = ({ data: { postUpdateRequest: { postCategory: isQuestion ? "질문" : "자유", - title, - content, + title: trimmedTitle, + content: trimmedContent, }, file: selectedImage ? [selectedImage] : [], }, @@ -238,6 +246,7 @@ const PostModifyForm = ({ className="placeholder:text-gray-250/87 mt-4 box-border h-90 w-full resize-none border-0 px-5 text-black outline-none typo-regular-1" placeholder="내용을 입력하세요" value={content} + maxLength={255} onChange={(e) => setContent(e.target.value)} />
diff --git a/apps/web/src/app/community/[boardCode]/create/PostForm.tsx b/apps/web/src/app/community/[boardCode]/create/PostForm.tsx index 21a15fd2..c3c19b8c 100644 --- a/apps/web/src/app/community/[boardCode]/create/PostForm.tsx +++ b/apps/web/src/app/community/[boardCode]/create/PostForm.tsx @@ -127,13 +127,31 @@ const PostForm = ({ boardCode }: PostFormProps) => { }; const submitPost = async () => { + const titleValue = titleRef.current?.querySelector("textarea")?.value.trim() || ""; + const trimmedContent = content.trim(); + + if (!titleValue) { + toast.error("제목을 입력해주세요."); + return; + } + + if (!trimmedContent) { + toast.error("내용을 입력해주세요."); + return; + } + + if (trimmedContent.length > 255) { + toast.error("내용은 255자 이하로 입력해주세요."); + return; + } + createPostMutation.mutate( { postCreateRequest: { boardCode: boardCode, postCategory: isQuestion ? "질문" : "자유", - title: titleRef.current?.querySelector("textarea")?.value || "", - content, + title: titleValue, + content: trimmedContent, isQuestion, }, file: selectedImage ? [selectedImage] : [], @@ -218,6 +236,7 @@ const PostForm = ({ boardCode }: PostFormProps) => { className="placeholder:text-gray-250/87 mt-4 box-border h-90 w-full resize-none border-0 px-5 text-black outline-none typo-regular-1" placeholder="내용을 입력하세요" value={content} + maxLength={255} onChange={(e) => setContent(e.target.value)} />
From eb651532a81c1cfb75b5556ddc3a0e4dde9e85e7 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 14 Mar 2026 20:39:45 +0900 Subject: [PATCH 3/3] fix(web): support 5 community images and unify upload logic --- apps/web/src/apis/community/api.ts | 5 +- .../[boardCode]/[postId]/Content.tsx | 10 +- .../[postId]/modify/PostModifyForm.tsx | 150 ++++++------------ .../community/[boardCode]/create/PostForm.tsx | 150 ++++++------------ .../_hooks/useCommunityImageUpload.ts | 127 +++++++++++++++ apps/web/src/constants/community.ts | 2 + 6 files changed, 240 insertions(+), 204 deletions(-) create mode 100644 apps/web/src/app/community/_hooks/useCommunityImageUpload.ts diff --git a/apps/web/src/apis/community/api.ts b/apps/web/src/apis/community/api.ts index 64eab740..52e7ba6d 100644 --- a/apps/web/src/apis/community/api.ts +++ b/apps/web/src/apis/community/api.ts @@ -1,4 +1,5 @@ import type { AxiosResponse } from "axios"; +import { COMMUNITY_MAX_UPLOAD_IMAGES } from "@/constants/community"; import type { CommentCreateRequest, CommentIdResponse, @@ -91,7 +92,7 @@ export const communityApi = { "postCreateRequest", new Blob([JSON.stringify(request.postCreateRequest)], { type: "application/json" }), ); - request.file.forEach((file) => { + request.file.slice(0, COMMUNITY_MAX_UPLOAD_IMAGES).forEach((file) => { convertedRequest.append("file", file); }); @@ -111,7 +112,7 @@ export const communityApi = { "postUpdateRequest", new Blob([JSON.stringify(request.postUpdateRequest)], { type: "application/json" }), ); - request.file.forEach((file) => { + request.file.slice(0, COMMUNITY_MAX_UPLOAD_IMAGES).forEach((file) => { convertedRequest.append("file", file); }); diff --git a/apps/web/src/app/community/[boardCode]/[postId]/Content.tsx b/apps/web/src/app/community/[boardCode]/[postId]/Content.tsx index 92d3fd29..714d630e 100644 --- a/apps/web/src/app/community/[boardCode]/[postId]/Content.tsx +++ b/apps/web/src/app/community/[boardCode]/[postId]/Content.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useDeleteLike, usePostLike } from "@/apis/community"; import Image from "@/components/ui/FallbackImage"; import LinkifyText from "@/components/ui/LinkifyText"; +import { COMMUNITY_MAX_UPLOAD_IMAGES } from "@/constants/community"; import { IconCloseFilled, IconPostLikeFilled, IconPostLikeOutline } from "@/public/svgs"; import { IconCommunication } from "@/public/svgs/community"; import type { PostImage as PostImageType, Post as PostType } from "@/types/community"; @@ -22,6 +23,7 @@ const Content = ({ post, postId }: ContentProps) => { const [selectedImageIndex, setSelectedImageIndex] = useState(null); const [likeCount, setLikeCount] = useState(0); const [isLiked, setIsLiked] = useState(false); + const postImages = (post.postFindPostImageResponses || []).slice(0, COMMUNITY_MAX_UPLOAD_IMAGES); const postLikeMutation = usePostLike(); const deleteLikeMutation = useDeleteLike(); @@ -85,12 +87,12 @@ const Content = ({ post, postId }: ContentProps) => {
- +
- {selectedImageIndex !== null && ( + {selectedImageIndex !== null && postImages[selectedImageIndex] && ( )} diff --git a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx index 66e3ac9c..cb96d586 100644 --- a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx +++ b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx @@ -1,9 +1,10 @@ "use client"; import { useRouter } from "next/navigation"; -import { type ChangeEvent, type DragEvent, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useUpdatePost } from "@/apis/community"; +import useCommunityImageUpload from "@/app/community/_hooks/useCommunityImageUpload"; import { toast } from "@/lib/zustand/useToastStore"; import { IconArrowBackFilled, IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; @@ -29,11 +30,20 @@ const PostModifyForm = ({ const [isQuestion, setIsQuestion] = useState(defaultIsQuestion); const textareaRef = useRef(null); const titleRef = useRef(null); - const imageUploadRef = useRef(null); - const [selectedImage, setSelectedImage] = useState(null); - const [imagePreviewUrl, setImagePreviewUrl] = useState(null); - const [isDraggingImage, setIsDraggingImage] = useState(false); - const dragDepthRef = useRef(0); + const { + maxImages, + imageUploadRef, + selectedImages, + imagePreviewUrls, + isDraggingImage, + handleImageChange, + handleImageDragEnter, + handleImageDragOver, + handleImageDragLeave, + handleImageDrop, + removeSelectedImage, + openImagePicker, + } = useCommunityImageUpload(); const router = useRouter(); const updatePostMutation = useUpdatePost(); @@ -65,84 +75,6 @@ const PostModifyForm = ({ return () => {}; }, []); - useEffect(() => { - if (!selectedImage) { - setImagePreviewUrl(null); - return; - } - - const objectUrl = URL.createObjectURL(selectedImage); - setImagePreviewUrl(objectUrl); - - return () => { - URL.revokeObjectURL(objectUrl); - }; - }, [selectedImage]); - - const selectImageFile = (file: File | null) => { - if (!file) { - setSelectedImage(null); - return; - } - - if (!file.type.startsWith("image/")) { - toast.error("이미지 파일만 업로드할 수 있습니다."); - return; - } - - setSelectedImage(file); - }; - - const handleImageChange = (event: ChangeEvent) => { - const file = event.target.files?.[0] ?? null; - selectImageFile(file); - }; - - const handleImageDragEnter = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - dragDepthRef.current += 1; - setIsDraggingImage(true); - }; - - const handleImageDragOver = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = "copy"; - }; - - const handleImageDragLeave = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - dragDepthRef.current -= 1; - if (dragDepthRef.current <= 0) { - dragDepthRef.current = 0; - setIsDraggingImage(false); - } - }; - - const handleImageDrop = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - dragDepthRef.current = 0; - setIsDraggingImage(false); - - const file = event.dataTransfer.files?.[0] ?? null; - if (!file) return; - - selectImageFile(file); - }; - - const removeSelectedImage = () => { - setSelectedImage(null); - if (imageUploadRef.current) { - imageUploadRef.current.value = ""; - } - }; - const submitPost = async () => { const trimmedTitle = title.trim(); const trimmedContent = content.trim(); @@ -172,7 +104,7 @@ const PostModifyForm = ({ title: trimmedTitle, content: trimmedContent, }, - file: selectedImage ? [selectedImage] : [], + file: selectedImages, }, }, { @@ -232,13 +164,20 @@ const PostModifyForm = ({ - +
@@ -250,19 +189,32 @@ const PostModifyForm = ({ onChange={(e) => setContent(e.target.value)} />
- {imagePreviewUrl ? ( + {imagePreviewUrls.length > 0 ? (
-

첨부 이미지

-
- 업로드 이미지 미리보기 - +

+ 첨부 이미지 ({selectedImages.length}/{maxImages}) +

+
+ {imagePreviewUrls.map((imagePreviewUrl, index) => ( +
+ {`업로드 + +
+ ))}
) : null} diff --git a/apps/web/src/app/community/[boardCode]/create/PostForm.tsx b/apps/web/src/app/community/[boardCode]/create/PostForm.tsx index c3c19b8c..eaa6e44c 100644 --- a/apps/web/src/app/community/[boardCode]/create/PostForm.tsx +++ b/apps/web/src/app/community/[boardCode]/create/PostForm.tsx @@ -1,8 +1,9 @@ "use client"; import { useRouter } from "next/navigation"; -import { type ChangeEvent, type DragEvent, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useCreatePost } from "@/apis/community"; +import useCommunityImageUpload from "@/app/community/_hooks/useCommunityImageUpload"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; import { toast } from "@/lib/zustand/useToastStore"; import { IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; @@ -15,13 +16,22 @@ const PostForm = ({ boardCode }: PostFormProps) => { const textareaRef = useRef(null); const titleRef = useRef(null); const [content, setContent] = useState(""); - const imageUploadRef = useRef(null); const router = useRouter(); const [isQuestion, setIsQuestion] = useState(false); - const [selectedImage, setSelectedImage] = useState(null); - const [imagePreviewUrl, setImagePreviewUrl] = useState(null); - const [isDraggingImage, setIsDraggingImage] = useState(false); - const dragDepthRef = useRef(0); + const { + maxImages, + imageUploadRef, + selectedImages, + imagePreviewUrls, + isDraggingImage, + handleImageChange, + handleImageDragEnter, + handleImageDragOver, + handleImageDragLeave, + handleImageDrop, + removeSelectedImage, + openImagePicker, + } = useCommunityImageUpload(); const createPostMutation = useCreatePost(); @@ -48,84 +58,6 @@ const PostForm = ({ boardCode }: PostFormProps) => { return () => {}; }, []); - useEffect(() => { - if (!selectedImage) { - setImagePreviewUrl(null); - return; - } - - const objectUrl = URL.createObjectURL(selectedImage); - setImagePreviewUrl(objectUrl); - - return () => { - URL.revokeObjectURL(objectUrl); - }; - }, [selectedImage]); - - const selectImageFile = (file: File | null) => { - if (!file) { - setSelectedImage(null); - return; - } - - if (!file.type.startsWith("image/")) { - toast.error("이미지 파일만 업로드할 수 있습니다."); - return; - } - - setSelectedImage(file); - }; - - const handleImageChange = (event: ChangeEvent) => { - const file = event.target.files?.[0] ?? null; - selectImageFile(file); - }; - - const handleImageDragEnter = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - dragDepthRef.current += 1; - setIsDraggingImage(true); - }; - - const handleImageDragOver = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = "copy"; - }; - - const handleImageDragLeave = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - dragDepthRef.current -= 1; - if (dragDepthRef.current <= 0) { - dragDepthRef.current = 0; - setIsDraggingImage(false); - } - }; - - const handleImageDrop = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - dragDepthRef.current = 0; - setIsDraggingImage(false); - - const file = event.dataTransfer.files?.[0] ?? null; - if (!file) return; - - selectImageFile(file); - }; - - const removeSelectedImage = () => { - setSelectedImage(null); - if (imageUploadRef.current) { - imageUploadRef.current.value = ""; - } - }; - const submitPost = async () => { const titleValue = titleRef.current?.querySelector("textarea")?.value.trim() || ""; const trimmedContent = content.trim(); @@ -154,7 +86,7 @@ const PostForm = ({ boardCode }: PostFormProps) => { content: trimmedContent, isQuestion, }, - file: selectedImage ? [selectedImage] : [], + file: selectedImages, }, { onSuccess: (data) => { @@ -221,14 +153,21 @@ const PostForm = ({ boardCode }: PostFormProps) => {
- +
@@ -240,19 +179,32 @@ const PostForm = ({ boardCode }: PostFormProps) => { onChange={(e) => setContent(e.target.value)} />
- {imagePreviewUrl ? ( + {imagePreviewUrls.length > 0 ? (
-

첨부 이미지

-
- 업로드 이미지 미리보기 - +

+ 첨부 이미지 ({selectedImages.length}/{maxImages}) +

+
+ {imagePreviewUrls.map((imagePreviewUrl, index) => ( +
+ {`업로드 + +
+ ))}
) : null} diff --git a/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts b/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts new file mode 100644 index 00000000..43bfa482 --- /dev/null +++ b/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts @@ -0,0 +1,127 @@ +"use client"; + +import { type ChangeEvent, type DragEvent, useEffect, useRef, useState } from "react"; +import { COMMUNITY_MAX_UPLOAD_IMAGES } from "@/constants/community"; +import { toast } from "@/lib/zustand/useToastStore"; + +type UseCommunityImageUploadOptions = { + maxImages?: number; +}; + +const useCommunityImageUpload = ({ maxImages = COMMUNITY_MAX_UPLOAD_IMAGES }: UseCommunityImageUploadOptions = {}) => { + const imageUploadRef = useRef(null); + const dragDepthRef = useRef(0); + const selectedImageCountRef = useRef(0); + const [selectedImages, setSelectedImages] = useState([]); + const [imagePreviewUrls, setImagePreviewUrls] = useState([]); + const [isDraggingImage, setIsDraggingImage] = useState(false); + + useEffect(() => { + selectedImageCountRef.current = selectedImages.length; + const objectUrls = selectedImages.map((image) => URL.createObjectURL(image)); + setImagePreviewUrls(objectUrls); + + return () => { + objectUrls.forEach((url) => URL.revokeObjectURL(url)); + }; + }, [selectedImages]); + + const appendImageFiles = (files: File[]) => { + if (files.length === 0) return; + + const imageFiles = files.filter((file) => file.type.startsWith("image/")); + if (imageFiles.length !== files.length) { + toast.error("이미지 파일만 업로드할 수 있습니다."); + } + + if (imageFiles.length === 0) return; + + const remainingSlots = maxImages - selectedImageCountRef.current; + if (remainingSlots <= 0) { + toast.error(`이미지는 최대 ${maxImages}장까지 업로드할 수 있습니다.`); + return; + } + + if (imageFiles.length > remainingSlots) { + toast.error(`이미지는 최대 ${maxImages}장까지 업로드할 수 있습니다.`); + } + + setSelectedImages((prev) => { + const currentRemainingSlots = Math.max(0, maxImages - prev.length); + const nextImages = [...prev, ...imageFiles.slice(0, currentRemainingSlots)]; + selectedImageCountRef.current = nextImages.length; + return nextImages; + }); + }; + + const handleImageChange = (event: ChangeEvent) => { + const files = Array.from(event.target.files ?? []); + appendImageFiles(files); + event.target.value = ""; + }; + + const handleImageDragEnter = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current += 1; + setIsDraggingImage(true); + }; + + const handleImageDragOver = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = "copy"; + }; + + const handleImageDragLeave = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current -= 1; + if (dragDepthRef.current <= 0) { + dragDepthRef.current = 0; + setIsDraggingImage(false); + } + }; + + const handleImageDrop = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragDepthRef.current = 0; + setIsDraggingImage(false); + + const files = Array.from(event.dataTransfer.files ?? []); + appendImageFiles(files); + }; + + const removeSelectedImage = (index: number) => { + setSelectedImages((prev) => { + const nextImages = prev.filter((_, imageIndex) => imageIndex !== index); + selectedImageCountRef.current = nextImages.length; + return nextImages; + }); + }; + + const openImagePicker = () => { + imageUploadRef.current?.click(); + }; + + return { + maxImages, + imageUploadRef, + selectedImages, + imagePreviewUrls, + isDraggingImage, + handleImageChange, + handleImageDragEnter, + handleImageDragOver, + handleImageDragLeave, + handleImageDrop, + removeSelectedImage, + openImagePicker, + }; +}; + +export default useCommunityImageUpload; diff --git a/apps/web/src/constants/community.ts b/apps/web/src/constants/community.ts index 60824f66..198a9770 100644 --- a/apps/web/src/constants/community.ts +++ b/apps/web/src/constants/community.ts @@ -6,3 +6,5 @@ export const COMMUNITY_BOARDS = [ ]; export const COMMUNITY_CATEGORIES = ["전체", "자유", "질문"]; + +export const COMMUNITY_MAX_UPLOAD_IMAGES = 5;