diff --git a/web/app/[code]/page.tsx b/web/app/[code]/page.tsx index 3a8290d..bd3a95b 100644 --- a/web/app/[code]/page.tsx +++ b/web/app/[code]/page.tsx @@ -12,7 +12,7 @@ import { PeerComparePanel } from "../../components/PeerComparePanel"; import { NewsList } from "../../components/NewsList"; import { OrderbookPanel } from "../../components/OrderbookPanel"; import { PeriodReturns } from "../../components/PeriodReturns"; -import { PeriodTabs, periodSpec, type Period } from "../../components/PeriodTabs"; +import { PeriodTabs, periodNeighbor, periodSpec, type Period } from "../../components/PeriodTabs"; import { PredictionPanel } from "../../components/PredictionPanel"; import { PriceHero } from "../../components/PriceHero"; import { PriceTargets } from "../../components/PriceTargets"; @@ -25,6 +25,7 @@ import { TradingValuePanel } from "../../components/TradingValuePanel"; import { api, type ChartPayload, type LatestPredictionResponse } from "../../lib/api"; import { recent } from "../../lib/recent"; import { readTargets, type Targets } from "../../lib/targets"; +import { watchlist } from "../../lib/watchlist"; import { markRecorded, wasRecordedToday } from "../../lib/views"; export default function CodePage({ params }: { params: { code: string } }) { @@ -78,6 +79,38 @@ export default function CodePage({ params }: { params: { code: string } }) { setTargets(readTargets(code)); }, [code]); + // 종목 페이지 키보드 단축키: + // ←/→ : 기간 탭 이전/다음 (양끝 clamp — wrap 안 함, 사용자가 끝 인지) + // S : 관심종목 토글 + // 입력 필드(검색창, 메모 등) 포커스 시 무시 — 텍스트 편집 방해 금지. + // modifier(Ctrl/Cmd/Alt) 가 눌리면 브라우저/OS 단축키 양보. + useEffect(() => { + if (typeof window === "undefined") return; + const isEditableTarget = (t: EventTarget | null): boolean => { + if (!(t instanceof HTMLElement)) return false; + const tag = t.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true; + if (t.isContentEditable) return true; + return false; + }; + const onKey = (e: KeyboardEvent) => { + if (e.ctrlKey || e.metaKey || e.altKey) return; + if (isEditableTarget(e.target)) return; + if (e.key === "ArrowLeft") { + e.preventDefault(); + setPeriod((cur) => periodNeighbor(cur, -1)); + } else if (e.key === "ArrowRight") { + e.preventDefault(); + setPeriod((cur) => periodNeighbor(cur, 1)); + } else if (e.key === "s" || e.key === "S") { + e.preventDefault(); + watchlist.toggle(code); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [code]); + useEffect(() => { let alive = true; setErr(null); diff --git a/web/components/PeriodTabs.tsx b/web/components/PeriodTabs.tsx index 276a40d..3cd5489 100644 --- a/web/components/PeriodTabs.tsx +++ b/web/components/PeriodTabs.tsx @@ -23,6 +23,15 @@ export function periodSpec(id: Period): PeriodSpec { return PERIODS.find((p) => p.id === id) ?? PERIODS[2]; } +// ←/→ 키 네비게이션용 — 양끝 clamp (wrap-around 안 함, 사용자가 끝에 갇혔는지 인지 가능). +export function periodNeighbor(id: Period, dir: -1 | 1): Period { + const idx = PERIODS.findIndex((p) => p.id === id); + if (idx < 0) return id; + const next = idx + dir; + if (next < 0 || next >= PERIODS.length) return id; + return PERIODS[next].id; +} + type Props = { value: Period; onChange: (id: Period, interval: ChartInterval, days: number) => void; diff --git a/web/components/ShortcutsHelp.tsx b/web/components/ShortcutsHelp.tsx index 7f19381..4cccfed 100644 --- a/web/components/ShortcutsHelp.tsx +++ b/web/components/ShortcutsHelp.tsx @@ -22,6 +22,8 @@ const SHORTCUTS: Shortcut[] = [ { scope: "전역", keys: ["Esc"], label: "팝오버 / 오버레이 닫기" }, { scope: "전역", keys: ["?"], label: "이 도움말 열기 / 닫기" }, { scope: "종목 페이지", keys: ["F"], label: "차트 전체화면 토글" }, + { scope: "종목 페이지", keys: ["←", "→"], label: "기간 탭 이전 / 다음" }, + { scope: "종목 페이지", keys: ["S"], label: "관심종목 추가 / 제거" }, { scope: "검색 팝오버", keys: ["↑", "↓"], label: "결과 이동" }, { scope: "검색 팝오버", keys: ["Enter"], label: "선택한 종목으로 이동" }, ];