From 397bbcc093a3cb02eda118a874dcc977bf0df37e Mon Sep 17 00:00:00 2001 From: jun-hwan00 <0528klkl@naver.com> Date: Sun, 24 Aug 2025 17:03:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20image=20=EA=B9=A8=EC=A7=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/editMeetingPage.tsx | 19 +- src/pages/step3.tsx | 22 +- src/widgets/meeting/editMeetingForm.tsx | 690 +++++++++++++----------- 3 files changed, 395 insertions(+), 336 deletions(-) diff --git a/src/pages/editMeetingPage.tsx b/src/pages/editMeetingPage.tsx index 9240321..75508df 100644 --- a/src/pages/editMeetingPage.tsx +++ b/src/pages/editMeetingPage.tsx @@ -1,13 +1,24 @@ -// import React from "react"; +import { useRef } from "react"; import { useNavigate } from "react-router-dom"; -// import StepFormLayout from "@/shared/ui/StepFormLayout"; -import EditMeetingForm from "@/widgets/meeting/editMeetingForm"; + +import EditMeetingForm, { + type EditMeetingFormRef, +} from "@/widgets/meeting/editMeetingForm"; import AnimatedPageLayout from "@/shared/layout"; import StepNavigation from "@/widgets/common/stepNavigation"; +import { useMeetingStore } from "@/store/meetingStore"; + const EditMeetingPage = () => { const navigate = useNavigate(); + const { setStartPoint } = useMeetingStore(); + const editFormRef = useRef(null); const handleSave = () => { + if (editFormRef.current) { + const filteredDepartures = editFormRef.current.getFilteredDepartures(); + setStartPoint(filteredDepartures); + } + navigate("/Plaza/step1_6"); }; @@ -31,7 +42,7 @@ const EditMeetingPage = () => { msOverflowStyle: "none", }} > - +
diff --git a/src/pages/step3.tsx b/src/pages/step3.tsx index 9b8a8ce..66fa34b 100644 --- a/src/pages/step3.tsx +++ b/src/pages/step3.tsx @@ -37,7 +37,10 @@ import IconActivity from "/src/shared/asset/icon/activity.svg?react"; import IconBar from "/src/shared/asset/icon/bar.svg?react"; import { IconMinus, IconDragHandle } from "@/shared/ui/svg"; import clsx from "clsx"; - +import restaurantIconUrl from "/src/shared/asset/icon/restaurant.svg"; +import cafeIconUrl from "/src/shared/asset/icon/cafe.svg"; +import activityIconUrl from "/src/shared/asset/icon/activity.svg"; +import barIconUrl from "/src/shared/asset/icon/bar.svg"; const StoreDetailModal = ({ storeId, isOpen, @@ -268,9 +271,12 @@ const MapComponent = ({ const getMapCenter = () => { if (!places || places.length === 0) return new navermaps.LatLng(37.5665, 126.978); - const latSum = places.reduce((sum, p) => sum + p.placelati, 0); - const lngSum = places.reduce((sum, p) => sum + p.placelong, 0); - return new navermaps.LatLng(latSum / places.length, lngSum / places.length); + const latSum = currentCoursePlaces.reduce((sum, p) => sum + p.placelati, 0); + const lngSum = currentCoursePlaces.reduce((sum, p) => sum + p.placelong, 0); + return new navermaps.LatLng( + latSum / currentCoursePlaces.length, + lngSum / currentCoursePlaces.length + ); }; const createMarkerIcon = (place: RecommendedPlace, index: number) => { @@ -544,10 +550,10 @@ const Step3_Page = () => { }; const categoryIconPaths = { - 음식점: "/src/shared/asset/icon/restaurant.svg", - 카페: "/src/shared/asset/icon/cafe.svg", - 액티비티: "/src/shared/asset/icon/activity.svg", - 술집: "/src/shared/asset/icon/bar.svg", + 음식점: restaurantIconUrl, + 카페: cafeIconUrl, + 액티비티: activityIconUrl, + 술집: barIconUrl, }; const categoryMapping: { [key: string]: string } = { diff --git a/src/widgets/meeting/editMeetingForm.tsx b/src/widgets/meeting/editMeetingForm.tsx index 456298f..2dae201 100644 --- a/src/widgets/meeting/editMeetingForm.tsx +++ b/src/widgets/meeting/editMeetingForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; import clsx from "clsx"; import { useMeetingStore, @@ -58,343 +58,385 @@ const atmosphereOptions: SelectionOption[] = [ { id: "INDULGENT", label: "디저트가 맛있는", IconComponent: IconDesert }, ]; -const EditMeetingForm = () => { - const { - groupPurpose, - setGroupPurpose, - meetDays: selectedDays, - setMeetDays: setSelectedDays, - meetTime: selectedTimes, - setMeetTime: setSelectedTimes, - place: places, - setPlace: setPlaces, - startPoint: departures, - setStartPoint: setDepartures, - } = useMeetingStore(); - - const [editingPlaceId, setEditingPlaceId] = useState(null); - const [overlayData, setOverlayData] = useState(null); - const [displaySubTypes, setDisplaySubTypes] = useState<{ - [key: number]: string; - }>({}); - const [displayValues, setDisplayValues] = useState<{ [id: number]: string }>( - {} - ); - - useEffect(() => { - const initialDisplayValues: { [id: number]: string } = {}; - departures.forEach((sp) => { - const combined = [sp.first, sp.second, sp.third] - .filter((part) => part && part.trim() !== "") - .join(" "); - if (combined) { - initialDisplayValues[sp.id || 0] = combined; +export interface EditMeetingFormRef { + getFilteredDepartures: () => StartPointRequest[]; +} + +interface EditMeetingFormProps {} + +const EditMeetingForm = forwardRef( + (props, ref) => { + const { + groupPurpose, + setGroupPurpose, + meetDays: selectedDays, + setMeetDays: setSelectedDays, + meetTime: selectedTimes, + setMeetTime: setSelectedTimes, + place: places, + setPlace: setPlaces, + startPoint: departures, + setStartPoint: setDepartures, + } = useMeetingStore(); + + const [editingPlaceId, setEditingPlaceId] = useState(null); + const [overlayData, setOverlayData] = useState(null); + const [displaySubTypes, setDisplaySubTypes] = useState<{ + [key: number]: string; + }>({}); + const [displayValues, setDisplayValues] = useState<{ + [id: number]: string; + }>({}); + + useEffect(() => { + const initialDisplayValues: { [id: number]: string } = {}; + departures.forEach((sp) => { + const combined = [sp.first, sp.second, sp.third] + .filter((part) => part && part.trim() !== "") + .join(" "); + if (combined) { + initialDisplayValues[sp.id || 0] = combined; + } + }); + setDisplayValues(initialDisplayValues); + }, []); + + useEffect(() => { + if (!places || places.length === 0) { + setPlaces([ + { + id: Date.now(), + placeType: null, + typeDetail: null, + atmosphere: null, + }, + ]); + return; } - }); - setDisplayValues(initialDisplayValues); - }, []); - useEffect(() => { - if (!places || places.length === 0) { - setPlaces([ - { id: Date.now(), placeType: null, typeDetail: null, atmosphere: null }, - ]); - return; - } - - const hasEmptySlot = places.some((p) => p.placeType === null); - - if (!hasEmptySlot && places.length < 8) { - const nextId = Math.max(...places.map((p) => p.id)) + 1; - setPlaces([ - ...places, - { id: nextId, placeType: null, typeDetail: null, atmosphere: null }, - ]); - } - }, [places, setPlaces]); - const purposeOptions = [ - { id: "date", label: "데이트" }, - { id: "business", label: "비즈니스" }, - { id: "study", label: "스터디" }, - { id: "social", label: "친목" }, - ]; - const dayOptions = [ - { ui: "월", api: "MONDAY" as DayType }, - { ui: "화", api: "TUESDAY" as DayType }, - { ui: "수", api: "WEDNESDAY" as DayType }, - { ui: "목", api: "THURSDAY" as DayType }, - { ui: "금", api: "FRIDAY" as DayType }, - { ui: "토", api: "SATURDAY" as DayType }, - { ui: "일", api: "SUNDAY" as DayType }, - ]; - const timeOptions = [ - { key: "MORNING" as TimeType, label: "오전" }, - { key: "LUNCH" as TimeType, label: "점심" }, - { key: "AFTERNOON" as TimeType, label: "오후" }, - { key: "EVENING" as TimeType, label: "저녁" }, - ]; - - const handleDaySelect = (dayApi: DayType) => { - if (selectedDays.includes(dayApi)) { - if (selectedDays.length > 1) { - setSelectedDays(selectedDays.filter((d) => d !== dayApi)); + + const hasEmptySlot = places.some((p) => p.placeType === null); + + if (!hasEmptySlot && places.length < 8) { + const nextId = Math.max(...places.map((p) => p.id)) + 1; + setPlaces([ + ...places, + { id: nextId, placeType: null, typeDetail: null, atmosphere: null }, + ]); } - } else { - setSelectedDays([dayApi]); - } - }; - - const handleTimeSelect = (timeKey: TimeType) => { - if (selectedTimes.includes(timeKey)) { - if (selectedTimes.length > 1) { - setSelectedTimes(selectedTimes.filter((t) => t !== timeKey)); + }, [places, setPlaces]); + + const purposeOptions = [ + { id: "date", label: "데이트" }, + { id: "business", label: "비즈니스" }, + { id: "study", label: "스터디" }, + { id: "social", label: "친목" }, + ]; + const dayOptions = [ + { ui: "월", api: "MONDAY" as DayType }, + { ui: "화", api: "TUESDAY" as DayType }, + { ui: "수", api: "WEDNESDAY" as DayType }, + { ui: "목", api: "THURSDAY" as DayType }, + { ui: "금", api: "FRIDAY" as DayType }, + { ui: "토", api: "SATURDAY" as DayType }, + { ui: "일", api: "SUNDAY" as DayType }, + ]; + const timeOptions = [ + { key: "MORNING" as TimeType, label: "오전" }, + { key: "LUNCH" as TimeType, label: "점심" }, + { key: "AFTERNOON" as TimeType, label: "오후" }, + { key: "EVENING" as TimeType, label: "저녁" }, + ]; + + const handleDaySelect = (dayApi: DayType) => { + if (selectedDays.includes(dayApi)) { + if (selectedDays.length > 1) { + setSelectedDays(selectedDays.filter((d) => d !== dayApi)); + } + } else { + setSelectedDays([dayApi]); } - } else { - setSelectedTimes([...selectedTimes, timeKey]); - } - }; - - const handleItemClick = (id: number) => { - setEditingPlaceId(id); - setOverlayData({ - title: "모임 장소 유형을 선택해주세요", - buttonText: "다음", - options: placeTypeOptions, - step: "main", - }); - }; - - const handleConfirm = (selectedId: string) => { - const isMainStep = overlayData?.step === "main"; - const editingId = editingPlaceId!; - if (isMainStep) { - const placeType = selectedId as PlaceType; - - setPlaces( - places.map((p) => - p.id === editingId ? { ...p, placeType, atmosphere: null } : p - ) - ); - setDisplaySubTypes((prev) => ({ ...prev, [editingId]: "" })); - if ( - placeType === "RESTAURANT" || - placeType === "BAR" || - placeType === "CAFE" - ) { - const nextOverlayMap = { - RESTAURANT: { - title: "음식점 유형을 선택해주세요", - options: foodTypeOptions, - }, - BAR: { title: "술집 유형을 선택해주세요", options: barTypeOptions }, - CAFE: { - title: "카페 유형을 선택해주세요", - options: atmosphereOptions, - }, - }; - setOverlayData({ - ...nextOverlayMap[placeType], - buttonText: "선택하기", - step: "sub", - }); + }; + + const handleTimeSelect = (timeKey: TimeType) => { + if (selectedTimes.includes(timeKey)) { + if (selectedTimes.length > 1) { + setSelectedTimes(selectedTimes.filter((t) => t !== timeKey)); + } } else { - setOverlayData(null); + setSelectedTimes([...selectedTimes, timeKey]); } - } else { - const currentPlace = places.find((p) => p.id === editingId); - if (currentPlace?.placeType === "CAFE") { + }; + + const handleItemClick = (id: number) => { + setEditingPlaceId(id); + setOverlayData({ + title: "모임 장소 유형을 선택해주세요", + buttonText: "다음", + options: placeTypeOptions, + step: "main", + }); + }; + + const handleConfirm = (selectedId: string) => { + const isMainStep = overlayData?.step === "main"; + const editingId = editingPlaceId!; + if (isMainStep) { + const placeType = selectedId as PlaceType; + setPlaces( places.map((p) => - p.id === editingId - ? { ...p, atmosphere: selectedId as AtmosphereType } - : p + p.id === editingId ? { ...p, placeType, atmosphere: null } : p ) ); + setDisplaySubTypes((prev) => ({ ...prev, [editingId]: "" })); + if ( + placeType === "RESTAURANT" || + placeType === "BAR" || + placeType === "CAFE" + ) { + const nextOverlayMap = { + RESTAURANT: { + title: "음식점 유형을 선택해주세요", + options: foodTypeOptions, + }, + BAR: { title: "술집 유형을 선택해주세요", options: barTypeOptions }, + CAFE: { + title: "카페 유형을 선택해주세요", + options: atmosphereOptions, + }, + }; + setOverlayData({ + ...nextOverlayMap[placeType], + buttonText: "선택하기", + step: "sub", + }); + } else { + setOverlayData(null); + } } else { - setDisplaySubTypes((prev) => ({ ...prev, [editingId]: selectedId })); + const currentPlace = places.find((p) => p.id === editingId); + if (currentPlace?.placeType === "CAFE") { + setPlaces( + places.map((p) => + p.id === editingId + ? { ...p, atmosphere: selectedId as AtmosphereType } + : p + ) + ); + } else { + setDisplaySubTypes((prev) => ({ ...prev, [editingId]: selectedId })); + } + setOverlayData(null); } - setOverlayData(null); - } - }; - - const handleAddPlace = () => { - if (places.length < 8) { - setPlaces([ - ...places, - { id: Date.now(), placeType: null, atmosphere: null, typeDetail: null }, - ]); - } - }; - - const handleRemovePlace = (id: number) => { - if (places.length > 1) { - setPlaces(places.filter((p) => p.id !== id)); - } - }; - - const parseAddress = (address: string) => { - if (!address || address.trim() === "") { - return { first: "", second: "", third: "" }; - } - - const parts = address.trim().split(/\s+/); - return { - first: parts[0] || "", - second: parts[1] || "", - third: parts.slice(2).join(" ") || "", }; - }; - - const uiDepartures: Departure[] = departures.map((sp) => ({ - id: sp.id, - type: sp.type, - value: displayValues[sp.id] || "", - })); - - const handleAddDeparture = () => { - if (departures.length < 5) { - const newId = Date.now(); - setDepartures([ - ...departures, - { id: newId, type: "member", first: "", second: "", third: "" }, - ]); - } - }; - - const handleRemoveDeparture = (id: number) => { - if (departures.length > 1) { - setDepartures(departures.filter((d) => d.id !== id)); - setDisplayValues((prev) => { - const newValues = { ...prev }; - delete newValues[id]; - return newValues; - }); - } - }; - const handleChangeDeparture = (id: number, value: string) => { - setDisplayValues((prev) => ({ ...prev, [id]: value })); + const handleAddPlace = () => { + if (places.length < 8) { + setPlaces([ + ...places, + { + id: Date.now(), + placeType: null, + atmosphere: null, + typeDetail: null, + }, + ]); + } + }; - const parsed = parseAddress(value); - setDepartures( - departures.map((d) => (d.id === id ? { ...d, ...parsed } : d)) + const handleRemovePlace = (id: number) => { + if (places.length > 1) { + setPlaces(places.filter((p) => p.id !== id)); + } + }; + + const parseAddress = (address: string) => { + if (!address || address.trim() === "") { + return { first: "", second: "", third: "" }; + } + + const parts = address.trim().split(/\s+/); + return { + first: parts[0] || "", + second: parts[1] || "", + third: parts.slice(2).join(" ") || "", + }; + }; + + const getFilteredDepartures = (): StartPointRequest[] => { + return departures + .map((sp) => { + const displayValue = displayValues[sp.id || 0] || ""; + if (displayValue.trim()) { + const parsed = parseAddress(displayValue); + return { ...sp, ...parsed }; + } + return { ...sp, first: "", second: "", third: "" }; + }) + .filter((sp) => { + const displayValue = displayValues[sp.id || 0] || ""; + return displayValue.trim() !== ""; + }); + }; + + useImperativeHandle(ref, () => ({ + getFilteredDepartures, + })); + + const uiDepartures: Departure[] = departures.map((sp) => ({ + id: sp.id, + type: sp.type, + value: displayValues[sp.id] || "", + })); + + const handleAddDeparture = () => { + if (departures.length < 5) { + const newId = Date.now(); + setDepartures([ + ...departures, + { id: newId, type: "member", first: "", second: "", third: "" }, + ]); + } + }; + + const handleRemoveDeparture = (id: number) => { + if (departures.length > 1) { + setDepartures(departures.filter((d) => d.id !== id)); + setDisplayValues((prev) => { + const newValues = { ...prev }; + delete newValues[id]; + return newValues; + }); + } + }; + + const handleChangeDeparture = (id: number, value: string) => { + setDisplayValues((prev) => ({ ...prev, [id]: value })); + + const parsed = parseAddress(value); + setDepartures( + departures.map((d) => (d.id === id ? { ...d, ...parsed } : d)) + ); + }; + + return ( + <> +
+
+

모임 목적

+
+ {purposeOptions.map((opt) => ( + + ))} +
+
+ +
+ +
+

모임 요일

+
+ {dayOptions.map((day) => ( + + ))} +
+
+ +
+ +
+

모임 시간

+
+ {timeOptions.map((time) => ( + + ))} +
+
+ +
+ +
+

장소 유형

+ +
+ +
+ +
+

출발 위치

+ { + if (e.key === "Enter") e.preventDefault(); + }} + /> +
+
+ + setOverlayData(null)} + > + {overlayData && ( + setOverlayData(null)} + /> + )} + + ); - }; - - return ( - <> -
-
-

모임 목적

-
- {purposeOptions.map((opt) => ( - - ))} -
-
- -
- -
-

모임 요일

-
- {dayOptions.map((day) => ( - - ))} -
-
- -
- -
-

모임 시간

-
- {timeOptions.map((time) => ( - - ))} -
-
- -
- -
-

장소 유형

- -
- -
- -
-

출발 위치

- { - if (e.key === "Enter") e.preventDefault(); - }} - /> -
-
- - setOverlayData(null)} - > - {overlayData && ( - setOverlayData(null)} - /> - )} - - - ); -}; + } +); + +EditMeetingForm.displayName = "EditMeetingForm"; export default EditMeetingForm;