위젯 기능 추가
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useDraggablePanel } from '../../hooks/useDraggablePanel';
|
||||
import { useFloatingWidgetResize } from '../../hooks/useFloatingWidgetResize';
|
||||
import { minSizeForGrid, type FloatingWidgetInstance } from '../../types/floatingWidget';
|
||||
import type { WidgetSlot } from '../../types/widgetDashboard';
|
||||
import WidgetSlotView from '../widgetDashboard/WidgetSlotView';
|
||||
import WidgetPickerModal from '../widgetDashboard/WidgetPickerModal';
|
||||
|
||||
interface Props {
|
||||
instance: FloatingWidgetInstance;
|
||||
focused: boolean;
|
||||
onClose: () => void;
|
||||
onFocus: () => void;
|
||||
onUpdateSlots: (slots: WidgetSlot[]) => void;
|
||||
onUpdateSize: (width: number, height: number) => void;
|
||||
}
|
||||
|
||||
const FloatingWidgetWindow: React.FC<Props> = ({
|
||||
instance,
|
||||
focused,
|
||||
onClose,
|
||||
onFocus,
|
||||
onUpdateSlots,
|
||||
onUpdateSize,
|
||||
}) => {
|
||||
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
panelRef,
|
||||
setPos,
|
||||
dragging,
|
||||
onHeaderPointerDown,
|
||||
headerTouchStyle,
|
||||
panelStyle,
|
||||
headerCursor,
|
||||
} = useDraggablePanel({
|
||||
centerOnMount: false,
|
||||
initialPosition: instance.position,
|
||||
});
|
||||
|
||||
const minSize = useMemo(
|
||||
() => minSizeForGrid(instance.rows, instance.cols),
|
||||
[instance.rows, instance.cols],
|
||||
);
|
||||
|
||||
const handleResize = useCallback((result: { width: number; height: number; x?: number }) => {
|
||||
if (result.x != null) {
|
||||
setPos(prev => ({ ...prev, x: result.x! }));
|
||||
}
|
||||
onUpdateSize(result.width, result.height);
|
||||
}, [onUpdateSize, setPos]);
|
||||
|
||||
const { onResizeSePointerDown, onResizeSwPointerDown } = useFloatingWidgetResize(
|
||||
panelRef,
|
||||
{ width: instance.width, height: instance.height },
|
||||
minSize,
|
||||
handleResize,
|
||||
);
|
||||
|
||||
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
|
||||
onUpdateSlots(instance.slots.map(s => (s.id === slotId ? updater(s) : s)));
|
||||
}, [instance.slots, onUpdateSlots]);
|
||||
|
||||
const applyWidgetType = useCallback((widgetType: string) => {
|
||||
if (!pickSlotId) return;
|
||||
updateSlot(pickSlotId, s => ({ ...s, widgetType, config: {} }));
|
||||
setPickSlotId(null);
|
||||
}, [pickSlotId, updateSlot]);
|
||||
|
||||
const clearWidget = useCallback((slotId: string) => {
|
||||
updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} }));
|
||||
}, [updateSlot]);
|
||||
|
||||
/* 창 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */
|
||||
useEffect(() => {
|
||||
const el = bodyRef.current;
|
||||
if (!el) return undefined;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
|
||||
}, 80);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const title = `위젯 ${instance.rows}×${instance.cols}`;
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={`fw-window${focused ? ' fw-window--focused' : ''}`}
|
||||
style={{
|
||||
...panelStyle,
|
||||
width: instance.width,
|
||||
height: instance.height,
|
||||
zIndex: instance.zIndex,
|
||||
cursor: dragging ? 'grabbing' : undefined,
|
||||
}}
|
||||
onMouseDown={onFocus}
|
||||
>
|
||||
<div
|
||||
className="fw-window-head"
|
||||
onPointerDown={onHeaderPointerDown}
|
||||
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
||||
>
|
||||
<span className="fw-window-title">{title}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="fw-window-close"
|
||||
title="위젯 창 닫기"
|
||||
aria-label="위젯 창 닫기"
|
||||
onPointerDown={e => e.stopPropagation()}
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={bodyRef}
|
||||
className="fw-window-body"
|
||||
style={{
|
||||
gridTemplateRows: `repeat(${instance.rows}, minmax(0, 1fr))`,
|
||||
gridTemplateColumns: `repeat(${instance.cols}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{instance.slots.map(slot => (
|
||||
<div key={slot.id} className="fw-window-cell">
|
||||
<WidgetSlotView
|
||||
slot={slot}
|
||||
zoneId="center"
|
||||
onPickWidget={id => setPickSlotId(id)}
|
||||
onClearWidget={id => clearWidget(id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="fw-window-resize-layer">
|
||||
<div
|
||||
className="fw-window-resize-handle fw-window-resize-handle--sw"
|
||||
role="separator"
|
||||
aria-label="위젯 창 크기 조절 (좌하단)"
|
||||
onPointerDown={onResizeSwPointerDown}
|
||||
/>
|
||||
<div
|
||||
className="fw-window-resize-handle fw-window-resize-handle--se"
|
||||
role="separator"
|
||||
aria-label="위젯 창 크기 조절 (우하단)"
|
||||
onPointerDown={onResizeSePointerDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WidgetPickerModal
|
||||
open={pickSlotId != null}
|
||||
category="all"
|
||||
onClose={() => setPickSlotId(null)}
|
||||
onSelect={applyWidgetType}
|
||||
/>
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingWidgetWindow;
|
||||
Reference in New Issue
Block a user