Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
68 changes: 22 additions & 46 deletions frontend/src/components/whiteboard/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
'use client';

import { useRef, useEffect, useState, useMemo } from 'react';
import { useRef, useState, useMemo } from 'react';

import Konva from 'konva';
import { Stage, Layer, Rect } from 'react-konva';
import { useCanvasStore } from '@/store/useCanvasStore';

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

import { useCanvasState } from '@/hooks/useCanvasState';

import { useWindowSize } from '@/hooks/useWindowSize';
import { useCanvasInteraction } from '@/hooks/useCanvasInteraction';
import { useCanvasShortcuts } from '@/hooks/useCanvasShortcuts';
import { useArrowHandles } from '@/hooks/useArrowHandles';

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

export default function Canvas() {
const stageScale = useCanvasStore((state) => state.stageScale);
const stagePos = useCanvasStore((state) => state.stagePos);
const canvasWidth = useCanvasStore((state) => state.canvasWidth);
const canvasHeight = useCanvasStore((state) => state.canvasHeight);
const items = useCanvasStore((state) => state.items);
const selectedId = useCanvasStore((state) => state.selectedId);
const editingTextId = useCanvasStore((state) => state.editingTextId);
const selectItem = useCanvasStore((state) => state.selectItem);
const updateItem = useCanvasStore((state) => state.updateItem);
const deleteItem = useCanvasStore((state) => state.deleteItem);
const setEditingTextId = useCanvasStore((state) => state.setEditingTextId);
const {
stageScale,
stagePos,
canvasWidth,
canvasHeight,
items,
selectedId,
editingTextId,
selectItem,
updateItem,
setEditingTextId,
} = useCanvasState();

const stageRef = useRef<Konva.Stage | null>(null);
const [isDraggingArrow, setIsDraggingArrow] = useState(false);
Expand Down Expand Up @@ -63,37 +70,12 @@ export default function Canvas() {
updateItem,
});

// 키보드 삭제
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!selectedId || editingTextId) return;

if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();

// 화살표 중간점 삭제 시도
if (isArrowSelected && selectedHandleIndex !== null) {
const deleted = deleteControlPoint();
if (deleted) return;
}

// 아이템 삭제
deleteItem(selectedId);
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
selectedId,
editingTextId,
deleteItem,
useCanvasShortcuts({
isArrowSelected,
selectedHandleIndex,
deleteControlPoint,
]);
});

// 선택 해제
const handleCheckDeselect = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => {
Expand All @@ -108,7 +90,6 @@ export default function Canvas() {
}
};

// 아이템 업데이트
const handleItemChange = (
id: string,
newAttributes: Partial<WhiteboardItem>,
Expand Down Expand Up @@ -141,7 +122,6 @@ export default function Canvas() {
clipWidth={canvasWidth}
clipHeight={canvasHeight}
>
{/* Canvas 경계 */}
<Rect
name="bg-rect"
x={0}
Expand All @@ -154,7 +134,6 @@ export default function Canvas() {
listening={true}
/>

{/* 아이템 렌더링 */}
{items.map((item) => (
<RenderItem
key={item.id}
Expand All @@ -178,7 +157,6 @@ export default function Canvas() {
/>
))}

{/* 화살표 핸들 (드래그 중이 아닐 때만) */}
{isArrowSelected && selectedItem && !isDraggingArrow && (
<ArrowHandles
arrow={selectedItem as ArrowItem}
Expand All @@ -190,7 +168,6 @@ export default function Canvas() {
/>
)}

{/* Transformer */}
<ItemTransformer
selectedId={selectedId}
items={items}
Expand All @@ -199,7 +176,6 @@ export default function Canvas() {
</Layer>
</Stage>

{/* 텍스트 편집 모드 */}
{editingTextId && editingItem && (
<TextArea
textId={editingTextId}
Expand Down
16 changes: 6 additions & 10 deletions frontend/src/components/whiteboard/items/RenderItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

import { Text, Arrow } from 'react-konva';
import { useCanvasStore } from '@/store/useCanvasStore';
import type {
TextItem,
ArrowItem,
WhiteboardItem,
} from '@/types/whiteboard';
import type { TextItem, ArrowItem, WhiteboardItem } from '@/types/whiteboard';

interface RenderItemProps {
item: WhiteboardItem;
Expand Down Expand Up @@ -86,7 +82,7 @@ export default function RenderItem({
{...arrowItem}
id={item.id}
draggable
hitStrokeWidth={30}
hitStrokeWidth={30}
onMouseDown={() => onSelect(item.id)}
onMouseEnter={(e) => {
const container = e.target.getStage()?.container();
Expand All @@ -109,15 +105,15 @@ export default function RenderItem({
onDragEnd={(e) => {
const pos = e.target.position();
const newPoints = arrowItem.points.map((p, i) =>
i % 2 === 0 ? p + pos.x : p + pos.y
i % 2 === 0 ? p + pos.x : p + pos.y,
);

e.target.position({ x: 0, y: 0 });

onChange({
points: newPoints,
});

onDragEnd?.();
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ interface ArrowHandlesProps {
selectedHandleIndex: number | null;
onHandleClick: (e: KonvaEventObject<MouseEvent>, index: number) => void;
onStartDrag: (e: KonvaEventObject<DragEvent>) => void;
onControlPointDrag: (pointIndex: number, e: KonvaEventObject<DragEvent>) => void;
onControlPointDrag: (
pointIndex: number,
e: KonvaEventObject<DragEvent>,
) => void;
onEndDrag: (e: KonvaEventObject<DragEvent>) => void;
}

Expand Down
16 changes: 7 additions & 9 deletions frontend/src/components/whiteboard/items/text/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function TextArea({

useEffect(() => {
if (!stageRef.current) return;

const textNode = stageRef.current.findOne('#' + textId) as Konva.Text;
if (!textNode) return;

Expand All @@ -43,10 +43,10 @@ export default function TextArea({

useEffect(() => {
if (!ref.current || !stageRef.current) return;

const textNode = stageRef.current.findOne('#' + textId) as Konva.Text;
if (!textNode) return;

const textarea = ref.current;

//textNode와 스타일 동기화
Expand Down Expand Up @@ -122,12 +122,10 @@ export default function TextArea({
const canvasWidth = nodeWidth / stageScale;
const canvasHeight = textarea.offsetHeight / stageScale;

const hasWidthChanged = Math.abs(
canvasWidth - lastBoundsRef.current.width,
) > 0.5;
const hasHeightChanged = Math.abs(
canvasHeight - lastBoundsRef.current.height,
) > 0.5;
const hasWidthChanged =
Math.abs(canvasWidth - lastBoundsRef.current.width) > 0.5;
const hasHeightChanged =
Math.abs(canvasHeight - lastBoundsRef.current.height) > 0.5;

if (!hasWidthChanged && !hasHeightChanged) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '@/assets/icons/whiteboard';

// Type import
import { ToolType, PanelType } from '@/types/whiteboardUI';
import { ToolType, PanelType } from '@/types/whiteboard/whiteboardUI';

// Constants import
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import NavButton from '@/components/whiteboard/common/NavButton';
import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';
import { useAddWhiteboardItem } from '@/hooks/useAddWhiteboardItem';

// ArrowRight -> / doubleArrow <-> / ChevronArrow >>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import NavButton from '@/components/whiteboard/common/NavButton';

import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';

import { CursorIcon, HandIcon } from '@/assets/icons/whiteboard';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import NavButton from '@/components/whiteboard/common/NavButton';

import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';

import { LineIcon } from '@/assets/icons/whiteboard';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import NavButton from '@/components/whiteboard/common/NavButton';

import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';

import { ImageIcon } from '@/assets/icons/common';
import { VideoIcon, YoutubeIcon } from '@/assets/icons/whiteboard';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import NavButton from '@/components/whiteboard/common/NavButton';

import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';

import {
CircleIcon,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';

// Stack 아이콘 관련
// import { STACK_LIST } from '@/constants/stackList';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import NavButton from '@/components/whiteboard/common/NavButton';
import { PanelProps } from '@/types/whiteboardUI';
import { PanelProps } from '@/types/whiteboard/whiteboardUI';
import { TextBoxIcon } from '@/assets/icons/whiteboard';
import { useAddWhiteboardItem } from '@/hooks/useAddWhiteboardItem';

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/constants/whiteboard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ToolType } from '@/types/whiteboardUI';
import { ToolType } from '@/types/whiteboard/whiteboardUI';

export const CURSOR_TOOLS: ToolType[] = ['move', 'select'];
export const SHAPE_TOOLS: ToolType[] = [
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/hooks/useArrowHandles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export function useArrowHandles({
stageRef,
updateItem,
}: UseArrowHandlesProps) {
const [selectedHandleIndex, setSelectedHandleIndex] = useState<number | null>(null);
const [selectedHandleIndex, setSelectedHandleIndex] = useState<number | null>(
null,
);

// 화살표 더블클릭 - 중간점 추가
const handleArrowDblClick = (arrowId: string) => {
Expand Down Expand Up @@ -58,7 +60,10 @@ export function useArrowHandles({
};

// 화살표 핸들 클릭
const handleHandleClick = (e: KonvaEventObject<MouseEvent>, index: number) => {
const handleHandleClick = (
e: KonvaEventObject<MouseEvent>,
index: number,
) => {
e.cancelBubble = true;
setSelectedHandleIndex(index);
};
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/hooks/useCanvasShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect } from 'react';
import { useCanvasStore } from '@/store/useCanvasStore';

interface UseCanvasShortcutsProps {
isArrowSelected: boolean;
selectedHandleIndex: number | null;
deleteControlPoint: () => boolean;
}

export const useCanvasShortcuts = ({
isArrowSelected,
selectedHandleIndex,
deleteControlPoint,
}: UseCanvasShortcutsProps) => {
const selectedId = useCanvasStore((state) => state.selectedId);
const editingTextId = useCanvasStore((state) => state.editingTextId);
const deleteItem = useCanvasStore((state) => state.deleteItem);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!selectedId || editingTextId) return;

if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();

if (isArrowSelected && selectedHandleIndex !== null) {
const deleted = deleteControlPoint();
if (deleted) return;
}

deleteItem(selectedId);
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
selectedId,
editingTextId,
deleteItem,
isArrowSelected,
selectedHandleIndex,
deleteControlPoint,
]);
};
Loading