이미지 미리보기 기능

This commit is contained in:
kjs
2025-09-05 14:52:10 +09:00
parent dcc459850c
commit 20cdcca171
16 changed files with 1093 additions and 48 deletions

View File

@@ -32,6 +32,9 @@ import {
Download,
Eye,
X,
ZoomIn,
ZoomOut,
RotateCw,
} from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
import { getCurrentUser, UserInfo } from "@/lib/api/client";
@@ -84,6 +87,49 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
const [editFormData, setEditFormData] = useState<Record<string, any>>({});
const [editingRowData, setEditingRowData] = useState<Record<string, any> | null>(null);
const [isEditing, setIsEditing] = useState(false);
// 이미지 미리보기 상태
const [previewImage, setPreviewImage] = useState<FileInfo | null>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
// 이미지 미리보기 핸들러들
const handlePreviewImage = useCallback((fileInfo: FileInfo) => {
setPreviewImage(fileInfo);
setShowPreviewModal(true);
setZoom(1);
setRotation(0);
}, []);
const closePreviewModal = useCallback(() => {
setShowPreviewModal(false);
setPreviewImage(null);
setZoom(1);
setRotation(0);
}, []);
const handleZoom = useCallback((direction: "in" | "out") => {
setZoom((prev) => {
if (direction === "in") {
return Math.min(prev + 0.25, 3);
} else {
return Math.max(prev - 0.25, 0.25);
}
});
}, []);
const handleRotate = useCallback(() => {
setRotation((prev) => (prev + 90) % 360);
}, []);
const formatFileSize = useCallback((bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}, []);
const [showFileModal, setShowFileModal] = useState(false);
const [currentFileData, setCurrentFileData] = useState<FileColumnData | null>(null);
const [currentFileColumn, setCurrentFileColumn] = useState<DataTableColumn | null>(null);
@@ -1812,10 +1858,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
variant="outline"
size="sm"
className="w-full"
onClick={() => {
// TODO: 이미지 미리보기 모달 구현
alert("이미지 미리보기 기능은 준비 중입니다.");
}}
onClick={() => handlePreviewImage(fileInfo)}
>
<Eye className="mr-1 h-4 w-4" />
@@ -1904,6 +1947,65 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
</DialogFooter>
</DialogContent>
</Dialog>
{/* 이미지 미리보기 다이얼로그 */}
<Dialog open={showPreviewModal} onOpenChange={closePreviewModal}>
<DialogContent className="max-h-[90vh] max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span className="truncate">{previewImage?.name}</span>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" onClick={() => handleZoom("out")} disabled={zoom <= 0.25}>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="min-w-[60px] text-center text-sm text-gray-500">{Math.round(zoom * 100)}%</span>
<Button size="sm" variant="outline" onClick={() => handleZoom("in")} disabled={zoom >= 3}>
<ZoomIn className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" onClick={handleRotate}>
<RotateCw className="h-4 w-4" />
</Button>
{previewImage && (
<Button
size="sm"
variant="outline"
onClick={() => {
handleDownloadFile(previewImage);
}}
>
<Download className="h-4 w-4" />
</Button>
)}
</div>
</DialogTitle>
</DialogHeader>
<div className="flex flex-1 items-center justify-center overflow-auto rounded-lg bg-gray-50 p-4">
{previewImage && (
<img
src={`${process.env.NEXT_PUBLIC_API_URL}/files/preview/${previewImage.id}?serverFilename=${previewImage.serverFilename}`}
alt={previewImage.name}
className="max-h-full max-w-full object-contain transition-transform duration-200"
style={{
transform: `scale(${zoom}) rotate(${rotation}deg)`,
}}
onError={() => {
console.error("이미지 로딩 실패:", previewImage);
toast.error("이미지를 불러올 수 없습니다.");
}}
/>
)}
</div>
{previewImage && (
<div className="flex items-center justify-between border-t pt-3 text-sm text-gray-500">
<div>: {formatFileSize(previewImage.size)}</div>
<div>: {previewImage.type}</div>
<div>: {new Date(previewImage.uploadedAt).toLocaleDateString("ko-KR")}</div>
</div>
)}
</DialogContent>
</Dialog>
</Card>
);
};