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
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VITE_KAKAO_MAP_API_KEY=
VITE_API_BASE_URL=
VITE_API_BASE_URL=https://whatthebus.duckdns.org
VITE_API_KEY=
49 changes: 25 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ function App() {
const langId = useId();
const [language, setLanguage] = useState<Language>(() => {
try {
const stored = typeof window !== "undefined" && window.localStorage
? localStorage.getItem("wtb:lang")
: null;
const stored =
typeof window !== "undefined" && window.localStorage
? localStorage.getItem("wtb:lang")
: null;
return stored === "en" ? "en" : "ko";
} catch {
return "ko";
Expand All @@ -67,10 +68,7 @@ function App() {

return (
<QueryClientProvider client={queryClient}>
<LanguageProvider
language={language}
setLanguage={setLanguage}
>
<LanguageProvider language={language} setLanguage={setLanguage}>
<ToastProvider>
<AppContent
mapId={mapId}
Expand Down Expand Up @@ -119,21 +117,24 @@ function AppContent({
const { toast } = useToast();
const errorShownRef = useRef(false);

const handleBusLocationError = useCallback((message: string) => {
// Only show toast once to avoid spamming on refetch failures
if (!errorShownRef.current) {
toast({
title: "버스 위치 조회 오류",
description: message,
variant: "destructive",
});
errorShownRef.current = true;
// Reset after 30 seconds to allow showing error again if it persists
setTimeout(() => {
errorShownRef.current = false;
}, 30000);
}
}, [toast]);
const handleBusLocationError = useCallback(
(message: string) => {
// Only show toast once to avoid spamming on refetch failures
if (!errorShownRef.current) {
toast({
title: "버스 위치 조회 오류",
description: message,
variant: "destructive",
});
errorShownRef.current = true;
// Reset after 30 seconds to allow showing error again if it persists
setTimeout(() => {
errorShownRef.current = false;
}, 30000);
}
},
[toast]
);

const { data: buses = [] } = useBusLocations(handleBusLocationError);
const handleBusNumberSelect = useBusSelection(buses, setBubbleStop);
Expand Down Expand Up @@ -167,8 +168,8 @@ function AppContent({
/>
</Suspense>
) : null}
<MapContainer
mapId={mapId}
<MapContainer
mapId={mapId}
selectedStopName={bubbleStop?.name}
onStopClick={handleStopClick}
>
Expand Down
6 changes: 2 additions & 4 deletions src/api/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { apiGet } from "../lib/api";
import { API_ENDPOINTS } from "../lib/endpoints";
import { handleApiError } from "../lib/error";

export const useBusLocations = (
onError?: (message: string) => void
) => {
export const useBusLocations = (onError?: (message: string) => void) => {
return useQuery({
queryKey: ["busLocations"],
queryFn: async () => {
Expand All @@ -21,7 +19,7 @@ export const useBusLocations = (
return [];
}
},
refetchInterval: 5000,
refetchInterval: 7000,
refetchIntervalInBackground: true,
retry: 2,
});
Expand Down
15 changes: 9 additions & 6 deletions src/components/Bubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function Bubble({ stop, onClose }: Props) {
document.body.appendChild(el);

const rawName = String(stop.name);

// Check if it's a bus key (starts with "bus.")
let displayName: string;
if (rawName.startsWith("bus.")) {
Expand All @@ -67,15 +67,18 @@ export default function Bubble({ stop, onClose }: Props) {
// It's a bus stop name
const translationKey = `busStop.${rawName}`;
const translatedName = t(translationKey);

// If translation key is returned as-is, use the raw name instead
const displayBaseName = translatedName === translationKey ? rawName : translatedName;
const displayBaseName =
translatedName === translationKey
? rawName
: translatedName;

displayName = displayBaseName;
if (DISPLAY_NAME_MAP[rawName]) {
const directionKey = DISPLAY_NAME_MAP[rawName].includes(
"죽전역"
)
const directionKey = DISPLAY_NAME_MAP[
rawName
].includes("죽전역")
? "direction.toJukjeon"
: "direction.toDKU";
displayName = `${displayBaseName} (${t(directionKey)})`;
Expand Down
4 changes: 3 additions & 1 deletion src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
<select
id={langId}
value={language}
onChange={(e) => setLanguage(e.target.value as Language)}
onChange={(e) =>
setLanguage(e.target.value as Language)
}
style={{
padding: 8,
borderRadius: 6,
Expand Down
58 changes: 0 additions & 58 deletions src/components/ui/button.tsx

This file was deleted.

12 changes: 8 additions & 4 deletions src/contexts/LanguageContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// Directions
"direction.toDKU": "단국대학교 방향",
"direction.toJukjeon": "죽전역 방향",

// Bus Labels
"bus.shuttleToDKU": "셔틀버스(단국대학교 방향)",
"bus.shuttleToJukjeon": "셔틀버스(죽전역 방향)",
Expand Down Expand Up @@ -55,7 +55,7 @@
// Directions
"direction.toDKU": "→ Dankook Univ.",
"direction.toJukjeon": "→ Jukjeon Stn.",

// Bus Labels
"bus.shuttleToDKU": "Shuttle Bus (→ Dankook Univ.)",
"bus.shuttleToJukjeon": "Shuttle Bus (→ Jukjeon Stn.)",
Expand Down Expand Up @@ -108,17 +108,21 @@
};

const formatBusNumber = (number: number): string => {
return language === "ko" ? `셔틀버스 ${number}` : `Shuttle Bus ${number}`;
return language === "ko"
? `셔틀버스 ${number}`
: `Shuttle Bus ${number}`;
};

return (
<LanguageContext.Provider value={{ language, setLanguage, t, formatTime, formatBusNumber }}>
<LanguageContext.Provider
value={{ language, setLanguage, t, formatTime, formatBusNumber }}
>
{children}
</LanguageContext.Provider>
);
}

export function useTranslation() {

Check warning on line 125 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
6 changes: 5 additions & 1 deletion src/hooks/useBusSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export const useBusSelection = (
: bus.direction === false
? "bus.shuttleToJukjeon"
: "bus.shuttle";
setBubbleStop({ lat: bus.lat, lng: bus.lng, name: directionKey });
setBubbleStop({
lat: bus.lat,
lng: bus.lng,
name: directionKey,
});
} catch {
/* ignore */
}
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useKakaoMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import type { KakaoMap } from "../types/kakao";

interface UseKakaoMapOptions {
mapId: string;
Expand All @@ -10,7 +11,7 @@ interface UseKakaoMapOptions {
}

export const useKakaoMap = ({ mapId, toast }: UseKakaoMapOptions) => {
const [mapInstance, setMapInstance] = useState<unknown>(null);
const [mapInstance, setMapInstance] = useState<KakaoMap | null>(null);

useEffect(() => {
const kakaoApiKey = import.meta.env.VITE_KAKAO_MAP_API_KEY;
Expand Down
33 changes: 25 additions & 8 deletions src/hooks/useMapOverlays.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import { useEffect } from "react";
import type { Bus } from "../data/bus";
import type { BusStop } from "../data/busStops";
import type { KakaoMap } from "../types/kakao";
import {
clearAllBusOverlays,
createBusOverlays,
createBusStopOverlays,
type OverlayHandle,
} from "../utils/mapOverlays";

export const useMapOverlays = (
map: unknown,
map: KakaoMap | null,
busStops: BusStop[],
buses: Bus[],
selectedStopName?: string,
onStopClick?: (stop: BusStop) => void
) => {
// 버스 정류장 오버레이 관리
useEffect(() => {
if (!map) return;

const overlays: OverlayHandle[] = [
...createBusStopOverlays(map, busStops, selectedStopName, onStopClick),
...createBusOverlays(map, buses),
];
const stopOverlays = createBusStopOverlays(
map,
busStops,
selectedStopName,
onStopClick
);

return () => {
overlays.forEach((overlay) => {
stopOverlays.forEach((overlay) => {
try {
overlay.setMap(null);
} catch {
Expand All @@ -36,5 +40,18 @@ export const useMapOverlays = (
}
});
};
}, [map, busStops, buses, selectedStopName, onStopClick]);
}, [map, busStops, selectedStopName, onStopClick]);

useEffect(() => {
// 컴포넌트 언마운트 또는 map 변경 시 정리
return () => {
clearAllBusOverlays();
};
}, []);

// 버스 오버레이 업데이트 (캐시 유지)
useEffect(() => {
if (!map) return;
createBusOverlays(map, buses);
}, [map, buses]);
};
18 changes: 13 additions & 5 deletions src/types/kakao.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export {};

interface KakaoLatLng {
export interface KakaoLatLng {
lat: number;
lng: number;
getLat(): number;
getLng(): number;
}

interface KakaoMap {
export interface KakaoMap {
setCenter: (pos: KakaoLatLng) => void;
setMinLevel: (level: number) => void;
setMaxLevel: (level: number) => void;
Expand All @@ -15,8 +15,16 @@ interface KakaoMap {
| { getLat: () => number; getLng: () => number };
}

interface KakaoOverlay {
export interface KakaoOverlay {
setMap: (map: KakaoMap | null) => void;
setPosition: (position: KakaoLatLng) => void;
getPosition(): KakaoLatLng;
setContent(content: HTMLElement | string): void;
getContent(): HTMLElement;
setVisible(visible: boolean): void;
getVisible(): boolean;
setZIndex(zIndex: number): void;
getZIndex(): number;
}

declare global {
Expand Down
Loading
Loading