Feat: 리뷰 사진 모달 추가
This commit is contained in:
parent
8debec1ec5
commit
839046f260
@ -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<Label[]>([]);
|
||||
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 (
|
||||
<Stage
|
||||
width={stageDimensions.width}
|
||||
height={stageDimensions.height}
|
||||
className="overflow-hidden bg-gray-200"
|
||||
>
|
||||
<Layer>
|
||||
<Image
|
||||
image={image}
|
||||
width={stageDimensions.width}
|
||||
height={stageDimensions.height}
|
||||
/>
|
||||
</Layer>
|
||||
<Layer>
|
||||
{labels.map((label) =>
|
||||
label.type === 'rectangle' ? (
|
||||
<Rect
|
||||
key={label.id}
|
||||
x={label.coordinates[0][0] * (stageDimensions.width / image.width)}
|
||||
y={label.coordinates[0][1] * (stageDimensions.height / image.height)}
|
||||
width={(label.coordinates[1][0] - label.coordinates[0][0]) * (stageDimensions.width / image.width)}
|
||||
height={(label.coordinates[1][1] - label.coordinates[0][1]) * (stageDimensions.height / image.height)}
|
||||
stroke={label.color}
|
||||
strokeWidth={2}
|
||||
listening={false}
|
||||
/>
|
||||
) : (
|
||||
<Line
|
||||
key={label.id}
|
||||
points={label.coordinates
|
||||
.flat()
|
||||
.map((point, index) =>
|
||||
index % 2 === 0
|
||||
? point * (stageDimensions.width / image.width)
|
||||
: point * (stageDimensions.height / image.height)
|
||||
)}
|
||||
stroke={label.color}
|
||||
strokeWidth={2}
|
||||
closed
|
||||
listening={false}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Layer>
|
||||
</Stage>
|
||||
);
|
||||
}
|
@ -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<string | null>(null);
|
||||
const [selectedLabelData, setSelectedLabelData] = useState<string | null>(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 (
|
||||
<div
|
||||
key={image.id}
|
||||
style={style}
|
||||
className={`relative flex items-center justify-between border p-2 ${
|
||||
selectedImages.includes(image.id) ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onOpenChange={(open) => setIsDialogOpen(open)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<span
|
||||
className="cursor-pointer truncate"
|
||||
onClick={() => handleOpenDialog(image.imagePath, image.dataPath)}
|
||||
>
|
||||
{image.imageTitle}
|
||||
</span>
|
||||
</DialogTrigger>
|
||||
{isDialogOpen && selectedImagePath && selectedLabelData && (
|
||||
<DialogContent className="h-[600px] w-[1000px]">
|
||||
<DialogHeader title={image.imageTitle} />
|
||||
<ImageWithLabels
|
||||
imagePath={selectedImagePath}
|
||||
labelData={selectedLabelData}
|
||||
width={400}
|
||||
height={400}
|
||||
/>
|
||||
</DialogContent>
|
||||
)}
|
||||
</Dialog>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant={selectedImages.includes(image.id) ? 'blue' : 'black'}
|
||||
size="sm"
|
||||
onClick={() => handleImageSelect(image.id)}
|
||||
className="px-3 py-1"
|
||||
type="button"
|
||||
>
|
||||
{selectedImages.includes(image.id) ? '해제' : '선택'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}, [allSavedImages, selectedImages, selectedImagePath, selectedLabelData, handleImageSelect, isDialogOpen]);
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
@ -44,35 +112,18 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI
|
||||
{allSavedImages && selectedImages.length === allSavedImages.length ? '전체 선택 해제' : '전체 선택'}
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="max-h-64 overflow-auto border p-2">
|
||||
<ul className="space-y-2">
|
||||
{allSavedImages && allSavedImages.length > 0 ? (
|
||||
allSavedImages.map((image) => (
|
||||
<li
|
||||
key={image.id}
|
||||
className={`relative flex items-center justify-between border p-2 ${
|
||||
selectedImages.includes(image.id) ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
|
||||
}`}
|
||||
<FixedSizeList
|
||||
height={260}
|
||||
itemCount={allSavedImages.length}
|
||||
itemSize={80}
|
||||
width="100%"
|
||||
>
|
||||
<span className="truncate">{image.imageTitle}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant={selectedImages.includes(image.id) ? 'blue' : 'black'}
|
||||
size="sm"
|
||||
onClick={() => handleImageSelect(image.id)}
|
||||
className="px-3 py-1"
|
||||
type="button"
|
||||
>
|
||||
{selectedImages.includes(image.id) ? '해제' : '선택'}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<p className="text-gray-500">저장된 이미지가 없습니다.</p>
|
||||
)}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user