diff --git a/package-lock.json b/package-lock.json index a877bec0..ec00cf0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "next": "^16.0.7", "react": "19.1.0", "react-dom": "19.1.0", - "swiper": "^12.0.2", "tailwind-merge": "^3.3.1", "zustand": "^5.0.8" }, @@ -12263,25 +12262,6 @@ "url": "https://opencollective.com/svgo" } }, - "node_modules/swiper": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", - "integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/swiperjs" - }, - { - "type": "open_collective", - "url": "http://opencollective.com/swiper" - } - ], - "license": "MIT", - "engines": { - "node": ">= 4.7.0" - } - }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", diff --git a/package.json b/package.json index 6b23f486..de85bd2a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "next": "^16.0.7", "react": "19.1.0", "react-dom": "19.1.0", - "swiper": "^12.0.2", "tailwind-merge": "^3.3.1", "zustand": "^5.0.8" }, diff --git a/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx b/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx index b438c7a6..c0c659e7 100644 --- a/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx +++ b/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx @@ -7,11 +7,7 @@ import { TextBox } from "@/shared/ui/TextBox/TextBox"; import { DropDown } from "@/shared/ui/DropDown/DropDown"; import Button from "@/shared/ui/Button/Button"; import Image from "next/image"; -import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation } from "swiper/modules"; -import "swiper/css"; -import "swiper/css/navigation"; -import "swiper/css/pagination"; +import { LoadingDots } from "@/shared/ui/Loading/LoadingDots"; import { apiFetch } from "@/shared/api/fetcher"; import { uploadImage } from "@/shared/api/uploadImage"; @@ -29,6 +25,7 @@ export const PostCreateModal = ({ onError, }: PostCreateModalProps) => { const [images, setImages] = useState([]); + const [imageError, setImageError] = useState(null); const [title, setTitle] = useState(""); const [price, setPrice] = useState(""); const [category, setCategory] = useState(""); @@ -56,7 +53,8 @@ export const PostCreateModal = ({ const selected = Array.from(files); setImages((prev) => [...prev, ...selected]); - e.target.value = ""; // input 값 초기화 + setImageError(null); + e.target.value = ""; }; const handleRemoveImage = (index: number) => { @@ -66,6 +64,12 @@ export const PostCreateModal = ({ const handleSubmit = async () => { try { if (!title || !price || !category) return; + if (images.length === 0) { + setImageError( + "이미지가 추가되지 않았습니다. 최소 1개의 이미지를 추가해주세요.", + ); + return; + } setIsLoading(true); const uploadedImageUrlArray: string[] = []; @@ -102,51 +106,10 @@ export const PostCreateModal = ({ const widthClass = "w-[290px] md:w-[510px] xl:w-[540px]"; - const imageSwiper = useMemo( - () => ( - - {images.map((file, idx) => ( - -
- {`preview-${idx}`} - -
-
- ))} -
- ), - [images], - ); - return (

게시물 추가

-
+
+ - {images.length > 0 && imageSwiper} +
+ {images.map((file, idx) => ( +
+ {`preview-${idx}`} + + +
+ ))} +
+ {imageError && ( + + {imageError} + + )} - {isLoading ? "추가 중 ..." : "추가하기"} + {isLoading ? ( + + 추가 중 + + ) : ( + "추가하기" + )}
diff --git a/src/features/editPost/ui/PostEditModal.tsx b/src/features/editPost/ui/PostEditModal.tsx index 5856317a..4fa430f3 100644 --- a/src/features/editPost/ui/PostEditModal.tsx +++ b/src/features/editPost/ui/PostEditModal.tsx @@ -7,45 +7,35 @@ import { TextBox } from "@/shared/ui/TextBox/TextBox"; import { DropDown } from "@/shared/ui/DropDown/DropDown"; import Button from "@/shared/ui/Button/Button"; import Image from "next/image"; -import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation } from "swiper/modules"; -import "swiper/css"; -import "swiper/css/navigation"; -import "swiper/css/pagination"; +import { LoadingDots } from "@/shared/ui/Loading/LoadingDots"; import { apiFetch } from "@/shared/api/fetcher"; import { uploadImage } from "@/shared/api/uploadImage"; -const ImageSwiperSlide = ( +const ImageItem = ( idx: number, url: string, - onRemoveButtonClick: (idx: number) => void, + onRemove: (idx: number) => void, ) => { return ( - -
- {`preview-${idx}`} - -
-
+ {`preview-${idx}`} + + + ); }; @@ -76,13 +66,23 @@ export const PostEditModal = ({ }: PostEditModalProps) => { const [imageUrls, setImageUrls] = useState(initImages); const [images, setImages] = useState([]); + const [imageChanged, setImageChanged] = useState(false); + const [imageError, setImageError] = useState(null); const [title, setTitle] = useState(initTitle); const [price, setPrice] = useState(initPrice); const [category, setCategory] = useState(initCategory); const [content, setContent] = useState(initContent); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const isChanged = + imageChanged || + title !== initTitle || + price !== initPrice || + category !== initCategory || + content !== initContent; const inputRef = useRef(null); + const categoryOptions = [ "전자제품/가전제품", "식료품", @@ -98,37 +98,43 @@ export const PostEditModal = ({ const handleFileSelect = (e: React.ChangeEvent) => { const files = e.currentTarget.files; - if (!files || files.length == 0) return; + if (!files || files.length === 0) return; const selected = Array.from(files); setImages((prev) => [...prev, ...selected]); - e.target.value = ""; // input 값 초기화 + e.target.value = ""; + setImageChanged(true); + setImageError(null); }; - const handleRemoveImageUrl = (index: number) => { - setImageUrls((prev) => prev.filter((_, i) => i !== index)); + const handleRemoveImageUrl = (idx: number) => { + setImageUrls((prev) => prev.filter((_, i) => i !== idx)); + setImageChanged(true); }; - const handleRemoveImage = (index: number) => { - setImages((prev) => prev.filter((_, i) => i !== index)); + + const handleRemoveImage = (idx: number) => { + setImages((prev) => prev.filter((_, i) => i !== idx)); + setImageChanged(true); }; const handleSubmit = async () => { + if (!isChanged) return; + try { - if ( - !title || - !price || - !category || - !content || - (imageUrls.length === 0 && images.length === 0) - ) + if (!title || !price || !category || !content) return; + if (imageUrls.length === 0 && images.length === 0) { + setImageError( + "이미지가 추가되지 않았습니다. 최소 1개의 이미지를 추가해주세요.", + ); return; + } + setIsLoading(true); - // 새로 추가한 이미지 URL 배열 생성 const uploadedImageUrlArray: string[] = []; for (const file of images) { - const uploadedImageUrl = await uploadImage(file); - uploadedImageUrlArray.push(uploadedImageUrl); + const url = await uploadImage(file); + uploadedImageUrlArray.push(url); } const body = { @@ -139,15 +145,13 @@ export const PostEditModal = ({ images: [...imageUrls, ...uploadedImageUrlArray], }; - const res = await apiFetch(`/api/postings/${postId}`, { + await apiFetch(`/api/postings/${postId}`, { method: "PATCH", body: JSON.stringify(body), }); - console.log("게시글 수정 성공! : ", res); onEdit?.(); } catch (error) { - console.error("게시글 수정 실패 : ", error); const message = error instanceof Error ? error.message : String(error); onError?.(message); } finally { @@ -156,30 +160,11 @@ export const PostEditModal = ({ }; const widthClass = "w-[290px] md:w-[510px] xl:w-[540px]"; - const imageSwiper = useMemo( - () => ( - - {imageUrls.map((url, idx) => - ImageSwiperSlide(idx, url, handleRemoveImageUrl), - )} - {images.map((file, idx) => - ImageSwiperSlide(idx, URL.createObjectURL(file), handleRemoveImage), - )} - - ), - [imageUrls, images], - ); return (
- 닫기 + 닫기

게시물 수정

@@ -206,6 +195,7 @@ export const PostEditModal = ({ className="h-8 w-8" /> + - {(imageUrls.length > 0 || images.length > 0) && imageSwiper} + {(imageUrls.length > 0 || images.length > 0) && ( +
+ {imageUrls.map((url, idx) => + ImageItem(idx, url, handleRemoveImageUrl), + )} + + {images.map((file, idx) => + ImageItem(idx, URL.createObjectURL(file), handleRemoveImage), + )} +
+ )}
+ {imageError && ( + + {imageError} + + )} - {isLoading ? "수정 중 ... " : "수정하기"} + {isLoading ? ( + + 수정 중 + + ) : ( + "수정하기" + )} diff --git a/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx b/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx index ee6ac7d3..0790505c 100644 --- a/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx +++ b/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx @@ -1,12 +1,14 @@ "use client"; -import React, { useState } from "react"; +import { useState } from "react"; import cn from "@/shared/lib/cn"; import { ProfileImageChangeInput } from "../ProfileImageChangeInput/ProfileImageChangeInput"; import { Input } from "@/entities/user/ui/Input/Input"; import { TextBox } from "@/shared/ui/TextBox/TextBox"; import { DropDown } from "@/shared/ui/DropDown/DropDown"; import Button from "@/shared/ui/Button/Button"; +import { LoadingDots } from "@/shared/ui/Loading/LoadingDots"; + import { apiFetch } from "@/shared/api/fetcher"; import { uploadImage } from "@/shared/api/uploadImage"; @@ -38,6 +40,7 @@ export const ProfileEditModal = ({ onError, }: ProfileEditModalProps) => { const [imageFile, setImageFile] = useState(null); + const [imageChanged, setImageChanged] = useState(false); const [nickname, setNickname] = useState(initialNickname); const [introduction, setIntroduction] = useState(initialIntroduction ?? ""); const [category, setCategory] = useState(initialCategory ?? ""); @@ -56,20 +59,30 @@ export const ProfileEditModal = ({ "반려동물/취미", ].map((cat) => ({ label: cat, value: cat })); + const isChanged = + imageChanged || + nickname !== initialNickname || + introduction !== (initialIntroduction ?? "") || + category !== (initialCategory ?? ""); + const handleSave = async () => { try { + if (!isChanged) return; setLoading(true); - let uploadedImageUrl = imageUrl; + let uploadedImageUrl: string | null | undefined = imageUrl; if (imageFile) { uploadedImageUrl = await uploadImage(imageFile); + } else if (imageChanged) { + uploadedImageUrl = null; } - const updateBody: Record = {}; + const updateBody: Record = {}; if (nickname.trim()) updateBody.nickname = nickname; if (introduction.trim()) updateBody.introduction = introduction; - if (uploadedImageUrl) updateBody.imageUrl = uploadedImageUrl; + if (uploadedImageUrl !== undefined) + updateBody.imageUrl = uploadedImageUrl; if (category.trim()) updateBody.category = category; await apiFetch("/api/users/me", { @@ -117,7 +130,13 @@ export const ProfileEditModal = ({
- + { + setImageFile(file); + setImageChanged(true); + }} + />
@@ -153,10 +172,16 @@ export const ProfileEditModal = ({
diff --git a/src/features/editProfile/ui/ProfileImageChangeInput/ProfileImageChangeInput.tsx b/src/features/editProfile/ui/ProfileImageChangeInput/ProfileImageChangeInput.tsx index 762cac20..052b2571 100644 --- a/src/features/editProfile/ui/ProfileImageChangeInput/ProfileImageChangeInput.tsx +++ b/src/features/editProfile/ui/ProfileImageChangeInput/ProfileImageChangeInput.tsx @@ -38,7 +38,7 @@ export const ProfileImageChangeInput = ({ return (