From b462dad4a9e316412da868fa4c955341349ba3ec Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 00:49:38 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=9D=B8=EB=AC=B8=EA=B4=80=20?= =?UTF-8?q?=EB=B2=84=EC=8A=A4=20=EC=A0=95=EB=A5=98=EC=9E=A5=20=EB=A7=B5?= =?UTF-8?q?=ED=95=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/busStops.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/busStops.ts b/src/data/busStops.ts index 2c31d70..c8795cc 100644 --- a/src/data/busStops.ts +++ b/src/data/busStops.ts @@ -8,5 +8,6 @@ export const busStops: BusStop[] = [ { name: "평화의광장", lat: 37.320146, lng: 127.12884 }, { name: "치과병원", lat: 37.322292, lng: 127.125436 }, { name: "정문", lat: 37.323352, lng: 127.125968 }, + { name: "인문관", lat: 37.32220999341863, lng: 127.128268 }, { name: "죽전역", lat: 37.324206, lng: 127.108205 }, ]; From e3c87cfbc3317b4a1fffdeed8dabd711ced8f833 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 01:01:34 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EC=9D=B8=EB=AC=B8=EA=B4=80=20?= =?UTF-8?q?=EB=B2=84=EC=8A=A4=20=EC=A0=95=EB=A5=98=EC=9E=A5=20=EB=A7=90?= =?UTF-8?q?=ED=92=8D=EC=84=A0=20=EB=B0=8F=20=ED=86=A0=EA=B8=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Bubble.tsx | 1 + src/contexts/LanguageContext.tsx | 2 ++ src/data/busStops.ts | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Bubble.tsx b/src/components/Bubble.tsx index 4d10bac..02f2d1e 100644 --- a/src/components/Bubble.tsx +++ b/src/components/Bubble.tsx @@ -7,6 +7,7 @@ const DISPLAY_NAME_MAP: Record = { 죽전역: "죽전역(단국대학교 방향)", 치과병원: "치과병원(단국대학교 방향)", 정문: "정문(죽전역 방향)", + 인문관: "인문관(죽전역 방향)" }; type Stop = { lat: number; lng: number; name: string }; diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index 3ca61d8..670e281 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -17,6 +17,7 @@ const translations: Translations = { "busStop.평화의광장": "평화의광장", "busStop.치과병원": "치과병원", "busStop.정문": "정문", + "busStop.인문관": "인문관", "busStop.죽전역": "죽전역", // Directions @@ -50,6 +51,7 @@ const translations: Translations = { "busStop.평화의광장": "Dankook Univ. Peace Square", "busStop.치과병원": "Dankook Univ. Dental Hospital", "busStop.정문": "Dankook Univ. Main Gate", + "busStop.인문관": "Humanities Building", "busStop.죽전역": "Jukjeon Stn/ Shinsegae S. City", // Directions diff --git a/src/data/busStops.ts b/src/data/busStops.ts index c8795cc..2c46570 100644 --- a/src/data/busStops.ts +++ b/src/data/busStops.ts @@ -8,6 +8,6 @@ export const busStops: BusStop[] = [ { name: "평화의광장", lat: 37.320146, lng: 127.12884 }, { name: "치과병원", lat: 37.322292, lng: 127.125436 }, { name: "정문", lat: 37.323352, lng: 127.125968 }, - { name: "인문관", lat: 37.32220999341863, lng: 127.128268 }, + { name: "인문관", lat: 37.322209, lng: 127.128268 }, { name: "죽전역", lat: 37.324206, lng: 127.108205 }, ]; From 51774e6e8b0d22669ed9884f1b18ec71ee0bc646 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 01:10:57 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EB=B6=88=EB=B6=84=EB=AA=85?= =?UTF-8?q?=ED=95=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/bus.ts | 2 +- src/lib/endpoints.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/bus.ts b/src/api/bus.ts index 9b198d5..45b9784 100644 --- a/src/api/bus.ts +++ b/src/api/bus.ts @@ -9,7 +9,7 @@ export const useBusLocations = (onError?: (message: string) => void) => { queryKey: ["busLocations"], queryFn: async () => { try { - const data = await apiGet(API_ENDPOINTS.BUS.LOCATION); + const data = await apiGet(API_ENDPOINTS.SHUTTLE.LOCATIONS); return Array.isArray(data) ? data : []; } catch (error) { const errorMessage = await handleApiError(error); diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index a6b9ad2..2080a89 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -1,6 +1,9 @@ export const API_ENDPOINTS = { - // 시내 버스 관련 - BUS: { - LOCATION: "api/shuttle/locations", + // 셔틀 버스 관련 + SHUTTLE: { + LOCATIONS: "api/shuttle/locations", }, + BUS: { + ARRIVALS: "api/bus/arrivals" + } } as const; From 54702d5770635f7ba07aa566933476d1b4cef52a Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 01:49:12 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EB=B2=84=EC=8A=A4=20=EC=A0=95?= =?UTF-8?q?=EB=A5=98=EC=9E=A5=20=EB=8F=84=EC=B0=A9=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=8C=A8=EC=B9=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/bus.ts | 39 ++++++++++ src/components/Bubble.tsx | 130 ++++++++++++++++--------------- src/contexts/LanguageContext.tsx | 4 + 3 files changed, 112 insertions(+), 61 deletions(-) diff --git a/src/api/bus.ts b/src/api/bus.ts index 45b9784..70800ba 100644 --- a/src/api/bus.ts +++ b/src/api/bus.ts @@ -24,3 +24,42 @@ export const useBusLocations = (onError?: (message: string) => void) => { retry: 2, }); }; + +// Types for arrivals API +export interface ArrivalBus { + routeName: string; + minutesLeft: number | null; + remainingSeats: number | null; +} + +export interface ArrivalStop { + stopCode: string; + stopName: string; + buses: ArrivalBus[]; +} + +export interface ArrivalsResponse { + updatedAt: string; + stops: ArrivalStop[]; +} + +export const useBusArrivals = (onError?: (message: string) => void) => { + return useQuery({ + queryKey: ["busArrivals"], + queryFn: async (): Promise => { + try { + const data = await apiGet( + API_ENDPOINTS.BUS.ARRIVALS + ); + return data || null; + } catch (error) { + const errorMessage = await handleApiError(error); + if (onError) onError(errorMessage); + return null; + } + }, + refetchInterval: 7000, + refetchIntervalInBackground: true, + retry: 2, + }); +}; diff --git a/src/components/Bubble.tsx b/src/components/Bubble.tsx index 02f2d1e..5eecec1 100644 --- a/src/components/Bubble.tsx +++ b/src/components/Bubble.tsx @@ -2,6 +2,7 @@ import { BusFront, X } from "lucide-react"; import { useEffect } from "react"; import { createRoot } from "react-dom/client"; import { useTranslation } from "../contexts/LanguageContext"; +import { useBusArrivals } from "../api/bus"; const DISPLAY_NAME_MAP: Record = { 죽전역: "죽전역(단국대학교 방향)", @@ -19,6 +20,7 @@ type Props = { export default function Bubble({ stop, onClose }: Props) { const { t, formatTime } = useTranslation(); + const { data: arrivals } = useBusArrivals(); useEffect(() => { if (typeof window.kakao === "undefined" || !window.map) return; @@ -86,9 +88,12 @@ export default function Bubble({ stop, onClose }: Props) { } } - // Pre-calculate time labels - const time1Label = formatTime(5); - const time2Label = formatTime(15); + + // Find arrival info for this stop (match by substring) + const arrivalStop = + arrivals?.stops?.find((s) => + String(s.stopName).includes(rawName) + ) || null; const root = createRoot(el); root.render( @@ -140,67 +145,70 @@ export default function Bubble({ stop, onClose }: Props) { pointerEvents: "none", }} /> -
+
{displayName}
{!rawName.startsWith("bus.") && ( <> -
- - - - 24 - - - | {time1Label} - - -
-
- - - - 720-3 - - - | {time2Label} - - -
+ {(arrivalStop?.buses ?? []).length === 0 && ( +
+ {t("common.noData")} +
+ )} + + {(arrivalStop?.buses ?? []).map((b, idx) => { + const route = String(b.routeName || ""); + const color = + route === "24" + ? "#f6c341" + : route === "720-3" + ? "#7dd3fc" + : "#000000"; + let timeLabel: string; + if (b.minutesLeft === null) { + timeLabel = t("common.noArrival"); + } else if (b.minutesLeft === 1) { + timeLabel = t("common.arrivingSoon"); + } else { + timeLabel = formatTime(b.minutesLeft); + } + + return ( +
+ + + + {b.routeName} + + + | {timeLabel} + + +
+ ); + } + )} )} @@ -258,7 +266,7 @@ export default function Bubble({ stop, onClose }: Props) { window.__currentBubbleOverlay = undefined; window.__currentBubbleStopName = undefined; }; - }, [stop, onClose, t, formatTime]); + }, [stop, onClose, t, formatTime, arrivals]); return null; } diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index 670e281..ebced5c 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -41,6 +41,8 @@ const translations: Translations = { "common.loading": "로딩 중...", "common.error": "오류가 발생했습니다", "common.noData": "데이터가 없습니다", + "common.noArrival": "도착 정보 없음", + "common.arrivingSoon": "곧 도착", }, en: { // BusStops @@ -75,6 +77,8 @@ const translations: Translations = { "common.loading": "Loading...", "common.error": "An error occurred", "common.noData": "No data available", + "common.noArrival": "No arrival info", + "common.arrivingSoon": "Arriving soon", }, }; From 7434015c8930ddeafc8907d298552a056872eeaa Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 02:02:27 +0900 Subject: [PATCH 5/7] =?UTF-8?q?style:=20lint=20check=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/bus.ts | 4 ++- src/components/Bubble.tsx | 55 +++++++++++++++++++++++++++------------ src/lib/endpoints.ts | 4 +-- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/api/bus.ts b/src/api/bus.ts index 70800ba..3acc35c 100644 --- a/src/api/bus.ts +++ b/src/api/bus.ts @@ -9,7 +9,9 @@ export const useBusLocations = (onError?: (message: string) => void) => { queryKey: ["busLocations"], queryFn: async () => { try { - const data = await apiGet(API_ENDPOINTS.SHUTTLE.LOCATIONS); + const data = await apiGet( + API_ENDPOINTS.SHUTTLE.LOCATIONS + ); return Array.isArray(data) ? data : []; } catch (error) { const errorMessage = await handleApiError(error); diff --git a/src/components/Bubble.tsx b/src/components/Bubble.tsx index 5eecec1..9922160 100644 --- a/src/components/Bubble.tsx +++ b/src/components/Bubble.tsx @@ -1,14 +1,14 @@ import { BusFront, X } from "lucide-react"; import { useEffect } from "react"; import { createRoot } from "react-dom/client"; -import { useTranslation } from "../contexts/LanguageContext"; import { useBusArrivals } from "../api/bus"; +import { useTranslation } from "../contexts/LanguageContext"; const DISPLAY_NAME_MAP: Record = { 죽전역: "죽전역(단국대학교 방향)", 치과병원: "치과병원(단국대학교 방향)", 정문: "정문(죽전역 방향)", - 인문관: "인문관(죽전역 방향)" + 인문관: "인문관(죽전역 방향)", }; type Stop = { lat: number; lng: number; name: string }; @@ -88,7 +88,6 @@ export default function Bubble({ stop, onClose }: Props) { } } - // Find arrival info for this stop (match by substring) const arrivalStop = arrivals?.stops?.find((s) => @@ -145,12 +144,13 @@ export default function Bubble({ stop, onClose }: Props) { pointerEvents: "none", }} /> -
+
{displayName}
{!rawName.startsWith("bus.") && ( <> - {(arrivalStop?.buses ?? []).length === 0 && ( + {(arrivalStop?.buses ?? []).length === + 0 && (
)} - {(arrivalStop?.buses ?? []).map((b, idx) => { - const route = String(b.routeName || ""); + {(arrivalStop?.buses ?? []).map( + (b, idx) => { + const route = String( + b.routeName || "" + ); const color = route === "24" ? "#f6c341" : route === "720-3" - ? "#7dd3fc" - : "#000000"; + ? "#7dd3fc" + : "#000000"; let timeLabel: string; if (b.minutesLeft === null) { - timeLabel = t("common.noArrival"); - } else if (b.minutesLeft === 1) { - timeLabel = t("common.arrivingSoon"); + timeLabel = + t("common.noArrival"); + } else if ( + b.minutesLeft === 1 + ) { + timeLabel = t( + "common.arrivingSoon" + ); } else { - timeLabel = formatTime(b.minutesLeft); + timeLabel = formatTime( + b.minutesLeft + ); } return ( @@ -184,14 +194,21 @@ export default function Bubble({ stop, onClose }: Props) { key={`${b.routeName}-${idx}`} style={{ display: "flex", - alignItems: "center", + alignItems: + "center", gap: "8px", - marginTop: idx === 0 ? "8px" : "6px", + marginTop: + idx === 0 + ? "8px" + : "6px", fontWeight: 400, fontSize: "16px", }} > - + {b.routeName} - + | {timeLabel} diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index 2080a89..617a5c7 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -4,6 +4,6 @@ export const API_ENDPOINTS = { LOCATIONS: "api/shuttle/locations", }, BUS: { - ARRIVALS: "api/bus/arrivals" - } + ARRIVALS: "api/bus/arrivals", + }, } as const; From bc66338cd0983e844f70bd003024ea268dded64f Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 02:24:13 +0900 Subject: [PATCH 6/7] =?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=8B=9C=EB=82=B4=EB=B2=84=EC=8A=A4=20?= =?UTF-8?q?=EB=8F=84=EC=B0=A9=20=EC=98=88=EC=A0=95=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=2030=EC=B4=88=20=ED=8F=B4=EB=A7=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/bus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/bus.ts b/src/api/bus.ts index 3acc35c..d5d6453 100644 --- a/src/api/bus.ts +++ b/src/api/bus.ts @@ -60,7 +60,7 @@ export const useBusArrivals = (onError?: (message: string) => void) => { return null; } }, - refetchInterval: 7000, + refetchInterval: 30000, refetchIntervalInBackground: true, retry: 2, }); From f9c1408e023224c323d907463c24e6c8acf495f1 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Fri, 21 Nov 2025 02:30:16 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20bus=EB=A5=BC=20shuttle=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20=EB=B6=88=EB=B6=84?= =?UTF-8?q?=EB=AA=85=ED=95=98=EA=B1=B0=EB=82=98=20=EA=B2=B9=EC=B9=98?= =?UTF-8?q?=EB=8A=94=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/bus.ts | 20 ++---------------- src/data/bus.ts | 39 ------------------------------------ src/hooks/useBusSelection.ts | 4 ++-- src/hooks/useMapOverlays.ts | 4 ++-- src/types/bus.ts | 23 +++++++++++++++++++++ src/utils/mapOverlays.ts | 4 ++-- 6 files changed, 31 insertions(+), 63 deletions(-) delete mode 100644 src/data/bus.ts create mode 100644 src/types/bus.ts diff --git a/src/api/bus.ts b/src/api/bus.ts index d5d6453..b0f40e3 100644 --- a/src/api/bus.ts +++ b/src/api/bus.ts @@ -1,15 +1,15 @@ import { useQuery } from "@tanstack/react-query"; -import type { Bus } from "../data/bus"; import { apiGet } from "../lib/api"; import { API_ENDPOINTS } from "../lib/endpoints"; import { handleApiError } from "../lib/error"; +import type { ArrivalsResponse, Shuttle } from "../types/bus"; export const useBusLocations = (onError?: (message: string) => void) => { return useQuery({ queryKey: ["busLocations"], queryFn: async () => { try { - const data = await apiGet( + const data = await apiGet( API_ENDPOINTS.SHUTTLE.LOCATIONS ); return Array.isArray(data) ? data : []; @@ -28,22 +28,6 @@ export const useBusLocations = (onError?: (message: string) => void) => { }; // Types for arrivals API -export interface ArrivalBus { - routeName: string; - minutesLeft: number | null; - remainingSeats: number | null; -} - -export interface ArrivalStop { - stopCode: string; - stopName: string; - buses: ArrivalBus[]; -} - -export interface ArrivalsResponse { - updatedAt: string; - stops: ArrivalStop[]; -} export const useBusArrivals = (onError?: (message: string) => void) => { return useQuery({ diff --git a/src/data/bus.ts b/src/data/bus.ts deleted file mode 100644 index 37e3cc3..0000000 --- a/src/data/bus.ts +++ /dev/null @@ -1,39 +0,0 @@ -export interface Bus { - shuttleId: string; - lat: number; - lng: number; - direction: boolean | null; -} - -export const buses: ReadonlyArray = [ - { - shuttleId: "bus1", - lat: 37.323494, - lng: 127.123008, - direction: null, - }, - { - shuttleId: "bus2", - lat: 37.323637, - lng: 127.120047, - direction: true, // 단국대학교 - }, - { - shuttleId: "bus3", - lat: 37.323779, - lng: 127.117087, - direction: false, // 죽전역 - }, - { - shuttleId: "bus4", - lat: 37.323921, - lng: 127.114126, - direction: true, // 단국대학교 - }, - { - shuttleId: "bus5", - lat: 37.324063, - lng: 127.111166, - direction: false, // 죽전역 - }, -]; diff --git a/src/hooks/useBusSelection.ts b/src/hooks/useBusSelection.ts index e97a9af..013efa1 100644 --- a/src/hooks/useBusSelection.ts +++ b/src/hooks/useBusSelection.ts @@ -1,9 +1,9 @@ import type { Dispatch, SetStateAction } from "react"; -import type { Bus } from "../data/bus"; +import type { Shuttle } from "../types/bus"; import { moveToLocation } from "./useMapMovement"; export const useBusSelection = ( - buses: Bus[], + buses: Shuttle[], setBubbleStop: Dispatch< SetStateAction<{ lat: number; lng: number; name: string } | undefined> > diff --git a/src/hooks/useMapOverlays.ts b/src/hooks/useMapOverlays.ts index 212de9c..10da403 100644 --- a/src/hooks/useMapOverlays.ts +++ b/src/hooks/useMapOverlays.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import type { Bus } from "../data/bus"; import type { BusStop } from "../data/busStops"; +import type { Shuttle } from "../types/bus"; import type { KakaoMap } from "../types/kakao"; import { clearAllBusOverlays, @@ -11,7 +11,7 @@ import { export const useMapOverlays = ( map: KakaoMap | null, busStops: BusStop[], - buses: Bus[], + buses: Shuttle[], selectedStopName?: string, onStopClick?: (stop: BusStop) => void ) => { diff --git a/src/types/bus.ts b/src/types/bus.ts new file mode 100644 index 0000000..e11249e --- /dev/null +++ b/src/types/bus.ts @@ -0,0 +1,23 @@ +export interface Shuttle { + shuttleId: string; + lat: number; + lng: number; + direction: boolean | null; +} + +export interface ArrivalBus { + routeName: string; + minutesLeft: number | null; + remainingSeats: number | null; +} + +export interface ArrivalStop { + stopCode: string; + stopName: string; + buses: ArrivalBus[]; +} + +export interface ArrivalsResponse { + updatedAt: string; + stops: ArrivalStop[]; +} diff --git a/src/utils/mapOverlays.ts b/src/utils/mapOverlays.ts index bb85b45..bc96572 100644 --- a/src/utils/mapOverlays.ts +++ b/src/utils/mapOverlays.ts @@ -1,6 +1,6 @@ import busIconSvg from "../assets/busIcon.svg"; -import type { Bus } from "../data/bus"; import type { BusStop } from "../data/busStops"; +import type { Shuttle } from "../types/bus"; import type { KakaoMap, KakaoOverlay } from "../types/kakao"; // 상수 정의 @@ -284,7 +284,7 @@ export const createBusStopOverlays = ( export const createBusOverlays = ( map: KakaoMap, - buses: Bus[] + buses: Shuttle[] ): OverlayHandle[] => { if (!map || typeof window === "undefined" || !window.kakao?.maps) return [];