Skip to content
29 changes: 27 additions & 2 deletions src/api/bus.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
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<Bus[]>(API_ENDPOINTS.BUS.LOCATION);
const data = await apiGet<Shuttle[]>(
API_ENDPOINTS.SHUTTLE.LOCATIONS
);
return Array.isArray(data) ? data : [];
} catch (error) {
const errorMessage = await handleApiError(error);
Expand All @@ -24,3 +26,26 @@ export const useBusLocations = (onError?: (message: string) => void) => {
retry: 2,
});
};

// Types for arrivals API

export const useBusArrivals = (onError?: (message: string) => void) => {
return useQuery({
queryKey: ["busArrivals"],
queryFn: async (): Promise<ArrivalsResponse | null> => {
try {
const data = await apiGet<ArrivalsResponse>(
API_ENDPOINTS.BUS.ARRIVALS
);
return data || null;
} catch (error) {
const errorMessage = await handleApiError(error);
if (onError) onError(errorMessage);
return null;
}
},
refetchInterval: 30000,
refetchIntervalInBackground: true,
retry: 2,
});
};
150 changes: 90 additions & 60 deletions src/components/Bubble.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { BusFront, X } from "lucide-react";
import { useEffect } from "react";
import { createRoot } from "react-dom/client";
import { useBusArrivals } from "../api/bus";
import { useTranslation } from "../contexts/LanguageContext";

const DISPLAY_NAME_MAP: Record<string, string> = {
죽전역: "죽전역(단국대학교 방향)",
치과병원: "치과병원(단국대학교 방향)",
정문: "정문(죽전역 방향)",
인문관: "인문관(죽전역 방향)",
};

type Stop = { lat: number; lng: number; name: string };
Expand All @@ -18,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;

Expand Down Expand Up @@ -85,9 +88,11 @@ 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(
Expand Down Expand Up @@ -144,62 +149,87 @@ export default function Bubble({ stop, onClose }: Props) {
</div>
{!rawName.startsWith("bus.") && (
<>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "8px",
fontWeight: 400,
fontSize: "16px",
}}
>
<BusFront
size={16}
color="#f6c341"
/>
<span>
<span
style={{
color: "#f6c341",
fontWeight: 700,
}}
>
24
</span>
<span style={{ marginLeft: 8 }}>
| {time1Label}
</span>
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "6px",
fontWeight: 400,
fontSize: "16px",
}}
>
<BusFront
size={16}
color="#7dd3fc"
/>
<span>
<span
style={{
color: "#7dd3fc",
fontWeight: 700,
}}
>
720-3
</span>
<span style={{ marginLeft: 8 }}>
| {time2Label}
</span>
</span>
</div>
{(arrivalStop?.buses ?? []).length ===
0 && (
<div
style={{
marginTop: "8px",
fontSize: "16px",
color: "#666",
}}
>
{t("common.noData")}
</div>
)}

{(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 (
<div
key={`${b.routeName}-${idx}`}
style={{
display: "flex",
alignItems:
"center",
gap: "8px",
marginTop:
idx === 0
? "8px"
: "6px",
fontWeight: 400,
fontSize: "16px",
}}
>
<BusFront
size={16}
color={color}
/>
<span>
<span
style={{
color,
fontWeight: 700,
}}
>
{b.routeName}
</span>
<span
style={{
marginLeft: 8,
}}
>
| {timeLabel}
</span>
</span>
</div>
);
}
)}
</>
)}
</button>
Expand Down Expand Up @@ -257,7 +287,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;
}
6 changes: 6 additions & 0 deletions src/contexts/LanguageContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"busStop.평화의광장": "평화의광장",
"busStop.치과병원": "치과병원",
"busStop.정문": "정문",
"busStop.인문관": "인문관",
"busStop.죽전역": "죽전역",

// Directions
Expand All @@ -40,6 +41,8 @@
"common.loading": "로딩 중...",
"common.error": "오류가 발생했습니다",
"common.noData": "데이터가 없습니다",
"common.noArrival": "도착 정보 없음",
"common.arrivingSoon": "곧 도착",
},
en: {
// BusStops
Expand All @@ -50,6 +53,7 @@
"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
Expand All @@ -73,6 +77,8 @@
"common.loading": "Loading...",
"common.error": "An error occurred",
"common.noData": "No data available",
"common.noArrival": "No arrival info",
"common.arrivingSoon": "Arriving soon",
},
};

Expand Down Expand Up @@ -122,7 +128,7 @@
);
}

export function useTranslation() {

Check warning on line 131 in src/contexts/LanguageContext.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(LanguageContext);
if (!context) {
throw new Error("useTranslation must be used within LanguageProvider");
Expand Down
39 changes: 0 additions & 39 deletions src/data/bus.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/data/busStops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.322209, lng: 127.128268 },
{ name: "죽전역", lat: 37.324206, lng: 127.108205 },
];
4 changes: 2 additions & 2 deletions src/hooks/useBusSelection.ts
Original file line number Diff line number Diff line change
@@ -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>
>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useMapOverlays.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,7 +11,7 @@ import {
export const useMapOverlays = (
map: KakaoMap | null,
busStops: BusStop[],
buses: Bus[],
buses: Shuttle[],
selectedStopName?: string,
onStopClick?: (stop: BusStop) => void
) => {
Expand Down
7 changes: 5 additions & 2 deletions src/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const API_ENDPOINTS = {
// 시내 버스 관련
// 셔틀 버스 관련
SHUTTLE: {
LOCATIONS: "api/shuttle/locations",
},
BUS: {
LOCATION: "api/shuttle/locations",
ARRIVALS: "api/bus/arrivals",
},
} as const;
23 changes: 23 additions & 0 deletions src/types/bus.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
Loading
Loading