스킬 정리 및 리팩토링
This commit is contained in:
@@ -33,98 +33,18 @@ import {
|
||||
toRealtimeTickBar,
|
||||
upsertRealtimeBar,
|
||||
} from "./chart-utils";
|
||||
|
||||
const UP_COLOR = "#ef4444";
|
||||
const MINUTE_SYNC_INTERVAL_MS = 30000;
|
||||
const REALTIME_STALE_THRESHOLD_MS = 12000;
|
||||
const CHART_MIN_HEIGHT = 220;
|
||||
|
||||
interface ChartPalette {
|
||||
backgroundColor: string;
|
||||
downColor: string;
|
||||
volumeDownColor: string;
|
||||
textColor: string;
|
||||
borderColor: string;
|
||||
gridColor: string;
|
||||
crosshairColor: string;
|
||||
}
|
||||
|
||||
const DEFAULT_CHART_PALETTE: ChartPalette = {
|
||||
backgroundColor: "#ffffff",
|
||||
downColor: "#2563eb",
|
||||
volumeDownColor: "rgba(37, 99, 235, 0.45)",
|
||||
textColor: "#6d28d9",
|
||||
borderColor: "#e9d5ff",
|
||||
gridColor: "#f3e8ff",
|
||||
crosshairColor: "#c084fc",
|
||||
};
|
||||
|
||||
function readCssVar(name: string, fallback: string) {
|
||||
if (typeof window === "undefined") return fallback;
|
||||
const value = window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(name)
|
||||
.trim();
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
function getChartPaletteFromCssVars(themeMode: "light" | "dark"): ChartPalette {
|
||||
const isDark = themeMode === "dark";
|
||||
const backgroundVar = isDark
|
||||
? "--brand-chart-background-dark"
|
||||
: "--brand-chart-background-light";
|
||||
const textVar = isDark
|
||||
? "--brand-chart-text-dark"
|
||||
: "--brand-chart-text-light";
|
||||
const borderVar = isDark
|
||||
? "--brand-chart-border-dark"
|
||||
: "--brand-chart-border-light";
|
||||
const gridVar = isDark
|
||||
? "--brand-chart-grid-dark"
|
||||
: "--brand-chart-grid-light";
|
||||
const crosshairVar = isDark
|
||||
? "--brand-chart-crosshair-dark"
|
||||
: "--brand-chart-crosshair-light";
|
||||
|
||||
return {
|
||||
backgroundColor: readCssVar(
|
||||
backgroundVar,
|
||||
DEFAULT_CHART_PALETTE.backgroundColor,
|
||||
),
|
||||
downColor: readCssVar(
|
||||
"--brand-chart-down",
|
||||
DEFAULT_CHART_PALETTE.downColor,
|
||||
),
|
||||
volumeDownColor: readCssVar(
|
||||
"--brand-chart-volume-down",
|
||||
DEFAULT_CHART_PALETTE.volumeDownColor,
|
||||
),
|
||||
textColor: readCssVar(textVar, DEFAULT_CHART_PALETTE.textColor),
|
||||
borderColor: readCssVar(borderVar, DEFAULT_CHART_PALETTE.borderColor),
|
||||
gridColor: readCssVar(gridVar, DEFAULT_CHART_PALETTE.gridColor),
|
||||
crosshairColor: readCssVar(
|
||||
crosshairVar,
|
||||
DEFAULT_CHART_PALETTE.crosshairColor,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const MINUTE_TIMEFRAMES: Array<{
|
||||
value: DashboardChartTimeframe;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "1m", label: "1분" },
|
||||
{ value: "30m", label: "30분" },
|
||||
{ value: "1h", label: "1시간" },
|
||||
];
|
||||
|
||||
const PERIOD_TIMEFRAMES: Array<{
|
||||
value: DashboardChartTimeframe;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "1d", label: "일" },
|
||||
{ value: "1w", label: "주" },
|
||||
];
|
||||
import {
|
||||
areBarsEqual,
|
||||
type ChartPalette,
|
||||
CHART_MIN_HEIGHT,
|
||||
DEFAULT_CHART_PALETTE,
|
||||
getChartPaletteFromCssVars,
|
||||
MINUTE_SYNC_INTERVAL_MS,
|
||||
MINUTE_TIMEFRAMES,
|
||||
PERIOD_TIMEFRAMES,
|
||||
REALTIME_STALE_THRESHOLD_MS,
|
||||
UP_COLOR,
|
||||
} from "./stock-line-chart-meta";
|
||||
|
||||
interface StockLineChartProps {
|
||||
symbol?: string;
|
||||
@@ -161,6 +81,7 @@ export function StockLineChart({
|
||||
const lastRealtimeAppliedAtRef = useRef(0);
|
||||
const chartPaletteRef = useRef<ChartPalette>(DEFAULT_CHART_PALETTE);
|
||||
const renderableBarsRef = useRef<ChartBar[]>([]);
|
||||
const initialThemeModeRef = useRef<"light" | "dark">("light");
|
||||
|
||||
const activeThemeMode: "light" | "dark" =
|
||||
resolvedTheme === "dark"
|
||||
@@ -172,6 +93,10 @@ export function StockLineChart({
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
useEffect(() => {
|
||||
initialThemeModeRef.current = activeThemeMode;
|
||||
}, [activeThemeMode]);
|
||||
|
||||
// 복수 이벤트에서 중복 로드를 막기 위한 ref 상태
|
||||
const loadingMoreRef = useRef(false);
|
||||
const loadMoreHandlerRef = useRef<() => Promise<void>>(async () => {});
|
||||
@@ -244,7 +169,9 @@ export function StockLineChart({
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to render chart series data:", error);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.error("Failed to render chart series data:", error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -296,7 +223,7 @@ export function StockLineChart({
|
||||
if (!container || chartRef.current) return;
|
||||
|
||||
// 브랜드 색상은 globals.css의 CSS 변수에서 읽어 차트(canvas)에도 동일하게 적용합니다.
|
||||
const palette = getChartPaletteFromCssVars(activeThemeMode);
|
||||
const palette = getChartPaletteFromCssVars(initialThemeModeRef.current);
|
||||
chartPaletteRef.current = palette;
|
||||
|
||||
const chart = createChart(container, {
|
||||
@@ -411,7 +338,7 @@ export function StockLineChart({
|
||||
volumeSeriesRef.current = null;
|
||||
setIsChartReady(false);
|
||||
};
|
||||
}, [activeThemeMode]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const chart = chartRef.current;
|
||||
@@ -460,6 +387,7 @@ export function StockLineChart({
|
||||
|
||||
initialLoadCompleteRef.current = false;
|
||||
let disposed = false;
|
||||
let initialLoadTimer: number | null = null;
|
||||
|
||||
const load = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -508,7 +436,7 @@ export function StockLineChart({
|
||||
setBars(mergedBars);
|
||||
setNextCursor(resolvedNextCursor);
|
||||
|
||||
window.setTimeout(() => {
|
||||
initialLoadTimer = window.setTimeout(() => {
|
||||
if (!disposed) initialLoadCompleteRef.current = true;
|
||||
}, 350);
|
||||
} catch (error) {
|
||||
@@ -531,6 +459,9 @@ export function StockLineChart({
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (initialLoadTimer !== null) {
|
||||
window.clearTimeout(initialLoadTimer);
|
||||
}
|
||||
};
|
||||
}, [credentials, symbol, timeframe]);
|
||||
|
||||
@@ -550,7 +481,7 @@ export function StockLineChart({
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!latestTick) return;
|
||||
if (bars.length === 0) return;
|
||||
if (renderableBarsRef.current.length === 0) return;
|
||||
|
||||
const dedupeKey = `${timeframe}:${latestTick.tickTime}:${latestTick.price}:${latestTick.tradeVolume}`;
|
||||
if (lastRealtimeKeyRef.current === dedupeKey) return;
|
||||
@@ -561,7 +492,7 @@ export function StockLineChart({
|
||||
lastRealtimeKeyRef.current = dedupeKey;
|
||||
lastRealtimeAppliedAtRef.current = Date.now();
|
||||
setBars((prev) => upsertRealtimeBar(prev, realtimeBar));
|
||||
}, [bars.length, latestTick, timeframe]);
|
||||
}, [latestTick, timeframe]);
|
||||
|
||||
/**
|
||||
* @description 분봉(1m/30m/1h)에서는 WS 공백 상황을 대비해 최신 페이지를 주기적으로 동기화합니다.
|
||||
@@ -715,25 +646,3 @@ export function StockLineChart({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function areBarsEqual(left: ChartBar[], right: ChartBar[]) {
|
||||
if (left.length !== right.length) return false;
|
||||
|
||||
for (let index = 0; index < left.length; index += 1) {
|
||||
const lhs = left[index];
|
||||
const rhs = right[index];
|
||||
if (!lhs || !rhs) return false;
|
||||
if (
|
||||
lhs.time !== rhs.time ||
|
||||
lhs.open !== rhs.open ||
|
||||
lhs.high !== rhs.high ||
|
||||
lhs.low !== rhs.low ||
|
||||
lhs.close !== rhs.close ||
|
||||
lhs.volume !== rhs.volume
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user