교육과정, 강의등록 완료1
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useRef, useEffect } from "react";
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import AdminSidebar from "@/app/components/AdminSidebar";
|
||||
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
|
||||
import DropdownIcon from "@/app/svgs/dropdownicon";
|
||||
import BackArrowSvg from "@/app/svgs/backarrow";
|
||||
import { getCourses, type Course } from "@/app/admin/courses/mockData";
|
||||
import CloseXOSvg from "@/app/svgs/closexo";
|
||||
import apiService from "@/app/lib/apiService";
|
||||
|
||||
type Lesson = {
|
||||
id: string;
|
||||
@@ -19,6 +21,7 @@ type Lesson = {
|
||||
};
|
||||
|
||||
export default function AdminLessonsPage() {
|
||||
const router = useRouter();
|
||||
const [lessons, setLessons] = useState<Lesson[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isRegistrationMode, setIsRegistrationMode] = useState(false);
|
||||
@@ -26,6 +29,8 @@ export default function AdminLessonsPage() {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [currentUser, setCurrentUser] = useState<string>("관리자");
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const rawLecturesRef = useRef<any[]>([]); // 원본 강좌 데이터 저장
|
||||
|
||||
// 등록 폼 상태
|
||||
const [selectedCourse, setSelectedCourse] = useState<string>("");
|
||||
@@ -33,9 +38,65 @@ export default function AdminLessonsPage() {
|
||||
const [learningGoal, setLearningGoal] = useState("");
|
||||
const [courseVideoCount, setCourseVideoCount] = useState(0);
|
||||
const [courseVideoFiles, setCourseVideoFiles] = useState<string[]>([]);
|
||||
const [courseVideoFileObjects, setCourseVideoFileObjects] = useState<File[]>([]);
|
||||
const [vrContentCount, setVrContentCount] = useState(0);
|
||||
const [vrContentFiles, setVrContentFiles] = useState<string[]>([]);
|
||||
const [vrContentFileObjects, setVrContentFileObjects] = useState<File[]>([]);
|
||||
const [questionFileCount, setQuestionFileCount] = useState(0);
|
||||
const [questionFileObject, setQuestionFileObject] = useState<File | null>(null);
|
||||
|
||||
// 에러 상태
|
||||
const [errors, setErrors] = useState<{
|
||||
selectedCourse?: string;
|
||||
lessonName?: string;
|
||||
learningGoal?: string;
|
||||
}>({});
|
||||
|
||||
// 교육과정명 매핑 함수
|
||||
const mapCourseNames = useCallback((lectures: any[]) => {
|
||||
const fetchedLessons: Lesson[] = lectures.map((lecture: any) => {
|
||||
// 첨부파일 정보 구성
|
||||
const attachmentParts: string[] = [];
|
||||
if (lecture.videoUrl) {
|
||||
attachmentParts.push('강좌영상 1개');
|
||||
}
|
||||
if (lecture.webglUrl) {
|
||||
attachmentParts.push('VR콘텐츠 1개');
|
||||
}
|
||||
if (lecture.csvKey) {
|
||||
attachmentParts.push('평가문제 1개');
|
||||
}
|
||||
const attachments = attachmentParts.length > 0
|
||||
? attachmentParts.join(', ')
|
||||
: '없음';
|
||||
|
||||
// subjectId로 교육과정명 찾기
|
||||
const subjectId = lecture.subjectId || lecture.subject_id;
|
||||
let courseName = '';
|
||||
if (subjectId && courses.length > 0) {
|
||||
const foundCourse = courses.find(course => course.id === String(subjectId));
|
||||
courseName = foundCourse?.courseName || '';
|
||||
}
|
||||
|
||||
// 교육과정명을 찾지 못한 경우 fallback
|
||||
if (!courseName) {
|
||||
courseName = lecture.subjectName || lecture.courseName || '';
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(lecture.id || lecture.lectureId || ''),
|
||||
courseName,
|
||||
lessonName: lecture.title || lecture.lessonName || '',
|
||||
attachments,
|
||||
questionCount: lecture.csvKey ? 1 : 0,
|
||||
createdBy: lecture.createdBy || lecture.instructorName || '관리자',
|
||||
createdAt: lecture.createdAt
|
||||
? new Date(lecture.createdAt).toISOString().split('T')[0]
|
||||
: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
});
|
||||
setLessons(fetchedLessons);
|
||||
}, [courses]);
|
||||
|
||||
// 교육과정 목록 가져오기
|
||||
useEffect(() => {
|
||||
@@ -51,6 +112,34 @@ export default function AdminLessonsPage() {
|
||||
fetchCourses();
|
||||
}, []);
|
||||
|
||||
// 강좌 리스트 조회
|
||||
useEffect(() => {
|
||||
async function fetchLectures() {
|
||||
try {
|
||||
const response = await apiService.getLectures();
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// 원본 데이터 저장
|
||||
rawLecturesRef.current = response.data;
|
||||
// 교육과정명 매핑 함수 호출
|
||||
mapCourseNames(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('강좌 리스트 조회 오류:', error);
|
||||
setLessons([]);
|
||||
rawLecturesRef.current = [];
|
||||
}
|
||||
}
|
||||
|
||||
fetchLectures();
|
||||
}, [mapCourseNames]);
|
||||
|
||||
// 교육과정 목록이 로드되면 강좌 리스트의 교육과정명 업데이트
|
||||
useEffect(() => {
|
||||
if (rawLecturesRef.current.length > 0) {
|
||||
mapCourseNames(rawLecturesRef.current);
|
||||
}
|
||||
}, [mapCourseNames]);
|
||||
|
||||
// 현재 사용자 정보 가져오기
|
||||
useEffect(() => {
|
||||
async function fetchCurrentUser() {
|
||||
@@ -144,66 +233,215 @@ export default function AdminLessonsPage() {
|
||||
setLearningGoal("");
|
||||
setCourseVideoCount(0);
|
||||
setCourseVideoFiles([]);
|
||||
setCourseVideoFileObjects([]);
|
||||
setVrContentCount(0);
|
||||
setVrContentFiles([]);
|
||||
setVrContentFileObjects([]);
|
||||
setQuestionFileCount(0);
|
||||
setQuestionFileObject(null);
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const handleRegisterClick = () => {
|
||||
setIsRegistrationMode(true);
|
||||
};
|
||||
|
||||
const handleSaveClick = () => {
|
||||
const handleSaveClick = async () => {
|
||||
// 유효성 검사
|
||||
if (!selectedCourse || !lessonName) {
|
||||
// TODO: 에러 메시지 표시
|
||||
const newErrors: {
|
||||
selectedCourse?: string;
|
||||
lessonName?: string;
|
||||
learningGoal?: string;
|
||||
} = {};
|
||||
|
||||
if (!selectedCourse) {
|
||||
newErrors.selectedCourse = "교육과정을 선택해 주세요.";
|
||||
}
|
||||
if (!lessonName.trim()) {
|
||||
newErrors.lessonName = "강좌명을 입력해 주세요.";
|
||||
}
|
||||
if (!learningGoal.trim()) {
|
||||
newErrors.learningGoal = "내용을 입력해 주세요.";
|
||||
}
|
||||
|
||||
// 에러가 있으면 표시하고 중단
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
// 첨부파일 정보 문자열 생성
|
||||
const attachmentParts: string[] = [];
|
||||
if (courseVideoCount > 0) {
|
||||
attachmentParts.push(`강좌영상 ${courseVideoCount}개`);
|
||||
// 에러 초기화
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
// 파일 업로드 및 키 추출
|
||||
let videoUrl: string | undefined;
|
||||
let webglUrl: string | undefined;
|
||||
let csvKey: string | undefined;
|
||||
|
||||
// 강좌 영상 업로드 (첫 번째 파일만 사용)
|
||||
if (courseVideoFileObjects.length > 0) {
|
||||
try {
|
||||
const uploadResponse = await apiService.uploadFile(courseVideoFileObjects[0]);
|
||||
if (uploadResponse.data) {
|
||||
const fileKey = uploadResponse.data.key
|
||||
|| uploadResponse.data.fileKey
|
||||
|| uploadResponse.data.id
|
||||
|| uploadResponse.data.imageKey
|
||||
|| uploadResponse.data.fileId
|
||||
|| (uploadResponse.data.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey))
|
||||
|| null;
|
||||
if (fileKey) {
|
||||
videoUrl = fileKey;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('강좌 영상 업로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// VR 콘텐츠 업로드 (첫 번째 파일만 사용)
|
||||
if (vrContentFileObjects.length > 0) {
|
||||
try {
|
||||
const uploadResponse = await apiService.uploadFile(vrContentFileObjects[0]);
|
||||
if (uploadResponse.data) {
|
||||
const fileKey = uploadResponse.data.key
|
||||
|| uploadResponse.data.fileKey
|
||||
|| uploadResponse.data.id
|
||||
|| uploadResponse.data.imageKey
|
||||
|| uploadResponse.data.fileId
|
||||
|| (uploadResponse.data.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey))
|
||||
|| null;
|
||||
if (fileKey) {
|
||||
webglUrl = fileKey;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('VR 콘텐츠 업로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 학습 평가 문제 업로드
|
||||
if (questionFileObject) {
|
||||
try {
|
||||
const uploadResponse = await apiService.uploadFile(questionFileObject);
|
||||
if (uploadResponse.data) {
|
||||
const fileKey = uploadResponse.data.key
|
||||
|| uploadResponse.data.fileKey
|
||||
|| uploadResponse.data.id
|
||||
|| uploadResponse.data.imageKey
|
||||
|| uploadResponse.data.fileId
|
||||
|| (uploadResponse.data.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey))
|
||||
|| null;
|
||||
if (fileKey) {
|
||||
csvKey = fileKey;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('학습 평가 문제 업로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// API 요청 body 구성
|
||||
const requestBody: {
|
||||
subjectId: number;
|
||||
title: string;
|
||||
objective: string;
|
||||
videoUrl?: string;
|
||||
webglUrl?: string;
|
||||
csvKey?: string;
|
||||
} = {
|
||||
subjectId: Number(selectedCourse),
|
||||
title: lessonName.trim(),
|
||||
objective: learningGoal.trim(),
|
||||
};
|
||||
|
||||
// 선택적 필드 추가
|
||||
if (videoUrl) {
|
||||
requestBody.videoUrl = videoUrl;
|
||||
}
|
||||
if (webglUrl) {
|
||||
requestBody.webglUrl = webglUrl;
|
||||
}
|
||||
if (csvKey) {
|
||||
requestBody.csvKey = csvKey;
|
||||
}
|
||||
|
||||
// 강좌 등록 API 호출
|
||||
const response = await apiService.createLecture(requestBody);
|
||||
|
||||
// 응답에서 id 추출 및 저장
|
||||
const lectureId = response.data?.id;
|
||||
if (!lectureId) {
|
||||
throw new Error('강좌 등록 응답에서 ID를 받지 못했습니다.');
|
||||
}
|
||||
|
||||
// 첨부파일 정보 문자열 생성
|
||||
const attachmentParts: string[] = [];
|
||||
if (courseVideoCount > 0) {
|
||||
attachmentParts.push(`강좌영상 ${courseVideoCount}개`);
|
||||
}
|
||||
if (vrContentCount > 0) {
|
||||
attachmentParts.push(`VR콘텐츠 ${vrContentCount}개`);
|
||||
}
|
||||
if (questionFileCount > 0) {
|
||||
attachmentParts.push(`평가문제 ${questionFileCount}개`);
|
||||
}
|
||||
const attachments = attachmentParts.length > 0
|
||||
? attachmentParts.join(', ')
|
||||
: '없음';
|
||||
|
||||
// 교육과정명 가져오기
|
||||
const courseName = courseOptions.find(c => c.id === selectedCourse)?.name || '';
|
||||
|
||||
// 새 강좌 생성 (API 응답의 id 사용)
|
||||
const newLesson: Lesson = {
|
||||
id: String(lectureId),
|
||||
courseName,
|
||||
lessonName,
|
||||
attachments,
|
||||
questionCount: questionFileCount,
|
||||
createdBy: currentUser,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
|
||||
// 강좌 목록에 추가
|
||||
setLessons(prev => [...prev, newLesson]);
|
||||
|
||||
// 등록 모드 종료 및 폼 초기화
|
||||
setIsRegistrationMode(false);
|
||||
setSelectedCourse("");
|
||||
setLessonName("");
|
||||
setLearningGoal("");
|
||||
setCourseVideoCount(0);
|
||||
setCourseVideoFiles([]);
|
||||
setCourseVideoFileObjects([]);
|
||||
setVrContentCount(0);
|
||||
setVrContentFiles([]);
|
||||
setVrContentFileObjects([]);
|
||||
setQuestionFileCount(0);
|
||||
setQuestionFileObject(null);
|
||||
|
||||
// 토스트 팝업 표시
|
||||
setShowToast(true);
|
||||
} catch (error) {
|
||||
console.error('강좌 등록 실패:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '강좌 등록 중 오류가 발생했습니다.';
|
||||
alert(errorMessage);
|
||||
}
|
||||
if (vrContentCount > 0) {
|
||||
attachmentParts.push(`VR콘텐츠 ${vrContentCount}개`);
|
||||
}
|
||||
if (questionFileCount > 0) {
|
||||
attachmentParts.push(`평가문제 ${questionFileCount}개`);
|
||||
}
|
||||
const attachments = attachmentParts.length > 0
|
||||
? attachmentParts.join(', ')
|
||||
: '없음';
|
||||
|
||||
// 교육과정명 가져오기
|
||||
const courseName = courseOptions.find(c => c.id === selectedCourse)?.name || '';
|
||||
|
||||
// 새 강좌 생성
|
||||
const newLesson: Lesson = {
|
||||
id: String(Date.now()),
|
||||
courseName,
|
||||
lessonName,
|
||||
attachments,
|
||||
questionCount: questionFileCount,
|
||||
createdBy: currentUser,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
|
||||
// 강좌 목록에 추가
|
||||
setLessons(prev => [...prev, newLesson]);
|
||||
|
||||
// 등록 모드 종료 및 폼 초기화
|
||||
setIsRegistrationMode(false);
|
||||
setSelectedCourse("");
|
||||
setLessonName("");
|
||||
setLearningGoal("");
|
||||
setCourseVideoCount(0);
|
||||
setCourseVideoFiles([]);
|
||||
setVrContentCount(0);
|
||||
setVrContentFiles([]);
|
||||
setQuestionFileCount(0);
|
||||
};
|
||||
|
||||
// 토스트 자동 닫기
|
||||
useEffect(() => {
|
||||
if (showToast) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 3000); // 3초 후 자동 닫기
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white">
|
||||
{/* 메인 레이아웃 */}
|
||||
@@ -254,7 +492,9 @@ export default function AdminLessonsPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className="w-full h-[40px] px-[12px] py-[8px] border border-[#dee1e6] rounded-[8px] bg-white flex items-center justify-between focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] cursor-pointer"
|
||||
className={`w-full h-[40px] px-[12px] py-[8px] border rounded-[8px] bg-white flex items-center justify-between focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] cursor-pointer ${
|
||||
errors.selectedCourse ? 'border-[#e63946]' : 'border-[#dee1e6]'
|
||||
}`}
|
||||
>
|
||||
<span className={`text-[16px] font-normal leading-[1.5] flex-1 text-left ${
|
||||
selectedCourse ? 'text-[#1b2027]' : 'text-[#6c7682]'
|
||||
@@ -266,7 +506,7 @@ export default function AdminLessonsPage() {
|
||||
<DropdownIcon stroke="#8C95A1" className="shrink-0" />
|
||||
</button>
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-[#dee1e6] rounded-[8px] shadow-lg z-20 max-h-[200px] overflow-y-auto">
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-[#dee1e6] rounded-[8px] shadow-lg z-20 max-h-[200px] overflow-y-auto dropdown-scroll">
|
||||
{courseOptions.map((course, index) => (
|
||||
<button
|
||||
key={course.id}
|
||||
@@ -274,6 +514,10 @@ export default function AdminLessonsPage() {
|
||||
onClick={() => {
|
||||
setSelectedCourse(course.id);
|
||||
setIsDropdownOpen(false);
|
||||
// 에러 초기화
|
||||
if (errors.selectedCourse) {
|
||||
setErrors(prev => ({ ...prev, selectedCourse: undefined }));
|
||||
}
|
||||
}}
|
||||
className={`w-full px-3 py-2 text-left text-[16px] font-normal leading-[1.5] hover:bg-[#f1f3f5] transition-colors cursor-pointer ${
|
||||
selectedCourse === course.id
|
||||
@@ -291,6 +535,11 @@ export default function AdminLessonsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errors.selectedCourse && (
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#e63946]">
|
||||
{errors.selectedCourse}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 강좌명 */}
|
||||
@@ -301,10 +550,23 @@ export default function AdminLessonsPage() {
|
||||
<input
|
||||
type="text"
|
||||
value={lessonName}
|
||||
onChange={(e) => setLessonName(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setLessonName(e.target.value);
|
||||
// 에러 초기화
|
||||
if (errors.lessonName) {
|
||||
setErrors(prev => ({ ...prev, lessonName: undefined }));
|
||||
}
|
||||
}}
|
||||
placeholder="강좌명을 입력해 주세요."
|
||||
className="h-[40px] px-[12px] py-[8px] border border-[#dee1e6] rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47]"
|
||||
className={`h-[40px] px-[12px] py-[8px] border rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] ${
|
||||
errors.lessonName ? 'border-[#e63946]' : 'border-[#dee1e6]'
|
||||
}`}
|
||||
/>
|
||||
{errors.lessonName && (
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#e63946]">
|
||||
{errors.lessonName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 학습 목표 */}
|
||||
@@ -318,10 +580,16 @@ export default function AdminLessonsPage() {
|
||||
onChange={(e) => {
|
||||
if (e.target.value.length <= 1000) {
|
||||
setLearningGoal(e.target.value);
|
||||
// 에러 초기화
|
||||
if (errors.learningGoal) {
|
||||
setErrors(prev => ({ ...prev, learningGoal: undefined }));
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="내용을 입력해 주세요. (최대 1,000자)"
|
||||
className="w-full h-[160px] px-[12px] py-[8px] border border-[#dee1e6] rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] resize-none"
|
||||
className={`w-full h-[160px] px-[12px] py-[8px] border rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] resize-none ${
|
||||
errors.learningGoal ? 'border-[#e63946]' : 'border-[#dee1e6]'
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute bottom-[8px] right-[12px]">
|
||||
<p className="text-[13px] font-normal leading-[1.4] text-[#6c7682] text-right">
|
||||
@@ -329,6 +597,11 @@ export default function AdminLessonsPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{errors.learningGoal && (
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#e63946]">
|
||||
{errors.learningGoal}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -357,22 +630,50 @@ export default function AdminLessonsPage() {
|
||||
multiple
|
||||
accept=".mp4,video/mp4"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
onChange={async (e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const MAX_SIZE = 30 * 1024 * 1024; // 30MB
|
||||
const mp4Files: string[] = [];
|
||||
const validFiles: File[] = [];
|
||||
const oversizedFiles: string[] = [];
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
// mp4 파일이고 30MB 이하인 파일만 필터링
|
||||
if (file.name.toLowerCase().endsWith('.mp4') && file.size <= MAX_SIZE) {
|
||||
mp4Files.push(file.name);
|
||||
// mp4 파일인지 확인
|
||||
if (!file.name.toLowerCase().endsWith('.mp4')) {
|
||||
return;
|
||||
}
|
||||
// 각 파일이 30MB 미만인지 검사
|
||||
if (file.size < MAX_SIZE) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
oversizedFiles.push(file.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (courseVideoCount + mp4Files.length <= 10) {
|
||||
setCourseVideoFiles(prev => [...prev, ...mp4Files]);
|
||||
setCourseVideoCount(prev => prev + mp4Files.length);
|
||||
// 30MB 이상인 파일이 있으면 알림
|
||||
if (oversizedFiles.length > 0) {
|
||||
alert(`다음 파일은 30MB 미만이어야 합니다:\n${oversizedFiles.join('\n')}`);
|
||||
}
|
||||
|
||||
// 파일 개수 제한 확인
|
||||
if (courseVideoCount + validFiles.length > 10) {
|
||||
alert('강좌 영상은 최대 10개까지 첨부할 수 있습니다.');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
try {
|
||||
// 다중 파일 업로드
|
||||
await apiService.uploadFiles(validFiles);
|
||||
setCourseVideoFiles(prev => [...prev, ...validFiles.map(f => f.name)]);
|
||||
setCourseVideoFileObjects(prev => [...prev, ...validFiles]);
|
||||
setCourseVideoCount(prev => prev + validFiles.length);
|
||||
} catch (error) {
|
||||
console.error('강좌 영상 업로드 실패:', error);
|
||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
||||
}
|
||||
}
|
||||
// input 초기화 (같은 파일 다시 선택 가능하도록)
|
||||
e.target.value = '';
|
||||
@@ -401,6 +702,7 @@ export default function AdminLessonsPage() {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCourseVideoFiles(prev => prev.filter((_, i) => i !== index));
|
||||
setCourseVideoFileObjects(prev => prev.filter((_, i) => i !== index));
|
||||
setCourseVideoCount(prev => prev - 1);
|
||||
}}
|
||||
className="size-[16px] flex items-center justify-center cursor-pointer hover:opacity-70 transition-opacity shrink-0"
|
||||
@@ -433,22 +735,50 @@ export default function AdminLessonsPage() {
|
||||
multiple
|
||||
accept=".zip,application/zip"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
onChange={async (e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const MAX_SIZE = 30 * 1024 * 1024; // 30MB
|
||||
const zipFiles: string[] = [];
|
||||
const validFiles: File[] = [];
|
||||
const oversizedFiles: string[] = [];
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
// zip 파일이고 30MB 이하인 파일만 필터링
|
||||
if (file.name.toLowerCase().endsWith('.zip') && file.size <= MAX_SIZE) {
|
||||
zipFiles.push(file.name);
|
||||
// zip 파일인지 확인
|
||||
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||||
return;
|
||||
}
|
||||
// 각 파일이 30MB 미만인지 검사
|
||||
if (file.size < MAX_SIZE) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
oversizedFiles.push(file.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (vrContentCount + zipFiles.length <= 10) {
|
||||
setVrContentFiles(prev => [...prev, ...zipFiles]);
|
||||
setVrContentCount(prev => prev + zipFiles.length);
|
||||
// 30MB 이상인 파일이 있으면 알림
|
||||
if (oversizedFiles.length > 0) {
|
||||
alert(`다음 파일은 30MB 미만이어야 합니다:\n${oversizedFiles.join('\n')}`);
|
||||
}
|
||||
|
||||
// 파일 개수 제한 확인
|
||||
if (vrContentCount + validFiles.length > 10) {
|
||||
alert('VR 콘텐츠는 최대 10개까지 첨부할 수 있습니다.');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
try {
|
||||
// 다중 파일 업로드
|
||||
await apiService.uploadFiles(validFiles);
|
||||
setVrContentFiles(prev => [...prev, ...validFiles.map(f => f.name)]);
|
||||
setVrContentFileObjects(prev => [...prev, ...validFiles]);
|
||||
setVrContentCount(prev => prev + validFiles.length);
|
||||
} catch (error) {
|
||||
console.error('VR 콘텐츠 업로드 실패:', error);
|
||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
||||
}
|
||||
}
|
||||
// input 초기화 (같은 파일 다시 선택 가능하도록)
|
||||
e.target.value = '';
|
||||
@@ -477,6 +807,7 @@ export default function AdminLessonsPage() {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setVrContentFiles(prev => prev.filter((_, i) => i !== index));
|
||||
setVrContentFileObjects(prev => prev.filter((_, i) => i !== index));
|
||||
setVrContentCount(prev => prev - 1);
|
||||
}}
|
||||
className="size-[16px] flex items-center justify-center cursor-pointer hover:opacity-70 transition-opacity shrink-0"
|
||||
@@ -515,14 +846,22 @@ export default function AdminLessonsPage() {
|
||||
type="file"
|
||||
accept=".csv"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
onChange={async (e) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
// CSV 파일만 허용
|
||||
if (file.name.toLowerCase().endsWith('.csv')) {
|
||||
setQuestionFileCount(1);
|
||||
try {
|
||||
// 단일 파일 업로드
|
||||
await apiService.uploadFile(file);
|
||||
setQuestionFileObject(file);
|
||||
setQuestionFileCount(1);
|
||||
} catch (error) {
|
||||
console.error('학습 평가 문제 업로드 실패:', error);
|
||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -618,10 +957,23 @@ export default function AdminLessonsPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedLessons.map((lesson) => (
|
||||
{paginatedLessons.map((lesson) => {
|
||||
// 원본 강좌 데이터 찾기
|
||||
const rawLecture = rawLecturesRef.current.find(
|
||||
(l: any) => String(l.id || l.lectureId) === lesson.id
|
||||
);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={lesson.id}
|
||||
className="h-12 hover:bg-[#F5F7FF] transition-colors"
|
||||
onClick={() => {
|
||||
// 원본 데이터를 sessionStorage에 저장
|
||||
if (rawLecture) {
|
||||
sessionStorage.setItem('selectedLecture', JSON.stringify(rawLecture));
|
||||
}
|
||||
router.push(`/admin/lessons/${lesson.id}`);
|
||||
}}
|
||||
className="h-12 hover:bg-[#F5F7FF] transition-colors cursor-pointer"
|
||||
>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{lesson.courseName}
|
||||
@@ -642,7 +994,8 @@ export default function AdminLessonsPage() {
|
||||
{lesson.createdAt}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -740,6 +1093,23 @@ export default function AdminLessonsPage() {
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 강좌 등록 완료 토스트 */}
|
||||
{showToast && (
|
||||
<div className="fixed right-[60px] bottom-[60px] z-50">
|
||||
<div className="bg-white border border-[#dee1e6] rounded-[8px] p-4 min-w-[360px] flex gap-[10px] items-center">
|
||||
<div className="relative shrink-0 w-[16.667px] h-[16.667px]">
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8.5" cy="8.5" r="8.5" fill="#384FBF"/>
|
||||
<path d="M5.5 8.5L7.5 10.5L11.5 6.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[15px] font-medium leading-[1.5] text-[#1b2027] text-nowrap">
|
||||
강좌가 등록되었습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user