Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
VITE_KAKAO_MAP_API_KEY=
VITE_KAKAO_MAP_API_KEY=
VITE_API_BASE_URL=
120 changes: 76 additions & 44 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ 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";

interface DevtoolsProps {
initialIsOpen?: boolean;
}

// Conditionally load ReactQueryDevtools only in development
const ReactQueryDevtools: ComponentType<DevtoolsProps> = import.meta.env.DEV
? lazy(() =>
import("@tanstack/react-query-devtools").then((module) => ({
Expand Down Expand Up @@ -61,54 +61,86 @@ function App() {
{ lat: number; lng: number; name: string } | undefined
>(undefined);

const handleBusNumberSelect = useBusSelection(setBubbleStop);

return (
<QueryClientProvider client={queryClient}>
<div
className="App"
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
}}
>
<SettingsButton
showSettings={showSettings}
onToggle={toggleSettings}
/>
<AppContent
mapId={mapId}
langId={langId}
language={language}
setLanguage={setLanguage}
showSettings={showSettings}
toggleSettings={toggleSettings}
bubbleStop={bubbleStop}
setBubbleStop={setBubbleStop}
/>
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
);
}

{showSettings ? (
<Suspense fallback={null}>
<SettingsPanel
langId={langId}
language={language}
setLanguage={setLanguage}
onClose={() => setShowSettings(false)}
/>
</Suspense>
) : null}
<MapContainer mapId={mapId}>
<Bubble
stop={bubbleStop}
onClose={() => setBubbleStop(undefined)}
/>
</MapContainer>
<BusStopsPanel
onBusNumberSelect={handleBusNumberSelect}
onToggleBubble={(stop) => {
setBubbleStop((prev) =>
prev === stop ? undefined : stop
);
}}
/>
</div>
{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<React.SetStateAction<{ lat: number; lng: number; name: string } | undefined>>;
}

function AppContent({
mapId,
langId,
language,
setLanguage,
showSettings,
toggleSettings,
bubbleStop,
setBubbleStop,
}: AppContentProps) {
const { data: buses = [] } = useBusLocations();
const handleBusNumberSelect = useBusSelection(buses, setBubbleStop);

return (
<div
className="App"
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
}}
>
<SettingsButton
showSettings={showSettings}
onToggle={toggleSettings}
/>

{showSettings ? (
<Suspense fallback={null}>
<ReactQueryDevtools initialIsOpen={false} />
<SettingsPanel
langId={langId}
language={language}
setLanguage={setLanguage}
onClose={toggleSettings}
/>
</Suspense>
)}
</QueryClientProvider>
) : null}
<MapContainer mapId={mapId}>
<Bubble
stop={bubbleStop}
onClose={() => setBubbleStop(undefined)}
/>
</MapContainer>
<BusStopsPanel
onBusNumberSelect={handleBusNumberSelect}
onToggleBubble={(stop) => {
setBubbleStop((prev) =>
prev === stop ? undefined : stop
);
}}
/>
</div>
);
}

Expand Down
16 changes: 16 additions & 0 deletions src/api/bus.ts
Original file line number Diff line number Diff line change
@@ -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<Bus[]>(API_ENDPOINTS.BUS.LOCATION);
return Array.isArray(data) ? data : [];
},
refetchInterval: 20000,
refetchIntervalInBackground: true,
});
};
5 changes: 3 additions & 2 deletions src/components/MapContainer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
Expand Down
10 changes: 5 additions & 5 deletions src/data/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface Bus {
shuttleId: string;
lat: number;
lng: number;
direction: string | null;
direction: boolean | null;
}

export const buses: ReadonlyArray<Bus> = [
Expand All @@ -16,24 +16,24 @@ export const buses: ReadonlyArray<Bus> = [
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, // 죽전역
},
];
7 changes: 4 additions & 3 deletions src/hooks/useBusSelection.ts
Original file line number Diff line number Diff line change
@@ -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>
>
Expand All @@ -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 */
Expand Down
16 changes: 16 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -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 <T, P = undefined>(
url: string,
params?: P
): Promise<T> => {
const response = await apiClient.get(url, {
searchParams: params as Record<string, string | number | boolean>,
});
return response.json<T>();
};
2 changes: 1 addition & 1 deletion src/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const API_ENDPOINTS = {
// 시내 버스 관련
BUS: {
SEARCH: "/bus/search",
LOCATION: "api/shuttle/location",
},
} as const;
Loading