From 117b7b040409695e4c47acc5efe871f673690ad0 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Tue, 11 Nov 2025 20:20:14 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=98=81=EC=96=B4=20=EC=96=B8?= =?UTF-8?q?=EC=96=B4=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95,=20=EC=84=A4=EC=A0=95=EA=B3=BC=20?= =?UTF-8?q?=EB=A7=90=ED=92=8D=EC=84=A0=20=EC=9A=B0=EC=84=A0=EC=88=9C?= =?UTF-8?q?=EC=9C=84=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 30 +++++---- src/components/Bubble.tsx | 28 +++++--- src/components/BusStops.tsx | 16 +++-- src/components/SettingsButton.tsx | 4 +- src/components/SettingsPanel.tsx | 18 ++--- src/contexts/LanguageContext.tsx | 107 ++++++++++++++++++++++++++++++ src/utils/mapOverlays.ts | 2 +- 7 files changed, 167 insertions(+), 38 deletions(-) create mode 100644 src/contexts/LanguageContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 0805478..42f8809 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ const SettingsPanel = lazy(() => import("./components/SettingsPanel")); import { QueryClientProvider } from "@tanstack/react-query"; import { useBusLocations } from "./api/bus"; +import { LanguageProvider } from "./contexts/LanguageContext"; import { useBusSelection } from "./hooks/useBusSelection"; import { queryClient } from "./lib/query-client"; @@ -63,19 +64,24 @@ function App() { return ( - - {import.meta.env.DEV && ( - - )} + > + + {import.meta.env.DEV && ( + + )} + ); } diff --git a/src/components/Bubble.tsx b/src/components/Bubble.tsx index a725639..6b90c99 100644 --- a/src/components/Bubble.tsx +++ b/src/components/Bubble.tsx @@ -1,6 +1,7 @@ import { BusFront, X } from "lucide-react"; import { useEffect } from "react"; import { createRoot } from "react-dom/client"; +import { useTranslation } from "../contexts/LanguageContext"; const DISPLAY_NAME_MAP: Record = { 죽전역: "죽전역(단국대학교 방향)", @@ -16,6 +17,7 @@ type Props = { }; export default function Bubble({ stop, onClose }: Props) { + const { t } = useTranslation(); useEffect(() => { if (typeof window.kakao === "undefined" || !window.map) return; @@ -48,14 +50,24 @@ export default function Bubble({ stop, onClose }: Props) { el.style.position = "fixed"; el.style.top = "16px"; el.style.left = "16px"; - el.style.right = "48px"; + el.style.right = "16px"; el.style.marginLeft = "auto"; el.style.marginRight = "auto"; - el.style.zIndex = "200"; + el.style.zIndex = "1001"; document.body.appendChild(el); const rawName = String(stop.name); - const displayName = DISPLAY_NAME_MAP[rawName] ?? rawName; + const translatedName = t(`busStop.${rawName}`); + + let displayName = translatedName; + if (DISPLAY_NAME_MAP[rawName]) { + const directionKey = DISPLAY_NAME_MAP[rawName].includes( + "죽전역" + ) + ? "direction.toJukjeon" + : "direction.toDKU"; + displayName = `${translatedName} (${t(directionKey)})`; + } const root = createRoot(el); root.render( @@ -64,8 +76,8 @@ export default function Bubble({ stop, onClose }: Props) { position: "fixed", top: "40px", left: "16px", - right: "48px", - zIndex: 200, + right: "16px", + zIndex: 10001, display: "flex", justifyContent: "center", }} @@ -136,7 +148,7 @@ export default function Bubble({ stop, onClose }: Props) { 24 - | 5분 남음 + | 5min @@ -164,7 +176,7 @@ export default function Bubble({ stop, onClose }: Props) { 720-3 - | 15분 남음 + | 15min @@ -225,7 +237,7 @@ export default function Bubble({ stop, onClose }: Props) { window.__currentBubbleOverlay = undefined; window.__currentBubbleStopName = undefined; }; - }, [stop, onClose]); + }, [stop, onClose, t]); return null; } diff --git a/src/components/BusStops.tsx b/src/components/BusStops.tsx index fde2f7d..852c455 100644 --- a/src/components/BusStops.tsx +++ b/src/components/BusStops.tsx @@ -1,7 +1,8 @@ import { ChevronDown } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; -import type { BusStop } from "../data/busStops"; import busIconSvg from "../assets/busIcon.svg"; +import { useTranslation } from "../contexts/LanguageContext"; +import type { BusStop } from "../data/busStops"; type Props = { busStops: BusStop[]; @@ -18,6 +19,7 @@ export default function BusStops({ onToggleBubble, busCount = 0, }: Props) { + const { t } = useTranslation(); const [openStops, setOpenStops] = useState(false); const [openNumbers, setOpenNumbers] = useState(false); const listId = useId(); @@ -101,7 +103,7 @@ export default function BusStops({ openStops ? "hidden" : "inline" }`} > - 버스 정류장 선택하기 + {t("busStops.selectStop")} - {stop.name} + {t(`busStop.${stop.name}`)} ))} @@ -157,9 +159,9 @@ export default function BusStops({ >
- 버스
@@ -168,7 +170,7 @@ export default function BusStops({ openNumbers ? "hidden" : "inline" }`} > - 버스 선택하기 + {t("busStops.selectBus")}
= ({ setLanguage, onClose, }) => { + const { t } = useTranslation(); return (
= ({ marginBottom: 12, }} > -
설정
+
{t("settings.title")}
@@ -101,7 +103,7 @@ const SettingsPanel: React.FC = ({ style={{ display: "flex", flexDirection: "column", gap: 6 }} >
- 문의하기 + {t("settings.contact")}
= ({ rel="noopener noreferrer" style={{ color: "#0ea5e9", textDecoration: "none" }} > - 문의하기(구글폼) + {t("settings.contact")} @@ -121,7 +123,7 @@ const SettingsPanel: React.FC = ({ style={{ display: "flex", flexDirection: "column", gap: 6 }} >
- 사용 가이드 + {t("settings.userGuide")}
diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx new file mode 100644 index 0000000..c253b11 --- /dev/null +++ b/src/contexts/LanguageContext.tsx @@ -0,0 +1,107 @@ +import { createContext, type ReactNode, useContext } from "react"; + +type Language = "ko" | "en"; + +interface Translations { + ko: Record; + en: Record; +} + +const translations: Translations = { + ko: { + // BusStops + "busStops.selectStop": "버스 정류장 선택하기", + "busStops.selectBus": "버스 선택하기", + + // Bus Stop Names + "busStop.평화의광장": "평화의광장", + "busStop.치과병원": "치과병원", + "busStop.정문": "정문", + "busStop.죽전역": "죽전역", + + // Directions + "direction.toDKU": "단국대학교 방향", + "direction.toJukjeon": "죽전역 방향", + + // SettingsPanel + "settings.title": "설정", + "settings.language": "언어", + "settings.korean": "한국어", + "settings.english": "English", + "settings.contact": "문의하기", + "settings.userGuide": "사용 가이드", + + // Common + "common.loading": "로딩 중...", + "common.error": "오류가 발생했습니다", + "common.noData": "데이터가 없습니다", + }, + en: { + // BusStops + "busStops.selectStop": "Select Bus Stop", + "busStops.selectBus": "Select Bus", + + // Bus Stop Names + "busStop.평화의광장": "Dankook Univ. Peace Square", + "busStop.치과병원": "Dankook Univ. Dental Hospital", + "busStop.정문": "Dankook Univ. Main Gate", + "busStop.죽전역": "Jukjeon Stn/ Shinsegae S. City", + + // Directions + "direction.toDKU": "→ Dankook Univ.", + "direction.toJukjeon": "→ Jukjeon Stn.", + + // SettingsPanel + "settings.title": "Settings", + "settings.language": "Language", + "settings.korean": "한국어", + "settings.english": "English", + "settings.contact": "Contact Us", + "settings.userGuide": "User Guide", + + // Common + "common.loading": "Loading...", + "common.error": "An error occurred", + "common.noData": "No data available", + }, +}; + +interface LanguageContextType { + language: Language; + setLanguage: (lang: Language) => void; + t: (key: string) => string; +} + +const LanguageContext = createContext( + undefined +); + +interface LanguageProviderProps { + children: ReactNode; + language: Language; + setLanguage: (lang: Language) => void; +} + +export function LanguageProvider({ + children, + language, + setLanguage, +}: LanguageProviderProps) { + const t = (key: string): string => { + return translations[language][key] || key; + }; + + return ( + + {children} + + ); +} + +export function useTranslation() { + const context = useContext(LanguageContext); + if (!context) { + throw new Error("useTranslation must be used within LanguageProvider"); + } + return context; +} diff --git a/src/utils/mapOverlays.ts b/src/utils/mapOverlays.ts index c582f31..85a8a48 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 busIconSvg from "../assets/busIcon.svg"; export interface OverlayHandle { setMap: (map: unknown) => void; From 2c71e071b16b8e370b8ca8a49373df3837ff73a4 Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Wed, 12 Nov 2025 13:50:25 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=ED=8F=B4=EB=A7=81=20=EC=A3=BC?= =?UTF-8?q?=EA=B8=B0=205000=EC=9C=BC=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 a5c2928..9154eb3 100644 --- a/src/api/bus.ts +++ b/src/api/bus.ts @@ -10,7 +10,7 @@ export const useBusLocations = () => { const data = await apiGet(API_ENDPOINTS.BUS.LOCATION); return Array.isArray(data) ? data : []; }, - refetchInterval: 20000, + refetchInterval: 5000, refetchIntervalInBackground: true, }); }; From 0878483540dab1fafc6157b616a9a90c424fe63d Mon Sep 17 00:00:00 2001 From: Kwon DaeGeun Date: Wed, 12 Nov 2025 14:07:19 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EC=A0=95=EB=A5=98=EC=9E=A5=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=20=EC=84=A4=EC=A0=95=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 17 ++++++++++------- src/components/Bubble.tsx | 8 ++++---- src/components/SettingsPanel.tsx | 8 +++++--- src/contexts/LanguageContext.tsx | 7 ++++++- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 42f8809..9d1305e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,14 +32,17 @@ const ReactQueryDevtools: ComponentType = import.meta.env.DEV ) : ((() => null) as ComponentType); +type Language = "ko" | "en"; + function App() { const mapId = useId(); const langId = useId(); - const [language, setLanguage] = useState(() => { + const [language, setLanguage] = useState(() => { try { - return typeof window !== "undefined" && window.localStorage - ? (localStorage.getItem("wtb:lang") ?? "ko") - : "ko"; + const stored = typeof window !== "undefined" && window.localStorage + ? localStorage.getItem("wtb:lang") + : null; + return stored === "en" ? "en" : "ko"; } catch { return "ko"; } @@ -65,7 +68,7 @@ function App() { return ( void; + language: Language; + setLanguage: (lang: Language) => void; showSettings: boolean; toggleSettings: () => void; bubbleStop: { lat: number; lng: number; name: string } | undefined; diff --git a/src/components/Bubble.tsx b/src/components/Bubble.tsx index 6b90c99..ab68659 100644 --- a/src/components/Bubble.tsx +++ b/src/components/Bubble.tsx @@ -17,7 +17,7 @@ type Props = { }; export default function Bubble({ stop, onClose }: Props) { - const { t } = useTranslation(); + const { t, formatTime } = useTranslation(); useEffect(() => { if (typeof window.kakao === "undefined" || !window.map) return; @@ -148,7 +148,7 @@ export default function Bubble({ stop, onClose }: Props) { 24 - | 5min + | {formatTime(5)} @@ -176,7 +176,7 @@ export default function Bubble({ stop, onClose }: Props) { 720-3 - | 15min + | {formatTime(15)} @@ -237,7 +237,7 @@ export default function Bubble({ stop, onClose }: Props) { window.__currentBubbleOverlay = undefined; window.__currentBubbleStopName = undefined; }; - }, [stop, onClose, t]); + }, [stop, onClose, t, formatTime]); return null; } diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 89f2227..da731c7 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -1,10 +1,12 @@ import { X } from "lucide-react"; import { useTranslation } from "../contexts/LanguageContext"; +type Language = "ko" | "en"; + interface SettingsPanelProps { langId: string; - language: string; - setLanguage: (lang: string) => void; + language: Language; + setLanguage: (lang: Language) => void; onClose: () => void; } @@ -83,7 +85,7 @@ const SettingsPanel: React.FC = ({