feat: 이미지 미리보기 기능 개선 및 확대/축소 컨트롤 추가
- 파일 관리 모달에 이미지 미리보기 기능을 개선하여 사용자가 선택한 파일을 보다 직관적으로 확인할 수 있도록 하였습니다. - 확대/축소 기능을 추가하여 사용자가 이미지의 세부 사항을 쉽게 확인할 수 있도록 하였습니다. - 드래그 앤 드롭으로 이미지 위치를 조정할 수 있는 기능을 추가하여 사용자 경험을 향상시켰습니다. - 모달 열릴 때 확대/축소 레벨과 이미지 위치를 초기화하여 일관된 사용자 경험을 제공합니다.
This commit is contained in:
parent
ad7c5923a6
commit
9994a47e54
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue