From e110acd878ebf2d1f70ac9264bc6abbbb39dcd90 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Wed, 5 Nov 2025 17:10:26 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EB=B2=84=EC=8A=A4=20=EC=A0=95?= =?UTF-8?q?=EB=A5=98=EC=9E=A5=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=A0=95?= =?UTF-8?q?=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 18 +++-- src/components/BusStops.tsx | 61 ++++++++++++----- src/components/BusStopsPanel.tsx | 6 ++ src/components/MapContainer.tsx | 9 ++- src/data/busStops.ts | 2 - src/hooks/useBusSelection.ts | 11 ++- src/hooks/useMapOverlays.ts | 7 +- src/lib/api.ts | 4 +- src/utils/mapOverlays.ts | 111 ++++++++++++++++++++++--------- 9 files changed, 166 insertions(+), 63 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4210316..70a17ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -73,7 +73,9 @@ function App() { bubbleStop={bubbleStop} setBubbleStop={setBubbleStop} /> - {import.meta.env.DEV && } + {import.meta.env.DEV && ( + + )} ); } @@ -86,7 +88,11 @@ interface AppContentProps { showSettings: boolean; toggleSettings: () => void; bubbleStop: { lat: number; lng: number; name: string } | undefined; - setBubbleStop: React.Dispatch>; + setBubbleStop: React.Dispatch< + React.SetStateAction< + { lat: number; lng: number; name: string } | undefined + > + >; } function AppContent({ @@ -126,7 +132,7 @@ function AppContent({ /> ) : null} - + setBubbleStop(undefined)} @@ -135,10 +141,10 @@ function AppContent({ { - setBubbleStop((prev) => - prev === stop ? undefined : stop - ); + setBubbleStop((prev) => (prev === stop ? undefined : stop)); }} + busCount={buses.length} + selectedStopName={bubbleStop?.name} /> ); diff --git a/src/components/BusStops.tsx b/src/components/BusStops.tsx index 6c8b20f..a26e08c 100644 --- a/src/components/BusStops.tsx +++ b/src/components/BusStops.tsx @@ -1,4 +1,4 @@ -import { Bus, ChevronDown, MapPin } from "lucide-react"; +import { Bus, ChevronDown } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; import type { BusStop } from "../data/busStops"; @@ -7,6 +7,8 @@ type Props = { onSelect: (stop: BusStop) => void; onBusNumberSelect?: (n: number) => void; onToggleBubble?: (stop?: BusStop) => void; + busCount?: number; + selectedStopName?: string; }; export default function BusStops({ @@ -14,6 +16,7 @@ export default function BusStops({ onSelect, onBusNumberSelect, onToggleBubble, + busCount = 0, }: Props) { const [openStops, setOpenStops] = useState(false); const [openNumbers, setOpenNumbers] = useState(false); @@ -73,7 +76,25 @@ export default function BusStops({ >
- + + 정류장 아이콘 + + +
( - ))} + {Array.from({ length: busCount }, (_, i) => i + 1).map( + (n) => ( + + ) + )}
diff --git a/src/components/BusStopsPanel.tsx b/src/components/BusStopsPanel.tsx index a1e2f92..ffa24e3 100644 --- a/src/components/BusStopsPanel.tsx +++ b/src/components/BusStopsPanel.tsx @@ -6,11 +6,15 @@ import BusStops from "./BusStops"; interface BusStopsPanelProps { onBusNumberSelect: (n: number) => void; onToggleBubble: (stop?: BusStop) => void; + busCount: number; + selectedStopName?: string; } export const BusStopsPanel = ({ onBusNumberSelect, onToggleBubble, + busCount, + selectedStopName, }: BusStopsPanelProps) => { return (
moveToLocation(stop.lat, stop.lng)} onBusNumberSelect={onBusNumberSelect} onToggleBubble={onToggleBubble} + busCount={busCount} + selectedStopName={selectedStopName} />
); diff --git a/src/components/MapContainer.tsx b/src/components/MapContainer.tsx index 940db01..a8a026a 100644 --- a/src/components/MapContainer.tsx +++ b/src/components/MapContainer.tsx @@ -9,14 +9,19 @@ import { useToast } from "./ui/use-toast"; interface MapContainerProps { mapId: string; children?: ReactNode; + selectedStopName?: string; } -export const MapContainer = ({ mapId, children }: MapContainerProps) => { +export const MapContainer = ({ + mapId, + children, + selectedStopName, +}: MapContainerProps) => { const { toast } = useToast(); const map = useKakaoMap({ mapId, toast }); const { data: buses = [] } = useBusLocations(); - useMapOverlays(map, [...busStops], buses); + useMapOverlays(map, [...busStops], buses, selectedStopName); useMapEventHandlers(mapId); return ( diff --git a/src/data/busStops.ts b/src/data/busStops.ts index c9e844c..2c31d70 100644 --- a/src/data/busStops.ts +++ b/src/data/busStops.ts @@ -6,9 +6,7 @@ export interface BusStop { export const busStops: BusStop[] = [ { name: "평화의광장", lat: 37.320146, lng: 127.12884 }, - { name: "종합실험동", lat: 37.320224, lng: 127.125729 }, { name: "치과병원", lat: 37.322292, lng: 127.125436 }, { name: "정문", lat: 37.323352, lng: 127.125968 }, - { name: "상경관", lat: 37.32221, lng: 127.128262 }, { name: "죽전역", lat: 37.324206, lng: 127.108205 }, ]; diff --git a/src/hooks/useBusSelection.ts b/src/hooks/useBusSelection.ts index 7f38cb2..467d7f3 100644 --- a/src/hooks/useBusSelection.ts +++ b/src/hooks/useBusSelection.ts @@ -15,8 +15,15 @@ export const useBusSelection = ( if (bus && Number.isFinite(bus.lat) && Number.isFinite(bus.lng)) { moveToLocation(bus.lat, bus.lng); try { - const direction = bus.direction === true ? "단국대학교" : bus.direction === false ? "죽전역" : ""; - const label = direction ? `셔틀버스(${direction} 방향)` : "셔틀버스"; + const direction = + bus.direction === true + ? "단국대학교" + : bus.direction === false + ? "죽전역" + : ""; + const label = direction + ? `셔틀버스(${direction} 방향)` + : "셔틀버스"; setBubbleStop({ lat: bus.lat, lng: bus.lng, name: label }); } catch { /* ignore */ diff --git a/src/hooks/useMapOverlays.ts b/src/hooks/useMapOverlays.ts index 1bc0ed1..5304d58 100644 --- a/src/hooks/useMapOverlays.ts +++ b/src/hooks/useMapOverlays.ts @@ -10,13 +10,14 @@ import { export const useMapOverlays = ( map: unknown, busStops: BusStop[], - buses: Bus[] + buses: Bus[], + selectedStopName?: string ) => { useEffect(() => { if (!map) return; const overlays: OverlayHandle[] = [ - ...createBusStopOverlays(map, busStops), + ...createBusStopOverlays(map, busStops, selectedStopName), ...createBusOverlays(map, buses), ]; @@ -29,5 +30,5 @@ export const useMapOverlays = ( } }); }; - }, [map, busStops, buses]); + }, [map, busStops, buses, selectedStopName]); }; diff --git a/src/lib/api.ts b/src/lib/api.ts index 8f6d690..ec61834 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -5,7 +5,9 @@ const apiClient = ky.create({ timeout: 10000, //credentials: "include", headers: { - ...(import.meta.env.VITE_API_KEY ? { "x-api-key": import.meta.env.VITE_API_KEY } : {}), + ...(import.meta.env.VITE_API_KEY + ? { "x-api-key": import.meta.env.VITE_API_KEY } + : {}), }, }); diff --git a/src/utils/mapOverlays.ts b/src/utils/mapOverlays.ts index 8c964ed..8a8ec47 100644 --- a/src/utils/mapOverlays.ts +++ b/src/utils/mapOverlays.ts @@ -6,26 +6,50 @@ export interface OverlayHandle { } // Helper to create Lucide icon as SVG element -const createIconSVG = (iconType: "mapPin" | "bus") => { +const createIconSVG = (iconType: "mapPin" | "bus", showCircle = false) => { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", "32"); - svg.setAttribute("height", "32"); - svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("width", "48"); + svg.setAttribute("height", "56"); + svg.setAttribute("viewBox", "0 0 24 40"); svg.setAttribute("fill", "none"); - svg.setAttribute("stroke", "#2563eb"); // text-blue-600 + svg.setAttribute("stroke", showCircle ? "#dc2626" : "#2563eb"); // red-600 or blue-600 svg.setAttribute("stroke-width", "2.5"); svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.style.display = "block"; if (iconType === "mapPin") { - // MapPin icon path with white fill - const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); - path1.setAttribute("d", "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"); - path1.setAttribute("fill", "white"); + // Semi-transparent circle at the bottom (only for selected stop) + if (showCircle) { + const bgCircle = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle" + ); + bgCircle.setAttribute("cx", "12"); + bgCircle.setAttribute("cy", "24"); + bgCircle.setAttribute("r", "14"); + bgCircle.setAttribute("fill", "#dc2626"); + bgCircle.setAttribute("fill-opacity", "0.2"); + bgCircle.setAttribute("stroke", "none"); + svg.appendChild(bgCircle); + } + + // MapPin icon path with blue or red fill + const path1 = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + path1.setAttribute( + "d", + "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" + ); + path1.setAttribute("fill", showCircle ? "#dc2626" : "#2563eb"); svg.appendChild(path1); - - const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + + const circle = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle" + ); circle.setAttribute("cx", "12"); circle.setAttribute("cy", "10"); circle.setAttribute("r", "3"); @@ -34,7 +58,10 @@ const createIconSVG = (iconType: "mapPin" | "bus") => { } else { // Bus icon paths with white fill background // Background rectangle for white fill - const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + const bgRect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); bgRect.setAttribute("x", "1"); bgRect.setAttribute("y", "5"); bgRect.setAttribute("width", "22"); @@ -43,31 +70,52 @@ const createIconSVG = (iconType: "mapPin" | "bus") => { bgRect.setAttribute("fill", "white"); bgRect.setAttribute("stroke", "none"); svg.appendChild(bgRect); - - const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + + const path1 = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); path1.setAttribute("d", "M8 6v6"); svg.appendChild(path1); - - const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + + const path2 = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); path2.setAttribute("d", "M2 12h19.6"); svg.appendChild(path2); - - const path3 = document.createElementNS("http://www.w3.org/2000/svg", "path"); - path3.setAttribute("d", "M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3"); + + const path3 = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + path3.setAttribute( + "d", + "M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3" + ); svg.appendChild(path3); - - const circle1 = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + + const circle1 = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle" + ); circle1.setAttribute("cx", "7"); circle1.setAttribute("cy", "18"); circle1.setAttribute("r", "2"); circle1.setAttribute("fill", "white"); svg.appendChild(circle1); - - const path4 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + + const path4 = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); path4.setAttribute("d", "M9 18h5"); svg.appendChild(path4); - - const circle2 = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + + const circle2 = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle" + ); circle2.setAttribute("cx", "16"); circle2.setAttribute("cy", "18"); circle2.setAttribute("r", "2"); @@ -80,21 +128,24 @@ const createIconSVG = (iconType: "mapPin" | "bus") => { export const createBusStopOverlays = ( map: unknown, - busStops: BusStop[] + busStops: BusStop[], + selectedStopName?: string ): OverlayHandle[] => { if (!map || typeof window === "undefined" || !window.kakao?.maps) return []; return busStops.map((stop) => { + const isSelected = selectedStopName === stop.name; + const busIconDiv = document.createElement("div"); - busIconDiv.style.width = "32px"; - busIconDiv.style.height = "32px"; + busIconDiv.style.width = "48px"; + busIconDiv.style.height = "56px"; busIconDiv.style.display = "flex"; busIconDiv.style.alignItems = "center"; busIconDiv.style.justifyContent = "center"; busIconDiv.setAttribute("role", "img"); busIconDiv.setAttribute("aria-label", `정류장: ${stop.name}`); - - const iconSVG = createIconSVG("mapPin"); + + const iconSVG = createIconSVG("mapPin", isSelected); busIconDiv.appendChild(iconSVG); const markerPosition = new window.kakao.maps.LatLng(stop.lat, stop.lng); From af83442d483efadcc52c0907ac2a214cd6f10728 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Wed, 5 Nov 2025 17:47:38 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=95=84=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 1 - src/components/BusStops.tsx | 1 - src/components/BusStopsPanel.tsx | 3 --- 3 files changed, 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 70a17ff..0805478 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -144,7 +144,6 @@ function AppContent({ setBubbleStop((prev) => (prev === stop ? undefined : stop)); }} busCount={buses.length} - selectedStopName={bubbleStop?.name} /> ); diff --git a/src/components/BusStops.tsx b/src/components/BusStops.tsx index a26e08c..ea69159 100644 --- a/src/components/BusStops.tsx +++ b/src/components/BusStops.tsx @@ -8,7 +8,6 @@ type Props = { onBusNumberSelect?: (n: number) => void; onToggleBubble?: (stop?: BusStop) => void; busCount?: number; - selectedStopName?: string; }; export default function BusStops({ diff --git a/src/components/BusStopsPanel.tsx b/src/components/BusStopsPanel.tsx index ffa24e3..d730329 100644 --- a/src/components/BusStopsPanel.tsx +++ b/src/components/BusStopsPanel.tsx @@ -7,14 +7,12 @@ interface BusStopsPanelProps { onBusNumberSelect: (n: number) => void; onToggleBubble: (stop?: BusStop) => void; busCount: number; - selectedStopName?: string; } export const BusStopsPanel = ({ onBusNumberSelect, onToggleBubble, busCount, - selectedStopName, }: BusStopsPanelProps) => { return (
); From 06fa80b215f79c08c438053bc0667840a871b798 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Wed, 5 Nov 2025 17:54:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=EB=90=9C=20=EB=B6=80=EB=B6=84=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=83=81=EC=86=8D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8C=80=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BusStops.tsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/components/BusStops.tsx b/src/components/BusStops.tsx index ea69159..1daec2d 100644 --- a/src/components/BusStops.tsx +++ b/src/components/BusStops.tsx @@ -119,26 +119,15 @@ export default function BusStops({ : "max-h-0 scale-y-0 opacity-0" }`} > - {[ - { name: "죽전역" }, - { name: "치과병원" }, - { name: "정문" }, - { name: "평화의광장" }, - ].map((stop) => ( + {busStops.map((stop) => (