위젯 기능 추가
This commit is contained in:
@@ -60,6 +60,7 @@ import {
|
||||
type ChartPaneSeparatorOptions,
|
||||
} from '../types/chartPaneSeparator';
|
||||
import { migrateLocalStorageToUiPreferences } from '../utils/uiPreferencesMigrate';
|
||||
import { reconcilePendingUiPreferencesOnLoad } from '../utils/uiPreferencesDb';
|
||||
import { loadLocalTheme } from '../utils/localThemeStorage';
|
||||
|
||||
// 전역 캐시 — 여러 컴포넌트에서 공유하여 중복 요청 방지
|
||||
@@ -79,6 +80,11 @@ export function getAppSettingsCache(): AppSettingsDto {
|
||||
return _cache ?? {};
|
||||
}
|
||||
|
||||
/** DB에서 앱 설정 로드가 완료되었는지 (null 캐시 = 미로드) */
|
||||
export function isAppSettingsCacheLoaded(): boolean {
|
||||
return _cache !== null;
|
||||
}
|
||||
|
||||
/** 낙관적 캐시 패치 — _cache 가 null 일 때도 다음 로드 전까지 임시 보관 */
|
||||
export function patchAppSettingsCache(patch: Partial<AppSettingsDto>): void {
|
||||
if (_cache !== null) {
|
||||
@@ -102,6 +108,7 @@ function ensureLoaded(): Promise<AppSettingsDto> {
|
||||
return _cache ?? {};
|
||||
}
|
||||
_cache = data ?? {};
|
||||
reconcilePendingUiPreferencesOnLoad();
|
||||
const migrated = migrateLocalStorageToUiPreferences(_cache);
|
||||
if (migrated?.changed) {
|
||||
try {
|
||||
|
||||
@@ -116,6 +116,7 @@ export function useDraggablePanel(options: UseDraggablePanelOptions = {}) {
|
||||
return {
|
||||
panelRef: internalRef,
|
||||
pos,
|
||||
setPos,
|
||||
dragging,
|
||||
/** @deprecated onHeaderPointerDown 사용 */
|
||||
onHeaderMouseDown: onHeaderPointerDown,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback, useEffect, useRef, type RefObject } from 'react';
|
||||
import {
|
||||
bindWindowDrag,
|
||||
isPrimaryPointerButton,
|
||||
} from '../utils/pointerDrag';
|
||||
|
||||
export interface Size2d {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface Position2d {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface FloatingWidgetResizeResult {
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
}
|
||||
|
||||
export type FloatingWidgetResizeCorner = 'se' | 'sw';
|
||||
|
||||
function clampSize(
|
||||
width: number,
|
||||
height: number,
|
||||
minSize: Size2d,
|
||||
maxW: number,
|
||||
maxH: number,
|
||||
): Size2d {
|
||||
return {
|
||||
width: Math.round(Math.max(minSize.width, Math.min(maxW, width))),
|
||||
height: Math.round(Math.max(minSize.height, Math.min(maxH, height))),
|
||||
};
|
||||
}
|
||||
|
||||
export function useFloatingWidgetResize(
|
||||
panelRef: RefObject<HTMLElement | null>,
|
||||
size: Size2d,
|
||||
minSize: Size2d,
|
||||
onResize: (result: FloatingWidgetResizeResult) => void,
|
||||
) {
|
||||
const unbindDragRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const endResize = useCallback(() => {
|
||||
unbindDragRef.current?.();
|
||||
unbindDragRef.current = null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
unbindDragRef.current?.();
|
||||
}, []);
|
||||
|
||||
const startResize = useCallback((
|
||||
e: React.PointerEvent,
|
||||
corner: FloatingWidgetResizeCorner,
|
||||
) => {
|
||||
if (!isPrimaryPointerButton(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const handle = e.currentTarget as HTMLElement;
|
||||
try {
|
||||
handle.setPointerCapture(e.pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const el = panelRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const startW = size.width;
|
||||
const startH = size.height;
|
||||
const startRight = rect.right;
|
||||
const maxH = window.innerHeight - rect.top - 8;
|
||||
|
||||
unbindDragRef.current?.();
|
||||
unbindDragRef.current = bindWindowDrag(
|
||||
(xy) => {
|
||||
if (corner === 'se') {
|
||||
const maxW = window.innerWidth - rect.left - 8;
|
||||
const next = clampSize(
|
||||
startW + xy.x - startX,
|
||||
startH + xy.y - startY,
|
||||
minSize,
|
||||
maxW,
|
||||
maxH,
|
||||
);
|
||||
onResize(next);
|
||||
return;
|
||||
}
|
||||
|
||||
/* sw — 우측 가장자리 고정, 좌측·하단 조절 */
|
||||
let width = startRight - xy.x;
|
||||
let x = xy.x;
|
||||
if (x < 8) {
|
||||
x = 8;
|
||||
width = startRight - 8;
|
||||
}
|
||||
const maxW = startRight - 8;
|
||||
const next = clampSize(width, startH + xy.y - startY, minSize, maxW, maxH);
|
||||
onResize({ ...next, x: Math.round(startRight - next.width) });
|
||||
},
|
||||
endResize,
|
||||
{ preventTouchScroll: true },
|
||||
);
|
||||
}, [panelRef, size.width, size.height, minSize, onResize, endResize]);
|
||||
|
||||
const onResizeSePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => startResize(e, 'se'),
|
||||
[startResize],
|
||||
);
|
||||
|
||||
const onResizeSwPointerDown = useCallback(
|
||||
(e: React.PointerEvent) => startResize(e, 'sw'),
|
||||
[startResize],
|
||||
);
|
||||
|
||||
return { onResizeSePointerDown, onResizeSwPointerDown };
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export interface RecentTrade {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const MAX_TRADES = 12;
|
||||
const MAX_TRADES_DEFAULT = 12;
|
||||
|
||||
interface UpbitTradeRaw {
|
||||
market: string;
|
||||
@@ -30,9 +30,11 @@ interface UpbitTradeWs {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function useUpbitRecentTrades(market: string, enabled = true) {
|
||||
export function useUpbitRecentTrades(market: string, enabled = true, maxTrades = MAX_TRADES_DEFAULT) {
|
||||
const [trades, setTrades] = useState<RecentTrade[]>([]);
|
||||
const [strength, setStrength] = useState<number | null>(null);
|
||||
const maxRef = useRef(maxTrades);
|
||||
maxRef.current = maxTrades;
|
||||
|
||||
const mountedRef = useRef(true);
|
||||
const marketRef = useRef(market);
|
||||
@@ -40,7 +42,8 @@ export function useUpbitRecentTrades(market: string, enabled = true) {
|
||||
|
||||
const pushTrade = useCallback((t: RecentTrade) => {
|
||||
setTrades(prev => {
|
||||
const next = [t, ...prev].slice(0, MAX_TRADES);
|
||||
const cap = maxRef.current;
|
||||
const next = [t, ...prev].slice(0, cap);
|
||||
const buyVol = next.filter(x => x.side === 'bid').reduce((s, x) => s + x.volume, 0);
|
||||
const sellVol = next.filter(x => x.side === 'ask').reduce((s, x) => s + x.volume, 0);
|
||||
if (sellVol > 0) setStrength((buyVol / sellVol) * 100);
|
||||
@@ -51,7 +54,7 @@ export function useUpbitRecentTrades(market: string, enabled = true) {
|
||||
|
||||
const fetchSnapshot = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/upbit-api/v1/trades/ticks?market=${marketRef.current}&count=${MAX_TRADES}`);
|
||||
const res = await fetch(`/upbit-api/v1/trades/ticks?market=${marketRef.current}&count=${maxRef.current}`);
|
||||
if (!res.ok) return;
|
||||
const data: UpbitTradeRaw[] = await res.json();
|
||||
const list: RecentTrade[] = data.map(d => ({
|
||||
|
||||
Reference in New Issue
Block a user