diff --git a/frontend/src/components/ImageSelection/ImageSelectionWithLabels.tsx b/frontend/src/components/ImageSelection/ImageSelectionWithLabels.tsx new file mode 100644 index 0000000..5912a3f --- /dev/null +++ b/frontend/src/components/ImageSelection/ImageSelectionWithLabels.tsx @@ -0,0 +1,104 @@ +import { useEffect, useState } from 'react'; +import { Image, Layer, Stage, Line, Rect } from 'react-konva'; +import useImage from 'use-image'; +import { Label, Shape } from '@/types'; + +interface ImageSelectionWithLabelsProps { + imagePath: string; + labelData: string; + width: number; + height: number; +} + +export default function ImageSelectionWithLabels({ + imagePath, + labelData, + width, + height, +}: ImageSelectionWithLabelsProps) { + const [image] = useImage(imagePath, 'anonymous'); + const [labels, setLabels] = useState([]); + const [stageDimensions, setStageDimensions] = useState<{ width: number; height: number }>({ width, height }); + + useEffect(() => { + if (image) { + const widthRatio = width / image.width; + const heightRatio = height / image.height; + const scale = Math.min(widthRatio, heightRatio); + setStageDimensions({ + width: image.width * scale, + height: image.height * scale, + }); + } + }, [image, width, height]); + + useEffect(() => { + const fetchLabelData = async () => { + try { + const response = await fetch(labelData); + const json: { shapes: Shape[] } = await response.json(); + const shapes = json.shapes.map((shape, index) => ({ + id: index, + categoryId: shape.categoryId, + color: shape.color, + type: shape.shape_type, + coordinates: shape.points, + })) as Label[]; + setLabels(shapes); + } catch (error) { + console.error('Failed to fetch label data:', error); + } + }; + + fetchLabelData(); + }, [labelData]); + + if (!stageDimensions || !image) return null; + + return ( + + + + + + {labels.map((label) => + label.type === 'rectangle' ? ( + + ) : ( + + index % 2 === 0 + ? point * (stageDimensions.width / image.width) + : point * (stageDimensions.height / image.height) + )} + stroke={label.color} + strokeWidth={2} + closed + listening={false} + /> + ) + )} + + + ); +} diff --git a/frontend/src/components/ImageSelection/index.tsx b/frontend/src/components/ImageSelection/index.tsx index e3a786a..1f47815 100644 --- a/frontend/src/components/ImageSelection/index.tsx +++ b/frontend/src/components/ImageSelection/index.tsx @@ -1,7 +1,11 @@ +import { useState, useCallback, useMemo } from 'react'; import { Label } from '@/components/ui/label'; import useRecursiveSavedImages from '@/hooks/useRecursiveSavedImages'; import { Button } from '@/components/ui/button'; -import { ScrollArea } from '@/components/ui/scroll-area'; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '@/components/ui/dialog'; +import ImageWithLabels from './ImageSelectionWithLabels'; +import { FixedSizeList } from 'react-window'; +import React from 'react'; interface ImageSelectionProps { projectId: string; @@ -11,16 +15,22 @@ interface ImageSelectionProps { export default function ImageSelection({ projectId, selectedImages, setSelectedImages }: ImageSelectionProps) { const { allSavedImages } = useRecursiveSavedImages(projectId, 0); + const [selectedImagePath, setSelectedImagePath] = useState(null); + const [selectedLabelData, setSelectedLabelData] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); - const handleImageSelect = (imageId: number) => { - const updatedImages = selectedImages.includes(imageId) - ? selectedImages.filter((id) => id !== imageId) - : [...selectedImages, imageId]; + const handleImageSelect = useCallback( + (imageId: number) => { + if (selectedImages.includes(imageId)) { + setSelectedImages(selectedImages.filter((id) => id !== imageId)); + } else { + setSelectedImages([...selectedImages, imageId]); + } + }, + [selectedImages, setSelectedImages] + ); - setSelectedImages(updatedImages); - }; - - const handleSelectAll = () => { + const handleSelectAll = useCallback(() => { if (allSavedImages) { if (selectedImages.length === allSavedImages.length) { setSelectedImages([]); @@ -28,8 +38,66 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI setSelectedImages(allSavedImages.map((image) => image.id)); } } + }, [allSavedImages, selectedImages, setSelectedImages]); + + const handleOpenDialog = (imagePath: string, labelData: string) => { + setSelectedImagePath(imagePath); + setSelectedLabelData(labelData); + setIsDialogOpen(true); }; + const Row = useMemo(() => { + return React.memo(({ index, style }: { index: number; style: React.CSSProperties }) => { + const image = allSavedImages[index]; + + return ( +
+ setIsDialogOpen(open)} + > + + handleOpenDialog(image.imagePath, image.dataPath)} + > + {image.imageTitle} + + + {isDialogOpen && selectedImagePath && selectedLabelData && ( + + + + + )} + +
+ +
+
+ ); + }); + }, [allSavedImages, selectedImages, selectedImagePath, selectedLabelData, handleImageSelect, isDialogOpen]); + return (
@@ -44,35 +112,18 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI {allSavedImages && selectedImages.length === allSavedImages.length ? '전체 선택 해제' : '전체 선택'}
- -
    - {allSavedImages && allSavedImages.length > 0 ? ( - allSavedImages.map((image) => ( -
  • - {image.imageTitle} -
    - -
    -
  • - )) - ) : ( -

    저장된 이미지가 없습니다.

    - )} -
-
+ {allSavedImages && allSavedImages.length > 0 ? ( + + {Row} + + ) : ( +

저장된 이미지가 없습니다.

+ )}
); }