diff --git a/frontend/src/api/imageApi.ts b/frontend/src/api/imageApi.ts index 92fb8de..293f043 100644 --- a/frontend/src/api/imageApi.ts +++ b/frontend/src/api/imageApi.ts @@ -42,7 +42,7 @@ export async function changeImageStatus( export async function uploadImageList(projectId: number, folderId: number, memberId: number, imageList: File[]) { return api .post( - `/projects/${projectId}/folders/${folderId}/images`, + `/projects/${projectId}/folders/${folderId}/images/file`, { imageList }, { params: { memberId }, @@ -51,43 +51,26 @@ export async function uploadImageList(projectId: number, folderId: number, membe .then(({ data }) => data); } -export async function uploadImageFolder(memberId: number, projectId: number, files: File[], parentId: number = 0) { +export async function uploadImageFolder(memberId: number, projectId: number, files: File[]) { const formData = new FormData(); files.forEach((file) => { - formData.append('files', file); + formData.append('folderZip', file); }); return api - .post( - `/projects/${projectId}/folders/${0}/images/upload`, - { folderZip: files, parentId }, - { - params: { memberId }, - } - ) - .then(({ data }) => data) - .catch((error) => { - return Promise.reject(error); - }); -} - -export async function uploadImageFolderZip(memberId: number, projectId: number, file: File, parentId: number = 0) { - const formData = new FormData(); - formData.append('folderZip', file); - formData.append('parentId', parentId.toString()); - - // const jsonData = { - // parentId, - // }; - // const blob = new Blob([JSON.stringify(jsonData)], { type: 'application/json' }); - // formData.append('parentId', blob); - - return api - .post(`/projects/${projectId}/folders/${0}/images/upload`, formData, { + .post(`/projects/${projectId}/folders/${0}/images/zip`, formData, { params: { memberId }, }) - .then(({ data }) => data) - .catch((error) => { - return Promise.reject(error); - }); + .then(({ data }) => data); +} + +export async function uploadImageZip(memberId: number, projectId: number, file: File) { + const formData = new FormData(); + formData.append('folderZip', file); + + return api + .post(`/projects/${projectId}/folders/${0}/images/zip`, formData, { + params: { memberId }, + }) + .then(({ data }) => data); } diff --git a/frontend/src/api/lablingApi.ts b/frontend/src/api/lablingApi.ts index 404dbed..7d53bef 100644 --- a/frontend/src/api/lablingApi.ts +++ b/frontend/src/api/lablingApi.ts @@ -1,12 +1,13 @@ import api from '@/api/axiosConfig'; -import { LabelingRequest } from '@/types'; -export async function saveImageLabels(projectId: number, imageId: number, memberId: number, data: LabelingRequest) { - return api - .post(`/projects/${projectId}/label/image/${imageId}`, data, { - params: { memberId }, - }) - .then(({ data }) => data); +export async function saveImageLabels( + projectId: number, + imageId: number, + data: { + data: string; + } +) { + return api.post(`/projects/${projectId}/label/image/${imageId}`, data).then(({ data }) => data); } export async function runAutoLabel(projectId: number, memberId: number) { diff --git a/frontend/src/components/CanvasControlBar/index.tsx b/frontend/src/components/CanvasControlBar/index.tsx index 134dca3..56fd7c1 100644 --- a/frontend/src/components/CanvasControlBar/index.tsx +++ b/frontend/src/components/CanvasControlBar/index.tsx @@ -1,8 +1,8 @@ import useCanvasStore from '@/stores/useCanvasStore'; -import { LucideIcon, MousePointer2, PenTool, Square } from 'lucide-react'; +import { LucideIcon, MousePointer2, PenTool, Save, Square } from 'lucide-react'; import { cn } from '@/lib/utils'; -export default function CanvasControlBar() { +export default function CanvasControlBar({ saveJson }: { saveJson: () => void }) { const drawState = useCanvasStore((state) => state.drawState); const setDrawState = useCanvasStore((state) => state.setDrawState); const buttonBaseClassName = 'rounded-lg p-2 transition-colors '; @@ -31,6 +31,16 @@ export default function CanvasControlBar() { ); })} + + ); } diff --git a/frontend/src/components/ImageCanvas/LabelRect.tsx b/frontend/src/components/ImageCanvas/LabelRect.tsx index 5d5a9df..182583c 100644 --- a/frontend/src/components/ImageCanvas/LabelRect.tsx +++ b/frontend/src/components/ImageCanvas/LabelRect.tsx @@ -7,11 +7,13 @@ export default function LabelRect({ isSelected, onSelect, info, + setLabel, dragLayer, }: { isSelected: boolean; onSelect: (evt: Konva.KonvaEventObject) => void; info: Label; + setLabel: (coordinate: [number, number][]) => void; dragLayer: Konva.Layer; }) { const rectRef = useRef(null); @@ -28,13 +30,19 @@ export default function LabelRect({ trRef.current?.moveToTop(); }; const handleMoveEnd = () => { - const rectPoints = rectRef.current?.points(); - const points = [ - [rectPoints![0], rectPoints![1]], - [rectPoints![4], rectPoints![5]], + const rect = rectRef.current?.getPosition(); + const scale = rectRef.current?.scale(); + + if (!rect || !scale) return; + + const points: [number, number][] = [ + [info.coordinates[0][0] * scale.x + rect.x, info.coordinates[0][1] * scale.y + rect.y], + [info.coordinates[1][0] * scale.x + rect.x, info.coordinates[1][1] * scale.y + rect.y], ]; - console.log(points); + setLabel(points); + rectRef.current?.setAbsolutePosition({ x: 0, y: 0 }); + rectRef.current?.scale({ x: 1, y: 1 }); }; useEffect(() => { diff --git a/frontend/src/components/ImageCanvas/PolygonTransformer.tsx b/frontend/src/components/ImageCanvas/PolygonTransformer.tsx index 639799e..5f310ef 100644 --- a/frontend/src/components/ImageCanvas/PolygonTransformer.tsx +++ b/frontend/src/components/ImageCanvas/PolygonTransformer.tsx @@ -1,4 +1,5 @@ import Konva from 'konva'; +import { Vector2d } from 'konva/lib/types'; import { useEffect, useRef } from 'react'; import { Circle, Group, Line } from 'react-konva'; @@ -24,6 +25,24 @@ const TRANSFORM_CHANGE_STR = [ export default function PolygonTransformer({ coordinates, setCoordinates, stage, dragLayer }: PolygonTransformerProps) { const anchorsRef = useRef(null); + const scale: Vector2d = { x: 1 / stage.getAbsoluteScale().x, y: 1 / stage.getAbsoluteScale().y }; + const handleClick = (index: number) => (e: Konva.KonvaEventObject) => { + if (e.evt.button === 0 && e.evt.detail === 2) { + const pos = stage.getRelativePointerPosition()!; + const newCoordinates: [number, number][] = [ + ...coordinates.slice(0, index + 1), + [pos.x, pos.y], + ...coordinates.slice(index + 1), + ]; + setCoordinates(newCoordinates); + return; + } + + if (e.evt.button !== 2) return; + + const newCoordinates = [...coordinates.slice(0, index), ...coordinates.slice(index + 1)]; + setCoordinates(newCoordinates); + }; const handleDragMove = (index: number) => (e: Konva.KonvaEventObject) => { const circle = e.target as Konva.Circle; const pos = circle.position(); @@ -31,7 +50,7 @@ export default function PolygonTransformer({ coordinates, setCoordinates, stage, newCoordinates[index] = [pos.x, pos.y]; setCoordinates(newCoordinates); - stage.batchDraw(); + // stage.batchDraw(); }; const handleMouseOver = (e: Konva.KonvaEventObject) => { const circle = e.target as Konva.Circle; @@ -94,10 +113,11 @@ export default function PolygonTransformer({ coordinates, setCoordinates, stage, fill="white" draggable strokeScaleEnabled={false} + onClick={handleClick(index)} onDragMove={handleDragMove(index)} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} - scale={{ x: 1 / stage.getAbsoluteScale().x, y: 1 / stage.getAbsoluteScale().y }} + scale={scale} perfectDrawEnabled={false} shadowForStrokeEnabled={false} /> diff --git a/frontend/src/components/ImageCanvas/index.tsx b/frontend/src/components/ImageCanvas/index.tsx index d755f4b..4a17871 100644 --- a/frontend/src/components/ImageCanvas/index.tsx +++ b/frontend/src/components/ImageCanvas/index.tsx @@ -9,9 +9,11 @@ import LabelPolygon from './LabelPolygon'; import CanvasControlBar from '../CanvasControlBar'; import useLabelJsonQuery from '@/queries/labelJson/useLabelJsonQuery'; import { Label } from '@/types'; +import { useParams } from 'react-router-dom'; export default function ImageCanvas() { - const { imagePath, dataPath } = useCanvasStore((state) => state.image)!; + const { projectId } = useParams<{ projectId: string }>(); + const { id: imageId, imagePath, dataPath } = useCanvasStore((state) => state.image)!; const { data: labelData } = useLabelJsonQuery(dataPath); const { shapes } = labelData; const selectedLabelId = useCanvasStore((state) => state.selectedLabelId); @@ -43,6 +45,25 @@ export default function ImageCanvas() { ); }, [setLabels, shapes]); + const setLabel = (index: number) => (coordinates: [number, number][]) => { + const newLabels = [...labels]; + newLabels[index].coordinates = coordinates; + setLabels(newLabels); + }; + const saveJson = () => { + const json = JSON.stringify({ + ...labelData, + shapes: labels.map(({ name, color, coordinates, type }) => ({ + label: name, + color, + shape_type: type === 'polygon' ? 'polygon' : 'rectangle', + points: coordinates, + })), + }); + + console.log(projectId, imageId, json); + // TOOD: api 연결 + }; const startDrawRect = () => { const { x, y } = stageRef.current!.getRelativePointerPosition()!; setRectPoints([ @@ -250,6 +271,7 @@ export default function ImageCanvas() { isSelected={label.id === selectedLabelId} onSelect={() => setSelectedLabelId(label.id)} info={label} + setLabel={setLabel(label.id)} dragLayer={dragLayerRef.current as Konva.Layer} /> ) : ( @@ -312,7 +334,7 @@ export default function ImageCanvas() { - + ) : (
diff --git a/frontend/src/components/ImageFolderUploadModal/ImageFolderUploadForm.tsx b/frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx similarity index 94% rename from frontend/src/components/ImageFolderUploadModal/ImageFolderUploadForm.tsx rename to frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx index f73f227..da6c92f 100644 --- a/frontend/src/components/ImageFolderUploadModal/ImageFolderUploadForm.tsx +++ b/frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx @@ -5,15 +5,7 @@ import { uploadImageFolder } from '@/api/imageApi'; import useAuthStore from '@/stores/useAuthStore'; import { X } from 'lucide-react'; -export default function ImageFolderUploadForm({ - onClose, - projectId, - parentId, -}: { - onClose: () => void; - projectId: number; - parentId: number; -}) { +export default function ImageUploadFolderForm({ onClose, projectId }: { onClose: () => void; projectId: number }) { const profile = useAuthStore((state) => state.profile); const memberId = profile?.id || 0; @@ -57,7 +49,7 @@ export default function ImageFolderUploadForm({ setIsUploading(true); setProgress(0); - await uploadImageFolder(memberId, projectId, files, parentId) + await uploadImageFolder(memberId, projectId, files) .then(() => { setProgress(100); }) diff --git a/frontend/src/components/ImageFolderUploadModal/index.tsx b/frontend/src/components/ImageUploadFolderModal/index.tsx similarity index 77% rename from frontend/src/components/ImageFolderUploadModal/index.tsx rename to frontend/src/components/ImageUploadFolderModal/index.tsx index 2a98405..3bf0a10 100644 --- a/frontend/src/components/ImageFolderUploadModal/index.tsx +++ b/frontend/src/components/ImageUploadFolderModal/index.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; import { Plus } from 'lucide-react'; -import ImageFolderUploadForm from './ImageFolderUploadForm'; +import ImageUploadFolderForm from './ImageUploadFolderForm'; -export default function ImageFolderUploadModal({ projectId, parentId = 0 }: { projectId: number; parentId: number }) { +export default function ImageUploadFolderModal({ projectId }: { projectId: number }) { const [isOpen, setIsOpen] = React.useState(false); const handleOpen = () => setIsOpen(true); @@ -24,10 +24,9 @@ export default function ImageFolderUploadModal({ projectId, parentId = 0 }: { pr - diff --git a/frontend/src/components/ImageFolderZipUploadModal/ImageFolderZipUploadForm.tsx b/frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx similarity index 92% rename from frontend/src/components/ImageFolderZipUploadModal/ImageFolderZipUploadForm.tsx rename to frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx index 39ac97c..865f8a4 100644 --- a/frontend/src/components/ImageFolderZipUploadModal/ImageFolderZipUploadForm.tsx +++ b/frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx @@ -1,19 +1,11 @@ import { useState } from 'react'; import { Button } from '../ui/button'; import { cn } from '@/lib/utils'; -import { uploadImageFolderZip } from '@/api/imageApi'; +import { uploadImageZip } from '@/api/imageApi'; import useAuthStore from '@/stores/useAuthStore'; import { X } from 'lucide-react'; -export default function ImageFolderZipUploadForm({ - onClose, - projectId, - parentId, -}: { - onClose: () => void; - projectId: number; - parentId: number; -}) { +export default function ImageUploadZipForm({ onClose, projectId }: { onClose: () => void; projectId: number }) { const profile = useAuthStore((state) => state.profile); const memberId = profile?.id || 0; @@ -58,7 +50,7 @@ export default function ImageFolderZipUploadForm({ setIsUploading(true); setProgress(0); - await uploadImageFolderZip(memberId, projectId, file, parentId) + await uploadImageZip(memberId, projectId, file) .then(() => { setProgress(100); }) diff --git a/frontend/src/components/ImageFolderZipUploadModal/index.tsx b/frontend/src/components/ImageUploadZipModal/index.tsx similarity index 75% rename from frontend/src/components/ImageFolderZipUploadModal/index.tsx rename to frontend/src/components/ImageUploadZipModal/index.tsx index d143280..b0a1e17 100644 --- a/frontend/src/components/ImageFolderZipUploadModal/index.tsx +++ b/frontend/src/components/ImageUploadZipModal/index.tsx @@ -1,15 +1,9 @@ import React from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; import { Plus } from 'lucide-react'; -import ImageFolderZipUploadForm from './ImageFolderZipUploadForm'; +import ImageUploadZipForm from './ImageUploadZipForm'; -export default function ImageFolderZipUploadModal({ - projectId, - parentId = 0, -}: { - projectId: number; - parentId: number; -}) { +export default function ImageUploadZipModal({ projectId }: { projectId: number }) { const [isOpen, setIsOpen] = React.useState(false); const handleOpen = () => setIsOpen(true); @@ -30,10 +24,9 @@ export default function ImageFolderZipUploadModal({ - diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index cd8476b..9ca080f 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -1,9 +1,10 @@ import { Project } from '@/types'; -import { SquarePenIcon, Upload } from 'lucide-react'; +import { Play, SquarePenIcon, Upload } from 'lucide-react'; import ProjectFileItem from './ProjectFileItem'; import ProjectDirectoryItem from './ProjectDirectoryItem'; import useFolderQuery from '@/queries/folders/useFolderQuery'; import useCanvasStore from '@/stores/useCanvasStore'; +import { Button } from '../ui/button'; export default function ProjectStructure({ project }: { project: Project }) { const image = useCanvasStore((state) => state.image); @@ -22,46 +23,62 @@ export default function ProjectStructure({ project }: { project: Project }) { ]; return ( -
-
-
-

{project.type}

-
- + +
+ {folderData.children.length === 0 && folderData.images.length === 0 ? ( +
+ 빈 프로젝트입니다. +
+ ) : ( +
+ {folderData.children.map((item) => ( + + ))} + {folderData.images.map((item) => ( + + ))} +
+ )} +
+ +
+ - - - {folderData.children.length === 0 && folderData.images.length === 0 ? ( -
- 빈 프로젝트입니다. -
- ) : ( -
- {folderData.children.map((item) => ( - - ))} - {folderData.images.map((item) => ( - - ))} -
- )} + + 자동 레이블링 + +
); } diff --git a/frontend/src/components/WorkspaceSidebar/index.tsx b/frontend/src/components/WorkspaceSidebar/index.tsx index 57431b4..ef6931c 100644 --- a/frontend/src/components/WorkspaceSidebar/index.tsx +++ b/frontend/src/components/WorkspaceSidebar/index.tsx @@ -14,9 +14,7 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac const setSidebarSize = useCanvasStore((state) => state.setSidebarSize); const navigate = useNavigate(); const { workspaceId } = useParams<{ workspaceId: string }>(); - // const [selectedProjectId, setSelectedProjectId] = useState(); const handleSelectProject = (projectId: string) => { - // setSelectedProjectId(projectId); navigate(`${webPath.workspace()}/${workspaceId}/${projectId}`); }; diff --git a/frontend/src/pages/ImageFolderUploadTest.tsx b/frontend/src/pages/ImageFolderUploadTest.tsx index 266ccf6..1eb567b 100644 --- a/frontend/src/pages/ImageFolderUploadTest.tsx +++ b/frontend/src/pages/ImageFolderUploadTest.tsx @@ -1,5 +1,5 @@ -import ImageFolderUploadModal from '@/components/ImageFolderUploadModal'; -import ImageFolderZipUploadModal from '@/components/ImageFolderZipUploadModal'; +import ImageUploadFolderModal from '@/components/ImageUploadFolderModal'; +import ImageUploadZipModal from '@/components/ImageUploadZipModal'; import { useParams } from 'react-router-dom'; export default function ImageFolderUploadTest() { @@ -8,14 +8,8 @@ export default function ImageFolderUploadTest() { return (
- - + +
); }