Merge branch 'fe/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into fe/refactor/admin-page

This commit is contained in:
정현조 2024-09-23 14:02:53 +09:00
commit 05f8a760ed
13 changed files with 167 additions and 138 deletions

View File

@ -42,7 +42,7 @@ export async function changeImageStatus(
export async function uploadImageList(projectId: number, folderId: number, memberId: number, imageList: File[]) { export async function uploadImageList(projectId: number, folderId: number, memberId: number, imageList: File[]) {
return api return api
.post( .post(
`/projects/${projectId}/folders/${folderId}/images`, `/projects/${projectId}/folders/${folderId}/images/file`,
{ imageList }, { imageList },
{ {
params: { memberId }, params: { memberId },
@ -51,43 +51,26 @@ export async function uploadImageList(projectId: number, folderId: number, membe
.then(({ data }) => data); .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(); const formData = new FormData();
files.forEach((file) => { files.forEach((file) => {
formData.append('files', file); formData.append('folderZip', file);
}); });
return api return api
.post( .post(`/projects/${projectId}/folders/${0}/images/zip`, formData, {
`/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, {
params: { memberId }, params: { memberId },
}) })
.then(({ data }) => data) .then(({ data }) => data);
.catch((error) => { }
return Promise.reject(error);
}); 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);
} }

View File

@ -1,12 +1,13 @@
import api from '@/api/axiosConfig'; import api from '@/api/axiosConfig';
import { LabelingRequest } from '@/types';
export async function saveImageLabels(projectId: number, imageId: number, memberId: number, data: LabelingRequest) { export async function saveImageLabels(
return api projectId: number,
.post(`/projects/${projectId}/label/image/${imageId}`, data, { imageId: number,
params: { memberId }, data: {
}) data: string;
.then(({ data }) => data); }
) {
return api.post(`/projects/${projectId}/label/image/${imageId}`, data).then(({ data }) => data);
} }
export async function runAutoLabel(projectId: number, memberId: number) { export async function runAutoLabel(projectId: number, memberId: number) {

View File

@ -1,8 +1,8 @@
import useCanvasStore from '@/stores/useCanvasStore'; 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'; import { cn } from '@/lib/utils';
export default function CanvasControlBar() { export default function CanvasControlBar({ saveJson }: { saveJson: () => void }) {
const drawState = useCanvasStore((state) => state.drawState); const drawState = useCanvasStore((state) => state.drawState);
const setDrawState = useCanvasStore((state) => state.setDrawState); const setDrawState = useCanvasStore((state) => state.setDrawState);
const buttonBaseClassName = 'rounded-lg p-2 transition-colors '; const buttonBaseClassName = 'rounded-lg p-2 transition-colors ';
@ -31,6 +31,16 @@ export default function CanvasControlBar() {
</button> </button>
); );
})} })}
<button
className={cn(buttonClassName, buttonBaseClassName)}
onClick={saveJson}
>
<Save
size={20}
color="black"
/>
</button>
</div> </div>
); );
} }

View File

@ -7,11 +7,13 @@ export default function LabelRect({
isSelected, isSelected,
onSelect, onSelect,
info, info,
setLabel,
dragLayer, dragLayer,
}: { }: {
isSelected: boolean; isSelected: boolean;
onSelect: (evt: Konva.KonvaEventObject<TouchEvent | MouseEvent>) => void; onSelect: (evt: Konva.KonvaEventObject<TouchEvent | MouseEvent>) => void;
info: Label; info: Label;
setLabel: (coordinate: [number, number][]) => void;
dragLayer: Konva.Layer; dragLayer: Konva.Layer;
}) { }) {
const rectRef = useRef<Konva.Line>(null); const rectRef = useRef<Konva.Line>(null);
@ -28,13 +30,19 @@ export default function LabelRect({
trRef.current?.moveToTop(); trRef.current?.moveToTop();
}; };
const handleMoveEnd = () => { const handleMoveEnd = () => {
const rectPoints = rectRef.current?.points(); const rect = rectRef.current?.getPosition();
const points = [ const scale = rectRef.current?.scale();
[rectPoints![0], rectPoints![1]],
[rectPoints![4], rectPoints![5]], 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(() => { useEffect(() => {

View File

@ -1,4 +1,5 @@
import Konva from 'konva'; import Konva from 'konva';
import { Vector2d } from 'konva/lib/types';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Circle, Group, Line } from 'react-konva'; import { Circle, Group, Line } from 'react-konva';
@ -24,6 +25,24 @@ const TRANSFORM_CHANGE_STR = [
export default function PolygonTransformer({ coordinates, setCoordinates, stage, dragLayer }: PolygonTransformerProps) { export default function PolygonTransformer({ coordinates, setCoordinates, stage, dragLayer }: PolygonTransformerProps) {
const anchorsRef = useRef<Konva.Group>(null); const anchorsRef = useRef<Konva.Group>(null);
const scale: Vector2d = { x: 1 / stage.getAbsoluteScale().x, y: 1 / stage.getAbsoluteScale().y };
const handleClick = (index: number) => (e: Konva.KonvaEventObject<MouseEvent>) => {
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<DragEvent>) => { const handleDragMove = (index: number) => (e: Konva.KonvaEventObject<DragEvent>) => {
const circle = e.target as Konva.Circle; const circle = e.target as Konva.Circle;
const pos = circle.position(); const pos = circle.position();
@ -31,7 +50,7 @@ export default function PolygonTransformer({ coordinates, setCoordinates, stage,
newCoordinates[index] = [pos.x, pos.y]; newCoordinates[index] = [pos.x, pos.y];
setCoordinates(newCoordinates); setCoordinates(newCoordinates);
stage.batchDraw(); // stage.batchDraw();
}; };
const handleMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => { const handleMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
const circle = e.target as Konva.Circle; const circle = e.target as Konva.Circle;
@ -94,10 +113,11 @@ export default function PolygonTransformer({ coordinates, setCoordinates, stage,
fill="white" fill="white"
draggable draggable
strokeScaleEnabled={false} strokeScaleEnabled={false}
onClick={handleClick(index)}
onDragMove={handleDragMove(index)} onDragMove={handleDragMove(index)}
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut} onMouseOut={handleMouseOut}
scale={{ x: 1 / stage.getAbsoluteScale().x, y: 1 / stage.getAbsoluteScale().y }} scale={scale}
perfectDrawEnabled={false} perfectDrawEnabled={false}
shadowForStrokeEnabled={false} shadowForStrokeEnabled={false}
/> />

View File

@ -9,9 +9,11 @@ import LabelPolygon from './LabelPolygon';
import CanvasControlBar from '../CanvasControlBar'; import CanvasControlBar from '../CanvasControlBar';
import useLabelJsonQuery from '@/queries/labelJson/useLabelJsonQuery'; import useLabelJsonQuery from '@/queries/labelJson/useLabelJsonQuery';
import { Label } from '@/types'; import { Label } from '@/types';
import { useParams } from 'react-router-dom';
export default function ImageCanvas() { 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 { data: labelData } = useLabelJsonQuery(dataPath);
const { shapes } = labelData; const { shapes } = labelData;
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId); const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
@ -43,6 +45,25 @@ export default function ImageCanvas() {
); );
}, [setLabels, shapes]); }, [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 startDrawRect = () => {
const { x, y } = stageRef.current!.getRelativePointerPosition()!; const { x, y } = stageRef.current!.getRelativePointerPosition()!;
setRectPoints([ setRectPoints([
@ -250,6 +271,7 @@ export default function ImageCanvas() {
isSelected={label.id === selectedLabelId} isSelected={label.id === selectedLabelId}
onSelect={() => setSelectedLabelId(label.id)} onSelect={() => setSelectedLabelId(label.id)}
info={label} info={label}
setLabel={setLabel(label.id)}
dragLayer={dragLayerRef.current as Konva.Layer} dragLayer={dragLayerRef.current as Konva.Layer}
/> />
) : ( ) : (
@ -312,7 +334,7 @@ export default function ImageCanvas() {
<Layer ref={dragLayerRef} /> <Layer ref={dragLayerRef} />
</Stage> </Stage>
<CanvasControlBar /> <CanvasControlBar saveJson={saveJson} />
</div> </div>
) : ( ) : (
<div></div> <div></div>

View File

@ -5,15 +5,7 @@ import { uploadImageFolder } from '@/api/imageApi';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
export default function ImageFolderUploadForm({ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose: () => void; projectId: number }) {
onClose,
projectId,
parentId,
}: {
onClose: () => void;
projectId: number;
parentId: number;
}) {
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0; const memberId = profile?.id || 0;
@ -57,7 +49,7 @@ export default function ImageFolderUploadForm({
setIsUploading(true); setIsUploading(true);
setProgress(0); setProgress(0);
await uploadImageFolder(memberId, projectId, files, parentId) await uploadImageFolder(memberId, projectId, files)
.then(() => { .then(() => {
setProgress(100); setProgress(100);
}) })

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import { Plus } from 'lucide-react'; 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 [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
@ -24,10 +24,9 @@ export default function ImageFolderUploadModal({ projectId, parentId = 0 }: { pr
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드" /> <DialogHeader title="폴더 업로드" />
<ImageFolderUploadForm <ImageUploadFolderForm
onClose={handleClose} onClose={handleClose}
projectId={projectId} projectId={projectId}
parentId={parentId}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,19 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { uploadImageFolderZip } from '@/api/imageApi'; import { uploadImageZip } from '@/api/imageApi';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
export default function ImageFolderZipUploadForm({ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: () => void; projectId: number }) {
onClose,
projectId,
parentId,
}: {
onClose: () => void;
projectId: number;
parentId: number;
}) {
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0; const memberId = profile?.id || 0;
@ -58,7 +50,7 @@ export default function ImageFolderZipUploadForm({
setIsUploading(true); setIsUploading(true);
setProgress(0); setProgress(0);
await uploadImageFolderZip(memberId, projectId, file, parentId) await uploadImageZip(memberId, projectId, file)
.then(() => { .then(() => {
setProgress(100); setProgress(100);
}) })

View File

@ -1,15 +1,9 @@
import React from 'react'; import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import ImageFolderZipUploadForm from './ImageFolderZipUploadForm'; import ImageUploadZipForm from './ImageUploadZipForm';
export default function ImageFolderZipUploadModal({ export default function ImageUploadZipModal({ projectId }: { projectId: number }) {
projectId,
parentId = 0,
}: {
projectId: number;
parentId: number;
}) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
@ -30,10 +24,9 @@ export default function ImageFolderZipUploadModal({
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader title="폴더 압축파일 업로드" /> <DialogHeader title="폴더 압축파일 업로드" />
<ImageFolderZipUploadForm <ImageUploadZipForm
onClose={handleClose} onClose={handleClose}
projectId={projectId} projectId={projectId}
parentId={parentId}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,9 +1,10 @@
import { Project } from '@/types'; import { Project } from '@/types';
import { SquarePenIcon, Upload } from 'lucide-react'; import { Play, SquarePenIcon, Upload } from 'lucide-react';
import ProjectFileItem from './ProjectFileItem'; import ProjectFileItem from './ProjectFileItem';
import ProjectDirectoryItem from './ProjectDirectoryItem'; import ProjectDirectoryItem from './ProjectDirectoryItem';
import useFolderQuery from '@/queries/folders/useFolderQuery'; import useFolderQuery from '@/queries/folders/useFolderQuery';
import useCanvasStore from '@/stores/useCanvasStore'; import useCanvasStore from '@/stores/useCanvasStore';
import { Button } from '../ui/button';
export default function ProjectStructure({ project }: { project: Project }) { export default function ProjectStructure({ project }: { project: Project }) {
const image = useCanvasStore((state) => state.image); const image = useCanvasStore((state) => state.image);
@ -22,46 +23,62 @@ export default function ProjectStructure({ project }: { project: Project }) {
]; ];
return ( return (
<div className="flex h-full flex-col overflow-y-auto px-1 pb-2"> <div className="flex h-full flex-col justify-between">
<header className="flex w-full items-center gap-2 rounded p-1"> <div className="flex flex-col overflow-y-auto px-1 pb-2">
<div className="flex w-full items-center gap-1 overflow-hidden pr-1"> <header className="flex w-full items-center gap-2 rounded p-1">
<h3 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h3> <div className="flex w-full items-center gap-1 overflow-hidden pr-1">
</div> <h3 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h3>
<button </div>
className="flex gap-1" <button
onClick={() => console.log('edit project')} className="flex gap-1"
onClick={() => console.log('edit project')}
>
<SquarePenIcon size={16} />
</button>
<button
className="flex gap-1"
onClick={() => console.log('upload image')}
>
<Upload size={16} />
</button>
</header>
{folderData.children.length === 0 && folderData.images.length === 0 ? (
<div className="body-small flex h-full select-none items-center justify-center text-gray-400">
.
</div>
) : (
<div className="caption flex flex-col">
{folderData.children.map((item) => (
<ProjectDirectoryItem
key={`${project.id}-${item.title}`}
item={item}
initialExpanded={true}
/>
))}
{folderData.images.map((item) => (
<ProjectFileItem
key={`${project.id}-${item.imageTitle}`}
item={item}
selected={image?.id === item.id}
/>
))}
</div>
)}
</div>
<div className="flex p-2.5">
<Button
variant="outlinePrimary"
className="w-full"
onClick={() => console.log('autolabel')}
> >
<SquarePenIcon size={16} /> <Play
</button> size={16}
<button className="mr-1"
className="flex gap-1" />
onClick={() => console.log('upload image')} <span> </span>
> </Button>
<Upload size={16} /> </div>
</button>
</header>
{folderData.children.length === 0 && folderData.images.length === 0 ? (
<div className="body-small flex h-full select-none items-center justify-center text-gray-400">
.
</div>
) : (
<div className="caption flex flex-col">
{folderData.children.map((item) => (
<ProjectDirectoryItem
key={`${project.id}-${item.title}`}
item={item}
initialExpanded={true}
/>
))}
{folderData.images.map((item) => (
<ProjectFileItem
key={`${project.id}-${item.imageTitle}`}
item={item}
selected={image?.id === item.id}
/>
))}
</div>
)}
</div> </div>
); );
} }

View File

@ -14,9 +14,7 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac
const setSidebarSize = useCanvasStore((state) => state.setSidebarSize); const setSidebarSize = useCanvasStore((state) => state.setSidebarSize);
const navigate = useNavigate(); const navigate = useNavigate();
const { workspaceId } = useParams<{ workspaceId: string }>(); const { workspaceId } = useParams<{ workspaceId: string }>();
// const [selectedProjectId, setSelectedProjectId] = useState<string | undefined>();
const handleSelectProject = (projectId: string) => { const handleSelectProject = (projectId: string) => {
// setSelectedProjectId(projectId);
navigate(`${webPath.workspace()}/${workspaceId}/${projectId}`); navigate(`${webPath.workspace()}/${workspaceId}/${projectId}`);
}; };

View File

@ -1,5 +1,5 @@
import ImageFolderUploadModal from '@/components/ImageFolderUploadModal'; import ImageUploadFolderModal from '@/components/ImageUploadFolderModal';
import ImageFolderZipUploadModal from '@/components/ImageFolderZipUploadModal'; import ImageUploadZipModal from '@/components/ImageUploadZipModal';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
export default function ImageFolderUploadTest() { export default function ImageFolderUploadTest() {
@ -8,14 +8,8 @@ export default function ImageFolderUploadTest() {
return ( return (
<div className="min-h-screen w-full"> <div className="min-h-screen w-full">
<ImageFolderUploadModal <ImageUploadFolderModal projectId={projectId} />
projectId={projectId} <ImageUploadZipModal projectId={projectId} />
parentId={0}
/>
<ImageFolderZipUploadModal
projectId={projectId}
parentId={0}
/>
</div> </div>
); );
} }