교육과정 관리 이미지 미리보기1
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import ModalCloseSvg from "@/app/svgs/closexsvg";
|
import ModalCloseSvg from "@/app/svgs/closexsvg";
|
||||||
import DropdownIcon from "@/app/svgs/dropdownicon";
|
import DropdownIcon from "@/app/svgs/dropdownicon";
|
||||||
|
import CloseXOSvg from "@/app/svgs/closexo";
|
||||||
import { getInstructors, type UserRow } from "@/app/admin/id/mockData";
|
import { getInstructors, type UserRow } from "@/app/admin/id/mockData";
|
||||||
import { type Course } from "./mockData";
|
import { type Course } from "./mockData";
|
||||||
|
|
||||||
@@ -20,8 +21,12 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
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 dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 강사 목록 가져오기
|
// 강사 목록 가져오기
|
||||||
const [instructors, setInstructors] = useState<UserRow[]>([]);
|
const [instructors, setInstructors] = useState<UserRow[]>([]);
|
||||||
@@ -66,6 +71,9 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setIsDeleteConfirmOpen(false);
|
setIsDeleteConfirmOpen(false);
|
||||||
|
setSelectedImage(null);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setIsDragging(false);
|
||||||
}
|
}
|
||||||
}, [open, editingCourse, instructors]);
|
}, [open, editingCourse, instructors]);
|
||||||
|
|
||||||
@@ -135,6 +143,99 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
setIsDeleteConfirmOpen(false);
|
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;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -281,7 +382,56 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="w-10 h-10 flex items-center justify-center shrink-0">
|
||||||
<svg
|
<svg
|
||||||
width="40"
|
width="40"
|
||||||
@@ -306,7 +456,12 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
미첨부 시 기본 이미지가 노출됩니다.
|
미첨부 시 기본 이미지가 노출됩니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{errors.image && (
|
||||||
|
<p className="text-[#f64c4c] text-[13px] leading-tight">{errors.image}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user