위젯 기능 추가
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/** 플로팅(팝업) 위젯 창 — 메뉴바에서 다중 실행 */
|
||||
|
||||
import { newWidgetId, newWidgetSlot, type WidgetSlot } from './widgetDashboard';
|
||||
|
||||
export interface FloatingWidgetLayoutPreset {
|
||||
id: string;
|
||||
rows: number;
|
||||
cols: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const FLOATING_WIDGET_LAYOUTS: FloatingWidgetLayoutPreset[] = [
|
||||
{ id: '1x1', rows: 1, cols: 1, label: '1×1' },
|
||||
{ id: '1x2', rows: 1, cols: 2, label: '1×2' },
|
||||
{ id: '2x1', rows: 2, cols: 1, label: '2×1' },
|
||||
{ id: '2x2', rows: 2, cols: 2, label: '2×2' },
|
||||
{ id: '1x3', rows: 1, cols: 3, label: '1×3' },
|
||||
{ id: '3x1', rows: 3, cols: 1, label: '3×1' },
|
||||
{ id: '2x3', rows: 2, cols: 3, label: '2×3' },
|
||||
{ id: '3x2', rows: 3, cols: 2, label: '3×2' },
|
||||
{ id: '3x3', rows: 3, cols: 3, label: '3×3' },
|
||||
];
|
||||
|
||||
export interface FloatingWidgetInstance {
|
||||
id: string;
|
||||
rows: number;
|
||||
cols: number;
|
||||
slots: WidgetSlot[];
|
||||
position: { x: number; y: number };
|
||||
width: number;
|
||||
height: number;
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
export function defaultSizeForGrid(rows: number, cols: number): { width: number; height: number } {
|
||||
const cellW = 280;
|
||||
const cellH = 220;
|
||||
const chromeW = 16;
|
||||
const chromeH = 44;
|
||||
return {
|
||||
width: Math.min(cols * cellW + chromeW, window.innerWidth - 40),
|
||||
height: Math.min(rows * cellH + chromeH, window.innerHeight - 80),
|
||||
};
|
||||
}
|
||||
|
||||
/** 그리드 칸당 최소 크기 — 리사이즈 하한 */
|
||||
export function minSizeForGrid(rows: number, cols: number): { width: number; height: number } {
|
||||
const cellMinW = 140;
|
||||
const cellMinH = 100;
|
||||
const chromeW = 16;
|
||||
const chromeH = 44;
|
||||
return {
|
||||
width: cols * cellMinW + chromeW,
|
||||
height: rows * cellMinH + chromeH,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFloatingWidgetInstance(
|
||||
rows: number,
|
||||
cols: number,
|
||||
index: number,
|
||||
baseZIndex: number,
|
||||
): FloatingWidgetInstance {
|
||||
const count = rows * cols;
|
||||
const { width, height } = defaultSizeForGrid(rows, cols);
|
||||
const offset = 28 * index;
|
||||
return {
|
||||
id: newWidgetId('fw'),
|
||||
rows,
|
||||
cols,
|
||||
slots: Array.from({ length: count }, () => newWidgetSlot()),
|
||||
position: {
|
||||
x: Math.min(80 + offset, Math.max(24, window.innerWidth - width - 24)),
|
||||
y: Math.min(72 + offset, Math.max(24, window.innerHeight - height - 24)),
|
||||
},
|
||||
width,
|
||||
height,
|
||||
zIndex: baseZIndex + index,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { VirtualCardViewMode, VirtualSessionConfig, VirtualTargetItem } fro
|
||||
import type { StrategyEditorMode } from '../utils/strategyEditorModeStorage';
|
||||
import type { UserStrategyTemplate } from '../utils/strategyTemplateStorage';
|
||||
import type { SidewaysFilterPaletteEntry } from '../utils/sidewaysFilterPaletteStorage';
|
||||
import type { WidgetLayoutSchema } from './widgetDashboard';
|
||||
|
||||
/** gc_app_settings.ui_preferences_json 구조 */
|
||||
export interface UiPreferences {
|
||||
@@ -54,6 +55,8 @@ export interface UiPreferences {
|
||||
/** 시간봉 필터 — 빈 문자열=전체, 그 외 candleType (1m, 5m, …) */
|
||||
candleFilter?: string;
|
||||
};
|
||||
/** 위젯 대시보드 — 단일 레이아웃 (기기 간 동기화) */
|
||||
widgetDashboard?: WidgetLayoutSchema;
|
||||
}
|
||||
|
||||
export const UI_PREFERENCES_VERSION = 1;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/** 위젯 대시보드 레이아웃 스키마 (uiPreferences.widgetDashboard) */
|
||||
|
||||
export type WidgetZoneId = 'left' | 'center' | 'right' | 'bottom';
|
||||
|
||||
export type WidgetZoneMode = 'single' | 'split' | 'tabs';
|
||||
|
||||
export type WidgetCategory = 'center' | 'side';
|
||||
|
||||
export interface WidgetSlot {
|
||||
id: string;
|
||||
widgetType: string | null;
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WidgetTabConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
/** 탭 내부 split 슬롯 (1~3) */
|
||||
splitCount: 1 | 2 | 3;
|
||||
slots: WidgetSlot[];
|
||||
}
|
||||
|
||||
export interface WidgetZoneConfig {
|
||||
mode: WidgetZoneMode;
|
||||
/** split 모드 — 상하 카드 분할 수 */
|
||||
splitCount?: 1 | 2 | 3;
|
||||
slots?: WidgetSlot[];
|
||||
tabs?: WidgetTabConfig[];
|
||||
}
|
||||
|
||||
export interface WidgetLayoutSchema {
|
||||
v: 1;
|
||||
zones: {
|
||||
left: WidgetZoneConfig | null;
|
||||
center: WidgetZoneConfig;
|
||||
right: WidgetZoneConfig | null;
|
||||
bottom: WidgetZoneConfig | null;
|
||||
};
|
||||
collapsed: {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
bottom?: boolean;
|
||||
};
|
||||
panelSizes: {
|
||||
leftWidth?: number;
|
||||
rightWidth?: number;
|
||||
footerHeight?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function newWidgetId(prefix = 'w'): string {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
export function newWidgetSlot(widgetType: string | null = null): WidgetSlot {
|
||||
return { id: newWidgetId('slot'), widgetType, config: {} };
|
||||
}
|
||||
|
||||
export function slotsForSplitCount(count: 1 | 2 | 3, defaults: (string | null)[]): WidgetSlot[] {
|
||||
return Array.from({ length: count }, (_, i) =>
|
||||
newWidgetSlot(defaults[i] ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
export function newWidgetTab(label: string, splitCount: 1 | 2 | 3 = 1, defaults: (string | null)[] = []): WidgetTabConfig {
|
||||
return {
|
||||
id: newWidgetId('tab'),
|
||||
label,
|
||||
splitCount,
|
||||
slots: slotsForSplitCount(splitCount, defaults),
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultWidgetLayout(): WidgetLayoutSchema {
|
||||
return {
|
||||
v: 1,
|
||||
zones: {
|
||||
left: {
|
||||
mode: 'single',
|
||||
splitCount: 1,
|
||||
slots: [newWidgetSlot('market.list')],
|
||||
},
|
||||
center: {
|
||||
mode: 'single',
|
||||
splitCount: 1,
|
||||
slots: [newWidgetSlot('chart.candle')],
|
||||
},
|
||||
right: {
|
||||
mode: 'split',
|
||||
splitCount: 2,
|
||||
slots: slotsForSplitCount(2, ['trade.buySell', 'trade.orderbook']),
|
||||
},
|
||||
bottom: {
|
||||
mode: 'single',
|
||||
splitCount: 1,
|
||||
slots: [newWidgetSlot('trade.history')],
|
||||
},
|
||||
},
|
||||
collapsed: {},
|
||||
panelSizes: {},
|
||||
};
|
||||
}
|
||||
|
||||
/** splitCount 변경 시 슬롯 수 맞춤 */
|
||||
export function normalizeZoneSlots(zone: WidgetZoneConfig): WidgetZoneConfig {
|
||||
if (zone.mode === 'tabs') {
|
||||
return {
|
||||
...zone,
|
||||
tabs: (zone.tabs ?? []).map(tab => {
|
||||
const count = tab.splitCount ?? 1;
|
||||
const slots = tab.slots ?? [];
|
||||
if (slots.length === count) return tab;
|
||||
if (slots.length > count) return { ...tab, slots: slots.slice(0, count) };
|
||||
return {
|
||||
...tab,
|
||||
slots: [
|
||||
...slots,
|
||||
...Array.from({ length: count - slots.length }, () => newWidgetSlot()),
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
const count = zone.splitCount ?? 1;
|
||||
const slots = zone.slots ?? [];
|
||||
if (slots.length === count) return zone;
|
||||
if (slots.length > count) return { ...zone, slots: slots.slice(0, count) };
|
||||
return {
|
||||
...zone,
|
||||
slots: [
|
||||
...slots,
|
||||
...Array.from({ length: count - slots.length }, () => newWidgetSlot()),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWidgetLayout(raw: unknown): WidgetLayoutSchema {
|
||||
const base = defaultWidgetLayout();
|
||||
if (!raw || typeof raw !== 'object') return base;
|
||||
const o = raw as Partial<WidgetLayoutSchema>;
|
||||
const zonesRaw = (o.zones ?? {}) as Partial<WidgetLayoutSchema['zones']>;
|
||||
const zones = zonesRaw;
|
||||
const normZone = (z: WidgetZoneConfig | null | undefined, fallback: WidgetZoneConfig | null): WidgetZoneConfig | null => {
|
||||
if (z === null) return null;
|
||||
if (!z) return fallback;
|
||||
return normalizeZoneSlots(z);
|
||||
};
|
||||
return {
|
||||
v: 1,
|
||||
zones: {
|
||||
left: normZone(zones.left as WidgetZoneConfig | null | undefined, base.zones.left),
|
||||
center: normZone(zones.center as WidgetZoneConfig | undefined, base.zones.center)!,
|
||||
right: normZone(zones.right as WidgetZoneConfig | null | undefined, base.zones.right),
|
||||
bottom: normZone(zones.bottom as WidgetZoneConfig | null | undefined, base.zones.bottom),
|
||||
},
|
||||
collapsed: { ...base.collapsed, ...(o.collapsed ?? {}) },
|
||||
panelSizes: { ...base.panelSizes, ...(o.panelSizes ?? {}) },
|
||||
};
|
||||
}
|
||||
|
||||
export function zoneSlotCount(zone: WidgetZoneConfig): number {
|
||||
if (zone.mode === 'tabs') {
|
||||
return (zone.tabs ?? []).reduce((n, t) => n + (t.slots?.length ?? t.splitCount ?? 1), 0);
|
||||
}
|
||||
return zone.slots?.length ?? zone.splitCount ?? 1;
|
||||
}
|
||||
Reference in New Issue
Block a user