From 4e7fe0fb0a91214137168c12f8175c763cad8aeb Mon Sep 17 00:00:00 2001 From: tjsdn052 Date: Tue, 3 Mar 2026 15:58:18 +0900 Subject: [PATCH 01/17] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20backgroundLaye?= =?UTF-8?q?r=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B8=B0=EC=A1=B4=20Canvas=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -Canvas.tsx 파일 분리 -배경 분리 --- frontend/src/components/whiteboard/Canvas.tsx | 15 +++-------- .../whiteboard/layers/BackgroundLayer.tsx | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/whiteboard/layers/BackgroundLayer.tsx diff --git a/frontend/src/components/whiteboard/Canvas.tsx b/frontend/src/components/whiteboard/Canvas.tsx index ef989dc2..b6c4047f 100644 --- a/frontend/src/components/whiteboard/Canvas.tsx +++ b/frontend/src/components/whiteboard/Canvas.tsx @@ -43,6 +43,7 @@ import RemoteSelectionIndicator from '@/components/whiteboard/remote/RemoteSelec import ArrowHandles from '@/components/whiteboard/items/arrow/ArrowHandles'; import SelectionBox from '@/components/whiteboard/SelectionBox'; import Portal from '@/components/common/Portal'; +import BackgroundLayer from '@/components/whiteboard/layers/BackgroundLayer'; const GEOMETRY_KEYS = ['x', 'y', 'width', 'height', 'rotation'] as const; @@ -600,17 +601,9 @@ export default function Canvas() { clipWidth={canvasWidth} clipHeight={canvasHeight} > - {/* Canvas 경계 */} - {/* 아이템 렌더링 */} diff --git a/frontend/src/components/whiteboard/layers/BackgroundLayer.tsx b/frontend/src/components/whiteboard/layers/BackgroundLayer.tsx new file mode 100644 index 00000000..3575f7e3 --- /dev/null +++ b/frontend/src/components/whiteboard/layers/BackgroundLayer.tsx @@ -0,0 +1,25 @@ +import { Rect } from 'react-konva'; + +interface BackgroundLayerProps { + canvasWidth: number; + canvasHeight: number; +} + +export default function BackgroundLayer({ + canvasWidth, + canvasHeight, +}: BackgroundLayerProps) { + return ( + + ); +} From c2e1210c5d6e12cccda8324d83837f1dac6f38b4 Mon Sep 17 00:00:00 2001 From: tjsdn052 Date: Tue, 3 Mar 2026 16:10:26 +0900 Subject: [PATCH 02/17] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20ItemRenderingLayer=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Canvas.tsx에서 렌더링 로직 분리 - visibleItems.map 기반 RenderItem 호출 로직을 ItemRenderingLayer로 이동함 --- frontend/src/components/whiteboard/Canvas.tsx | 106 +++---------- .../whiteboard/layers/ItemRenderingLayer.tsx | 149 ++++++++++++++++++ 2 files changed, 172 insertions(+), 83 deletions(-) create mode 100644 frontend/src/components/whiteboard/layers/ItemRenderingLayer.tsx diff --git a/frontend/src/components/whiteboard/Canvas.tsx b/frontend/src/components/whiteboard/Canvas.tsx index b6c4047f..74bc1d69 100644 --- a/frontend/src/components/whiteboard/Canvas.tsx +++ b/frontend/src/components/whiteboard/Canvas.tsx @@ -3,7 +3,7 @@ import { useRef, useState, useMemo, useEffect, useCallback } from 'react'; import Konva from 'konva'; -import { Stage, Layer, Rect, Line } from 'react-konva'; +import { Stage, Layer, Rect } from 'react-konva'; import type { WhiteboardItem, @@ -16,10 +16,7 @@ import { useWhiteboardSharedStore } from '@/store/useWhiteboardSharedStore'; import { useWhiteboardLocalStore } from '@/store/useWhiteboardLocalStore'; import { useWhiteboardAwarenessStore } from '@/store/useWhiteboardAwarenessStore'; import { cn } from '@/utils/cn'; -import { - updateBoundArrows, - getDraggingArrowPoints, -} from '@/utils/arrowBinding'; +import { updateBoundArrows } from '@/utils/arrowBinding'; import { getViewportRect, filterVisibleItems } from '@/utils/viewport'; import { useItemActions } from '@/hooks/useItemActions'; @@ -34,7 +31,6 @@ import { useSelectionBox } from '@/hooks/useSelectionBox'; import { useMultiDrag } from '@/hooks/useMultiDrag'; import { usePinchZoom } from '@/hooks/usePinchZoom'; -import RenderItem from '@/components/whiteboard/items/RenderItem'; import TextArea from '@/components/whiteboard/items/text/TextArea'; import ShapeTextArea from '@/components/whiteboard/items/shape/ShapeTextArea'; import ItemTransformer from '@/components/whiteboard/controls/ItemTransformer'; @@ -44,6 +40,7 @@ import ArrowHandles from '@/components/whiteboard/items/arrow/ArrowHandles'; import SelectionBox from '@/components/whiteboard/SelectionBox'; import Portal from '@/components/common/Portal'; import BackgroundLayer from '@/components/whiteboard/layers/BackgroundLayer'; +import ItemRenderingLayer from '@/components/whiteboard/layers/ItemRenderingLayer'; const GEOMETRY_KEYS = ['x', 'y', 'width', 'height', 'rotation'] as const; @@ -607,83 +604,26 @@ export default function Canvas() { /> {/* 아이템 렌더링 */} - {visibleItems.map((item) => { - let displayItem = item; - - const multiDragPos = getMultiDragPosition(item.id); - if (multiDragPos && 'x' in item && 'y' in item) { - displayItem = { - ...item, - x: multiDragPos.x, - y: multiDragPos.y, - } as WhiteboardItem; - } - - if ( - !multiDragPos && - item.type === 'arrow' && - localDraggingId && - localDraggingPos && - (item.startBinding?.elementId === localDraggingId || - item.endBinding?.elementId === localDraggingId) - ) { - const targetShape = items.find( - (it) => it.id === localDraggingId, - ) as ShapeItem; - if (targetShape) { - const tempPoints = getDraggingArrowPoints( - item as ArrowItem, - localDraggingId, - localDraggingPos.x, - localDraggingPos.y, - targetShape, - localDraggingPos.width, - localDraggingPos.height, - localDraggingPos.rotation, - ); - if (tempPoints) { - displayItem = { - ...displayItem, - points: tempPoints, - } as WhiteboardItem; - } - } - } - - if ( - displayItem.id === singleSelectedId && - (displayItem.type === 'arrow' || displayItem.type === 'line') && - draggingPoints - ) { - displayItem = { - ...displayItem, - points: draggingPoints, - } as WhiteboardItem; - } - - return ( - - handleItemChange(item.id, newAttributes) - } - onArrowDblClick={handleArrowDblClick} - onShapeDblClick={handleShapeDblClick} - onDragStart={() => { - if (item.type === 'arrow' || item.type === 'line') { - setIsDraggingArrow(true); - } - startMultiDrag(item.id); - }} - onDragMove={handleDragMoveItem} - onTransformMove={handleTransformMoveItem} - onDragEnd={handleDragEndItem} - /> - ); - })} + {isArrowOrLineSelected && selectedItem && !isDraggingArrow && ( { x: number; y: number } | null; + handleSelectItem: ( + id: string, + e: Konva.KonvaEventObject, + ) => void; + handleItemChange: ( + id: string, + newAttributes: Partial, + ) => void; + handleArrowDblClick: (id: string) => void; + handleShapeDblClick: (id: string) => void; + setIsDraggingArrow: (isDragging: boolean) => void; + startMultiDrag: (id: string) => void; + handleDragMoveItem: (id: string, x: number, y: number) => void; + handleTransformMoveItem: ( + id: string, + x: number, + y: number, + w: number, + h: number, + rotation: number, + ) => void; + handleDragEndItem: () => void; +} + +export default function ItemRenderingLayer({ + items, + visibleItems, + singleSelectedId, + draggingPoints, + localDraggingId, + localDraggingPos, + getMultiDragPosition, + selectedIds, + handleSelectItem, + handleItemChange, + handleArrowDblClick, + handleShapeDblClick, + setIsDraggingArrow, + startMultiDrag, + handleDragMoveItem, + handleTransformMoveItem, + handleDragEndItem, +}: ItemRenderingLayerProps) { + return ( + <> + {visibleItems.map((item) => { + let displayItem = item; + + const multiDragPos = getMultiDragPosition(item.id); + if (multiDragPos && 'x' in item && 'y' in item) { + displayItem = { + ...item, + x: multiDragPos.x, + y: multiDragPos.y, + } as WhiteboardItem; + } + + if ( + !multiDragPos && + item.type === 'arrow' && + localDraggingId && + localDraggingPos && + (item.startBinding?.elementId === localDraggingId || + item.endBinding?.elementId === localDraggingId) + ) { + const targetShape = items.find( + (it) => it.id === localDraggingId, + ) as ShapeItem; + if (targetShape) { + const tempPoints = getDraggingArrowPoints( + item as ArrowItem, + localDraggingId, + localDraggingPos.x, + localDraggingPos.y, + targetShape, + localDraggingPos.width, + localDraggingPos.height, + localDraggingPos.rotation, + ); + if (tempPoints) { + displayItem = { + ...displayItem, + points: tempPoints, + } as WhiteboardItem; + } + } + } + + if ( + displayItem.id === singleSelectedId && + (displayItem.type === 'arrow' || displayItem.type === 'line') && + draggingPoints + ) { + displayItem = { + ...displayItem, + points: draggingPoints, + } as WhiteboardItem; + } + + return ( + + handleItemChange(item.id, newAttributes) + } + onArrowDblClick={handleArrowDblClick} + onShapeDblClick={handleShapeDblClick} + onDragStart={() => { + if (item.type === 'arrow' || item.type === 'line') { + setIsDraggingArrow(true); + } + startMultiDrag(item.id); + }} + onDragMove={handleDragMoveItem} + onTransformMove={handleTransformMoveItem} + onDragEnd={handleDragEndItem} + /> + ); + })} + + ); +} From e7e912277623a67bd8706528869381171d16f161 Mon Sep 17 00:00:00 2001 From: tjsdn052 Date: Tue, 3 Mar 2026 16:40:36 +0900 Subject: [PATCH 03/17] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20collaborationL?= =?UTF-8?q?ayer=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Canvas.tsx 분리 - RemoteSelectionLayer 로직 분리 - RemoteSelectionIndicator를 CollaborationLayer로 통합 --- frontend/src/components/whiteboard/Canvas.tsx | 21 ++------- .../whiteboard/layers/CollaborationLayer.tsx | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/whiteboard/layers/CollaborationLayer.tsx diff --git a/frontend/src/components/whiteboard/Canvas.tsx b/frontend/src/components/whiteboard/Canvas.tsx index 74bc1d69..cfac6494 100644 --- a/frontend/src/components/whiteboard/Canvas.tsx +++ b/frontend/src/components/whiteboard/Canvas.tsx @@ -34,13 +34,12 @@ import { usePinchZoom } from '@/hooks/usePinchZoom'; import TextArea from '@/components/whiteboard/items/text/TextArea'; import ShapeTextArea from '@/components/whiteboard/items/shape/ShapeTextArea'; import ItemTransformer from '@/components/whiteboard/controls/ItemTransformer'; -import RemoteSelectionLayer from '@/components/whiteboard/remote/RemoteSelectionLayer'; -import RemoteSelectionIndicator from '@/components/whiteboard/remote/RemoteSelectionIndicator'; import ArrowHandles from '@/components/whiteboard/items/arrow/ArrowHandles'; import SelectionBox from '@/components/whiteboard/SelectionBox'; import Portal from '@/components/common/Portal'; import BackgroundLayer from '@/components/whiteboard/layers/BackgroundLayer'; import ItemRenderingLayer from '@/components/whiteboard/layers/ItemRenderingLayer'; +import CollaborationLayer from '@/components/whiteboard/layers/CollaborationLayer'; const GEOMETRY_KEYS = ['x', 'y', 'width', 'height', 'rotation'] as const; @@ -654,22 +653,10 @@ export default function Canvas() { {/* 선택 박스 */} - {/* 내 멀티 선택 개별 박스 */} - {selectedIds.length > 1 && - selectedIds.map((itemId) => ( - - ))} - - {/* 다른 사용자의 선택 표시 */} - diff --git a/frontend/src/components/whiteboard/layers/CollaborationLayer.tsx b/frontend/src/components/whiteboard/layers/CollaborationLayer.tsx new file mode 100644 index 00000000..28edf379 --- /dev/null +++ b/frontend/src/components/whiteboard/layers/CollaborationLayer.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Konva from 'konva'; + +import type { WhiteboardItem } from '@/types/whiteboard'; + +import RemoteSelectionLayer from '@/components/whiteboard/remote/RemoteSelectionLayer'; +import RemoteSelectionIndicator from '@/components/whiteboard/remote/RemoteSelectionIndicator'; + +interface CollaborationLayerProps { + myUserId: string | null; + items: WhiteboardItem[]; + selectedIds: string[]; + singleSelectedId: string | null; + stageRef: React.RefObject; +} + +export default function CollaborationLayer({ + myUserId, + items, + selectedIds, + singleSelectedId, + stageRef, +}: CollaborationLayerProps) { + return ( + <> + {/* 본인 선택 박스 */} + {selectedIds.length > 1 && + selectedIds.map((itemId) => ( + + ))} + + {/* 다른 사용자의 선택 표시 */} + + + ); +} From 0f11f76643a8578fbddb7ee27d242e580328203f Mon Sep 17 00:00:00 2001 From: tjsdn052 Date: Tue, 3 Mar 2026 17:07:15 +0900 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20textEditorLaye?= =?UTF-8?q?r=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canvas.tsx에서 분리 - 텍스트 편집 로직 분리 --- frontend/src/components/whiteboard/Canvas.tsx | 71 +++-------------- .../whiteboard/layers/TextEditorLayer.tsx | 78 +++++++++++++++++++ 2 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 frontend/src/components/whiteboard/layers/TextEditorLayer.tsx diff --git a/frontend/src/components/whiteboard/Canvas.tsx b/frontend/src/components/whiteboard/Canvas.tsx index cfac6494..7c64c59c 100644 --- a/frontend/src/components/whiteboard/Canvas.tsx +++ b/frontend/src/components/whiteboard/Canvas.tsx @@ -5,12 +5,7 @@ import { useRef, useState, useMemo, useEffect, useCallback } from 'react'; import Konva from 'konva'; import { Stage, Layer, Rect } from 'react-konva'; -import type { - WhiteboardItem, - TextItem, - ArrowItem, - ShapeItem, -} from '@/types/whiteboard'; +import type { WhiteboardItem, ArrowItem, ShapeItem } from '@/types/whiteboard'; import { useWhiteboardSharedStore } from '@/store/useWhiteboardSharedStore'; import { useWhiteboardLocalStore } from '@/store/useWhiteboardLocalStore'; @@ -31,15 +26,13 @@ import { useSelectionBox } from '@/hooks/useSelectionBox'; import { useMultiDrag } from '@/hooks/useMultiDrag'; import { usePinchZoom } from '@/hooks/usePinchZoom'; -import TextArea from '@/components/whiteboard/items/text/TextArea'; -import ShapeTextArea from '@/components/whiteboard/items/shape/ShapeTextArea'; import ItemTransformer from '@/components/whiteboard/controls/ItemTransformer'; import ArrowHandles from '@/components/whiteboard/items/arrow/ArrowHandles'; import SelectionBox from '@/components/whiteboard/SelectionBox'; -import Portal from '@/components/common/Portal'; import BackgroundLayer from '@/components/whiteboard/layers/BackgroundLayer'; import ItemRenderingLayer from '@/components/whiteboard/layers/ItemRenderingLayer'; import CollaborationLayer from '@/components/whiteboard/layers/CollaborationLayer'; +import TextEditorLayer from '@/components/whiteboard/layers/TextEditorLayer'; const GEOMETRY_KEYS = ['x', 'y', 'width', 'height', 'rotation'] as const; @@ -253,15 +246,6 @@ export default function Canvas() { [handleWheel], ); - const editingItem = useMemo( - () => - items.find((item) => item.id === editingTextId) as - | TextItem - | ShapeItem - | undefined, - [items, editingTextId], - ); - const singleSelectedId = selectedIds.length === 1 ? selectedIds[0] : null; const selectedItem = useMemo( () => @@ -670,49 +654,14 @@ export default function Canvas() { - {/* 텍스트 편집 모드 */} - {editingTextId && editingItem && editingItem.type === 'text' && ( - -