From 65d494a15a7d44cb1f85cb9e0f9d974f4c0c0d37 Mon Sep 17 00:00:00 2001 From: jhynsoo Date: Thu, 5 Sep 2024 09:30:54 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=BA=94=EB=B2=84=EC=8A=A4=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EA=B0=81=ED=98=95=20=EA=B7=B8=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20-=20S11P21S002-135?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CanvasControlBar/index.tsx | 12 +- .../src/components/Footer/index.stories.tsx | 1 + .../src/components/Header/index.stories.tsx | 1 + .../components/ImageCanvas/index.stories.tsx | 1 + frontend/src/components/ImageCanvas/index.tsx | 157 +++++++++++++----- .../ImageUploadModal/index.stories.tsx | 1 + .../MemberAddModal/index.stories.tsx | 1 + .../components/ProjectCard/index.stories.tsx | 1 + .../ReviewQuest/ReviewRequestItem.stories.tsx | 1 + .../components/ReviewQuest/index.stories.tsx | 1 + .../WorkspaceLabelBar/index.stories.tsx | 7 + frontend/src/stores/useCanvasStore.ts | 4 + 12 files changed, 148 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/CanvasControlBar/index.tsx b/frontend/src/components/CanvasControlBar/index.tsx index 8b01ed6..134dca3 100644 --- a/frontend/src/components/CanvasControlBar/index.tsx +++ b/frontend/src/components/CanvasControlBar/index.tsx @@ -1,9 +1,13 @@ +import useCanvasStore from '@/stores/useCanvasStore'; import { LucideIcon, MousePointer2, PenTool, Square } from 'lucide-react'; import { cn } from '@/lib/utils'; export default function CanvasControlBar() { + const drawState = useCanvasStore((state) => state.drawState); + const setDrawState = useCanvasStore((state) => state.setDrawState); const buttonBaseClassName = 'rounded-lg p-2 transition-colors '; const buttonClassName = 'hover:bg-gray-100'; + const activeButtonClassName = 'bg-primary stroke-white'; const controls: { [key: string]: LucideIcon } = { pointer: MousePointer2, rect: Square, @@ -17,9 +21,13 @@ export default function CanvasControlBar() { return ( ); })} diff --git a/frontend/src/components/Footer/index.stories.tsx b/frontend/src/components/Footer/index.stories.tsx index c865462..d1f7c41 100644 --- a/frontend/src/components/Footer/index.stories.tsx +++ b/frontend/src/components/Footer/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import type { Meta, StoryObj } from '@storybook/react'; import Footer from './index'; diff --git a/frontend/src/components/Header/index.stories.tsx b/frontend/src/components/Header/index.stories.tsx index 4bce462..5cbc684 100644 --- a/frontend/src/components/Header/index.stories.tsx +++ b/frontend/src/components/Header/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import type { Meta, StoryObj } from '@storybook/react'; import Header from './index'; diff --git a/frontend/src/components/ImageCanvas/index.stories.tsx b/frontend/src/components/ImageCanvas/index.stories.tsx index 279a5ec..1885fb8 100644 --- a/frontend/src/components/ImageCanvas/index.stories.tsx +++ b/frontend/src/components/ImageCanvas/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import { Meta } from '@storybook/react'; import ImageCanvas from '.'; diff --git a/frontend/src/components/ImageCanvas/index.tsx b/frontend/src/components/ImageCanvas/index.tsx index 9b56bdb..7bf4ffb 100644 --- a/frontend/src/components/ImageCanvas/index.tsx +++ b/frontend/src/components/ImageCanvas/index.tsx @@ -1,12 +1,13 @@ import useCanvasStore from '@/stores/useCanvasStore'; import Konva from 'konva'; import { useEffect, useRef, useState } from 'react'; -import { Image, Layer, Stage } from 'react-konva'; +import { Image, Layer, Rect, Stage } from 'react-konva'; import useImage from 'use-image'; import { Label } from '@/types'; import LabelRect from './LabelRect'; import { Vector2d } from 'konva/lib/types'; import LabelPolygon from './LabelPolygon'; +import CanvasControlBar from '../CanvasControlBar'; const mockLabels: Label[] = Array.from({ length: 10 }, (_, i) => { const startX = Math.random() * 1200 + 300; @@ -45,11 +46,59 @@ export default function ImageCanvas() { const labels = useCanvasStore((state) => state.labels) ?? []; const [selectedId, setSelectedId] = useState(null); const [image, imageStatus] = useImage(imageUrl); + const [rectPoints, setRectPoints] = useState<[number, number][]>([]); + const drawState = useCanvasStore((state) => state.drawState); + const addLabel = useCanvasStore((state) => state.addLabel); + const startDrawRect = () => { + const { x, y } = stageRef.current!.getRelativePointerPosition()!; + setRectPoints([ + [x, y], + [x, y], + ]); + }; + const updateDrawingRect = () => { + if (rectPoints.length === 0) return; + + const { x, y } = stageRef.current!.getRelativePointerPosition()!; + setRectPoints([rectPoints[0], [x, y]]); + }; + const endDrawRect = () => { + if (drawState !== 'rect' || rectPoints.length === 0) return; + if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) { + setRectPoints([]); + return; + } + setRectPoints([]); + const color = Math.floor(Math.random() * 65535) + .toString(16) + .padStart(4, '0'); + addLabel({ + id: labels.length, + name: 'label', + type: 'rect', + color: `#${color}ff`, + coordinates: rectPoints, + }); + }; const handleClick = (e: Konva.KonvaEventObject) => { + const isLeftMouseClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0; + + if (drawState !== 'pointer' && isLeftMouseClicked) { + stageRef.current?.stopDrag(); + if (drawState === 'rect') { + startDrawRect(); + } + return; + } if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') { setSelectedId(null); } }; + const handleMouseMove = () => { + if (drawState === 'rect' && rectPoints.length) { + updateDrawingRect(); + } + }; const handleZoom = (e: Konva.KonvaEventObject) => { const scaleBy = 1.05; const oldScale = scale.current; @@ -98,45 +147,77 @@ export default function ImageCanvas() { useCanvasStore.setState({ labels: mockLabels }); }, []); + useEffect(() => { + if (!stageRef.current) return; + stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair'; + + if (drawState !== 'pointer') { + setSelectedId(null); + } + }, [drawState]); + return imageStatus === 'loaded' ? ( - - - - - - {labels.map((label) => - label.type === 'rect' ? ( - setSelectedId(label.id)} - info={label} - dragLayer={dragLayerRef.current as Konva.Layer} +
+ + + + + + {labels.map((label) => + label.type === 'rect' ? ( + setSelectedId(label.id)} + info={label} + dragLayer={dragLayerRef.current as Konva.Layer} + /> + ) : ( + setSelectedId(label.id)} + info={label} + stage={stageRef.current as Konva.Stage} + dragLayer={dragLayerRef.current as Konva.Layer} + /> + ) + )} + {rectPoints.length ? ( + - ) : ( - setSelectedId(label.id)} - info={label} - stage={stageRef.current as Konva.Stage} - dragLayer={dragLayerRef.current as Konva.Layer} - /> - ) - )} - - - + ) : null} + + + + + +
) : (
); diff --git a/frontend/src/components/ImageUploadModal/index.stories.tsx b/frontend/src/components/ImageUploadModal/index.stories.tsx index b8a7599..12c044c 100644 --- a/frontend/src/components/ImageUploadModal/index.stories.tsx +++ b/frontend/src/components/ImageUploadModal/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import { useState } from 'react'; import ImageUploadModal from './index'; diff --git a/frontend/src/components/MemberAddModal/index.stories.tsx b/frontend/src/components/MemberAddModal/index.stories.tsx index 790dd6d..bf8c9d4 100644 --- a/frontend/src/components/MemberAddModal/index.stories.tsx +++ b/frontend/src/components/MemberAddModal/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import MemberAddModal from '.'; export default { diff --git a/frontend/src/components/ProjectCard/index.stories.tsx b/frontend/src/components/ProjectCard/index.stories.tsx index 2e3025c..dbfd653 100644 --- a/frontend/src/components/ProjectCard/index.stories.tsx +++ b/frontend/src/components/ProjectCard/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import { Meta, StoryObj } from '@storybook/react'; import ProjectCard from '.'; diff --git a/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx b/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx index e9e1a22..a58e92d 100644 --- a/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx +++ b/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import type { Meta, StoryObj } from '@storybook/react'; import ReviewRequestItem from './ReviewRequestItem'; diff --git a/frontend/src/components/ReviewQuest/index.stories.tsx b/frontend/src/components/ReviewQuest/index.stories.tsx index 9d11e7c..21fb3df 100644 --- a/frontend/src/components/ReviewQuest/index.stories.tsx +++ b/frontend/src/components/ReviewQuest/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import type { Meta, StoryObj } from '@storybook/react'; import ReviewRequest from '.'; diff --git a/frontend/src/components/WorkspaceLabelBar/index.stories.tsx b/frontend/src/components/WorkspaceLabelBar/index.stories.tsx index 5b45dee..f768e44 100644 --- a/frontend/src/components/WorkspaceLabelBar/index.stories.tsx +++ b/frontend/src/components/WorkspaceLabelBar/index.stories.tsx @@ -1,3 +1,4 @@ +import '@/index.css'; import { Meta } from '@storybook/react'; import WorkspaceLabelBar from '.'; import { Label } from '@/types'; @@ -17,16 +18,22 @@ const labels: Label[] = [ id: 1, name: 'Label 1', color: '#FFaa33', + type: 'rect', + coordinates: [], }, { id: 2, name: 'Label 2', color: '#aaFF55', + type: 'rect', + coordinates: [], }, { id: 3, name: 'Label 3', color: '#77aaFF', + type: 'rect', + coordinates: [], }, ]; diff --git a/frontend/src/stores/useCanvasStore.ts b/frontend/src/stores/useCanvasStore.ts index 60ed8a5..234cc72 100644 --- a/frontend/src/stores/useCanvasStore.ts +++ b/frontend/src/stores/useCanvasStore.ts @@ -4,19 +4,23 @@ import { create } from 'zustand'; interface CanvasState { image: string; labels: Label[]; + drawState: 'pen' | 'rect' | 'pointer'; changeImage: (image: string, labels: Label[]) => void; addLabel: (label: Label) => void; removeLabel: (labelId: number) => void; updateLabel: (label: Label) => void; + setDrawState: (state: 'pen' | 'rect' | 'pointer') => void; } const useCanvasStore = create()((set) => ({ image: '', labels: [], + drawState: 'pointer', changeImage: (image: string, labels: Label[]) => set({ image, labels }), addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })), removeLabel: (labelId: number) => set((state) => ({ labels: state.labels.filter((label) => label.id !== labelId) })), updateLabel: (label: Label) => set((state) => ({ labels: state.labels.map((l) => (l.id === label.id ? label : l)) })), + setDrawState: (drawState) => set({ drawState }), })); export default useCanvasStore;