diff --git a/web/components/StockChart.tsx b/web/components/StockChart.tsx index cdd37ab..74acd9b 100644 --- a/web/components/StockChart.tsx +++ b/web/components/StockChart.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createChart, type CandlestickData, @@ -236,6 +236,10 @@ export function StockChart({ chart, prediction, targets }: Props) { const [macdOn, setMacdOn] = useState(false); // crosshair 위에 떠 있는 한국어 라벨 — null 이면 hover 안 함 (기본 마지막 봉 표시). const [hover, setHover] = useState(null); + // 전체화면 모드 — Fullscreen API 가 동작하면 그게 우선, 안되면 fixed inset 오버레이 폴백. + // 두 경로 모두 같은 state 로 통일 — UI(버튼 라벨/컨테이너 클래스) 분기 단순. + const wrapperRef = useRef(null); + const [fullscreen, setFullscreen] = useState(false); const isIntraday = chart.interval === "10m"; const showSma = !isIntraday; // 10분봉에선 의미 적음 — UI 도 숨김. @@ -745,6 +749,61 @@ export function StockChart({ chart, prediction, targets }: Props) { }; }, [showRsi, showMacd, chart]); + // 전체화면 토글 — Fullscreen API 가능하면 native, 아니면 오버레이 폴백. + // iOS Safari 등 element.requestFullscreen 미지원 환경은 자연히 폴백 경로로. + const toggleFullscreen = useCallback(async () => { + const el = wrapperRef.current; + if (!el || typeof document === "undefined") return; + const docAny = document as Document & { fullscreenEnabled?: boolean }; + if (docAny.fullscreenEnabled && typeof el.requestFullscreen === "function") { + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await el.requestFullscreen(); + } + return; + } catch { + // 권한 거부/실패 → 오버레이 폴백. + } + } + setFullscreen((v) => !v); + }, []); + + // Native fullscreen 상태 → state 동기화. ESC 로 빠져나오면 여기로 들어옴. + useEffect(() => { + if (typeof document === "undefined") return; + const onChange = () => setFullscreen(!!document.fullscreenElement); + document.addEventListener("fullscreenchange", onChange); + return () => document.removeEventListener("fullscreenchange", onChange); + }, []); + + // F 키 = 토글, ESC = 폴백 오버레이 해제 (native 는 브라우저가 알아서). + // 입력 필드에 포커스가 있으면 무시 — 메모 작성 등 방해 금지. + useEffect(() => { + if (typeof window === "undefined") return; + const onKey = (e: KeyboardEvent) => { + const t = e.target as HTMLElement | null; + if ( + t && + (t.tagName === "INPUT" || + t.tagName === "TEXTAREA" || + t.tagName === "SELECT" || + t.isContentEditable) + ) { + return; + } + if (e.key === "f" || e.key === "F") { + e.preventDefault(); + void toggleFullscreen(); + } else if (e.key === "Escape" && fullscreen && !document.fullscreenElement) { + setFullscreen(false); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [toggleFullscreen, fullscreen]); + const toggle = (w: SmaWindow) => { setActive((cur) => { const next = new Set(cur); @@ -754,8 +813,18 @@ export function StockChart({ chart, prediction, targets }: Props) { }); }; + // 전체화면(native or 폴백) 일 때는 viewport 를 꽉 채워서 차트 면적 최대화. + // 일반 모드는 기존 카드 스타일 유지. + const wrapperCls = fullscreen + ? "fixed inset-0 z-50 flex w-full flex-col overflow-auto bg-zinc-950 p-3" + : "w-full rounded-lg border border-zinc-800 bg-zinc-900/30 p-2"; + // 차트 본체 높이 — 전체화면이면 가용 공간을 차지하도록 flex-1. + const chartBoxCls = fullscreen + ? "relative min-h-[300px] flex-1 w-full" + : "relative h-[460px] w-full"; + return ( -
+
{showSma && ( <> @@ -832,12 +901,21 @@ export function StockChart({ chart, prediction, targets }: Props) { }} disabled={!chart.ohlcv.length} className="ml-auto flex items-center gap-1 rounded-full border border-zinc-800 px-2 py-0.5 text-[10px] text-zinc-400 transition hover:border-zinc-600 hover:text-zinc-200 disabled:cursor-not-allowed disabled:opacity-40" - title="현재 봉 데이터를 CSV 로 내보내기 (UTF-8 BOM, Excel 호환)" + title={`현재 로드된 기간(${chart.range.from} ~ ${chart.range.to}) 의 봉 데이터를 CSV 로 내보내기 (UTF-8 BOM, Excel 호환)`} > ⬇ CSV +
-
+