위젯 기능 추가

This commit is contained in:
Macbook
2026-06-14 01:03:51 +09:00
parent 95595f7e9e
commit 6936792ad4
57 changed files with 6728 additions and 540 deletions
@@ -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;