교육과정 관리 이미지 미리보기1
This commit is contained in:
@@ -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,7 +382,56 @@ 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">
|
||||
<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"
|
||||
@@ -306,7 +456,12 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
미첨부 시 기본 이미지가 노출됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{errors.image && (
|
||||
<p className="text-[#f64c4c] text-[13px] leading-tight">{errors.image}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user