diff --git a/.env.sample b/.env.sample index 1bd8647..4f01c63 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,2 @@ -VITE_KAKAO_MAP_API_KEY= \ No newline at end of file +VITE_KAKAO_MAP_API_KEY= +VITE_API_BASE_URL= \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ecbb313..4210316 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { SettingsButton } from "./components/SettingsButton"; const SettingsPanel = lazy(() => import("./components/SettingsPanel")); import { QueryClientProvider } from "@tanstack/react-query"; +import { useBusLocations } from "./api/bus"; import { useBusSelection } from "./hooks/useBusSelection"; import { queryClient } from "./lib/query-client"; @@ -22,7 +23,6 @@ interface DevtoolsProps { initialIsOpen?: boolean; } -// Conditionally load ReactQueryDevtools only in development const ReactQueryDevtools: ComponentType = import.meta.env.DEV ? lazy(() => import("@tanstack/react-query-devtools").then((module) => ({ @@ -61,54 +61,86 @@ function App() { { lat: number; lng: number; name: string } | undefined >(undefined); - const handleBusNumberSelect = useBusSelection(setBubbleStop); - return ( -
- + + {import.meta.env.DEV && } + + ); +} - {showSettings ? ( - - setShowSettings(false)} - /> - - ) : null} - - setBubbleStop(undefined)} - /> - - { - setBubbleStop((prev) => - prev === stop ? undefined : stop - ); - }} - /> -
- {import.meta.env.DEV && ( +interface AppContentProps { + mapId: string; + langId: string; + language: string; + setLanguage: (lang: string) => void; + showSettings: boolean; + toggleSettings: () => void; + bubbleStop: { lat: number; lng: number; name: string } | undefined; + setBubbleStop: React.Dispatch>; +} + +function AppContent({ + mapId, + langId, + language, + setLanguage, + showSettings, + toggleSettings, + bubbleStop, + setBubbleStop, +}: AppContentProps) { + const { data: buses = [] } = useBusLocations(); + const handleBusNumberSelect = useBusSelection(buses, setBubbleStop); + + return ( +
+ + + {showSettings ? ( - + - )} - + ) : null} + + setBubbleStop(undefined)} + /> + + { + setBubbleStop((prev) => + prev === stop ? undefined : stop + ); + }} + /> +
); } diff --git a/src/api/bus.ts b/src/api/bus.ts new file mode 100644 index 0000000..a5c2928 --- /dev/null +++ b/src/api/bus.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import type { Bus } from "../data/bus"; +import { apiGet } from "../lib/api"; +import { API_ENDPOINTS } from "../lib/endpoints"; + +export const useBusLocations = () => { + return useQuery({ + queryKey: ["busLocations"], + queryFn: async () => { + const data = await apiGet(API_ENDPOINTS.BUS.LOCATION); + return Array.isArray(data) ? data : []; + }, + refetchInterval: 20000, + refetchIntervalInBackground: true, + }); +}; diff --git a/src/components/MapContainer.tsx b/src/components/MapContainer.tsx index 1c3962f..940db01 100644 --- a/src/components/MapContainer.tsx +++ b/src/components/MapContainer.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { buses } from "../data/bus"; +import { useBusLocations } from "../api/bus"; import { busStops } from "../data/busStops"; import { useKakaoMap } from "../hooks/useKakaoMap"; import { useMapEventHandlers } from "../hooks/useMapEventHandlers"; @@ -14,8 +14,9 @@ interface MapContainerProps { export const MapContainer = ({ mapId, children }: MapContainerProps) => { const { toast } = useToast(); const map = useKakaoMap({ mapId, toast }); + const { data: buses = [] } = useBusLocations(); - useMapOverlays(map, [...busStops], [...buses]); + useMapOverlays(map, [...busStops], buses); useMapEventHandlers(mapId); return ( diff --git a/src/data/bus.ts b/src/data/bus.ts index 8e83183..37e3cc3 100644 --- a/src/data/bus.ts +++ b/src/data/bus.ts @@ -2,7 +2,7 @@ export interface Bus { shuttleId: string; lat: number; lng: number; - direction: string | null; + direction: boolean | null; } export const buses: ReadonlyArray = [ @@ -16,24 +16,24 @@ export const buses: ReadonlyArray = [ shuttleId: "bus2", lat: 37.323637, lng: 127.120047, - direction: "단국대학교", + direction: true, // 단국대학교 }, { shuttleId: "bus3", lat: 37.323779, lng: 127.117087, - direction: "죽전역", + direction: false, // 죽전역 }, { shuttleId: "bus4", lat: 37.323921, lng: 127.114126, - direction: "단국대학교", + direction: true, // 단국대학교 }, { shuttleId: "bus5", lat: 37.324063, lng: 127.111166, - direction: "죽전역", + direction: false, // 죽전역 }, ]; diff --git a/src/hooks/useBusSelection.ts b/src/hooks/useBusSelection.ts index b70da98..7f38cb2 100644 --- a/src/hooks/useBusSelection.ts +++ b/src/hooks/useBusSelection.ts @@ -1,8 +1,9 @@ import type { Dispatch, SetStateAction } from "react"; -import { buses } from "../data/bus"; +import type { Bus } from "../data/bus"; import { moveToLocation } from "./useMapMovement"; export const useBusSelection = ( + buses: Bus[], setBubbleStop: Dispatch< SetStateAction<{ lat: number; lng: number; name: string } | undefined> > @@ -14,8 +15,8 @@ export const useBusSelection = ( if (bus && Number.isFinite(bus.lat) && Number.isFinite(bus.lng)) { moveToLocation(bus.lat, bus.lng); try { - const dir = bus.direction?.trim() ?? ""; - const label = dir ? `셔틀버스(${dir} 방향)` : "셔틀버스"; + 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/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..1205d73 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,16 @@ +import ky from "ky"; + +const apiClient = ky.create({ + prefixUrl: import.meta.env.VITE_API_BASE_URL || "", + timeout: 10000, +}); + +export const apiGet = async ( + url: string, + params?: P +): Promise => { + const response = await apiClient.get(url, { + searchParams: params as Record, + }); + return response.json(); +}; diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index 5bc1057..742acf7 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -1,6 +1,6 @@ export const API_ENDPOINTS = { // 시내 버스 관련 BUS: { - SEARCH: "/bus/search", + LOCATION: "api/shuttle/location", }, } as const;