교육과정 관리 이미지 미리보기1

This commit is contained in:
2025-11-27 01:00:36 +09:00
parent 5a2d770589
commit e2b7330c5e

View File

@@ -3,6 +3,7 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import ModalCloseSvg from "@/app/svgs/closexsvg";
import DropdownIcon from "@/app/svgs/dropdownicon";
import CloseXOSvg from "@/app/svgs/closexo";
import { getInstructors, type UserRow } from "@/app/admin/id/mockData";
import { type Course } from "./mockData";
@@ -20,8 +21,12 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// 강사 목록 가져오기
const [instructors, setInstructors] = useState<UserRow[]>([]);
@@ -66,6 +71,9 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
setIsDropdownOpen(false);
setErrors({});
setIsDeleteConfirmOpen(false);
setSelectedImage(null);
setPreviewUrl(null);
setIsDragging(false);
}
}, [open, editingCourse, instructors]);
@@ -135,6 +143,99 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
setIsDeleteConfirmOpen(false);
};
// 파일 유효성 검사
const validateImageFile = (file: File): string | null => {
const maxSize = 30 * 1024 * 1024; // 30MB
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (!allowedTypes.includes(file.type)) {
return "PNG 또는 JPG 파일만 업로드 가능합니다.";
}
if (file.size > maxSize) {
return "파일 크기는 30MB 미만이어야 합니다.";
}
return null;
};
// 이미지 파일 처리
const handleImageFile = (file: File) => {
const error = validateImageFile(file);
if (error) {
setErrors((prev) => ({ ...prev, image: error }));
return;
}
setSelectedImage(file);
setErrors((prev) => {
const next = { ...prev };
delete next.image;
return next;
});
// 미리보기 URL 생성
const reader = new FileReader();
reader.onloadend = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(file);
};
// 파일 선택 핸들러
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleImageFile(file);
}
};
// 클릭으로 파일 선택
const handleImageAreaClick = () => {
fileInputRef.current?.click();
};
// 드래그 오버 핸들러
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
// 드래그 리브 핸들러
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
// 드롭 핸들러
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const file = e.dataTransfer.files?.[0];
if (file) {
handleImageFile(file);
}
};
// 이미지 삭제 핸들러
const handleRemoveImage = (e: React.MouseEvent) => {
e.stopPropagation();
setSelectedImage(null);
setPreviewUrl(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
setErrors((prev) => {
const next = { ...prev };
delete next.image;
return next;
});
};
if (!open) return null;
return (
@@ -281,32 +382,86 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
</span>
</div>
</div>
<div className="bg-gray-50 border border-[#dee1e6] border-dashed h-[192px] rounded-[8px] flex flex-col items-center justify-center gap-3 cursor-pointer hover:bg-gray-100 transition-colors px-0 py-4">
<div className="w-10 h-10 flex items-center justify-center shrink-0">
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 13.3333V26.6667M13.3333 20H26.6667"
stroke="#8C95A1"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div className="text-center">
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] whitespace-pre">
( )
<br aria-hidden="true" />
.
</p>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg"
onChange={handleFileSelect}
className="hidden"
aria-label="이미지 파일 선택"
/>
<div
onClick={handleImageAreaClick}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border border-dashed min-h-[192px] rounded-[8px] flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors px-0 relative overflow-hidden ${
isDragging
? "bg-blue-50 border-blue-300"
: previewUrl
? "bg-white border-[#dee1e6]"
: "bg-gray-50 border-[#dee1e6] hover:bg-gray-100"
}`}
>
{previewUrl ? (
<>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative flex items-start">
<img
src={previewUrl}
alt="미리보기"
className="h-[160px] w-auto object-contain"
/>
<button
type="button"
onClick={handleRemoveImage}
className="w-[18px] h-[18px] flex items-center justify-center ml-1 z-10 hover:opacity-80 transition-opacity shrink-0"
aria-label="이미지 삭제"
>
<CloseXOSvg width={18} height={18} />
</button>
</div>
</div>
<div className="absolute inset-0 bg-black/0 hover:bg-black/20 transition-colors flex items-center justify-center my-4">
<div className="text-center opacity-0 hover:opacity-100 transition-opacity">
<p className="text-[14px] font-normal leading-[1.5] text-white">
</p>
</div>
</div>
</>
) : (
<>
<div className="w-10 h-10 flex items-center justify-center shrink-0">
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 13.3333V26.6667M13.3333 20H26.6667"
stroke="#8C95A1"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div className="text-center">
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] whitespace-pre">
( )
<br aria-hidden="true" />
.
</p>
</div>
</>
)}
</div>
{errors.image && (
<p className="text-[#f64c4c] text-[13px] leading-tight">{errors.image}</p>
)}
</div>
</div>
</div>