From 550ed8acf1daf255c5828df11f5c13441a1433a4 Mon Sep 17 00:00:00 2001 From: yihyun-kim1 Date: Fri, 23 Aug 2024 21:18:48 +0900 Subject: [PATCH] fix: fix with Uploading Image logic & type error --- apis/place/PlaceApi.ts | 9 +- apis/place/types/dto/index.ts | 4 +- app/add-course/_components/AddCourse.tsx | 25 ++- app/add-course/_components/InputWithImage.tsx | 137 +++++++++++++--- app/add-course/_components/InputWithLabel.tsx | 5 +- app/add-course/_components/PlaceContainer.tsx | 2 +- .../_hooks/useAddPlaceDetailForm.ts | 6 +- .../detail/_components/AddPlaceDetail.tsx | 155 ++++++++++++------ .../common/BottomSheet/SheetWithCourse.tsx | 2 +- .../common/Cards/CardForCopiedContent.tsx | 32 ++-- .../common/Cards/CardWithAutoCompleteData.tsx | 20 ++- .../common/Cards/CardWithImageSmall.tsx | 4 +- components/ui/badge.tsx | 4 +- model/index.ts | 2 +- next.config.mjs | 6 +- package.json | 3 +- public/png/img_sample.png | Bin 12776 -> 12926 bytes 17 files changed, 286 insertions(+), 130 deletions(-) diff --git a/apis/place/PlaceApi.ts b/apis/place/PlaceApi.ts index 8790596..44e85fc 100644 --- a/apis/place/PlaceApi.ts +++ b/apis/place/PlaceApi.ts @@ -39,7 +39,14 @@ class PlaceApi { formData.append("addPlaceRequest", JSON.stringify(payload.addPlaceRequest)); - formData.append("placeImages", JSON.stringify(payload?.placeImages)); + if ( + !payload.addPlaceRequest.autoCompletedPlaceImageUrls || + payload.addPlaceRequest.autoCompletedPlaceImageUrls.length === 0 + ) { + payload.placeImages?.forEach((image) => { + formData.append("placeImages", image); + }); + } const { data } = await this.axios({ method: "POST", diff --git a/apis/place/types/dto/index.ts b/apis/place/types/dto/index.ts index 1e9c262..9093ba5 100644 --- a/apis/place/types/dto/index.ts +++ b/apis/place/types/dto/index.ts @@ -44,15 +44,17 @@ export interface AddPlaceRequestDto { phoneNumber?: string | null; starGrade?: number | null; memo?: string; + origin: string; voteLikeCount?: number | null; voteDislikeCount?: number | null; longitude?: number | null; latitude?: number | null; + autoCompletedPlaceImageUrls?: string[]; } export interface CreatePlacePayloadDto { addPlaceRequest: AddPlaceRequestDto; - placeImages?: string[]; + placeImages?: File[]; } export interface ModifyPlaceRequestDto { diff --git a/app/add-course/_components/AddCourse.tsx b/app/add-course/_components/AddCourse.tsx index c021b2b..ceb36a7 100644 --- a/app/add-course/_components/AddCourse.tsx +++ b/app/add-course/_components/AddCourse.tsx @@ -30,12 +30,14 @@ const AddCourse = ({ data }: AddCourseProps) => { const sliderRef = useRef(null); const isMobile = useIsMobile(); const { clipboardText } = useCopyPasted(); + const [placeUrl, setPlaceUrl] = useState(""); const [showInput, setShowInput] = useState(true); const [showAlternateInput, setShowAlternateInput] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [isReadyToVote, setIsReadyToVote] = useState(false); const searchParams = useSearchParams(); + const roomUid = searchParams.get("roomUid") || ""; const { roomInfo, @@ -51,10 +53,10 @@ const AddCourse = ({ data }: AddCourseProps) => { setAutoData, } = useCourseContext(); const [selectedChip, setSelectedChip] = useState( - categoryList && categoryList.length > 0 ? categoryList[0].scheduleId : null + null ); - const { data: currentPlacesData } = useGetPlacesQuery({ + const { data: currentPlacesData, refetch } = useGetPlacesQuery({ variables: { roomUid }, }); @@ -68,12 +70,22 @@ const AddCourse = ({ data }: AddCourseProps) => { const isValidClipboardText = validateClipboardText(clipboardText); const isValidPlaceUrl = validateClipboardText(placeUrl); + useEffect(() => { + if (categoryList && categoryList.length > 0) { + setSelectedChip(categoryList[0].scheduleId); + } + }, [categoryList]); + useEffect(() => { if (currentPlacesData) { setRoomPlacesInfo(currentPlacesData); } }, [currentPlacesData, setRoomPlacesInfo]); + useEffect(() => { + refetch(); + }, [currentPlacesData, setRoomPlacesInfo]); + const { mutate: createPlaceMutate } = useCreatePlace({ options: { onSuccess: (res) => { @@ -141,15 +153,18 @@ const AddCourse = ({ data }: AddCourseProps) => { useEffect(() => { const fetchData = async () => { - if (!isMobile && isValidClipboardText) { + if (!isMobile && clipboardText && isValidClipboardText) { setShowInput(false); setIsClipboardText(true); createPlaceMutate({ url: clipboardText }); - } else if (isValidPlaceUrl) { + } else if (placeUrl && isValidPlaceUrl) { setShowInput(false); setIsClipboardText(true); createPlaceMutate({ url: placeUrl }); - } else if (!isValidClipboardText || !isValidPlaceUrl) { + } else if ( + (clipboardText && !isValidClipboardText) || + (placeUrl && !isValidPlaceUrl) + ) { setShowInput(false); setShowAlternateInput(true); setTimeout(() => { diff --git a/app/add-course/_components/InputWithImage.tsx b/app/add-course/_components/InputWithImage.tsx index 02ccff3..8d20732 100644 --- a/app/add-course/_components/InputWithImage.tsx +++ b/app/add-course/_components/InputWithImage.tsx @@ -1,40 +1,47 @@ -"use client"; import * as React from "react"; -import { useEffect, useState } from "react"; +import { useState, useCallback } from "react"; import Image from "next/image"; import { cn } from "@/lib/utils"; export interface InputProps extends React.InputHTMLAttributes { - onFilesChange?: (files: string[]) => void; + onFilesChange?: (formData: FormData) => void; } const InputWithImage = React.forwardRef( ({ className, onFilesChange, ...props }, ref) => { - const [uploadedFiles, setUploadedFiles] = useState([]); + const [uploadedFiles, setUploadedFiles] = useState([]); - useEffect(() => { - if (onFilesChange) { - onFilesChange(uploadedFiles); - } - }, [uploadedFiles, onFilesChange]); - - const handleFileChange = (e: React.ChangeEvent) => { - const files = e.target.files; - if (files) { - const newFiles = Array.from(files) - .slice(0, 3 - uploadedFiles.length) // 최대 3개의 파일만 업로드 - .map( - (file) => - `${process.env.NEXT_PUBLIC_DNS_URL}/${URL.createObjectURL(file)}` - ); - - setUploadedFiles((prevFiles) => [...prevFiles, ...newFiles]); - } - }; + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const files = e.target.files; + if (files) { + const newFiles = Array.from(files).slice(0, 3 - uploadedFiles.length); // 최대 3개의 파일 + const updatedFiles = [...uploadedFiles, ...newFiles]; + setUploadedFiles(updatedFiles); + + // FormData 생성 및 파일 추가 + const formData = new FormData(); + updatedFiles.forEach((file) => formData.append("placeImages", file)); + + if (onFilesChange) { + onFilesChange(formData); + } + } + }, + [uploadedFiles, onFilesChange] + ); const handleDeleteFile = (index: number) => { - setUploadedFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); + const updatedFiles = uploadedFiles.filter((_, i) => i !== index); + setUploadedFiles(updatedFiles); + + const formData = new FormData(); + updatedFiles.forEach((file) => formData.append("placeImages", file)); + + if (onFilesChange) { + onFilesChange(formData); + } }; return ( @@ -53,8 +60,10 @@ const InputWithImage = React.forwardRef( {uploadedFiles.map((file, index) => (
{`${file}`} +//
+// ))} +// +// +// ); +// } +// ); + +// InputWithImage.displayName = "InputWithImage"; + +// export { InputWithImage }; diff --git a/app/add-course/_components/InputWithLabel.tsx b/app/add-course/_components/InputWithLabel.tsx index 164be98..e0bf108 100644 --- a/app/add-course/_components/InputWithLabel.tsx +++ b/app/add-course/_components/InputWithLabel.tsx @@ -5,12 +5,11 @@ import { cn } from "@/lib/utils"; export interface InputWithLabelProps extends React.InputHTMLAttributes { iconSrc?: string; - onChange: (e: React.ChangeEvent) => void; required?: boolean; } const InputWithLabel = React.forwardRef( - ({ className, iconSrc, value, onChange, required, type, ...props }, ref) => { + ({ className, iconSrc, value, required, type, ...props }, ref) => { return (
@@ -29,8 +28,6 @@ const InputWithLabel = React.forwardRef( "flex-1 bg-transparent text-[#8B95A1] text-[16px] outline-none", className )} - value={value} - onChange={onChange} ref={ref} {...props} /> diff --git a/app/add-course/_components/PlaceContainer.tsx b/app/add-course/_components/PlaceContainer.tsx index 84b8afb..3ff71c4 100644 --- a/app/add-course/_components/PlaceContainer.tsx +++ b/app/add-course/_components/PlaceContainer.tsx @@ -38,7 +38,7 @@ export const PlaceContainer: React.FC = ({ origin={place?.origin} place={place?.name} link={place?.url} - rating={place?.starGrade?.toString()} + rating={place?.starGrade} reviewCount={place?.reviewCount} images={place?.placeImageUrls?.contents} category={scheduleInfo} diff --git a/app/add-course/_hooks/useAddPlaceDetailForm.ts b/app/add-course/_hooks/useAddPlaceDetailForm.ts index 113bd67..5761428 100644 --- a/app/add-course/_hooks/useAddPlaceDetailForm.ts +++ b/app/add-course/_hooks/useAddPlaceDetailForm.ts @@ -10,11 +10,11 @@ export type CommonPlaceDetailFormType = { url?: string; reviewCount?: number; starGrade?: number; - openingHours?: string; + openingHours?: string | null; phoneNumber?: string; address?: string; memo?: string; - pictures: string[]; + pictures?: File[]; }; const addPlaceDetailSchema = z.object({ @@ -28,7 +28,7 @@ const addPlaceDetailSchema = z.object({ phoneNumber: z.string().optional(), address: z.string().optional(), memo: z.string().optional(), - pictures: z.array(z.string()), + pictures: z.array(z.any().optional()), }); export const useAddPlaceDetailForm = ( diff --git a/app/add-course/detail/_components/AddPlaceDetail.tsx b/app/add-course/detail/_components/AddPlaceDetail.tsx index 9e1f2c5..ba92775 100644 --- a/app/add-course/detail/_components/AddPlaceDetail.tsx +++ b/app/add-course/detail/_components/AddPlaceDetail.tsx @@ -15,6 +15,7 @@ import { categoryImageMap } from "@/lib/utils"; import { useAddPlaceDetailForm } from "../../_hooks/useAddPlaceDetailForm"; import { FormProvider } from "react-hook-form"; import { useCreatePlace } from "@/apis/place/PlaceApi.mutation"; +import { match } from "assert"; export type DefaultImageType = { id: string; @@ -24,22 +25,31 @@ export type DefaultImageType = { export const DEFAULT_IMAGES: DefaultImageType[] = [ { id: "DISH", - src: "default_food.png", + src: "/png/default_food.png", }, { id: "DESSERT", - src: "default_dessert.png", + src: "/png/default_dessert.png", }, { id: "ALCOHOL", - src: "default_alcohol.png", + src: "/png/default_alcohol.png", }, { id: "ARCADE", - src: "default_arcade.png", + src: "/png/default_arcade.png", }, ]; +const createFileFromImagePath = async ( + imagePath: string, + fileName: string +): Promise => { + const response = await fetch(imagePath); + const blob = await response.blob(); + return new File([blob], fileName, { type: blob.type }); +}; + const AddPlaceDetail: React.FC = () => { const router = useRouter(); const [selectedChips, setSelectedChips] = useState([]); @@ -57,15 +67,49 @@ const AddPlaceDetail: React.FC = () => { roomPlacesInfo, } = useCourseContext(); + const payloadForRequest = useMemo(() => { + const values = methods.watch(); + const scheduleIds = selectedChips; + + return { + scheduleIds: scheduleIds, + name: autoData && autoData.data ? autoData.data.name : values.name, + url: autoData && autoData.data ? autoData.data.url : values.url || "-", + address: values.address || "-", + openingHours: + autoData && autoData.data + ? autoData.data.openingHours + : values.openingHours || "-", + phoneNumber: + autoData && autoData.data + ? autoData.data.phoneNumber + : values.phoneNumber || "-", + reviewCount: autoData && autoData.data ? autoData.data.reviewCount : 0, + starGrade: autoData && autoData.data ? autoData.data.starGrade : 0, + memo: values.memo || "-", + origin: autoData && autoData.data ? autoData.data.origin : "MANUAL", + voteLikeCount: 0, + voteDislikeCount: 0, + longitude: 0, + latitude: 0, + autoCompletedPlaceImageUrls: + (autoData && autoData.data.placeImageUrls.contents) || [], + }; + }, [methods, selectedChips, categoryList, autoData]); + + useEffect(() => { + console.log("payloadForRequest changed:", payloadForRequest); + }, [payloadForRequest]); + const { mutate: createPlaceMutate } = useCreatePlace({ options: { onSuccess: (res) => { const placeResponse: PlaceResponseDto = res.data; setIsClipboardText(false); - addPlaceInfo(placeResponse); // + addPlaceInfo(placeResponse); - router.back(); + router.replace(`/add-course?roomUid=${roomUid}`); }, onError: (error) => { console.error("장소 등록 실패:", error); @@ -93,13 +137,20 @@ const AddPlaceDetail: React.FC = () => { useEffect(() => { if (autoData) { + console.log( + autoData, + "autoCompletedPlaceImageUrlsautoCompletedPlaceImageUrls" + ); setIsClipboardText(true); - const { name, url, address, phoneNumber } = autoData.data || {}; + const { name, url, address, phoneNumber, openingHours } = + autoData.data || {}; methods.setValue("name", name); methods.setValue("url", url); methods.setValue("address", address); methods.setValue("phoneNumber", phoneNumber); + methods.setValue("openingHours", openingHours); + const values = methods.getValues(); console.log("values", values); } @@ -118,53 +169,50 @@ const AddPlaceDetail: React.FC = () => { return categoryList?.find((category) => category.scheduleId === chipId); }); - const defaultImages = selectedCategories - .map((category) => { - const matchedImage = DEFAULT_IMAGES.find( - (image) => image.id === category!.type - ); + const defaultImages = ( + await Promise.all( + selectedCategories.map(async (category) => { + const matchedImage = DEFAULT_IMAGES.find( + (image) => image.id === category!.type + ); + if (matchedImage) { + const file = await createFileFromImagePath( + matchedImage.src, + matchedImage.src.split("/").pop() || "default.png" + ); + // 파일 정보를 콘솔에 출력 + console.log({ + name: file.name, + size: file.size, + type: file.type, + }); + return file; + } + return null; + }) + ) + ).filter((file): file is File => file !== null); - const res = `${process.env.NEXT_PUBLIC_DNS_URL}/png/${matchedImage?.src}`; - console.log(res, "=====matched?"); - return res; - }) - .filter((src) => src !== null); - console.log("Default Images to be used:", defaultImages); - const payloadForRequest: AddPlaceRequestDto = { - scheduleIds: scheduleIds, - name: autoData && autoData.data ? autoData.data.name : values.name, - url: autoData && autoData.data ? autoData.data.url : values.url || "-", - address: values.address || "-", - openingHours: - autoData && autoData.data - ? autoData.data.openingHours - : values.openingHours - ? values.openingHours - : "-", - phoneNumber: - autoData && autoData.data - ? autoData.data.phoneNumber - : values.phoneNumber - ? values.phoneNumber - : "-", - reviewCount: autoData && autoData.data ? autoData.data.reviewCount : 0, - starGrade: autoData && autoData.data ? autoData.data.starGrade : 0, - memo: values.memo || "-", - voteLikeCount: 0, - voteDislikeCount: 0, - longitude: 0, - latitude: 0, - }; + const formImages = + values.pictures && Array.isArray(values.pictures) + ? values.pictures.filter((file): file is File => file instanceof File) + : []; - const formImages = values.pictures - ? values.pictures.filter((file): file is string => file !== null) - : []; + console.log("formImages", formImages); + const placeImages = + autoData !== null + ? [] + : formImages.length > 0 + ? formImages + : defaultImages; const payload = { addPlaceRequest: payloadForRequest, - placeImages: formImages.length > 0 ? formImages : defaultImages, + ...(autoData !== null ? {} : { placeImages }), }; + console.log(placeImages, "placeimg"); + createPlaceMutate({ roomUid, payload, @@ -174,7 +222,6 @@ const AddPlaceDetail: React.FC = () => { useEffect(() => { return () => { setIsClipboardText(false); - // setAutoData(null); }; }, []); @@ -271,8 +318,16 @@ const AddPlaceDetail: React.FC = () => { className="w-[80px] h-[80px]" id="picture" type="file" - onFilesChange={(files) => { - methods.setValue("pictures", files); + onFilesChange={(formData) => { + const files = Array.from( + formData.getAll("placeImages") + ); + methods.setValue( + "pictures", + files.filter( + (file): file is File => file instanceof File + ) + ); }} multiple /> diff --git a/components/common/BottomSheet/SheetWithCourse.tsx b/components/common/BottomSheet/SheetWithCourse.tsx index 186604c..8354485 100644 --- a/components/common/BottomSheet/SheetWithCourse.tsx +++ b/components/common/BottomSheet/SheetWithCourse.tsx @@ -35,7 +35,7 @@ export function SheetWithCourse({ handleItemClick }: SheetWithCourseProps) {
diff --git a/components/common/Cards/CardForCopiedContent.tsx b/components/common/Cards/CardForCopiedContent.tsx index 584ebb8..f60eae1 100644 --- a/components/common/Cards/CardForCopiedContent.tsx +++ b/components/common/Cards/CardForCopiedContent.tsx @@ -34,26 +34,14 @@ export const CardForCopiedContent: React.FC = ({ origin, }) => { const router = useRouter(); - // const { addPlaceInfo } = useCourseContext(); - + const formattedStarGrade = starGrade?.toFixed(2); const onButtonClick = () => { - // addPlaceInfo({ - // name, - // url, - // placeImageUrls, - // address, - // phoneNumber, - // starGrade, - // reviewCount, - // origin, - // }); - router.push(`add-course/detail?roomUid=${roomUidStorage?.get()?.roomUid}`); }; return (
@@ -84,12 +72,16 @@ export const CardForCopiedContent: React.FC = ({ unoptimized />
- - {starGrade} - - - ({reviewCount}) - + {starGrade && ( + + {formattedStarGrade} + + )} + {reviewCount && ( + + ({reviewCount}) + + )}
-
+
{autoData?.data?.placeImageUrls.contents.map((src, index) => ( {`food${index ))}
- {autoData && ( + {placeUrl && (