From d29cb07624e41bd39a6c7e4d099f471de5e718c7 Mon Sep 17 00:00:00 2001 From: yihyun-kim1 Date: Wed, 21 Aug 2024 17:45:35 +0900 Subject: [PATCH] WIP) Reflect changes related to adding place logic --- apis/origin-place/types/dto/index.ts | 1 + apis/place/PlaceApi.ts | 14 +- apis/place/types/dto/index.ts | 26 +- app/add-course/_components/AddCourse.tsx | 52 +-- app/add-course/_components/LabelWithValue.tsx | 39 +++ app/add-course/_components/PlaceContainer.tsx | 21 +- .../_hooks/useAddPlaceDetailForm.ts | 43 +++ .../detail/_components/AddPlaceDetail.tsx | 328 ++++++++++++++++++ .../detail/_components/EditPlaceDetail.tsx | 268 ++++++++++++++ .../detail/_components/PlaceDetail.tsx | 272 --------------- app/add-course/detail/edit/page.tsx | 12 + app/add-course/detail/page.tsx | 4 +- app/edit-course/_components/EditCourse.tsx | 4 +- .../common/Cards/CardForCopiedContent.tsx | 31 +- .../common/Cards/CardWithAutoCompleteData.tsx | 106 +++++- components/common/Cards/CardWithImage.tsx | 2 +- .../common/Cards/CardWithImageSmall.tsx | 46 +-- components/common/Modal/ModalWithCategory.tsx | 2 +- components/common/Modal/ModalWithVote.tsx | 32 ++ lib/utils.ts | 2 +- package.json | 1 + pnpm-lock.yaml | 12 + providers/contexts/course/course-handler.tsx | 42 ++- providers/contexts/course/course-state.ts | 13 +- providers/course-provider.tsx | 8 +- public/svg/ic_horizon.svg | 9 + 26 files changed, 1003 insertions(+), 387 deletions(-) create mode 100644 app/add-course/_components/LabelWithValue.tsx create mode 100644 app/add-course/_hooks/useAddPlaceDetailForm.ts create mode 100644 app/add-course/detail/_components/AddPlaceDetail.tsx create mode 100644 app/add-course/detail/_components/EditPlaceDetail.tsx delete mode 100644 app/add-course/detail/_components/PlaceDetail.tsx create mode 100644 app/add-course/detail/edit/page.tsx create mode 100644 components/common/Modal/ModalWithVote.tsx create mode 100644 public/svg/ic_horizon.svg diff --git a/apis/origin-place/types/dto/index.ts b/apis/origin-place/types/dto/index.ts index f924946..93c5d49 100644 --- a/apis/origin-place/types/dto/index.ts +++ b/apis/origin-place/types/dto/index.ts @@ -1,5 +1,6 @@ export type PlaceAutoCompleteResponse = { data: { + openingHours?: string | null | undefined; name: string; url: string; placeImageUrls: { diff --git a/apis/place/PlaceApi.ts b/apis/place/PlaceApi.ts index 8463a6e..12422a2 100644 --- a/apis/place/PlaceApi.ts +++ b/apis/place/PlaceApi.ts @@ -37,14 +37,24 @@ class PlaceApi { placeImages: string[]; }; }): Promise> => { + //SuccessPlaceTypeGroupResponse[] + const formData = new FormData(); + + formData.append("addPlaceRequest", JSON.stringify(payload.addPlaceRequest)); + + payload.placeImages.forEach((image) => { + formData.append("placeImages", image); + }); + const { data } = await this.axios({ method: "POST", url: `/rooms/${roomUid}/places`, - data: payload, + data: formData, headers: { "Content-Type": "multipart/form-data", }, }); + return data; }; @@ -68,7 +78,7 @@ class PlaceApi { payload, }: { roomUid: string; - placeId: string; + placeId: number; payload: { modifyPlaceRequest: ModifyPlaceRequestDto; newPlaceImages: string[]; diff --git a/apis/place/types/dto/index.ts b/apis/place/types/dto/index.ts index 82540ba..33348f6 100644 --- a/apis/place/types/dto/index.ts +++ b/apis/place/types/dto/index.ts @@ -2,7 +2,7 @@ * API Response */ export interface PlaceResponseDto { - id: number; + id?: number; roomUid: string; scheduleId: number; name: string; @@ -15,6 +15,7 @@ export interface PlaceResponseDto { starGrade?: number; origin: "AVOCADO" | "LEMON" | "MANUAL"; memo?: string; + openingHours?: string; reviewCount?: number; confirmed?: boolean; } @@ -34,15 +35,15 @@ export interface SuccessPlaceTypeGroupResponse { * API Payloads */ export interface AddPlaceRequestDto { - scheduleId: number; - type: string; + scheduleIds: number[]; name: string; url?: string; address?: string | null; + openingHours?: string | null; reviewCount?: number | null; phoneNumber?: string | null; starGrade?: number | null; - memo: string; + memo?: string; voteLikeCount?: number | null; voteDislikeCount?: number | null; longitude?: number | null; @@ -56,14 +57,17 @@ export interface CreatePlacePayloadDto { export interface ModifyPlaceRequestDto { scheduleId: number; - scheduleType: string; name: string; - url: string; + url?: string; deleteTargetUrls: string[]; address: string; - phoneNumber: string; - starGrade: number; - memo: string; - voteLikeCount: number; - voteDislikeCount: number; + phoneNumber?: string; + starGrade?: number; + memo?: string; + reviewCount?: number; + openingHours: string; + voteLikeCount?: number; + voteDislikeCount?: number; + longitude?: number; + latitude?: number; } diff --git a/app/add-course/_components/AddCourse.tsx b/app/add-course/_components/AddCourse.tsx index 254aafa..87430c9 100644 --- a/app/add-course/_components/AddCourse.tsx +++ b/app/add-course/_components/AddCourse.tsx @@ -19,10 +19,7 @@ import { useCreatePlace } from "@/apis/origin-place/OriginPlaceApi.mutation"; import { PlaceContainer } from "./PlaceContainer"; import useShare from "@/hooks/useShare"; import { RoomResponse } from "@/apis/room/types/model"; -import { - PlaceResponseDto, - ScheduleTypeGroupResponse, -} from "@/apis/place/types/dto"; +import { ModalWithVote } from "@/components/common/Modal/ModalWithVote"; export interface AddCourseProps { data: RoomResponse; @@ -36,19 +33,20 @@ const AddCourse = ({ data }: AddCourseProps) => { 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, setRoomInfo, + roomPlacesInfo, setRoomPlacesInfo, categoryList, setCategoryList, - autoPlaceInfo, - setAutoPlaceInfo, isClipboardText, setIsClipboardText, + addPlaceInfo, autoData, setAutoData, } = useCourseContext(); @@ -80,21 +78,6 @@ const AddCourse = ({ data }: AddCourseProps) => { } }, [currentPlacesData, setRoomPlacesInfo]); - const hasPlaces = useMemo(() => { - if ( - !currentPlacesData || - !Array.isArray(currentPlacesData) || - currentPlacesData.length === 0 - ) { - console.error("currentPlacesData is not in expected format or is empty."); - return false; - } - - return currentPlacesData.some( - (schedule) => Array.isArray(schedule.places) && schedule.places.length > 0 - ); - }, [currentPlacesData]); - const { mutate: createPlaceMutate } = useCreatePlace({ options: { onSuccess: (res) => { @@ -127,7 +110,11 @@ const AddCourse = ({ data }: AddCourseProps) => { ?.places || []; return matchingPlaces; } - }, [currentPlacesData, selectedCategory]); + }, [currentPlacesData, selectedCategory, roomPlacesInfo]); + + const hasPlaces = useMemo(() => { + return filteredPlaces.length > 0; + }, [filteredPlaces]); const fetchCoursePageData = async (roomUid: string) => { try { @@ -147,6 +134,14 @@ const AddCourse = ({ data }: AddCourseProps) => { fetchCoursePageData(roomUid); }, [roomUid]); + useEffect(() => { + if (filteredPlaces.length > 0) { + setIsReadyToVote(true); + } else { + setIsReadyToVote(false); + } + }, [filteredPlaces]); + useEffect(() => { const fetchData = async () => { if (!isMobile && isValidClipboardText) { @@ -196,7 +191,6 @@ const AddCourse = ({ data }: AddCourseProps) => { const handleChipClick = (index: number) => { setSelectedChip(index === selectedChip ? null : index); - setSelectedCategory(index === selectedCategory ? null : index); }; @@ -205,11 +199,14 @@ const AddCourse = ({ data }: AddCourseProps) => { return (
-

+

{roomInfo?.name}

-
); }; diff --git a/app/add-course/_components/LabelWithValue.tsx b/app/add-course/_components/LabelWithValue.tsx new file mode 100644 index 0000000..39d884d --- /dev/null +++ b/app/add-course/_components/LabelWithValue.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; + +export interface LabelWithValueProps + extends React.InputHTMLAttributes { + iconSrc?: string; + required?: boolean; +} + +const LabelWithValue = React.forwardRef( + ({ className, iconSrc, value, required, type = "text", ...props }, ref) => { + return ( +
+ {iconSrc && ( + icon + )} + +
+ ); + } +); +LabelWithValue.displayName = "LabelWithValue"; +export { LabelWithValue }; diff --git a/app/add-course/_components/PlaceContainer.tsx b/app/add-course/_components/PlaceContainer.tsx index 34a04e6..84b8afb 100644 --- a/app/add-course/_components/PlaceContainer.tsx +++ b/app/add-course/_components/PlaceContainer.tsx @@ -1,5 +1,14 @@ -import { ScheduleTypeGroupResponse } from "@/apis/place/types/dto"; +"use client"; +import { + PlaceResponseDto, + ScheduleTypeGroupResponse, +} from "@/apis/place/types/dto"; import { CardWithImageSmall } from "@/components/common/Cards/CardWithImageSmall"; +import { categoryImageMap } from "@/lib/utils"; +import { useCourseContext } from "@/providers/course-provider"; +import { roomUidStorage } from "@/utils/web-storage/room-uid"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export interface PlacesContainerProps { placesData: ScheduleTypeGroupResponse; @@ -12,6 +21,15 @@ export const PlaceContainer: React.FC = ({ }) => { const { places } = placesData; + const router = useRouter(); + const { selectedPlaceInfo, setSelectedPlaceInfo } = useCourseContext(); + const handleCardClick = (place: PlaceResponseDto) => { + setSelectedPlaceInfo(place); + router.push( + `add-course/detail/edit?roomUid=${roomUidStorage?.get()?.roomUid}` + ); + }; + return (
{places.map((place) => ( @@ -24,6 +42,7 @@ export const PlaceContainer: React.FC = ({ reviewCount={place?.reviewCount} images={place?.placeImageUrls?.contents} category={scheduleInfo} + onButtonClick={() => handleCardClick(place)} />
))} diff --git a/app/add-course/_hooks/useAddPlaceDetailForm.ts b/app/add-course/_hooks/useAddPlaceDetailForm.ts new file mode 100644 index 0000000..113bd67 --- /dev/null +++ b/app/add-course/_hooks/useAddPlaceDetailForm.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import { UseFormProps, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +export type CommonPlaceDetailFormType = { + // 해당 파일에서는 사용자가 핸들링 가능한 값만 정의 + scheduleId: number; + name: string; + type: string; + url?: string; + reviewCount?: number; + starGrade?: number; + openingHours?: string; + phoneNumber?: string; + address?: string; + memo?: string; + pictures: string[]; +}; + +const addPlaceDetailSchema = z.object({ + scheduleId: z.number(), + name: z.string(), + type: z.string(), + url: z.string().optional(), + reviewCount: z.number().optional(), + starGrade: z.number().optional(), + openingHours: z.string().optional(), + phoneNumber: z.string().optional(), + address: z.string().optional(), + memo: z.string().optional(), + pictures: z.array(z.string()), +}); + +export const useAddPlaceDetailForm = ( + options?: UseFormProps +) => { + return useForm({ + resolver: zodResolver(addPlaceDetailSchema), + mode: "onChange", + reValidateMode: "onChange", + ...options, + }); +}; diff --git a/app/add-course/detail/_components/AddPlaceDetail.tsx b/app/add-course/detail/_components/AddPlaceDetail.tsx new file mode 100644 index 0000000..5fa3d6d --- /dev/null +++ b/app/add-course/detail/_components/AddPlaceDetail.tsx @@ -0,0 +1,328 @@ +"use client"; + +import NavigationBar from "@/components/common/Navigation/NavigationBar"; +import Image from "next/image"; +import { CategoryChip } from "../../_components/CategoryChip"; +import { InputWithLabel } from "../../_components/InputWithLabel"; +import { CardWithAutoCompleteData } from "@/components/common/Cards/CardWithAutoCompleteData"; +import { InputWithImage } from "../../_components/InputWithImage"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCourseContext } from "@/providers/course-provider"; +import { AddPlaceRequestDto, PlaceResponseDto } from "@/apis/place/types/dto"; +import { useEffect, useRef, useState, useMemo } from "react"; +import placeApi from "@/apis/place/PlaceApi"; +import { categoryImageMap } from "@/lib/utils"; +import { useAddPlaceDetailForm } from "../../_hooks/useAddPlaceDetailForm"; +import { FormProvider } from "react-hook-form"; +import { useCreatePlace } from "@/apis/place/PlaceApi.mutation"; + +export type DefaultImageType = { + id: string; + src: string; +}; + +export const DEFAULT_IMAGES: DefaultImageType[] = [ + { + id: "DISH", + src: "default_food.png", + }, + { + id: "DESSERT", + src: "default_dessert.png", + }, + { + id: "ALCOHOL", + src: "default_alcohol.png", + }, + { + id: "ARCADE", + src: "default_arcade.png", + }, +]; + +const AddPlaceDetail: React.FC = () => { + const router = useRouter(); + const [selectedChips, setSelectedChips] = useState([]); + const methods = useAddPlaceDetailForm(); + const searchParams = useSearchParams(); + const roomUid = searchParams.get("roomUid") || ""; + const { + categoryList, + isClipboardText, + setIsClipboardText, + autoData, + setAutoData, + addPlaceInfo, + roomPlacesInfo, + } = useCourseContext(); + + const { mutate: createPlaceMutate } = useCreatePlace({ + options: { + onSuccess: (res) => { + const placeResponse: PlaceResponseDto = res.data; + setIsClipboardText(false); + addPlaceInfo(placeResponse); + router.back(); + }, + onError: (error) => { + console.error("장소 등록 실패:", error); + }, + }, + }); + + const handleChipClick = (index: number) => { + setSelectedChips((prevChips) => { + if (prevChips.includes(index)) { + return prevChips.filter((chip) => chip !== index); // 이미 선택된 경우 선택 해제 + } else { + return [...prevChips, index]; // 새로운 칩 선택 + } + }); + console.log(selectedChips, "Selected Chips"); + }; + + const selectedCategory = useMemo(() => { + const result = selectedChips.map((chipId) => + categoryList?.find((category) => category.scheduleId === chipId) + ); + + return result; + }, [selectedChips, categoryList]); + + useEffect(() => { + if (autoData) { + setIsClipboardText(true); + + const { name, url, address, phoneNumber } = autoData.data || {}; + methods.setValue("name", name); + methods.setValue("url", url); + methods.setValue("address", address); + methods.setValue("phoneNumber", phoneNumber); + const values = methods.getValues(); + console.log("values", values); + } + }, [autoData]); + + const onCompleteButtonClick = async () => { + const values = methods.getValues(); + console.log("values", values); + if (!values.name || !selectedChips || !selectedCategory) { + return; + } + + const scheduleIds = selectedChips; + + const selectedCategories = scheduleIds.map((chipId) => { + return categoryList?.find((category) => category.scheduleId === chipId); + }); + + const defaultImages = selectedCategories + .map((category) => { + const matchedImage = DEFAULT_IMAGES.find( + (image) => image.id === category!.type + ); + + console.log(matchedImage, "=====matched?"); + return `${process.env.NEXT_PUBLIC_DNS_URL}/png/${matchedImage?.src}`; + }) + .filter((src) => src !== null); + + const payload: 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, + }; + console.log("payload:", payload); + createPlaceMutate({ + roomUid, + payload: { + addPlaceRequest: payload, + placeImages: values.pictures + ? values.pictures.filter((file): file is string => file !== null) + : defaultImages, + }, + }); + }; + + useEffect(() => { + return () => { + setIsClipboardText(false); + // setAutoData(null); + }; + }, []); + + return ( + +
+ router.back()} + > + ic_arrow_left_24.png +

장소 추가하기

+
+ } + rightSlot={ +

+ 완료 +

+ } + /> +
+
+
+

+ 카테고리 +

+

+ 필수 +

+
+
+ 여러 개 선택 가능해요 +
+
+ {categoryList?.map( + (item) => + item.scheduleId !== null && + item.scheduleId !== undefined && + item.name && ( + handleChipClick(item.scheduleId as number)} + /> + ) + )} +
+
+
+ {isClipboardText === true ? ( +
+

+ 메모 +

+ + +
+ ) : ( +
+
+
+

+ 장소 이름 +

+

+ 필수 +

+
+
+ +
+
+ { + methods.setValue("pictures", files); + }} + multiple + /> +
+
+
+

+ 링크 +

+ +
+
+

+ 영업정보 +

+ + + +
+
+

+ 메모 +

+ +
+
+ )} +
+
+
+ + ); +}; + +export default AddPlaceDetail; diff --git a/app/add-course/detail/_components/EditPlaceDetail.tsx b/app/add-course/detail/_components/EditPlaceDetail.tsx new file mode 100644 index 0000000..04adf9a --- /dev/null +++ b/app/add-course/detail/_components/EditPlaceDetail.tsx @@ -0,0 +1,268 @@ +"use client"; + +import NavigationBar from "@/components/common/Navigation/NavigationBar"; +import Image from "next/image"; +import { CategoryChip } from "../../_components/CategoryChip"; +import { InputWithLabel } from "../../_components/InputWithLabel"; +import { CardWithAutoCompleteData } from "@/components/common/Cards/CardWithAutoCompleteData"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCourseContext } from "@/providers/course-provider"; +import { + AddPlaceRequestDto, + ModifyPlaceRequestDto, + PlaceResponseDto, +} from "@/apis/place/types/dto"; +import { useEffect, useState } from "react"; +import placeApi from "@/apis/place/PlaceApi"; +import { categoryImageMap } from "@/lib/utils"; +import { FormProvider } from "react-hook-form"; +import { useAddPlaceDetailForm } from "../../_hooks/useAddPlaceDetailForm"; +import { InputWithImage } from "../../_components/InputWithImage"; +import { useUpdatePlace } from "@/apis/place/PlaceApi.mutation"; + +const EditPlaceDetail: React.FC = () => { + const router = useRouter(); + const methods = useAddPlaceDetailForm(); + const [placeInfo, setPlaceInfo] = useState(null); + const [selectedChip, setSelectedChip] = useState(null); + const searchParams = useSearchParams(); + const roomUid = searchParams.get("roomUid") || ""; + + const { categoryList, selectedPlaceInfo, setSelectedPlaceInfo } = + useCourseContext(); + + const { mutate: updatePlaceMutate } = useUpdatePlace({ + options: { + onSuccess: () => { + router.back(); + }, + onError: (error) => { + console.error("장소 수정 실패:", error); + }, + }, + }); + + const updatePlaceInfo = (_placeInfo: PlaceResponseDto) => { + setPlaceInfo(_placeInfo); + }; + + useEffect(() => { + if (selectedPlaceInfo !== null) { + updatePlaceInfo(selectedPlaceInfo); + } + }, [selectedPlaceInfo]); + + const handleChipClick = (index: number) => { + setSelectedChip(index === selectedChip ? null : index); + }; + + useEffect(() => { + if (selectedPlaceInfo) { + console.log(selectedPlaceInfo, "selectedPlaceInfo?"); + const { name, url, address, phoneNumber, memo } = selectedPlaceInfo || {}; + methods.setValue("name", name); + methods.setValue("url", url); + methods.setValue("address", address); + methods.setValue("phoneNumber", phoneNumber); + methods.setValue("memo", memo); + } + }, [selectedPlaceInfo]); + + const onCompleteButtonClick = async () => { + const values = methods.getValues(); + console.log("values", values); + if (!values.name || !selectedChip || !selectedPlaceInfo) { + return; + } + + const newImages = values.pictures || ["/png/food.png"]; // 사용자가 새로 추가한 이미지들 + const currentImages = selectedPlaceInfo.placeImageUrls.contents || []; // 기존 이미지들 + + const deleteTargetUrls = currentImages.filter( + (image) => !newImages.includes(image) + ); + const newPlaceImages = newImages.map((image) => + typeof image === "string" ? image : URL.createObjectURL(image) + ); + + const payload: ModifyPlaceRequestDto = { + scheduleId: + (selectedPlaceInfo.scheduleId as number) || + (values.scheduleId as number), + name: values.name ? values.name : selectedPlaceInfo.name, + url: values.url ? values.url : "-", + address: values.address ? values.address : "-", + phoneNumber: values.phoneNumber ? values.phoneNumber : "-", + starGrade: values.starGrade ? values.starGrade : 0, + reviewCount: values.reviewCount ? values.reviewCount : 0, + memo: values.memo ? values.memo : "-", + openingHours: values.openingHours ? values.openingHours : "-", + voteLikeCount: 0, + voteDislikeCount: 0, + longitude: 0, + latitude: 0, + deleteTargetUrls, + }; + + updatePlaceMutate({ + roomUid, + placeId: selectedPlaceInfo.id as number, + payload: { + modifyPlaceRequest: payload, + newPlaceImages, + }, + }); + }; + + useEffect(() => { + return () => { + setSelectedPlaceInfo(null); + }; + }, []); + + return ( + +
+ router.back()} + > + ic_arrow_left_24.png +

+ 장소 수정하기 +

+
+ } + rightSlot={ +

+ 완료 +

+ } + /> +
+
+
+

+ 카테고리 +

+

+ 필수 +

+
+
+ 여러 개 선택 가능해요 +
+
+ {categoryList?.map( + (item) => + item.scheduleId !== null && + item.scheduleId !== undefined && + item.name && ( + handleChipClick(item.scheduleId as number)} + /> + ) + )} +
+
+ +
+
+
+

+ 장소 이름 +

+

+ 필수 +

+
+
+ +
+
+ { + methods.setValue("pictures", files); + }} + multiple + /> +
+
+ +
+

+ 링크 +

+ +
+
+

+ 영업정보 +

+ + + +
+
+

+ 메모 +

+ +
+
+ {/* */} +
+ +
+ ); +}; + +export default EditPlaceDetail; diff --git a/app/add-course/detail/_components/PlaceDetail.tsx b/app/add-course/detail/_components/PlaceDetail.tsx deleted file mode 100644 index 409656d..0000000 --- a/app/add-course/detail/_components/PlaceDetail.tsx +++ /dev/null @@ -1,272 +0,0 @@ -"use client"; - -import NavigationBar from "@/components/common/Navigation/NavigationBar"; -import Image from "next/image"; -import { CategoryChip } from "../../_components/CategoryChip"; -import { InputWithLabel } from "../../_components/InputWithLabel"; -import { CardWithAutoCompleteData } from "@/components/common/Cards/CardWithAutoCompleteData"; -import { InputWithImage } from "../../_components/InputWithImage"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useCourseContext } from "@/providers/course-provider"; -import { AddPlaceRequestDto } from "@/apis/place/types/dto"; -import { useEffect, useState } from "react"; -import placeApi from "@/apis/place/PlaceApi"; -import { categoryImageMap } from "@/lib/utils"; - -const PlaceDetail: React.FC = () => { - const router = useRouter(); - const [placeName, setPlaceName] = useState(""); - const [url, setUrl] = useState(""); - const [openingHours, setOpeningHours] = useState(""); - const [address, setAddress] = useState(""); - const [phoneNumber, setPhoneNumber] = useState(""); - const [memoContent, setMemoContent] = useState(""); - const [selectedChip, setSelectedChip] = useState([]); - const [reviewCount, setReviewCount] = useState(null); - const [pictures, setPictures] = useState([]); - const searchParams = useSearchParams(); - const roomUid = searchParams.get("roomUid") || ""; - const { categoryList, isClipboardText, setIsClipboardText, autoPlaceInfo } = - useCourseContext(); - - const handleChipClick = (index: number) => { - setSelectedChip((prevSelectedChips) => { - if (prevSelectedChips.includes(index)) { - return prevSelectedChips.filter((chip) => chip !== index); - } else { - return [...prevSelectedChips, index]; - } - }); - }; - - useEffect(() => { - if (autoPlaceInfo && autoPlaceInfo[0]) { - setPlaceName(autoPlaceInfo[0].name); - } - }, [autoPlaceInfo]); - - const onCompleteButtonClick = async () => { - if (!placeName || selectedChip.length === 0) { - return; - } - - try { - await Promise.all( - selectedChip.map(async (chipId) => { - const selectedCategory = categoryList?.find( - (category) => category.scheduleId === chipId - ); - if (!selectedCategory) return; - - const defaultImage = categoryImageMap[selectedCategory.name]; - const payload: AddPlaceRequestDto = { - scheduleId: selectedCategory.scheduleId as number, - type: selectedCategory.name, - name: - autoPlaceInfo && autoPlaceInfo[0] - ? autoPlaceInfo[0].name - : placeName, - url: - autoPlaceInfo && autoPlaceInfo[0] - ? autoPlaceInfo[0].url - : url || "-", - address: address || "-", - phoneNumber: - autoPlaceInfo && autoPlaceInfo[0] - ? autoPlaceInfo[0].phoneNumber - : phoneNumber - ? phoneNumber - : "-", - reviewCount: reviewCount || 0, - starGrade: - autoPlaceInfo && autoPlaceInfo[0] - ? autoPlaceInfo[0].starGrade - : 0, - memo: memoContent || "-", - voteLikeCount: 0, - voteDislikeCount: 0, - longitude: 0, - latitude: 0, - }; - - await placeApi.createPlace({ - roomUid, - payload: { - addPlaceRequest: payload, - placeImages: pictures.length > 0 ? pictures : [defaultImage], - }, - }); - }) - ); - - console.log("장소 생성 성공"); - router.push("/add-course"); - } catch (error) { - console.error("장소 생성 실패:", error); - } - }; - - return ( -
- router.back()} - > - ic_arrow_left_24.png -

setIsClipboardText(false)} - > - 장소 추가하기 -

-
- } - rightSlot={ -

- 완료 -

- } - /> -
-
-
-

- 카테고리 -

-

- 필수 -

-
-
- 여러 개 선택 가능해요 -
-
- {categoryList?.map( - (item) => - item.scheduleId !== null && - item.scheduleId !== undefined && - item.name && ( - handleChipClick(item.scheduleId as number)} - /> - ) - )} -
-
-
- {isClipboardText === true ? ( -
-

- 메모 -

- setMemoContent(e.target.value)} - /> - -
- ) : ( -
-
-
-

- 장소 이름 -

-

- 필수 -

-
-
- setPlaceName(e.target.value)} - /> -
-
- -
-
- -
-

- 링크 -

- setUrl(e.target.value)} - /> -
-
-

- 영업정보 -

- setOpeningHours(e.target.value)} - /> - setAddress(e.target.value)} - /> - setPhoneNumber(e.target.value)} - /> -
-
-

- 메모 -

- setMemoContent(e.target.value)} - /> -
-
- )} -
-
- - ); -}; - -export default PlaceDetail; diff --git a/app/add-course/detail/edit/page.tsx b/app/add-course/detail/edit/page.tsx new file mode 100644 index 0000000..ea80d62 --- /dev/null +++ b/app/add-course/detail/edit/page.tsx @@ -0,0 +1,12 @@ +import React, { Suspense } from "react"; +import EditPlaceDetail from "../_components/EditPlaceDetail"; + +const EditDetailPage = () => { + return ( + + + + ); +}; + +export default EditDetailPage; diff --git a/app/add-course/detail/page.tsx b/app/add-course/detail/page.tsx index ce949fb..270a66f 100644 --- a/app/add-course/detail/page.tsx +++ b/app/add-course/detail/page.tsx @@ -1,10 +1,10 @@ import React, { Suspense } from "react"; -import PlaceDetail from "./_components/PlaceDetail"; +import AddPlaceDetail from "./_components/AddPlaceDetail"; const AddDetailPage = () => { return ( - + ); }; diff --git a/app/edit-course/_components/EditCourse.tsx b/app/edit-course/_components/EditCourse.tsx index 0b31545..eab2c4d 100644 --- a/app/edit-course/_components/EditCourse.tsx +++ b/app/edit-course/_components/EditCourse.tsx @@ -32,7 +32,9 @@ const EditCourse = () => { width={24} height={24} /> -

순서 편집하기

+

+ 순서 편집하기 +

} /> diff --git a/components/common/Cards/CardForCopiedContent.tsx b/components/common/Cards/CardForCopiedContent.tsx index 2f0b06c..68d8765 100644 --- a/components/common/Cards/CardForCopiedContent.tsx +++ b/components/common/Cards/CardForCopiedContent.tsx @@ -3,7 +3,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { useCourseContext } from "@/providers/course-provider"; import { roomUidStorage } from "@/utils/web-storage/room-uid"; import Image from "next/image"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; export type PlaceAutoCompleteData = { name: string; @@ -17,6 +17,7 @@ export type PlaceAutoCompleteData = { reviewCount: number; category?: string; origin: "AVOCADO" | "LEMON" | "MANUAL"; + openingHours?: string; }; export type CardForCopiedContentProps = PlaceAutoCompleteData; @@ -33,20 +34,19 @@ export const CardForCopiedContent: React.FC = ({ origin, }) => { const router = useRouter(); - const { addPlaceInfo } = useCourseContext(); + // const { addPlaceInfo } = useCourseContext(); const onButtonClick = () => { - addPlaceInfo({ - name, - url, - placeImageUrls, - address, - phoneNumber, - starGrade, - reviewCount, - category, - origin, - }); + // addPlaceInfo({ + // name, + // url, + // placeImageUrls, + // address, + // phoneNumber, + // starGrade, + // reviewCount, + // origin, + // }); router.push(`add-course/detail?roomUid=${roomUidStorage?.get()?.roomUid}`); }; @@ -76,11 +76,12 @@ export const CardForCopiedContent: React.FC = ({
naver
@@ -96,7 +97,7 @@ export const CardForCopiedContent: React.FC = ({ onClick={onButtonClick} > plusIcon { - const router = useRouter(); - const { autoPlaceInfo } = useCourseContext(); +export interface CardWithAutoCompleteDataProps { + register: UseFormRegister; +} + +export const CardWithAutoCompleteData = ({ + register, +}: CardWithAutoCompleteDataProps) => { + const { isClipboardText, autoData } = useCourseContext(); + console.log(autoData, "card======="); return ( -
-

- {autoPlaceInfo[0]?.category} -

+
-
+
-
- {autoPlaceInfo[0]?.name} -
+
{autoData?.data.name}
naver { />
- {autoPlaceInfo[0]?.starGrade} ( - {autoPlaceInfo[0]?.reviewCount}) + {autoData?.data?.starGrade} ({autoData?.data?.reviewCount})
- {autoPlaceInfo[0]?.placeImageUrls.contents.map((src, index) => ( + {autoData?.data?.placeImageUrls.contents.map((src, index) => ( {
- {autoPlaceInfo[0]?.url ? ( + {autoData && ( - ) : null} + )} + {isClipboardText ? ( +
+
+ + + +
+
+ ) : ( +
+
+

+ 영업정보 +

+
+
+ + horizon + + horizon + +
+
+ )}
); }; diff --git a/components/common/Cards/CardWithImage.tsx b/components/common/Cards/CardWithImage.tsx index 434ae07..129dfed 100644 --- a/components/common/Cards/CardWithImage.tsx +++ b/components/common/Cards/CardWithImage.tsx @@ -121,7 +121,7 @@ export function CardWithLike({ size }: CardSizeProps) { > like = ({ reviewCount, images, category, + onButtonClick, }) => { - const router = useRouter(); - const { addPlaceInfo } = useCourseContext(); - + console.log(category, "========="); const defaultImage = category ? categoryImageMap[category] : "/png/default_food.png"; - const onButtonClick = () => { - router.push(`add-course/detail?roomUid=${roomUidStorage?.get()?.roomUid}`); - }; const originLogoSrc = React.useMemo( () => - origin === "AVOCADO" ? "/svg/naver-icon.svg" : "/svg/kakao-icon.svg", + origin === "AVOCADO" + ? "/svg/naver-icon.svg" + : origin === "LEMON" + ? "/svg/kakao-icon.svg" + : null, [origin] ); @@ -54,21 +54,23 @@ export const CardWithImageSmall: React.FC = ({ {place} -
- logo - - {rating} - - - ({reviewCount}) - -
+ {originLogoSrc !== null && ( +
+ logo + + {rating} + + + ({reviewCount}) + +
+ )}
diff --git a/components/common/Modal/ModalWithCategory.tsx b/components/common/Modal/ModalWithCategory.tsx index d8894e0..dec9466 100644 --- a/components/common/Modal/ModalWithCategory.tsx +++ b/components/common/Modal/ModalWithCategory.tsx @@ -19,7 +19,7 @@ export const ModalWithCategory = ({
-

+

{modalText}

diff --git a/components/common/Modal/ModalWithVote.tsx b/components/common/Modal/ModalWithVote.tsx new file mode 100644 index 0000000..47d1f5f --- /dev/null +++ b/components/common/Modal/ModalWithVote.tsx @@ -0,0 +1,32 @@ +import { Button } from "../Button/Button"; + +interface ModalWithCategoryProps { + onButtonClick: () => void; +} + +export const ModalWithVote = ({ onButtonClick }: ModalWithCategoryProps) => { + return ( +
+
+
+

+ 후보지가 없는 카테고리가 있어요. +
+ 추가 후 다시 눌러주세요! +

+
+
+ + +
+
+
+ ); +}; diff --git a/lib/utils.ts b/lib/utils.ts index 919f733..a3d56ff 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -41,7 +41,7 @@ export const iconInfo = [ ]; export const categoryImageMap: { [key: string]: string } = { - FOOD: "/png/default_food.png", + DISH: "/png/default_food.png", DESSERT: "/png/default_dessert.png", ALCOHOL: "/png/default_alcohol.png", ARCADE: "/png/default_arcade.png", diff --git a/package.json b/package.json index f090b33..40e5f24 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ed3e9c..a3838aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.51.5(react@18.3.1)) '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -143,6 +146,11 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@hookform/resolvers@3.9.0': + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2070,6 +2078,10 @@ snapshots: '@eslint/js@8.57.0': {} + '@hookform/resolvers@3.9.0(react-hook-form@7.51.5(react@18.3.1))': + dependencies: + react-hook-form: 7.51.5(react@18.3.1) + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 diff --git a/providers/contexts/course/course-handler.tsx b/providers/contexts/course/course-handler.tsx index ff2a541..2777ee4 100644 --- a/providers/contexts/course/course-handler.tsx +++ b/providers/contexts/course/course-handler.tsx @@ -1,40 +1,70 @@ import React from "react"; import useCourseState from "./course-state"; import { PlaceAutoCompleteData } from "@/components/common/Cards/CardForCopiedContent"; +import { + PlaceResponseDto, + ScheduleTypeGroupResponse, +} from "@/apis/place/types/dto"; +import { PlaceAutoCompleteResponse } from "@/apis/origin-place/types/dto"; const useCourseHandler = () => { const { roomInfo, + autoPlaceInfo, roomPlacesInfo, categoryList, isClipboardText, - autoPlaceInfo, + selectedPlaceInfo, autoData, setCategoryList, setIsClipboardText, setRoomInfo, - setRoomPlacesInfo, setAutoPlaceInfo, + setRoomPlacesInfo, + setSelectedPlaceInfo, setAutoData, } = useCourseState(); - const addPlaceInfo = (newPlace: PlaceAutoCompleteData) => { - setAutoPlaceInfo((prevPlaces) => [...prevPlaces, newPlace]); + const addPlaceInfo = (newPlace: PlaceResponseDto) => { + console.log(roomPlacesInfo, "초기 상태"); + setRoomPlacesInfo((roomPlacesInfo) => { + // roomPlacesInfo가 null이 아닌 경우만 진행 + if (roomPlacesInfo) { + return roomPlacesInfo.map((group) => { + if (group.scheduleId === newPlace.scheduleId) { + // 동일한 scheduleId를 가진 그룹을 찾아 places에 newPlace를 추가 + console.log(group.scheduleId, newPlace.scheduleId, "id 상태"); + + return { + ...group, + places: [...group.places, newPlace], + }; + } + console.log(newPlace, "새 장소", roomPlacesInfo, "변화 상태"); + return group; + }); + } + + // 만약 roomPlacesInfo가 null인 경우에는 그대로 반환 + return roomPlacesInfo; + }); }; return { addPlaceInfo, + autoPlaceInfo, roomInfo, roomPlacesInfo, categoryList, isClipboardText, - autoPlaceInfo, + selectedPlaceInfo, autoData, setCategoryList, setIsClipboardText, setRoomInfo, - setRoomPlacesInfo, setAutoPlaceInfo, + setRoomPlacesInfo, + setSelectedPlaceInfo, setAutoData, }; }; diff --git a/providers/contexts/course/course-state.ts b/providers/contexts/course/course-state.ts index 413b59f..3625fda 100644 --- a/providers/contexts/course/course-state.ts +++ b/providers/contexts/course/course-state.ts @@ -1,5 +1,6 @@ import { PlaceAutoCompleteResponse } from "@/apis/origin-place/types/dto"; import { + PlaceResponseDto, ScheduleTypeGroupResponse, SuccessPlaceTypeGroupResponse, } from "@/apis/place/types/dto"; @@ -16,9 +17,9 @@ const useCourseState = () => { const [roomPlacesInfo, setRoomPlacesInfo] = useState< ScheduleTypeGroupResponse[] | null >(null); - const [autoPlaceInfo, setAutoPlaceInfo] = useState( - [] - ); + const [autoPlaceInfo, setAutoPlaceInfo] = useState([]); + const [selectedPlaceInfo, setSelectedPlaceInfo] = + useState(null); const [isClipboardText, setIsClipboardText] = useState(false); const [autoData, setAutoData] = useState( null @@ -26,14 +27,16 @@ const useCourseState = () => { return { roomInfo, + autoPlaceInfo, roomPlacesInfo, categoryList, - autoPlaceInfo, + selectedPlaceInfo, isClipboardText, setRoomInfo, + setAutoPlaceInfo, setRoomPlacesInfo, setCategoryList, - setAutoPlaceInfo, + setSelectedPlaceInfo, setIsClipboardText, autoData, setAutoData, diff --git a/providers/course-provider.tsx b/providers/course-provider.tsx index dbbaf50..4430f9e 100644 --- a/providers/course-provider.tsx +++ b/providers/course-provider.tsx @@ -14,16 +14,18 @@ export const CourseProvider: React.FC<{ children: ReactNode }> = ({ addPlaceInfo, categoryList, isClipboardText, - autoPlaceInfo, + selectedPlaceInfo, roomInfo, roomPlacesInfo, autoData, setCategoryList, setIsClipboardText, - setAutoPlaceInfo, + setSelectedPlaceInfo, setRoomInfo, setRoomPlacesInfo, setAutoData, + autoPlaceInfo, + setAutoPlaceInfo, } = useCourseHandler(); return ( @@ -35,6 +37,8 @@ export const CourseProvider: React.FC<{ children: ReactNode }> = ({ setRoomPlacesInfo, categoryList, setCategoryList, + selectedPlaceInfo, + setSelectedPlaceInfo, autoPlaceInfo, setAutoPlaceInfo, addPlaceInfo, diff --git a/public/svg/ic_horizon.svg b/public/svg/ic_horizon.svg new file mode 100644 index 0000000..9d81cd6 --- /dev/null +++ b/public/svg/ic_horizon.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file