위젯영역 리사이즈

This commit is contained in:
Macbook
2026-06-14 01:18:10 +09:00
parent 57677ef39c
commit a3dcfe5cb2
5 changed files with 336 additions and 22 deletions
@@ -1,8 +1,15 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useDraggablePanel } from '../../hooks/useDraggablePanel';
import { useFloatingWidgetGridSplit } from '../../hooks/useFloatingWidgetGridSplit';
import { useFloatingWidgetResize } from '../../hooks/useFloatingWidgetResize';
import { minSizeForGrid, type FloatingWidgetInstance } from '../../types/floatingWidget';
import {
defaultGridFr,
gridSplitPositions,
gridTemplateFr,
minSizeForGrid,
type FloatingWidgetInstance,
} from '../../types/floatingWidget';
import type { WidgetSlot } from '../../types/widgetDashboard';
import WidgetSlotView from '../widgetDashboard/WidgetSlotView';
import WidgetPickerModal from '../widgetDashboard/WidgetPickerModal';
@@ -14,6 +21,7 @@ interface Props {
onFocus: () => void;
onUpdateSlots: (slots: WidgetSlot[]) => void;
onUpdateSize: (width: number, height: number) => void;
onUpdateGridFr: (rowFr: number[], colFr: number[]) => void;
}
const FloatingWidgetWindow: React.FC<Props> = ({
@@ -23,9 +31,23 @@ const FloatingWidgetWindow: React.FC<Props> = ({
onFocus,
onUpdateSlots,
onUpdateSize,
onUpdateGridFr,
}) => {
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const bodyRef = useRef<HTMLDivElement>(null);
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
const rowFr = instance.rowFr?.length === instance.rows
? instance.rowFr
: defaultGridFr(instance.rows);
const colFr = instance.colFr?.length === instance.cols
? instance.colFr
: defaultGridFr(instance.cols);
const rowFrRef = useRef(rowFr);
const colFrRef = useRef(colFr);
rowFrRef.current = rowFr;
colFrRef.current = colFr;
const {
panelRef,
@@ -59,6 +81,25 @@ const FloatingWidgetWindow: React.FC<Props> = ({
handleResize,
);
const handleColFrChange = useCallback((next: number[]) => {
onUpdateGridFr(rowFrRef.current, next);
}, [onUpdateGridFr]);
const handleRowFrChange = useCallback((next: number[]) => {
onUpdateGridFr(next, colFrRef.current);
}, [onUpdateGridFr]);
const { onColSplitPointerDown, onRowSplitPointerDown } = useFloatingWidgetGridSplit({
onColFrChange: handleColFrChange,
onRowFrChange: handleRowFrChange,
getColFr: () => colFrRef.current,
getRowFr: () => rowFrRef.current,
getBodySize: () => ({
width: bodyRef.current?.clientWidth ?? bodySize.width,
height: bodyRef.current?.clientHeight ?? bodySize.height,
}),
});
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
onUpdateSlots(instance.slots.map(s => (s.id === slotId ? updater(s) : s)));
}, [instance.slots, onUpdateSlots]);
@@ -73,23 +114,35 @@ const FloatingWidgetWindow: React.FC<Props> = ({
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(() => {
const scheduleLayoutSync = () => {
setBodySize({ width: el.clientWidth, height: el.clientHeight });
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
}, 80);
});
};
scheduleLayoutSync();
const ro = new ResizeObserver(() => scheduleLayoutSync());
ro.observe(el);
return () => {
if (timer) clearTimeout(timer);
ro.disconnect();
};
}, []);
}, [instance.rows, instance.cols]);
const colSplitPositions = useMemo(
() => gridSplitPositions(colFr, bodySize.width, 1),
[colFr, bodySize.width],
);
const rowSplitPositions = useMemo(
() => gridSplitPositions(rowFr, bodySize.height, 1),
[rowFr, bodySize.height],
);
const title = `위젯 ${instance.rows}×${instance.cols}`;
@@ -125,24 +178,53 @@ const FloatingWidgetWindow: React.FC<Props> = ({
</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 className="fw-window-body-wrap">
<div
ref={bodyRef}
className="fw-window-body"
style={{
gridTemplateRows: gridTemplateFr(rowFr),
gridTemplateColumns: gridTemplateFr(colFr),
}}
>
{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>
{(colSplitPositions.length > 0 || rowSplitPositions.length > 0) && (
<div className="fw-grid-split-layer" aria-hidden="true">
{colSplitPositions.map((left, index) => (
<div
key={`col-${index}`}
className="fw-grid-splitter fw-grid-splitter--v"
style={{ left }}
role="separator"
aria-orientation="vertical"
aria-label={`${index + 1}·${index + 2} 경계 조절`}
onPointerDown={onColSplitPointerDown(index)}
/>
))}
{rowSplitPositions.map((top, index) => (
<div
key={`row-${index}`}
className="fw-grid-splitter fw-grid-splitter--h"
style={{ top }}
role="separator"
aria-orientation="horizontal"
aria-label={`${index + 1}·${index + 2} 경계 조절`}
onPointerDown={onRowSplitPointerDown(index)}
/>
))}
</div>
))}
)}
</div>
<div className="fw-window-resize-layer">