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
41 changes: 24 additions & 17 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 { type Language, LanguageProvider } from "./contexts/LanguageContext";
import { useBusSelection } from "./hooks/useBusSelection";
import { queryClient } from "./lib/query-client";

Expand All @@ -34,11 +35,12 @@ const ReactQueryDevtools: ComponentType<DevtoolsProps> = import.meta.env.DEV
function App() {
const mapId = useId();
const langId = useId();
const [language, setLanguage] = useState(() => {
const [language, setLanguage] = useState<Language>(() => {
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";
}
Expand All @@ -63,28 +65,33 @@ function App() {

return (
<QueryClientProvider client={queryClient}>
<AppContent
mapId={mapId}
langId={langId}
<LanguageProvider
language={language}
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>
);
}

interface AppContentProps {
mapId: string;
langId: string;
language: string;
setLanguage: (lang: string) => void;
language: Language;
setLanguage: (lang: Language) => void;
showSettings: boolean;
toggleSettings: () => void;
bubbleStop: { lat: number; lng: number; name: string } | undefined;
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, formatTime } = 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분 남음
| {formatTime(5)}
</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분 남음
| {formatTime(15)}
</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, formatTime]);

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
24 changes: 13 additions & 11 deletions src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { X } from "lucide-react";
import { type Language, useTranslation } from "../contexts/LanguageContext";

interface SettingsPanelProps {
langId: string;
language: string;
setLanguage: (lang: string) => void;
language: Language;
setLanguage: (lang: Language) => void;
onClose: () => void;
}

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,20 +78,20 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
htmlFor={langId}
style={{ fontSize: 14, color: "#111827" }}
>
언어
{t("settings.language")}
</label>
<select
id={langId}
value={language}
onChange={(e) => setLanguage(e.target.value)}
onChange={(e) => setLanguage(e.target.value as Language)}
style={{
padding: 8,
borderRadius: 6,
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
Loading
Loading