From a3dcfe5cb26c18abacdb0e76a203d1cc67d288dd Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 14 Jun 2026 01:18:10 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=84=EC=A0=AF=EC=98=81=EC=97=AD=20?= =?UTF-8?q?=EB=A6=AC=EC=82=AC=EC=9D=B4=EC=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floatingWidgets/FloatingWidgetLayer.tsx | 7 + .../floatingWidgets/FloatingWidgetWindow.tsx | 126 +++++++++++++++--- .../src/hooks/useFloatingWidgetGridSplit.ts | 112 ++++++++++++++++ frontend/src/styles/floatingWidget.css | 50 +++++++ frontend/src/types/floatingWidget.ts | 63 +++++++++ 5 files changed, 336 insertions(+), 22 deletions(-) create mode 100644 frontend/src/hooks/useFloatingWidgetGridSplit.ts diff --git a/frontend/src/components/floatingWidgets/FloatingWidgetLayer.tsx b/frontend/src/components/floatingWidgets/FloatingWidgetLayer.tsx index 48b8de8..2804ce8 100644 --- a/frontend/src/components/floatingWidgets/FloatingWidgetLayer.tsx +++ b/frontend/src/components/floatingWidgets/FloatingWidgetLayer.tsx @@ -120,6 +120,12 @@ const FloatingWidgetLayer: React.FC = ({ ))); }, []); + 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 = ({ 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)} /> ))} diff --git a/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx b/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx index 0577cec..ba55290 100644 --- a/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx +++ b/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx @@ -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 = ({ @@ -23,9 +31,23 @@ const FloatingWidgetWindow: React.FC = ({ onFocus, onUpdateSlots, onUpdateSize, + onUpdateGridFr, }) => { const [pickSlotId, setPickSlotId] = useState(null); const bodyRef = useRef(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 = ({ 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 = ({ updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} })); }, [updateSlot]); - /* 창 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */ + /* 창·그리드 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */ useEffect(() => { const el = bodyRef.current; if (!el) return undefined; let timer: ReturnType | 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 = ({ -
- {instance.slots.map(slot => ( -
- setPickSlotId(id)} - onClearWidget={id => clearWidget(id)} - /> +
+
+ {instance.slots.map(slot => ( +
+ setPickSlotId(id)} + onClearWidget={id => clearWidget(id)} + /> +
+ ))} +
+ + {(colSplitPositions.length > 0 || rowSplitPositions.length > 0) && ( +