Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4e7fe0f
🤖 Refactor: backgroundLayer 컴포넌트 생성 및 기존 Canvas 컴포넌트에서 분리
tjsdn052 Mar 3, 2026
c2e1210
🤖 Refactor: 아이템 렌더링 로직을 ItemRenderingLayer로 이동
tjsdn052 Mar 3, 2026
e7e9122
🤖 Refactor: collaborationLayer 컴포넌트 생성 및 분리
tjsdn052 Mar 3, 2026
0f11f76
🤖 Refactor: textEditorLayer 분리
tjsdn052 Mar 3, 2026
9b6d418
🤖 Refactor: 상호작용 렌더링 InteractionLayer로 분리
tjsdn052 Mar 3, 2026
b262207
📍 Feat: 화살표 핸들 선택 상태 관리위해 화이트보드 local 스토어 상태 추가
tjsdn052 Mar 3, 2026
7e2fb47
📍 Feat: 화살표 핸들 삽입, 삭제 액션 추가
tjsdn052 Mar 3, 2026
3c03e1d
🤖 Refactor: useArrowHandles에서 내부 상태 제거 후 Store 활용하도록 변경
tjsdn052 Mar 3, 2026
2407bd7
🤖 Refactor: useCanvasShortcuts에서 직접 전달받던 props 제거
tjsdn052 Mar 3, 2026
d752201
📍 Feat: canvas에서 뷰포트 업데이트 및 필터링 로직 훅으로 분리
tjsdn052 Mar 3, 2026
dd04737
🤖 Refactor: 중간 상태 props 제거
tjsdn052 Mar 3, 2026
f20e6fd
🤖 Refactor: 화살표 드래그 부분 렌더링 최적화
tjsdn052 Mar 3, 2026
718e22d
📍 Feat: 마우스/터치/휠 등 캔버스 이벤트 처리 로직 분리
tjsdn052 Mar 3, 2026
114e86b
🤖 Refactor: canvas 컴포넌트 로직 분리
tjsdn052 Mar 3, 2026
f65867b
🔨 Fix: 화살표 핸들 드래그 시 임시 화살표, 원본 화살표 두 가지 화살표가 렌더링되는 문제 수정
tjsdn052 Mar 3, 2026
644fe35
🔨 Fix: 라인 더블클릭시 핸들이 추가되지 않던 문제 수정
tjsdn052 Mar 3, 2026
f93082f
🌈 Update: 주석 변경
tjsdn052 Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
549 changes: 74 additions & 475 deletions frontend/src/components/whiteboard/Canvas.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { Circle } from 'react-konva';
import { KonvaEventObject } from 'konva/lib/Node';
import { getControlPoints } from '@/utils/arrow';
import { useCursorStyle } from '@/hooks/useCursorStyle';
import type { ArrowItem } from '@/types/whiteboard';
import type { ArrowItem, LineItem } from '@/types/whiteboard';

interface ArrowHandlesProps {
arrow: ArrowItem;
arrow: ArrowItem | LineItem;
selectedHandleIndex: number | null;
onHandleClick: (e: KonvaEventObject<MouseEvent>, index: number) => void;
onDragStart: (handleType: 'start' | 'end' | 'mid') => void;
onStartDrag: (e: KonvaEventObject<DragEvent>) => void;
onControlPointDrag: (
pointIndex: number,
Expand All @@ -24,6 +25,7 @@ export default function ArrowHandles({
arrow,
selectedHandleIndex,
onHandleClick,
onDragStart,
onStartDrag,
onControlPointDrag,
onEndDrag,
Expand Down Expand Up @@ -52,6 +54,7 @@ export default function ArrowHandles({
stroke="#0369A1"
strokeWidth={2}
draggable
onDragStart={() => onDragStart('start')}
onDragMove={onStartDrag}
onDragEnd={() => onDragEnd('start')}
onClick={(e) => onHandleClick(e, 0)}
Expand All @@ -72,6 +75,7 @@ export default function ArrowHandles({
stroke="#B8E6FE"
strokeWidth={isHandleSelected ? 3 : 2}
draggable
onDragStart={() => onDragStart('mid')}
onDragMove={(e) => onControlPointDrag(point.index, e)}
onDragEnd={() => onDragEnd('mid')}
onClick={(e) => onHandleClick(e, point.index)}
Expand All @@ -90,6 +94,7 @@ export default function ArrowHandles({
stroke="#0369A1"
strokeWidth={2}
draggable
onDragStart={() => onDragStart('end')}
onDragMove={onEndDrag}
onDragEnd={() => onDragEnd('end')}
onClick={(e) => onHandleClick(e, points.length - 2)}
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/whiteboard/layers/BackgroundLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Rect } from 'react-konva';

interface BackgroundLayerProps {
canvasWidth: number;
canvasHeight: number;
}

export default function BackgroundLayer({
canvasWidth,
canvasHeight,
}: BackgroundLayerProps) {
return (
<Rect
name="bg-rect"
x={0}
y={0}
width={canvasWidth}
height={canvasHeight}
fill="white"
stroke="gray"
strokeWidth={2}
listening={true}
/>
);
}
47 changes: 47 additions & 0 deletions frontend/src/components/whiteboard/layers/CollaborationLayer.tsx
Original file line number Diff line number Diff line change
@@ -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<Konva.Stage | null>;
}

export default function CollaborationLayer({
myUserId,
items,
selectedIds,
singleSelectedId,
stageRef,
}: CollaborationLayerProps) {
return (
<>
{/* 본인 선택 박스 */}
{selectedIds.length > 1 &&
selectedIds.map((itemId) => (
<RemoteSelectionIndicator
key={`my-selection-${itemId}`}
selectedId={itemId}
userColor="#0369A1"
items={items}
stageRef={stageRef}
/>
))}

{/* 다른 사용자의 선택 표시 */}
<RemoteSelectionLayer
myUserId={myUserId ?? ''}
selectedId={singleSelectedId}
items={items}
stageRef={stageRef}
/>
</>
);
}
118 changes: 118 additions & 0 deletions frontend/src/components/whiteboard/layers/InteractionLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import Konva from 'konva';
import { Rect } from 'react-konva';

import type { WhiteboardItem, ArrowItem, LineItem } from '@/types/whiteboard';

import { useArrowHandles } from '@/hooks/useArrowHandles';
import { useItemActions } from '@/hooks/useItemActions';

import ItemTransformer from '@/components/whiteboard/controls/ItemTransformer';
import ArrowHandles from '@/components/whiteboard/items/arrow/ArrowHandles';
import SelectionBox from '@/components/whiteboard/SelectionBox';
import RenderItem from '@/components/whiteboard/items/RenderItem';

interface InteractionLayerProps {
isArrowOrLineSelected: boolean;
selectedItem: WhiteboardItem | null;
selectedIds: string[];
items: WhiteboardItem[];
stageRef: React.RefObject<Konva.Stage | null>;
isDraggingArrow: boolean;
isDraggingHandle: boolean;
setIsDraggingHandle: (isDragging: boolean) => void;
}

export default function InteractionLayer({
isArrowOrLineSelected,
selectedItem,
selectedIds,
items,
stageRef,
isDraggingArrow,
setIsDraggingHandle,
}: InteractionLayerProps) {
const { updateItem } = useItemActions();
const {
selectedHandleIndex,
handleHandleClick,
handleHandleDragStart,
handleArrowStartDrag,
handleArrowControlPointDrag,
handleArrowEndDrag,
handleHandleDragEnd,
draggingPoints,
snapIndicator,
} = useArrowHandles({
arrow: isArrowOrLineSelected
? (selectedItem as ArrowItem | LineItem)
: null,
items,
updateItem,
setIsDraggingArrow: setIsDraggingHandle,
});

return (
<>
{/* 화살표 드래그 중 임시 렌더링 */}
{draggingPoints && selectedItem && (
<RenderItem
item={
{
...selectedItem,
points: draggingPoints,
} as ArrowItem | LineItem
}
isSelected={true}
onSelect={() => {}}
onChange={() => {}}
onArrowDblClick={() => {}}
onShapeDblClick={() => {}}
onDragStart={() => {}}
onDragMove={() => {}}
onTransformMove={() => {}}
onDragEnd={() => {}}
/>
)}

{/* 화살표 핸들 */}
{isArrowOrLineSelected && selectedItem && !isDraggingArrow && (
<ArrowHandles
arrow={selectedItem as ArrowItem | LineItem}
selectedHandleIndex={selectedHandleIndex}
onHandleClick={handleHandleClick}
onDragStart={handleHandleDragStart}
onStartDrag={handleArrowStartDrag}
onControlPointDrag={handleArrowControlPointDrag}
onEndDrag={handleArrowEndDrag}
onDragEnd={handleHandleDragEnd}
draggingPoints={draggingPoints}
/>
)}

{/* 부착 표시 */}
{snapIndicator && (
<Rect
x={snapIndicator.x}
y={snapIndicator.y}
width={snapIndicator.width}
height={snapIndicator.height}
rotation={snapIndicator.rotation}
stroke="#0096FF"
strokeWidth={3}
cornerRadius={3}
/>
)}

{/* 선택 박스 */}
<SelectionBox />

{/* 내 Transformer */}
<ItemTransformer
selectedIds={selectedIds}
items={items}
stageRef={stageRef}
/>
</>
);
}
150 changes: 150 additions & 0 deletions frontend/src/components/whiteboard/layers/ItemRenderingLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from 'react';
import Konva from 'konva';

import type { WhiteboardItem, ShapeItem, ArrowItem } from '@/types/whiteboard';
import { getDraggingArrowPoints } from '@/utils/arrowBinding';
import { useItemActions } from '@/hooks/useItemActions';
import RenderItem from '@/components/whiteboard/items/RenderItem';

interface ItemRenderingLayerProps {
items: WhiteboardItem[];
visibleItems: WhiteboardItem[];
selectedIds: string[];
singleSelectedId: string | null;
isDraggingArrow: boolean;
isDraggingHandle: boolean;
localDraggingId: string | null;
localDraggingPos: {
x: number;
y: number;
width?: number;
height?: number;
rotation?: number;
} | null;
getMultiDragPosition: (id: string) => { x: number; y: number } | null;
handleSelectItem: (
id: string,
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
handleItemChange: (
id: string,
newAttributes: Partial<WhiteboardItem>,
) => void;
handleShapeDblClick: (id: string) => void;
setIsDraggingArrow: (isDragging: boolean) => void;
setIsDraggingHandle: (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,
isDraggingHandle,
localDraggingId,
localDraggingPos,
getMultiDragPosition,
selectedIds,
handleSelectItem,
handleItemChange,
handleShapeDblClick,
setIsDraggingArrow,
startMultiDrag,
handleDragMoveItem,
handleTransformMoveItem,
handleDragEndItem,
}: ItemRenderingLayerProps) {
const { insertArrowControlPoint } = useItemActions();
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 (
isDraggingHandle &&
item.id === singleSelectedId &&
(item.type === 'arrow' || item.type === 'line')
) {
return null;
}

return (
<RenderItem
key={item.id}
item={displayItem}
isSelected={selectedIds.includes(item.id)}
onSelect={handleSelectItem}
onChange={(newAttributes) =>
handleItemChange(item.id, newAttributes)
}
onArrowDblClick={insertArrowControlPoint}
onShapeDblClick={handleShapeDblClick}
onDragStart={() => {
if (
item.id === singleSelectedId &&
(item.type === 'arrow' || item.type === 'line')
) {
// 핸들 드래그와 구분하기 위한 상태는 InteractionLayer에서 관리
}
setIsDraggingArrow(true);
startMultiDrag(item.id);
}}
onDragMove={handleDragMoveItem}
onTransformMove={handleTransformMoveItem}
onDragEnd={handleDragEndItem}
/>
);
})}
</>
);
}
Loading