스킬 정리 및 리팩토링

This commit is contained in:
2026-02-26 09:05:17 +09:00
parent 4c52d6d82f
commit 406af7408a
71 changed files with 3776 additions and 3934 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1,126 @@
import type { DashboardChartTimeframe } from "@/features/trade/types/trade.types";
import type { ChartBar } from "./chart-utils";
export const UP_COLOR = "#ef4444";
export const MINUTE_SYNC_INTERVAL_MS = 30000;
export const REALTIME_STALE_THRESHOLD_MS = 12000;
export const CHART_MIN_HEIGHT = 220;
export interface ChartPalette {
backgroundColor: string;
downColor: string;
volumeDownColor: string;
textColor: string;
borderColor: string;
gridColor: string;
crosshairColor: string;
}
export const DEFAULT_CHART_PALETTE: ChartPalette = {
backgroundColor: "#ffffff",
downColor: "#2563eb",
volumeDownColor: "rgba(37, 99, 235, 0.45)",
textColor: "#6d28d9",
borderColor: "#e9d5ff",
gridColor: "#f3e8ff",
crosshairColor: "#c084fc",
};
export const MINUTE_TIMEFRAMES: Array<{
value: DashboardChartTimeframe;
label: string;
}> = [
{ value: "1m", label: "1분" },
{ value: "30m", label: "30분" },
{ value: "1h", label: "1시간" },
];
export const PERIOD_TIMEFRAMES: Array<{
value: DashboardChartTimeframe;
label: string;
}> = [
{ value: "1d", label: "일" },
{ value: "1w", label: "주" },
];
/**
* @description 브랜드 CSS 변수에서 차트 팔레트를 읽어옵니다.
* @see features/trade/components/chart/StockLineChart.tsx 차트 생성/테마 반영
*/
export 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,
),
};
}
/**
* @description 차트 데이터 배열이 동일한지 비교합니다.
* @see features/trade/components/chart/StockLineChart.tsx 분봉 동기화 시 불필요한 상태 업데이트 방지
*/
export 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;
}
function readCssVar(name: string, fallback: string) {
if (typeof window === "undefined") return fallback;
const value = window
.getComputedStyle(document.documentElement)
.getPropertyValue(name)
.trim();
return value || fallback;
}