Merge branch 'fe/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into fe/fix/error-api
This commit is contained in:
commit
ff4ec00d01
@ -11,5 +11,5 @@ export async function saveImageLabels(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runAutoLabel(projectId: number, modelId = 1) {
|
export async function runAutoLabel(projectId: number, modelId = 1) {
|
||||||
return api.post(`/projects/${projectId}/auto`, { modelId }).then(({ data }) => data);
|
return api.post(`/projects/${projectId}/auto`, { modelId }, { timeout: 0 }).then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Label } from '@/types';
|
import { Label } from '@/types';
|
||||||
|
import rectSort from '@/utils/rectSort';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Line, Transformer } from 'react-konva';
|
import { Line, Transformer } from 'react-konva';
|
||||||
@ -39,8 +40,9 @@ export default function LabelRect({
|
|||||||
[info.coordinates[0][0] * scale.x + rect.x, info.coordinates[0][1] * scale.y + rect.y],
|
[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],
|
[info.coordinates[1][0] * scale.x + rect.x, info.coordinates[1][1] * scale.y + rect.y],
|
||||||
];
|
];
|
||||||
|
const sortedPoints = rectSort(points as [[number, number], [number, number]]);
|
||||||
|
|
||||||
setLabel(points);
|
setLabel(sortedPoints);
|
||||||
rectRef.current?.setPosition({ x: 0, y: 0 });
|
rectRef.current?.setPosition({ x: 0, y: 0 });
|
||||||
rectRef.current?.scale({ x: 1, y: 1 });
|
rectRef.current?.scale({ x: 1, y: 1 });
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import useSaveImageLabelsQuery from '@/queries/projects/useSaveImageLabelsQuery';
|
import useSaveImageLabelsQuery from '@/queries/projects/useSaveImageLabelsQuery';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import CommentLabel from './CommentLabel';
|
import CommentLabel from './CommentLabel';
|
||||||
|
import rectSort from '@/utils/rectSort';
|
||||||
|
|
||||||
export default function ImageCanvas() {
|
export default function ImageCanvas() {
|
||||||
const { project, folderId, categories } = useProjectStore();
|
const { project, folderId, categories } = useProjectStore();
|
||||||
@ -188,11 +189,11 @@ export default function ImageCanvas() {
|
|||||||
|
|
||||||
const endDrawRect = () => {
|
const endDrawRect = () => {
|
||||||
if (drawState !== 'rect' || rectPoints.length === 0) return;
|
if (drawState !== 'rect' || rectPoints.length === 0) return;
|
||||||
|
setRectPoints([]);
|
||||||
if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) {
|
if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) {
|
||||||
setRectPoints([]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRectPoints([]);
|
const sortedPoints = rectSort(rectPoints as [[number, number], [number, number]]);
|
||||||
|
|
||||||
const color = Math.floor(Math.random() * 0xffffff)
|
const color = Math.floor(Math.random() * 0xffffff)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
@ -203,7 +204,7 @@ export default function ImageCanvas() {
|
|||||||
categoryId: categories[0]!.id,
|
categoryId: categories[0]!.id,
|
||||||
type: 'rectangle',
|
type: 'rectangle',
|
||||||
color: `#${color}`,
|
color: `#${color}`,
|
||||||
coordinates: rectPoints,
|
coordinates: sortedPoints,
|
||||||
});
|
});
|
||||||
setDrawState('pointer');
|
setDrawState('pointer');
|
||||||
setSelectedLabelId(id);
|
setSelectedLabelId(id);
|
||||||
|
@ -6,7 +6,7 @@ import ImageUploadPresingedForm from '@/components/ImageUploadPresignedModal/Ima
|
|||||||
export default function ImageUploadPresignedModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
export default function ImageUploadPresignedModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||||
|
|
||||||
const handleOpen = () => setIsOpen(true);
|
const handleOpen = () => setIsOpen(true);
|
||||||
const handleClose = () => setIsOpen(false);
|
const handleClose = () => setIsOpen(false);
|
||||||
const handleFileCount = (fileCount: number) => {
|
const handleFileCount = (fileCount: number) => {
|
||||||
@ -27,7 +27,7 @@ export default function ImageUploadPresignedModal({ projectId, folderId }: { pro
|
|||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount}) PreSigned` : '파일 업로드 PreSigned'} />
|
<DialogHeader title={fileCount > 0 ? `파일 업로드 PreSigned (${fileCount})` : '파일 업로드 PreSigned'} />
|
||||||
<ImageUploadPresingedForm
|
<ImageUploadPresingedForm
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onFileCount={handleFileCount}
|
onFileCount={handleFileCount}
|
||||||
|
@ -169,9 +169,19 @@ export default function ProjectCreateForm({ onSubmit }: { onSubmit: (data: Proje
|
|||||||
<div className="mb-1 flex w-full flex-col gap-2">
|
<div className="mb-1 flex w-full flex-col gap-2">
|
||||||
<div className="body-strong">카테고리</div>
|
<div className="body-strong">카테고리</div>
|
||||||
<Tabs defaultValue="manual">
|
<Tabs defaultValue="manual">
|
||||||
<TabsList className="mb-4">
|
<TabsList className="mb-4 rounded-none border-transparent bg-transparent shadow-none">
|
||||||
<TabsTrigger value="manual">직접 입력</TabsTrigger>
|
<TabsTrigger
|
||||||
<TabsTrigger value="file">파일로 입력</TabsTrigger>
|
value="manual"
|
||||||
|
className="rounded-none border-b-2 border-transparent bg-transparent shadow-none data-[state=active]:border-blue-500 data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
직접 입력
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="file"
|
||||||
|
className="rounded-none border-b-2 border-transparent bg-transparent shadow-none data-[state=active]:border-blue-500 data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
파일로 입력
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="manual">
|
<TabsContent value="manual">
|
||||||
<div className="mt-2 flex gap-2">
|
<div className="mt-2 flex gap-2">
|
||||||
@ -192,7 +202,7 @@ export default function ProjectCreateForm({ onSubmit }: { onSubmit: (data: Proje
|
|||||||
<TabsContent value="file">
|
<TabsContent value="file">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-2 flex w-full items-center justify-center rounded-lg border-2 border-dashed p-5 text-center transition-colors',
|
'mt-2 flex h-40 w-full items-center justify-center rounded-lg border-2 border-dashed p-5 text-center transition-colors',
|
||||||
isDragging ? 'border-blue-500 bg-blue-100' : 'border-gray-300 bg-gray-100'
|
isDragging ? 'border-blue-500 bg-blue-100' : 'border-gray-300 bg-gray-100'
|
||||||
)}
|
)}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
@ -200,9 +210,15 @@ export default function ProjectCreateForm({ onSubmit }: { onSubmit: (data: Proje
|
|||||||
onDrop={(event) => handleDrop(event, field.onChange)}
|
onDrop={(event) => handleDrop(event, field.onChange)}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<p className="text-gray-500">
|
{isDragging ? (
|
||||||
파일을 업로드하려면 여기를 클릭하거나 파일을 드래그하여 여기에 놓으세요.
|
<p className="text-primary">드래그한 파일을 여기에 놓으세요</p>
|
||||||
</p>
|
) : (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
파일을 업로드하려면 여기를 클릭하거나
|
||||||
|
<br />
|
||||||
|
파일을 드래그하여 여기에 놓으세요.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
@ -30,7 +30,6 @@ export default function WorkspaceBrowseLayout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const workspaces = workspacesResponse?.workspaceResponses ?? [];
|
const workspaces = workspacesResponse?.workspaceResponses ?? [];
|
||||||
console.log(workspaces);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -12,7 +12,7 @@ import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
|
|||||||
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
|
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
|
||||||
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
||||||
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
||||||
import ImageUploadPresignedForm from '../ImageUploadPresignedModal/ImageUploadPresignedForm.tsx'
|
import ImageUploadPresignedForm from '../ImageUploadPresignedModal/ImageUploadPresignedForm.tsx';
|
||||||
|
|
||||||
export default function WorkspaceDropdownMenu({
|
export default function WorkspaceDropdownMenu({
|
||||||
projectId,
|
projectId,
|
||||||
@ -25,9 +25,10 @@ export default function WorkspaceDropdownMenu({
|
|||||||
}) {
|
}) {
|
||||||
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
|
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
|
||||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||||
|
const [isOpenUploadPresigned, setIsOpenUploadPresigned] = React.useState<boolean>(false);
|
||||||
|
const [presignedCount, setPresignedCount] = React.useState<number>(0);
|
||||||
const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState<boolean>(false);
|
const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadPresigned, setIsOpenUploadPresigned] = React.useState<boolean>(false);
|
|
||||||
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
||||||
|
|
||||||
const handleOpenUploadFile = () => setIsOpenUploadFile(true);
|
const handleOpenUploadFile = () => setIsOpenUploadFile(true);
|
||||||
@ -36,6 +37,20 @@ export default function WorkspaceDropdownMenu({
|
|||||||
setIsOpenUploadFile(false);
|
setIsOpenUploadFile(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileCount = (fileCount: number) => {
|
||||||
|
setFileCount(fileCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenUploadPresigned = () => setIsOpenUploadPresigned(true);
|
||||||
|
|
||||||
|
const handleCloseUploadPresigned = () => {
|
||||||
|
setIsOpenUploadPresigned(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePresignedCount = (fileCount: number) => {
|
||||||
|
setPresignedCount(fileCount);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenUploadFolderFile = () => setIsOpenUploadFolderFile(true);
|
const handleOpenUploadFolderFile = () => setIsOpenUploadFolderFile(true);
|
||||||
|
|
||||||
const handleCloseUploadFolderFile = () => {
|
const handleCloseUploadFolderFile = () => {
|
||||||
@ -48,22 +63,12 @@ export default function WorkspaceDropdownMenu({
|
|||||||
setIsOpenUploadFolder(false);
|
setIsOpenUploadFolder(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenUploadPresigned = () => setIsOpenUploadPresigned(true);
|
|
||||||
|
|
||||||
const handleCloseUploadPresigned = () => {
|
|
||||||
setIsOpenUploadPresigned(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenUploadZip = () => setIsOpenUploadZip(true);
|
const handleOpenUploadZip = () => setIsOpenUploadZip(true);
|
||||||
|
|
||||||
const handleCloseUploadZip = () => {
|
const handleCloseUploadZip = () => {
|
||||||
setIsOpenUploadZip(false);
|
setIsOpenUploadZip(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileCount = (fileCount: number) => {
|
|
||||||
setFileCount(fileCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -83,9 +88,9 @@ export default function WorkspaceDropdownMenu({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleOpenUploadPresigned}>파일 업로드 (PresignedUrl 이용)</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFolderFile}>폴더 업로드 (파일 업로드 API 이용)</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFolderFile}>폴더 업로드 (파일 업로드 API 이용)</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (백엔드 구현 필요)</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (백엔드 구현 필요)</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadPresigned}>폴더 업로드 (PresignedUrl 이용)</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -107,6 +112,25 @@ export default function WorkspaceDropdownMenu({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isOpenUploadPresigned}
|
||||||
|
onOpenChange={setIsOpenUploadPresigned}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader
|
||||||
|
title={presignedCount > 0 ? `파일 업로드 PreSigned (${presignedCount})` : '파일 업로드 PreSigned'}
|
||||||
|
/>
|
||||||
|
<ImageUploadPresignedForm
|
||||||
|
onClose={handleCloseUploadPresigned}
|
||||||
|
onRefetch={onRefetch}
|
||||||
|
onFileCount={handlePresignedCount}
|
||||||
|
projectId={projectId}
|
||||||
|
folderId={folderId}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpenUploadFolderFile}
|
open={isOpenUploadFolderFile}
|
||||||
onOpenChange={setIsOpenUploadFolderFile}
|
onOpenChange={setIsOpenUploadFolderFile}
|
||||||
@ -139,23 +163,6 @@ export default function WorkspaceDropdownMenu({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={isOpenUploadPresigned}
|
|
||||||
onOpenChange={setIsOpenUploadPresigned}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild></DialogTrigger>
|
|
||||||
<DialogContent className="max-w-2xl">
|
|
||||||
<DialogHeader title='파일 업로드' />
|
|
||||||
<ImageUploadPresignedForm
|
|
||||||
onClose={handleCloseUploadPresigned}
|
|
||||||
onRefetch={onRefetch}
|
|
||||||
onFileCount={handleFileCount}
|
|
||||||
projectId={projectId}
|
|
||||||
folderId={folderId}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpenUploadZip}
|
open={isOpenUploadZip}
|
||||||
onOpenChange={setIsOpenUploadZip}
|
onOpenChange={setIsOpenUploadZip}
|
||||||
|
48
frontend/src/utils/rectSort.test.ts
Normal file
48
frontend/src/utils/rectSort.test.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import rectSort from './rectSort';
|
||||||
|
|
||||||
|
describe('rectSort', () => {
|
||||||
|
it('should sort coordinates when first point is top-left and second point is bottom-right', () => {
|
||||||
|
const result = rectSort([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort coordinates when first point is bottom-right and second point is top-left', () => {
|
||||||
|
const result = rectSort([
|
||||||
|
[3, 4],
|
||||||
|
[1, 2],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort coordinates when first point is bottom-left and second point is top-right', () => {
|
||||||
|
const result = rectSort([
|
||||||
|
[1, 4],
|
||||||
|
[3, 2],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort coordinates when first point is top-right and second point is bottom-left', () => {
|
||||||
|
const result = rectSort([
|
||||||
|
[3, 2],
|
||||||
|
[1, 4],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
9
frontend/src/utils/rectSort.ts
Normal file
9
frontend/src/utils/rectSort.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function rectSort([[x1, y1], [x2, y2]]: [[number, number], [number, number]]): [
|
||||||
|
[number, number],
|
||||||
|
[number, number],
|
||||||
|
] {
|
||||||
|
return [
|
||||||
|
[Math.min(x1, x2), Math.min(y1, y2)],
|
||||||
|
[Math.max(x1, x2), Math.max(y1, y2)],
|
||||||
|
];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user