Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
30 changes: 18 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -63,19 +64,24 @@ function App() {

return (
<QueryClientProvider client={queryClient}>
<AppContent
mapId={mapId}
langId={langId}
language={language}
<LanguageProvider
language={language as "ko" | "en"}
setLanguage={setLanguage}
showSettings={showSettings}
toggleSettings={toggleSettings}
bubbleStop={bubbleStop}
setBubbleStop={setBubbleStop}
/>
{import.meta.env.DEV && (
<ReactQueryDevtools initialIsOpen={false} />
)}
>
<AppContent
mapId={mapId}
langId={langId}
language={language}
setLanguage={setLanguage}
showSettings={showSettings}
toggleSettings={toggleSettings}
bubbleStop={bubbleStop}
setBubbleStop={setBubbleStop}
/>
{import.meta.env.DEV && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</LanguageProvider>
</QueryClientProvider>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const useBusLocations = () => {
const data = await apiGet<Bus[]>(API_ENDPOINTS.BUS.LOCATION);
return Array.isArray(data) ? data : [];
},
refetchInterval: 20000,
refetchInterval: 5000,
refetchIntervalInBackground: true,
});
};
28 changes: 20 additions & 8 deletions src/components/Bubble.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
죽전역: "죽전역(단국대학교 방향)",
Expand All @@ -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;

Expand Down Expand Up @@ -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(
Expand All @@ -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",
}}
Expand Down Expand Up @@ -136,7 +148,7 @@ export default function Bubble({ stop, onClose }: Props) {
24
</span>
<span style={{ marginLeft: 8 }}>
| 5분 남음
| 5min
</span>
</span>
</div>
Expand Down Expand Up @@ -164,7 +176,7 @@ export default function Bubble({ stop, onClose }: Props) {
720-3
</span>
<span style={{ marginLeft: 8 }}>
| 15분 남음
| 15min
</span>
</span>
</div>
Expand Down Expand Up @@ -225,7 +237,7 @@ export default function Bubble({ stop, onClose }: Props) {
window.__currentBubbleOverlay = undefined;
window.__currentBubbleStopName = undefined;
};
}, [stop, onClose]);
}, [stop, onClose, t]);

return null;
}
16 changes: 9 additions & 7 deletions src/components/BusStops.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -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();
Expand Down Expand Up @@ -101,7 +103,7 @@ export default function BusStops({
openStops ? "hidden" : "inline"
}`}
>
버스 정류장 선택하기
{t("busStops.selectStop")}
</span>
</div>
<ChevronDown
Expand Down Expand Up @@ -133,7 +135,7 @@ export default function BusStops({
}
className="hover:-translate-y-0.5 min-h-[56px] cursor-pointer rounded-xl border-0 bg-blue-600 px-4 py-4 font-bold text-base text-white transition-all duration-200 hover:bg-blue-700 hover:shadow-lg active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:translate-y-0 disabled:hover:shadow-none"
>
{stop.name}
{t(`busStop.${stop.name}`)}
</button>
))}
</section>
Expand All @@ -157,9 +159,9 @@ export default function BusStops({
>
<div className="inline-flex items-center gap-3">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center transition-transform duration-200 group-hover:scale-110">
<img
src={busIconSvg}
alt="버스"
<img
src={busIconSvg}
alt="버스"
style={{ width: "20px", height: "38px" }}
/>
</div>
Expand All @@ -168,7 +170,7 @@ export default function BusStops({
openNumbers ? "hidden" : "inline"
}`}
>
버스 선택하기
{t("busStops.selectBus")}
</span>
</div>
<ChevronDown
Expand Down
4 changes: 2 additions & 2 deletions src/components/SettingsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const SettingsButton = ({
style={{
position: "fixed",
top: 40,
right: 12,
zIndex: 10000,
right: 16,
zIndex: 1000,
background: "white",
border: "1px solid #e5e7eb",
borderRadius: 8,
Expand Down
18 changes: 10 additions & 8 deletions src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { X } from "lucide-react";
import { useTranslation } from "../contexts/LanguageContext";

interface SettingsPanelProps {
langId: string;
Expand All @@ -13,6 +14,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
setLanguage,
onClose,
}) => {
const { t } = useTranslation();
return (
<div
role="dialog"
Expand Down Expand Up @@ -47,7 +49,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
marginBottom: 12,
}}
>
<div style={{ fontWeight: 600 }}>설정</div>
<div style={{ fontWeight: 600 }}>{t("settings.title")}</div>
<button
type="button"
onClick={onClose}
Expand Down Expand Up @@ -76,7 +78,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
htmlFor={langId}
style={{ fontSize: 14, color: "#111827" }}
>
언어
{t("settings.language")}
</label>
<select
id={langId}
Expand All @@ -88,8 +90,8 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
border: "1px solid #e5e7eb",
}}
>
<option value="ko">한국어</option>
<option value="en">English</option>
<option value="ko">{t("settings.korean")}</option>
<option value="en">{t("settings.english")}</option>
</select>
</div>

Expand All @@ -101,15 +103,15 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
style={{ display: "flex", flexDirection: "column", gap: 6 }}
>
<div style={{ fontSize: 14, color: "#111827" }}>
문의하기
{t("settings.contact")}
</div>
<a
href="https://forms.gle/your-google-form-id"
target="_blank"
rel="noopener noreferrer"
style={{ color: "#0ea5e9", textDecoration: "none" }}
>
문의하기(구글폼)
{t("settings.contact")}
</a>
</div>

Expand All @@ -121,7 +123,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
style={{ display: "flex", flexDirection: "column", gap: 6 }}
>
<div style={{ fontSize: 14, color: "#111827" }}>
사용 가이드
{t("settings.userGuide")}
</div>
<div style={{ fontSize: 15, color: "#374151" }}>
<a
Expand All @@ -130,7 +132,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
rel="noopener noreferrer"
style={{ color: "#0ea5e9", textDecoration: "none" }}
>
사용 가이드 보기 (Notion)
{t("settings.userGuide")}
</a>
</div>
</div>
Expand Down
107 changes: 107 additions & 0 deletions src/contexts/LanguageContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { createContext, type ReactNode, useContext } from "react";

type Language = "ko" | "en";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Language 타입을 export하여 공유하세요.

Language 타입이 여러 파일에서 중복 정의되고 있습니다. 이 타입을 export하여 다른 파일에서 import하도록 하면 타입 일관성을 유지할 수 있습니다.

다음과 같이 수정하세요:

-type Language = "ko" | "en";
+export type Language = "ko" | "en";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type Language = "ko" | "en";
export type Language = "ko" | "en";
🤖 Prompt for AI Agents
In src/contexts/LanguageContext.tsx around line 3, the Language type is
currently declared locally ("ko" | "en") and duplicated across the repo; export
this type from this file (e.g., export type Language = "ko" | "en") so other
modules can import it, and update/replace duplicate local declarations in other
files to import this exported Language type to ensure a single source of truth.


interface Translations {
ko: Record<string, string>;
en: Record<string, string>;
}

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<LanguageContextType | undefined>(
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 (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
}

export function useTranslation() {

Check warning on line 101 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");
}
return context;
}
2 changes: 1 addition & 1 deletion src/utils/mapOverlays.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading