feat: 이미지 미리보기 기능 개선 및 확대/축소 컨트롤 추가

- 파일 관리 모달에 이미지 미리보기 기능을 개선하여 사용자가 선택한 파일을 보다 직관적으로 확인할 수 있도록 하였습니다.
- 확대/축소 기능을 추가하여 사용자가 이미지의 세부 사항을 쉽게 확인할 수 있도록 하였습니다.
- 드래그 앤 드롭으로 이미지 위치를 조정할 수 있는 기능을 추가하여 사용자 경험을 향상시켰습니다.
- 모달 열릴 때 확대/축소 레벨과 이미지 위치를 초기화하여 일관된 사용자 경험을 제공합니다.
This commit is contained in:
kjs 2026-02-05 14:07:15 +09:00
parent ad7c5923a6
commit 9994a47e54
1 changed files with 133 additions and 25 deletions

View File

@ -18,7 +18,10 @@ import {
Archive,
Presentation,
X,
Star
Star,
ZoomIn,
ZoomOut,
RotateCcw,
} from "lucide-react";
import { formatFileSize } from "@/lib/utils";
import { FileViewerModal } from "./FileViewerModal";
@ -54,7 +57,12 @@ export const FileManagerModal: React.FC<FileManagerModalProps> = ({
const [isViewerOpen, setIsViewerOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<FileInfo | null>(null); // 선택된 파일 (좌측 미리보기용)
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null); // 이미지 미리보기 URL
const [zoomLevel, setZoomLevel] = useState(1); // 🔍 확대/축소 레벨
const [imagePosition, setImagePosition] = useState({ x: 0, y: 0 }); // 🖱️ 이미지 위치
const [isDragging, setIsDragging] = useState(false); // 🖱️ 드래그 중 여부
const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); // 🖱️ 드래그 시작 위치
const fileInputRef = useRef<HTMLInputElement>(null);
const imageContainerRef = useRef<HTMLDivElement>(null);
// 파일 아이콘 가져오기
const getFileIcon = (fileExt: string) => {
@ -146,6 +154,8 @@ export const FileManagerModal: React.FC<FileManagerModalProps> = ({
// 파일 클릭 시 미리보기 로드
const handleFileClick = async (file: FileInfo) => {
setSelectedFile(file);
setZoomLevel(1); // 🔍 파일 선택 시 확대/축소 레벨 초기화
setImagePosition({ x: 0, y: 0 }); // 🖱️ 이미지 위치 초기화
// 이미지 파일인 경우 미리보기 로드
// 🔑 점(.)을 제거하고 확장자만 비교
@ -195,18 +205,50 @@ export const FileManagerModal: React.FC<FileManagerModalProps> = ({
};
}, [previewImageUrl]);
// 🔑 모달이 열릴 때 첫 번째 파일을 자동으로 선택
// 🔑 모달이 열릴 때 첫 번째 파일을 자동으로 선택하고 확대/축소 레벨 초기화
React.useEffect(() => {
if (isOpen && uploadedFiles.length > 0 && !selectedFile) {
const firstFile = uploadedFiles[0];
handleFileClick(firstFile);
if (isOpen) {
setZoomLevel(1); // 🔍 모달 열릴 때 확대/축소 레벨 초기화
setImagePosition({ x: 0, y: 0 }); // 🖱️ 이미지 위치 초기화
if (uploadedFiles.length > 0 && !selectedFile) {
const firstFile = uploadedFiles[0];
handleFileClick(firstFile);
}
}
}, [isOpen, uploadedFiles, selectedFile]);
// 🖱️ 마우스 드래그 핸들러
const handleMouseDown = (e: React.MouseEvent) => {
if (zoomLevel > 1) {
setIsDragging(true);
setDragStart({ x: e.clientX - imagePosition.x, y: e.clientY - imagePosition.y });
}
};
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging && zoomLevel > 1) {
setImagePosition({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y,
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
// 🔍 확대/축소 레벨이 1로 돌아가면 위치도 초기화
React.useEffect(() => {
if (zoomLevel <= 1) {
setImagePosition({ x: 0, y: 0 });
}
}, [zoomLevel]);
return (
<>
<Dialog open={isOpen} onOpenChange={() => {}}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden [&>button]:hidden">
<DialogContent className="max-w-[95vw] w-[1400px] max-h-[90vh] overflow-hidden [&>button]:hidden">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<DialogTitle className="text-lg font-semibold">
({uploadedFiles.length})
@ -267,31 +309,97 @@ export const FileManagerModal: React.FC<FileManagerModalProps> = ({
</div>
)}
{/* 좌우 분할 레이아웃 */}
{/* 좌우 분할 레이아웃 - 좌측 넓게, 우측 고정 너비 */}
<div className="flex-1 flex gap-4 min-h-0">
{/* 좌측: 이미지 미리보기 */}
<div className="w-1/2 border border-gray-200 rounded-lg bg-gray-50 flex items-center justify-center overflow-hidden">
{selectedFile && previewImageUrl ? (
<img
src={previewImageUrl}
alt={selectedFile.realFileName}
className="max-w-full max-h-full object-contain"
/>
) : selectedFile ? (
<div className="flex flex-col items-center text-gray-400">
{getFileIcon(selectedFile.fileExt)}
<p className="mt-2 text-sm"> </p>
{/* 좌측: 이미지 미리보기 (확대/축소 가능) */}
<div className="flex-1 border border-gray-200 rounded-lg bg-gray-900 flex flex-col overflow-hidden relative">
{/* 확대/축소 컨트롤 */}
{selectedFile && previewImageUrl && (
<div className="absolute top-3 left-3 z-10 flex items-center gap-1 bg-black/60 rounded-lg p-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20"
onClick={() => setZoomLevel(prev => Math.max(0.25, prev - 0.25))}
disabled={zoomLevel <= 0.25}
>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-white text-xs min-w-[50px] text-center">
{Math.round(zoomLevel * 100)}%
</span>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20"
onClick={() => setZoomLevel(prev => Math.min(4, prev + 0.25))}
disabled={zoomLevel >= 4}
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-white hover:bg-white/20"
onClick={() => setZoomLevel(1)}
>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex flex-col items-center text-gray-400">
<ImageIcon className="w-16 h-16 mb-2" />
<p className="text-sm"> </p>
)}
{/* 이미지 미리보기 영역 - 마우스 휠로 확대/축소, 드래그로 이동 */}
<div
ref={imageContainerRef}
className={`flex-1 flex items-center justify-center overflow-hidden p-4 ${
zoomLevel > 1 ? (isDragging ? 'cursor-grabbing' : 'cursor-grab') : 'cursor-zoom-in'
}`}
onWheel={(e) => {
if (selectedFile && previewImageUrl) {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setZoomLevel(prev => Math.min(4, Math.max(0.25, prev + delta)));
}
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{selectedFile && previewImageUrl ? (
<img
src={previewImageUrl}
alt={selectedFile.realFileName}
className="transition-transform duration-100 select-none"
style={{
transform: `translate(${imagePosition.x}px, ${imagePosition.y}px) scale(${zoomLevel})`,
transformOrigin: 'center center',
}}
draggable={false}
/>
) : selectedFile ? (
<div className="flex flex-col items-center text-gray-400">
{getFileIcon(selectedFile.fileExt)}
<p className="mt-2 text-sm"> </p>
</div>
) : (
<div className="flex flex-col items-center text-gray-400">
<ImageIcon className="w-16 h-16 mb-2" />
<p className="text-sm"> </p>
</div>
)}
</div>
{/* 파일 정보 바 */}
{selectedFile && (
<div className="bg-black/60 text-white text-xs px-3 py-2 text-center truncate">
{selectedFile.realFileName}
</div>
)}
</div>
{/* 우측: 파일 목록 */}
<div className="w-1/2 border border-gray-200 rounded-lg overflow-hidden flex flex-col">
{/* 우측: 파일 목록 (고정 너비) */}
<div className="w-[400px] shrink-0 border border-gray-200 rounded-lg overflow-hidden flex flex-col">
<div className="p-3 border-b border-gray-200 bg-gray-50">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-700">