feat(chart): fullscreen mode + CSV scope label clarification
- StockChart 툴바에 `⛶ 전체화면` 버튼 추가. Fullscreen API 우선,
미지원/거부 시 fixed inset 오버레이 폴백. F 키 토글, ESC 해제.
- 입력 필드(INPUT/TEXTAREA/SELECT/contentEditable) 포커스 시 단축키 무시.
- fullscreenchange 이벤트로 native 상태 동기화 → ESC 로 빠져나와도 UI 정합.
- CSV 버튼 title 을 '현재 로드된 기간(from ~ to)' 으로 명시.
리뷰어 지적 (1e0c917): "현재 보이는 봉" 이라는 표현은 timeScale
visible range 와 혼동 — 실제는 PeriodTabs 가 선택한 range 전체.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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<CrosshairInfo | null>(null);
|
||||
// 전체화면 모드 — Fullscreen API 가 동작하면 그게 우선, 안되면 fixed inset 오버레이 폴백.
|
||||
// 두 경로 모두 같은 state 로 통일 — UI(버튼 라벨/컨테이너 클래스) 분기 단순.
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(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 (
|
||||
<div className="w-full rounded-lg border border-zinc-800 bg-zinc-900/30 p-2">
|
||||
<div ref={wrapperRef} className={wrapperCls}>
|
||||
<div className="mb-1 flex flex-wrap items-center gap-1.5 px-1">
|
||||
{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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void toggleFullscreen()}
|
||||
className="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"
|
||||
title={fullscreen ? "전체화면 해제 (ESC / F)" : "전체화면 (F)"}
|
||||
aria-pressed={fullscreen}
|
||||
>
|
||||
{fullscreen ? "⛶ 해제" : "⛶ 전체화면"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative h-[460px] w-full">
|
||||
<div className={chartBoxCls}>
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
<CrosshairLabel info={hover ?? lastSummary} isHover={hover != null} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user