diff --git a/dh.html b/dh.html new file mode 100644 index 0000000..df3268f --- /dev/null +++ b/dh.html @@ -0,0 +1,194 @@ + + + + + + Dropdown Interaction + + +
+
+
+
베이커리
+
더보기
+
+
+ 맛있는 빵집 +
+
+ +
+
+ + 3 +
+
+ 이어령이어령, 이어령이어령, 김규리, 김규리, 김규리 +
+ +
+ + +
+ + + + diff --git a/fe/public/pin/.svg b/fe/public/pin/.svg new file mode 100644 index 0000000..4243960 --- /dev/null +++ b/fe/public/pin/.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fe/public/pin/level1.svg b/fe/public/pin/level1.svg index f29d2a3..aed8716 100644 --- a/fe/public/pin/level1.svg +++ b/fe/public/pin/level1.svg @@ -1,22 +1,23 @@ - - - - - - - - + + + + + + + + + - + - + - - + + diff --git a/fe/public/pin/level2.svg b/fe/public/pin/level2.svg index 8dbdf2e..9e85cf1 100644 --- a/fe/public/pin/level2.svg +++ b/fe/public/pin/level2.svg @@ -1,22 +1,23 @@ - - - - - - - - + + + + + + + + + - + - + - - + + diff --git a/fe/public/pin/level3.svg b/fe/public/pin/level3.svg index c52938a..6a5385c 100644 --- a/fe/public/pin/level3.svg +++ b/fe/public/pin/level3.svg @@ -1,22 +1,23 @@ - - - - - - - - + + + + + + + + + - + - + - - + + diff --git a/fe/public/pin/level4.svg b/fe/public/pin/level4.svg index a41a7c2..c64063f 100644 --- a/fe/public/pin/level4.svg +++ b/fe/public/pin/level4.svg @@ -1,22 +1,23 @@ - - - - - - - - + + + + + + + + + - + - + - - + + diff --git a/fe/public/pin/recommendPing.svg b/fe/public/pin/recommendPing.svg new file mode 100644 index 0000000..4243960 --- /dev/null +++ b/fe/public/pin/recommendPing.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fe/public/profile/level1.svg b/fe/public/profile/level1.svg new file mode 100644 index 0000000..4c0408c --- /dev/null +++ b/fe/public/profile/level1.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/public/profile/level2.svg b/fe/public/profile/level2.svg new file mode 100644 index 0000000..db49621 --- /dev/null +++ b/fe/public/profile/level2.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/public/profile/level3.svg b/fe/public/profile/level3.svg new file mode 100644 index 0000000..6159274 --- /dev/null +++ b/fe/public/profile/level3.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/public/profile/level4.svg b/fe/public/profile/level4.svg new file mode 100644 index 0000000..9fb321f --- /dev/null +++ b/fe/public/profile/level4.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/public/profile/profil1.svg b/fe/public/profile/profil1.svg deleted file mode 100644 index d36dd23..0000000 --- a/fe/public/profile/profil1.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/fe/public/profile/profil2.svg b/fe/public/profile/profil2.svg deleted file mode 100644 index 1d63af7..0000000 --- a/fe/public/profile/profil2.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/fe/public/profile/profil3.svg b/fe/public/profile/profil3.svg deleted file mode 100644 index db47008..0000000 --- a/fe/public/profile/profil3.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/fe/public/profile/profil4.svg b/fe/public/profile/profil4.svg deleted file mode 100644 index 207a4da..0000000 --- a/fe/public/profile/profil4.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fe/public/profile/recommendProfile.svg b/fe/public/profile/recommendProfile.svg new file mode 100644 index 0000000..86e2e1c --- /dev/null +++ b/fe/public/profile/recommendProfile.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/fe/public/svg/bulb.svg b/fe/public/svg/bulb.svg new file mode 100644 index 0000000..1d2d153 --- /dev/null +++ b/fe/public/svg/bulb.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fe/public/svg/dropdown.svg b/fe/public/svg/dropdown.svg new file mode 100644 index 0000000..732ef4b --- /dev/null +++ b/fe/public/svg/dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/fe/public/svg/edit.svg b/fe/public/svg/edit.svg index 8e1f26b..9a6c01c 100644 --- a/fe/public/svg/edit.svg +++ b/fe/public/svg/edit.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/fe/public/svg/my-location.svg b/fe/public/svg/my-location.svg index acbbe81..4805a59 100644 --- a/fe/public/svg/my-location.svg +++ b/fe/public/svg/my-location.svg @@ -1,9 +1,9 @@ - - - - - - + + + + + + diff --git a/fe/public/svg/people.svg b/fe/public/svg/people.svg new file mode 100644 index 0000000..c2e58f7 --- /dev/null +++ b/fe/public/svg/people.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fe/public/svg/recommendCancle.svg b/fe/public/svg/recommendCancle.svg new file mode 100644 index 0000000..b986695 --- /dev/null +++ b/fe/public/svg/recommendCancle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fe/public/svg/refresh.svg b/fe/public/svg/refresh.svg index 30a2667..9342b4a 100644 --- a/fe/public/svg/refresh.svg +++ b/fe/public/svg/refresh.svg @@ -1,5 +1,5 @@ - - + + diff --git a/fe/public/svg/seeMore.svg b/fe/public/svg/seeMore.svg new file mode 100644 index 0000000..c325e79 --- /dev/null +++ b/fe/public/svg/seeMore.svg @@ -0,0 +1,3 @@ + + + diff --git a/fe/public/svg/share.svg b/fe/public/svg/share.svg index c9fafe9..9c69c9f 100644 --- a/fe/public/svg/share.svg +++ b/fe/public/svg/share.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx b/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx index ed2923c..1ac7657 100644 --- a/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx +++ b/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx @@ -1,12 +1,18 @@ import React, { useState, useEffect, useRef } from "react"; -import Image from "next/image"; import { useRouter } from "next/navigation"; + +import Image from "next/image"; import { useLocationStore } from "../stores/useLocationStore"; import { useMarkerStore } from "../load-mappin/stores/useMarkerStore"; +import { RecommendButton } from "./RecommendButton"; +import { RecommendInActive } from "./RecommendInActive"; +import LocationButton from "./LocationButton"; +import { RecommendActive } from "./RecommendActive"; interface NonMember { nonMemberId: number; name: string; + profileSvg: string; } interface Ping { @@ -15,6 +21,8 @@ interface Ping { px: number; py: number; url: string; + type: string; + nonMembers: NonMember[]; } interface BottomDrawerProps { @@ -23,56 +31,82 @@ interface BottomDrawerProps { id: string; } -export default function BottomDrawer({ +interface RecommendPing { + iconLevel: number; + placeName: string; + sid: string; + px: number; + py: number; + url: string; + type: string; +} + +export function BottomDrawer({ nonMembers: initialNonMembers, eventName: initialEventName, id, -}: BottomDrawerProps) { - const [eventName, setEventName] = useState(initialEventName); +}: BottomDrawerProps): JSX.Element { + const [eventName, setEventName] = useState(initialEventName); const [selectedButton, setSelectedButton] = useState(null); const [nonMembers, setNonMembers] = useState(initialNonMembers); - const [memberProfiles, setMemberProfiles] = useState<{ - [key: number]: string; - }>({}); const [allPings, setAllPings] = useState([]); + const [isRecommend, setIsRecommend] = useState(false); + const [isRecommended, setIsRecommended] = useState(false); + const [neighborhood, setNeighborhood] = useState(""); + const [nonRecommend, setNonRecommend] = useState(false); + const { setCustomMarkers } = useMarkerStore(); const moveToLocation = useLocationStore((state) => state.moveToLocation); + const router = useRouter(); - const profileImagesRef = useRef([ - "/profile/profil1.svg", - "/profile/profil2.svg", - "/profile/profil3.svg", - "/profile/profil4.svg", - ]); + const lastPingElementRef = useRef(null); const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL; - useEffect(() => { - const profiles = nonMembers.reduce( - (acc, member) => { - const randomImage = - profileImagesRef.current[ - Math.floor(Math.random() * profileImagesRef.current.length) - ]; - acc[member.nonMemberId] = randomImage; - return acc; - }, - {} as { [key: number]: string } - ); - setMemberProfiles(profiles); - }, [nonMembers]); - useEffect(() => { const fetchAllPings = async () => { try { const response = await fetch(`${apiUrl}/nonmembers/pings?uuid=${id}`); if (response.ok) { const data = await response.json(); - setAllPings(data.pings || []); - setCustomMarkers(data.pings || []); + let recommendProfile = []; + if (data.recommendPings && data.recommendPings.length > 0) { + setIsRecommended(true); + recommendProfile = data.recommendPings.map( + (ping: RecommendPing) => ({ + iconLevel: 10, // Fixed icon level + nonMembers: [ + { + nonMemberId: -1, + name: "추천 모핑", // Fixed name + profileSvg: "/profile/recommendProfile.svg", // Fixed profileSvg + }, + ], + url: ping.url, + placeName: ping.placeName, + px: ping.px, + py: ping.py, + type: ping.type, + }) + ); + } + setEventName(data.eventName || ""); + setNonMembers([ + ...(recommendProfile[0]?.nonMembers || []), + ...(data.nonMembers || []), + ]); + setAllPings([ + ...(data.pings || []), // 기존 pings + ...(recommendProfile || []), // recommendProfile 추가 + ]); + setCustomMarkers([ + ...(data.pings || []), // 기존 pings + ...(recommendProfile || []), // recommendProfile 추가 + ]); + setNeighborhood(data.neighborhood); } } catch (error) { - console.log("Error:", error); + console.error("Error:", error); } }; fetchAllPings(); @@ -90,96 +124,76 @@ export default function BottomDrawer({ } }; - const handleButtonClick = async (nonMemberId: number) => { - const isDeselect = selectedButton === nonMemberId; - setSelectedButton(isDeselect ? null : nonMemberId); + const handleRecommendAllowClick = () => { + setIsRecommend(true); + }; - if (isDeselect) { - setCustomMarkers(allPings); - } else { - try { - const response = await fetch( - `${apiUrl}/nonmembers/pings/${nonMemberId}`, - { method: "GET", headers: { "Content-Type": "application/json" } } - ); + const handleAddToMorphing = async () => { + const Km = 1.0; + let found = false; - if (response.ok) { - const data = await response.json(); - const filteredPings = data.pings.map((ping: Ping) => ({ - ...ping, - iconLevel: 1, - })); - setCustomMarkers(filteredPings); - } else { - console.log("Failed to fetch data:", response.status); + try { + const response = await fetch( + `${apiUrl}/nonmembers/pings/recommend?uuid=${id}&radiusInKm=${Km}`, + { method: "GET" } + ); + if (response.ok) { + const data = await response.json(); + if (data.recommendPings.length === 0) { + setNonRecommend(true); + } else if (data.recommendPings.length >= 5) { + setCustomMarkers(data.recommendPings); + found = true; + setIsRecommend(found); } - } catch (error) { - console.log("Error:", error); + } else { + console.error( + "Failed to fetch recommended data, status:", + response.status + ); } + } catch (error) { + console.error("Error fetching recommended data:", error); } }; - const handleAddButtonClick = () => { - router.push(`/event-maps/${id}/load-mappin/forms/name-pin`); + const handleRecommendCancle = () => { + setIsRecommend(false); }; - const handleEditBtn = () => { - if (selectedButton !== null) { - router.push(`/event-maps/${id}/${selectedButton}`); - } + const handleButtonClick = (nonMemberId: number) => { + const isSelected = selectedButton === nonMemberId; + setSelectedButton(isSelected ? null : nonMemberId); + + const pingsToShow = isSelected + ? [...allPings] + : allPings.filter((ping) => + ping.nonMembers.some((member) => member.nonMemberId === nonMemberId) + ); + + setCustomMarkers(pingsToShow); }; - const handleShare = () => { - if (navigator.share) { - navigator.share({ url: window.location.href }).then().catch(); - } else { - alert("이 브라우저에서는 공유 기능을 지원하지 않습니다."); - } + const handleAddButtonClick = () => { + router.push(`/event-maps/${id}/load-mappin/forms/name-pin`); }; const handleRefresh = async () => { try { const response = await fetch( - `${apiUrl}/nonmembers/pings/refresh-all?uuid=${id}`, - { - method: "GET", - headers: { "Content-Type": "application/json" }, - } + `${apiUrl}/nonmembers/pings/refresh?uuid=${id}` ); - if (response.ok) { const data = await response.json(); - console.log(data); setEventName(data.eventName); setNonMembers(data.nonMembers); setAllPings(data.pings || []); setCustomMarkers(data.pings || []); } else { - console.log("Failed to fetch refreshed data:", response.status); + console.error("Failed to fetch refreshed data:", response.status); } } catch (error) { - console.log("Error refreshing data:", error); - } - }; - - const handleDrawerClick = ( - event: - | React.MouseEvent - | React.KeyboardEvent - ) => { - if (event.type === "keydown") { - const keyboardEvent = event as React.KeyboardEvent; - if (keyboardEvent.key === "Enter" || keyboardEvent.key === " ") { - setSelectedButton(null); - setCustomMarkers(allPings); - } - } else if (event.type === "click") { - const mouseEvent = event as React.MouseEvent; - const target = mouseEvent.target as HTMLElement; - if (!target.closest("button")) { - setSelectedButton(null); - setCustomMarkers(allPings); - } + console.error("Error refreshing data:", error); } }; @@ -187,34 +201,15 @@ export default function BottomDrawer({
{ - if (e.key === "Enter" || e.key === " ") { - handleDrawerClick(e); - } - }} + className="bottom-drawer w-full h-[760px] bg-grayscale-90 z-10 rounded-t-xlarge" > -
- - + {!isRecommend && !isRecommended && ( +
+ +
+ )} +
+
-
-
{eventName}
-
- {selectedButton !== null ? ( - - ) : ( - - )} -
-
-
-
- -
- {nonMembers.map((member) => ( -
- -
{member.name}
-
- ))} -
+ + {isRecommend ? ( + + ) : ( + + )}
); } + +export default BottomDrawer; diff --git a/fe/src/app/event-maps/[id]/components/ButtonComponents.tsx b/fe/src/app/event-maps/[id]/components/ButtonComponents.tsx new file mode 100644 index 0000000..aa18dc3 --- /dev/null +++ b/fe/src/app/event-maps/[id]/components/ButtonComponents.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import Image from "next/image"; + +interface ButtonProps { + onClick: () => void; +} + +export function ShareButton({ onClick }: ButtonProps) { + return ( + + ); +} + +export function RefreshButton({ onClick }: ButtonProps) { + return ( + + ); +} + +export function EditButton({ onClick }: ButtonProps) { + return ( + + ); +} + +interface MemberButtonProps { + member: { + nonMemberId: number; + name: string; + profileSvg: string; + }; + isSelected: boolean; + onClick: (id: number) => void; +} + +export function MemberButton({ + member, + isSelected, + onClick, +}: MemberButtonProps) { + return ( + + ); +} diff --git a/fe/src/app/event-maps/[id]/components/LocationButton.tsx b/fe/src/app/event-maps/[id]/components/LocationButton.tsx new file mode 100644 index 0000000..eb268ed --- /dev/null +++ b/fe/src/app/event-maps/[id]/components/LocationButton.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import Image from "next/image"; + +interface ButtonProps { + onClick: () => void; +} + +function LocationButton({ onClick }: ButtonProps) { + return ( + + ); +} + +export default LocationButton; diff --git a/fe/src/app/event-maps/[id]/components/MapComponent.tsx b/fe/src/app/event-maps/[id]/components/MapComponent.tsx index 871c846..9dde6de 100644 --- a/fe/src/app/event-maps/[id]/components/MapComponent.tsx +++ b/fe/src/app/event-maps/[id]/components/MapComponent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef } from "react"; import { useLocationStore } from "../stores/useLocationStore"; import { useMarkerStore } from "../load-mappin/stores/useMarkerStore"; @@ -7,22 +7,84 @@ interface MapComponentProps { py: number; } -export default function MapComponent({ px, py }: MapComponentProps) { +interface NonMember { + nonMemberId: number; + name: string; + profileSvg: string; +} + +interface Ping { + placeName: string; + url: string; + nonMembers: NonMember[]; + type: string; + px: number; + py: number; + iconLevel: number; +} + +const transformPingData = (ping: unknown): Ping => { + if (typeof ping !== "object" || ping === null) { + throw new Error("Invalid ping data"); + } + + const { + placeName = "Unknown Place", + url = "#", + nonMembers = [], + type = "Unknown", + px = 0, + py = 0, + iconLevel = 1, + } = ping as Partial; + + return { + placeName, + url, + nonMembers: (nonMembers as NonMember[]).map((member) => ({ + ...member, + profileSvg: member.profileSvg || "https://default-image.svg", + })), + type, + px, + py, + iconLevel, + }; +}; + +export default function MapComponent({ + px, + py, +}: MapComponentProps): JSX.Element { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const { customMarkers } = useMarkerStore(); const { center } = useLocationStore(); - const [selectedMarker, setSelectedMarker] = useState(null); - const markersRef = useRef([]); // 초기값을 빈 배열로 설정 + const markersRef = useRef([]); + const infoWindowRef = useRef(null); + const previousMarkerIndexRef = useRef(null); + + const getHtmlIconByLevel = ( + level: number, + placeName: string, + isSelected = false + ) => { + const iconWidth = isSelected ? 35 : 28; + const iconHeight = isSelected ? 40 : 32; + const textColor = level === 1 || level === 10 ? "#000000" : "#FA8980"; + const textShadow = + "-1px 0px #FFFFFF, 0px 1px #FFFFFF, 1px 0px #FFFFFF, 0px -1px #FFFFFF"; - const getIconByLevel = (level: number, isSelected: boolean = false) => { - const size = isSelected ? 44 : 36; return { - url: `/pin/level${level}.svg`, - size: new window.naver.maps.Size(size, size), - scaledSize: new window.naver.maps.Size(size, size), - origin: new window.naver.maps.Point(0, 0), - anchor: new window.naver.maps.Point(size / 2, size), + content: `
+
+ ${placeName} +
+
`, + size: new window.naver.maps.Size(iconWidth, iconHeight), + anchor: new window.naver.maps.Point(iconWidth / 2, iconHeight + 15), }; }; @@ -55,75 +117,230 @@ export default function MapComponent({ px, py }: MapComponentProps) { }, [px, py]); useEffect(() => { - if (!mapInstanceRef.current || !customMarkers) return; + if (!mapInstanceRef.current) return; - // 이전 마커 제거 - (markersRef.current || []).forEach((marker) => marker.setMap(null)); + markersRef.current.forEach((marker) => marker.setMap(null)); markersRef.current = []; - // 새로운 마커 추가 customMarkers.forEach((ping, index) => { - const marker = new window.naver.maps.Marker({ - position: new window.naver.maps.LatLng(ping.py, ping.px), - map: mapInstanceRef.current!, - icon: getIconByLevel(ping.iconLevel), - }); - markersRef.current.push(marker); + const transformedPing = transformPingData(ping); - const nonMemberNames = (ping.nonMembers || []) - .map((member) => member.name) - .join(", "); - - const infoWindowContent = ` -
- ${nonMemberNames}
- - 가게 정보 바로 보기 › - -
-
- `; + const markerOptions: naver.maps.MarkerOptions = { + position: new window.naver.maps.LatLng( + transformedPing.py, + transformedPing.px + ), + map: mapInstanceRef.current || undefined, + icon: getHtmlIconByLevel( + transformedPing.iconLevel, + transformedPing.placeName, + false + ), + clickable: true, + }; - const infoWindow = new window.naver.maps.InfoWindow({ - content: infoWindowContent, - borderWidth: 0, - disableAnchor: true, - backgroundColor: "transparent", - }); + const marker = new window.naver.maps.Marker(markerOptions); + markersRef.current.push(marker); window.naver.maps.Event.addListener(marker, "click", () => { - if (selectedMarker !== null && selectedMarker !== index) { - markersRef.current[selectedMarker].setIcon( - getIconByLevel(customMarkers[selectedMarker].iconLevel) + if ( + previousMarkerIndexRef.current !== null && + previousMarkerIndexRef.current !== index + ) { + markersRef.current[previousMarkerIndexRef.current].setIcon( + getHtmlIconByLevel( + customMarkers[previousMarkerIndexRef.current].iconLevel, + customMarkers[previousMarkerIndexRef.current].placeName, + false + ) ); } - setSelectedMarker(index); - marker.setIcon(getIconByLevel(ping.iconLevel, true)); - infoWindow.open(mapInstanceRef.current!, marker); + marker.setIcon( + getHtmlIconByLevel( + transformedPing.iconLevel, + transformedPing.placeName, + true + ) + ); + previousMarkerIndexRef.current = index; + + if (infoWindowRef.current) { + infoWindowRef.current.close(); + } + + const infoWindow = new window.naver.maps.InfoWindow({ + content: ` +
+
+
+
${transformedPing.type}
+ +
+
+ ${transformedPing.placeName} +
+
+ +
+
+ + ${transformedPing.nonMembers.length} +
+
+ ${transformedPing.nonMembers.map((member) => member.name).join(", ")} +
+ +
+ + +
+ `, + borderWidth: 0, // 보더 제거 + backgroundColor: "transparent", // 백그라운드 설정 + disableAnchor: true, // 앵커 비활성화 + }); + + if (mapInstanceRef.current) { + infoWindow.open(mapInstanceRef.current, marker); + } + infoWindowRef.current = infoWindow; }); + }); + }, [customMarkers]); + useEffect(() => { + if (mapInstanceRef.current) { window.naver.maps.Event.addListener( - mapInstanceRef.current!, + mapInstanceRef.current, "click", () => { - if (selectedMarker !== null) { - markersRef.current[selectedMarker].setIcon( - getIconByLevel(customMarkers[selectedMarker].iconLevel) + if (previousMarkerIndexRef.current !== null) { + markersRef.current[previousMarkerIndexRef.current].setIcon( + getHtmlIconByLevel( + customMarkers[previousMarkerIndexRef.current].iconLevel, + customMarkers[previousMarkerIndexRef.current].placeName, + false + ) ); - setSelectedMarker(null); - infoWindow.close(); + previousMarkerIndexRef.current = null; + } + + if (infoWindowRef.current) { + infoWindowRef.current.close(); } } ); - }); - }, [customMarkers, selectedMarker]); + } + }, [customMarkers]); useEffect(() => { if (mapInstanceRef.current) { - mapInstanceRef.current.setCenter( - new window.naver.maps.LatLng(center.latitude, center.longitude) + const currentCenter = mapInstanceRef.current.getCenter(); + const targetCenter = new window.naver.maps.LatLng( + center.latitude, + center.longitude ); + + if (!currentCenter.equals(targetCenter)) { + mapInstanceRef.current.setCenter(targetCenter); + } } }, [center]); diff --git a/fe/src/app/event-maps/[id]/components/RecommendActive.tsx b/fe/src/app/event-maps/[id]/components/RecommendActive.tsx new file mode 100644 index 0000000..77d1648 --- /dev/null +++ b/fe/src/app/event-maps/[id]/components/RecommendActive.tsx @@ -0,0 +1,98 @@ +import React, { Dispatch, SetStateAction } from "react"; +import Image from "next/image"; + +interface RecommendActiveProps { + neighborhood: string; + handleRecommendCancle: () => void; + handleAddToMorphing: () => void; + setIsRecommend: Dispatch>; + setNonRecommend: Dispatch>; + nonRecommend: boolean; +} + +export function RecommendActive({ + neighborhood, + handleRecommendCancle, + handleAddToMorphing, + setIsRecommend, + setNonRecommend, + nonRecommend, +}: RecommendActiveProps): JSX.Element { + return ( +
+ {/* nonRecommend이 true일 때 다른 UI를 렌더링 */} + {nonRecommend ? ( + <> + {/* 추천 데이터가 없을 때 UI */} +
+
+
근처에 추천할만한 공간이 없어요
+
+ +
+
+ +
+ + ) : ( + <> + {/* 추천 데이터가 있을 때 기본 UI */} +
+
+
우리끼리만 보는
+
+
+ {neighborhood} +
+
인기 공간
+
+
+ +
+
+ +
+ + )} +
+ ); +} + +export default RecommendActive; diff --git a/fe/src/app/event-maps/[id]/components/RecommendButton.tsx b/fe/src/app/event-maps/[id]/components/RecommendButton.tsx new file mode 100644 index 0000000..a82beb7 --- /dev/null +++ b/fe/src/app/event-maps/[id]/components/RecommendButton.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import Image from "next/image"; + +interface RecommendButtonProps { + onClick: () => void; +} + +export function RecommendButton({ + onClick, +}: RecommendButtonProps): JSX.Element { + return ( + + ); +} diff --git a/fe/src/app/event-maps/[id]/components/RecommendInActive.tsx b/fe/src/app/event-maps/[id]/components/RecommendInActive.tsx new file mode 100644 index 0000000..efa69ed --- /dev/null +++ b/fe/src/app/event-maps/[id]/components/RecommendInActive.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { + ShareButton, + RefreshButton, + EditButton, + MemberButton, +} from "./ButtonComponents"; +import StoreItem from "./StoreItem"; + +interface NonMember { + nonMemberId: number; + name: string; + profileSvg: string; +} + +interface Ping { + iconLevel: number; + placeName: string; + px: number; + py: number; + url: string; + type: string; +} + +interface RecommendInActiveProps { + nonMembers: NonMember[]; + handleButtonClick: (nonMemberId: number) => void; + handleAddButtonClick: () => void; + allPings: Ping[]; + lastPingElementRef: React.RefObject; + selectedButton: number | null; + handleRefresh: () => void; + eventName: string; + id: string; +} + +export function RecommendInActive({ + nonMembers, + handleButtonClick, + handleAddButtonClick, + allPings, + lastPingElementRef, + selectedButton, + handleRefresh, + eventName, + id, +}: RecommendInActiveProps): JSX.Element { + const router = useRouter(); + + return ( + <> +
+
{eventName}
+
+ navigator.share({ url: window.location.href })} + /> + {selectedButton !== null ? ( + router.push(`/event-maps/${id}/${selectedButton}`)} // router 사용 + /> + ) : ( + + )} +
+
+
+
+ +
+ {nonMembers.map((member) => ( +
+ handleButtonClick(member.nonMemberId)} + /> +
{member.name}
+
+ ))} +
+
+ {allPings.map((ping, index) => ( + + ))} +
+ + ); +} + +export default RecommendInActive; diff --git a/fe/src/app/event-maps/[id]/components/StoreItem.tsx b/fe/src/app/event-maps/[id]/components/StoreItem.tsx new file mode 100644 index 0000000..742c296 --- /dev/null +++ b/fe/src/app/event-maps/[id]/components/StoreItem.tsx @@ -0,0 +1,46 @@ +import React, { forwardRef } from "react"; +import Image from "next/image"; + +interface StoreItemProps { + name: string; + type: string; + url: string; + iconLevel: number; +} + +const StoreItem = forwardRef( + ({ name, type, url, iconLevel }, ref) => ( +
+
+ { + e.currentTarget.src = "/profile/default.svg"; // Fallback to a default image + }} + alt="edit" + width={40} + height={40} + /> +
+
{name}
+
{type}
+
+
+ +
+ ) +); + +export default StoreItem; diff --git a/fe/src/app/event-maps/[id]/hooks/useDrawer.ts b/fe/src/app/event-maps/[id]/hooks/useDrawer.ts index b78f155..7b296c4 100644 --- a/fe/src/app/event-maps/[id]/hooks/useDrawer.ts +++ b/fe/src/app/event-maps/[id]/hooks/useDrawer.ts @@ -1,22 +1,47 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useSpring } from "@react-spring/web"; const useDrawer = () => { + const [stopPoints, setStopPoints] = useState([]); + + // 뷰포트 크기에 따른 스톱 포인트 계산 + const updateStopPoints = () => { + let stopPointsPercent; + if (window.matchMedia("(max-height: 668px)").matches) { + // 작은 기종 + stopPointsPercent = [54, 30, 0, -20]; + } else if (window.matchMedia("(max-height: 850px)").matches) { + // 중간 기종 + stopPointsPercent = [59, 24, 0, -15.5]; + } else { + // 큰 기종 + stopPointsPercent = [57, 22.5, 0, -14]; + } + const vh = window.innerHeight; + setStopPoints(stopPointsPercent.map((p) => vh * (p / 100) * -1)); + }; + + useEffect(() => { + updateStopPoints(); // 초기 설정 + window.addEventListener("resize", updateStopPoints); // 창 크기 변경에 따라 업데이트 + return () => window.removeEventListener("resize", updateStopPoints); + }, []); + const [isOpen, setIsOpen] = useState(false); - const [{ y }, api] = useSpring(() => ({ y: 0 })); + const [{ y }, api] = useSpring(() => ({ y: stopPoints[3] || 0 })); const openDrawer = () => { setIsOpen(true); - api.start({ y: 0 }); + api.start({ y: stopPoints[0] }); }; const closeDrawer = () => { setIsOpen(false); - api.start({ y: 130 }); + api.start({ y: stopPoints[2] }); }; const setPosition = (newY: number) => { - const limitedY = Math.min(Math.max(newY, 0), 130); + const limitedY = Math.max(Math.min(newY, stopPoints[3]), stopPoints[0]); // y 값 제한 api.start({ y: limitedY }); }; @@ -25,8 +50,8 @@ const useDrawer = () => { isOpen, openDrawer, closeDrawer, - setPosition, - api, // 필요한 경우 외부에서 제어할 수 있도록 spring API를 반환 + setPosition, // 함수를 반환 객체에 추가 + stopPoints, }; }; diff --git a/fe/src/app/event-maps/[id]/load-mappin/stores/useMarkerStore.ts b/fe/src/app/event-maps/[id]/load-mappin/stores/useMarkerStore.ts index 760c1db..4878887 100644 --- a/fe/src/app/event-maps/[id]/load-mappin/stores/useMarkerStore.ts +++ b/fe/src/app/event-maps/[id]/load-mappin/stores/useMarkerStore.ts @@ -1,24 +1,37 @@ import { create } from "zustand"; +interface NonMember { + nonMemberId: number; + name: string; +} + interface Ping { iconLevel: number; placeName: string; px: number; py: number; url: string; - nonMembers: { nonMemberId: number; name: string }[]; + nonMembers: NonMember[]; } export type PingWithoutNonMembers = Omit; interface MarkerStoreState { customMarkers: Ping[]; - setCustomMarkers: (pings: Ping[] | PingWithoutNonMembers[]) => void; + setCustomMarkers: ( + pings: Ping[] | PingWithoutNonMembers[] | ((prev: Ping[]) => Ping[]) + ) => void; resetMarkers: () => void; } export const useMarkerStore = create((set) => ({ customMarkers: [], - setCustomMarkers: (pings) => set({ customMarkers: pings as Ping[] }), + setCustomMarkers: (pings) => { + if (typeof pings === "function") { + set((state) => ({ customMarkers: pings(state.customMarkers) })); + } else { + set({ customMarkers: pings as Ping[] }); + } + }, resetMarkers: () => set({ customMarkers: [] }), })); diff --git a/fe/src/app/event-maps/[id]/page.tsx b/fe/src/app/event-maps/[id]/page.tsx index 1f4fa81..6e64051 100644 --- a/fe/src/app/event-maps/[id]/page.tsx +++ b/fe/src/app/event-maps/[id]/page.tsx @@ -2,11 +2,10 @@ import React, { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import Image from "next/image"; import { a } from "@react-spring/web"; import { useDrag } from "@use-gesture/react"; import MapComponent from "./components/MapComponent"; -import BottomDrawer from "./components/BottomDrawer"; +import { BottomDrawer } from "./components/BottomDrawer"; import useDrawer from "./hooks/useDrawer"; import { useLocationStore } from "./stores/useLocationStore"; import { useMarkerStore } from "./load-mappin/stores/useMarkerStore"; @@ -15,6 +14,7 @@ import ExitModal from "./components/EventMapExitModal"; interface NonMember { nonMemberId: number; name: string; + profileSvg?: string; } interface Ping { @@ -35,7 +35,7 @@ interface Data { } export default function Page() { - const { y, openDrawer, closeDrawer, setPosition } = useDrawer(); + const { y, openDrawer, closeDrawer, setPosition, stopPoints } = useDrawer(); const { id } = useParams(); const parsedId = Array.isArray(id) ? id[0] : id; const [data, setData] = useState(null); @@ -45,12 +45,8 @@ export default function Page() { const router = useRouter(); useEffect(() => { - console.log("useEffect triggered with id:", id); - const fetchData = async () => { try { - console.log("Fetching data for id:", id); - const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/pings?uuid=${id}`, { @@ -64,14 +60,11 @@ export default function Page() { if (response.ok) { const result = await response.json(); setData(result); - console.log("API Response Data:", JSON.stringify(result, null, 2)); if (result.px && result.py) { - console.log("Moving to location:", result.py, result.px); moveToLocation(result.py, result.px); } if (result.pings) { - console.log("Setting custom markers:", result.pings); setCustomMarkers(result.pings); } } else { @@ -91,7 +84,7 @@ export default function Page() { }, [id, data, moveToLocation, setCustomMarkers]); const handleBackbtn = () => { - setIsModalOpen(true); + router.replace("/eventcreate-page"); }; const handleExit = () => { @@ -106,10 +99,16 @@ export default function Page() { const bind = useDrag( ({ last, movement: [, my], memo = y.get() }) => { if (last) { - if (my + memo > 100) { - closeDrawer(); - } else { + const newY = my + memo; + const closestStopPoint = stopPoints.reduce((prev, curr) => + Math.abs(curr - newY) < Math.abs(prev - newY) ? curr : prev + ); + setPosition(closestStopPoint); + + if (closestStopPoint === stopPoints[0]) { openDrawer(); + } else if (closestStopPoint === stopPoints[2]) { + closeDrawer(); } } else { setPosition(my + memo); @@ -124,13 +123,13 @@ export default function Page() { return (
-
+
{data && ( @@ -138,11 +137,17 @@ export default function Page() { `translateY(${val}px)`), // Use translateY from y value + touchAction: "none", + }} + className="w-full h-[218px] fixed bottom-0 z-10 bg-white shadow-lg rounded-t-lg" > ({ + ...member, + profileSvg: member.profileSvg || "/profile/default.svg", + }))} eventName={data.eventName} id={parsedId} /> diff --git a/fe/src/app/event-maps/[id]/types/types.ts b/fe/src/app/event-maps/[id]/types/types.ts index a239088..ca4c3c9 100644 --- a/fe/src/app/event-maps/[id]/types/types.ts +++ b/fe/src/app/event-maps/[id]/types/types.ts @@ -1,3 +1,9 @@ +declare global { + interface Window { + toggleDropdown: () => void; + } +} + export interface Location { latitude: number; longitude: number; diff --git a/fe/tailwind.config.ts b/fe/tailwind.config.ts index 9bed041..1446c31 100644 --- a/fe/tailwind.config.ts +++ b/fe/tailwind.config.ts @@ -171,6 +171,15 @@ const config: Config = { animation: { fadein: "fadein 2s ease-in-out", }, + scrollbarHide: { + "&::-webkit-scrollbar": { + display: "none", + }, + "&": { + "-ms-overflow-style": "none", + "scrollbar-width": "none", + }, + }, }, plugins: [require("tailwind-scrollbar-hide")], };