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 { Label } from '@/components/ui/label';
|
||||||
import useRecursiveSavedImages from '@/hooks/useRecursiveSavedImages';
|
import useRecursiveSavedImages from '@/hooks/useRecursiveSavedImages';
|
||||||
import { Button } from '@/components/ui/button';
|
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 {
|
interface ImageSelectionProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -11,16 +15,22 @@ interface ImageSelectionProps {
|
|||||||
|
|
||||||
export default function ImageSelection({ projectId, selectedImages, setSelectedImages }: ImageSelectionProps) {
|
export default function ImageSelection({ projectId, selectedImages, setSelectedImages }: ImageSelectionProps) {
|
||||||
const { allSavedImages } = useRecursiveSavedImages(projectId, 0);
|
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 handleImageSelect = useCallback(
|
||||||
const updatedImages = selectedImages.includes(imageId)
|
(imageId: number) => {
|
||||||
? selectedImages.filter((id) => id !== imageId)
|
if (selectedImages.includes(imageId)) {
|
||||||
: [...selectedImages, imageId];
|
setSelectedImages(selectedImages.filter((id) => id !== imageId));
|
||||||
|
} else {
|
||||||
|
setSelectedImages([...selectedImages, imageId]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedImages, setSelectedImages]
|
||||||
|
);
|
||||||
|
|
||||||
setSelectedImages(updatedImages);
|
const handleSelectAll = useCallback(() => {
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
|
||||||
if (allSavedImages) {
|
if (allSavedImages) {
|
||||||
if (selectedImages.length === allSavedImages.length) {
|
if (selectedImages.length === allSavedImages.length) {
|
||||||
setSelectedImages([]);
|
setSelectedImages([]);
|
||||||
@ -28,8 +38,66 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI
|
|||||||
setSelectedImages(allSavedImages.map((image) => image.id));
|
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 (
|
return (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<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 ? '전체 선택 해제' : '전체 선택'}
|
{allSavedImages && selectedImages.length === allSavedImages.length ? '전체 선택 해제' : '전체 선택'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="max-h-64 overflow-auto border p-2">
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{allSavedImages && allSavedImages.length > 0 ? (
|
{allSavedImages && allSavedImages.length > 0 ? (
|
||||||
allSavedImages.map((image) => (
|
<FixedSizeList
|
||||||
<li
|
height={260}
|
||||||
key={image.id}
|
itemCount={allSavedImages.length}
|
||||||
className={`relative flex items-center justify-between border p-2 ${
|
itemSize={80}
|
||||||
selectedImages.includes(image.id) ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
|
width="100%"
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="truncate">{image.imageTitle}</span>
|
{Row}
|
||||||
<div className="flex items-center space-x-2">
|
</FixedSizeList>
|
||||||
<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>
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500">저장된 이미지가 없습니다.</p>
|
<p className="text-gray-500">저장된 이미지가 없습니다.</p>
|
||||||
)}
|
)}
|
||||||
</ul>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user