위젯영역 리사이즈
This commit is contained in:
@@ -120,6 +120,12 @@ const FloatingWidgetLayer: React.FC<Props> = ({
|
||||
)));
|
||||
}, []);
|
||||
|
||||
const handleUpdateGridFr = useCallback((id: string, rowFr: number[], colFr: number[]) => {
|
||||
setInstances(prev => prev.map(inst => (
|
||||
inst.id === id ? { ...inst, rowFr, colFr } : inst
|
||||
)));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onInstancesChange?.(instances.length);
|
||||
}, [instances.length, onInstancesChange]);
|
||||
@@ -154,6 +160,7 @@ const FloatingWidgetLayer: React.FC<Props> = ({
|
||||
onFocus={() => bringToFront(inst.id)}
|
||||
onUpdateSlots={slots => handleUpdateSlots(inst.id, slots)}
|
||||
onUpdateSize={(w, h) => handleUpdateSize(inst.id, w, h)}
|
||||
onUpdateGridFr={(rowFr, colFr) => handleUpdateGridFr(inst.id, rowFr, colFr)}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user