강좌 삭제 구현1

This commit is contained in:
2025-11-28 21:37:05 +09:00
parent 03b4fa108a
commit 5a26d96386
6 changed files with 877 additions and 250 deletions

View File

@@ -39,11 +39,14 @@ export default function AdminLessonsPage() {
const [courseVideoCount, setCourseVideoCount] = useState(0);
const [courseVideoFiles, setCourseVideoFiles] = useState<string[]>([]);
const [courseVideoFileObjects, setCourseVideoFileObjects] = useState<File[]>([]);
const [courseVideoFileKeys, setCourseVideoFileKeys] = useState<string[]>([]);
const [vrContentCount, setVrContentCount] = useState(0);
const [vrContentFiles, setVrContentFiles] = useState<string[]>([]);
const [vrContentFileObjects, setVrContentFileObjects] = useState<File[]>([]);
const [vrContentFileKeys, setVrContentFileKeys] = useState<string[]>([]);
const [questionFileCount, setQuestionFileCount] = useState(0);
const [questionFileObject, setQuestionFileObject] = useState<File | null>(null);
const [questionFileKey, setQuestionFileKey] = useState<string | null>(null);
// 에러 상태
const [errors, setErrors] = useState<{
@@ -234,11 +237,14 @@ export default function AdminLessonsPage() {
setCourseVideoCount(0);
setCourseVideoFiles([]);
setCourseVideoFileObjects([]);
setCourseVideoFileKeys([]);
setVrContentCount(0);
setVrContentFiles([]);
setVrContentFileObjects([]);
setVrContentFileKeys([]);
setQuestionFileCount(0);
setQuestionFileObject(null);
setQuestionFileKey(null);
setErrors({});
};
@@ -274,72 +280,27 @@ export default function AdminLessonsPage() {
setErrors({});
try {
// 파일 업로드 및 키 추출
let videoUrl: string | undefined;
let webglUrl: string | undefined;
// 이미 업로드된 fileKey 배열 사용
const videoFileKeys = courseVideoFileKeys;
const vrFileKeys = vrContentFileKeys;
const csvFileKey = questionFileKey;
// 강좌 영상 fileKey 배열 (모든 fileKey를 배열로 저장)
let videoUrl: string[] | undefined;
if (videoFileKeys.length > 0) {
videoUrl = videoFileKeys; // 모든 fileKey를 배열로 저장
}
// VR 콘텐츠 fileKey 배열 (모든 fileKey를 배열로 저장)
let webglUrl: string[] | undefined;
if (vrFileKeys.length > 0) {
webglUrl = vrFileKeys; // 모든 fileKey를 배열로 저장
}
// 학습 평가 문제 fileKey
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);
}
if (csvFileKey) {
csvKey = csvFileKey;
}
// API 요청 body 구성
@@ -347,8 +308,8 @@ export default function AdminLessonsPage() {
subjectId: number;
title: string;
objective: string;
videoUrl?: string;
webglUrl?: string;
videoUrl?: string | string[];
webglUrl?: string | string[];
csvKey?: string;
} = {
subjectId: Number(selectedCourse),
@@ -357,10 +318,10 @@ export default function AdminLessonsPage() {
};
// 선택적 필드 추가
if (videoUrl) {
if (videoUrl && videoUrl.length > 0) {
requestBody.videoUrl = videoUrl;
}
if (webglUrl) {
if (webglUrl && webglUrl.length > 0) {
requestBody.webglUrl = webglUrl;
}
if (csvKey) {
@@ -416,11 +377,14 @@ export default function AdminLessonsPage() {
setCourseVideoCount(0);
setCourseVideoFiles([]);
setCourseVideoFileObjects([]);
setCourseVideoFileKeys([]);
setVrContentCount(0);
setVrContentFiles([]);
setVrContentFileObjects([]);
setVrContentFileKeys([]);
setQuestionFileCount(0);
setQuestionFileObject(null);
setQuestionFileKey(null);
// 토스트 팝업 표시
setShowToast(true);
@@ -628,37 +592,56 @@ export default function AdminLessonsPage() {
<input
type="file"
multiple
accept=".mp4,video/mp4"
accept=".mp4,.avi,.mov,.wmv,.flv,.webm,.mkv,video/mp4,video/avi,video/quicktime,video/x-ms-wmv,video/x-flv,video/webm,video/x-matroska"
className="hidden"
onChange={async (e) => {
const files = e.target.files;
if (!files) return;
const MAX_SIZE = 30 * 1024 * 1024; // 30MB
const MAX_COUNT = 10; // 최대 10개
const validFiles: File[] = [];
const oversizedFiles: string[] = [];
const invalidTypeFiles: string[] = [];
// 영상 파일 확장자 확인
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'];
Array.from(files).forEach((file) => {
// mp4 파일인지 확인
if (!file.name.toLowerCase().endsWith('.mp4')) {
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (!videoExtensions.includes(fileExtension)) {
invalidTypeFiles.push(file.name);
return;
}
// 각 파일이 30MB 미만인지 검사
if (file.size < MAX_SIZE) {
// 각 파일이 30MB 이하인지 검사
if (file.size <= MAX_SIZE) {
validFiles.push(file);
} else {
oversizedFiles.push(file.name);
}
});
// 30MB 이상인 파일이 있으면 알림
// 파일 타입 오류
if (invalidTypeFiles.length > 0) {
alert(`다음 파일은 영상 파일 형식만 가능합니다 (MP4, AVI, MOV, WMV, FLV, WEBM, MKV):\n${invalidTypeFiles.join('\n')}`);
}
// 30MB 초과 파일이 있으면 알림
if (oversizedFiles.length > 0) {
alert(`다음 파일은 30MB 미만이어야 합니다:\n${oversizedFiles.join('\n')}`);
alert(`다음 파일은 30MB 이하여야 합니다:\n${oversizedFiles.join('\n')}`);
}
// 파일 개수 제한 확인
if (courseVideoCount + validFiles.length > 10) {
alert('강좌 영상은 최대 10개까지 첨부할 수 있습니다.');
const totalCount = courseVideoCount + validFiles.length;
if (totalCount > MAX_COUNT) {
const availableCount = MAX_COUNT - courseVideoCount;
alert(`강좌 영상은 최대 ${MAX_COUNT}개까지 첨부할 수 있습니다. (현재 ${courseVideoCount}개, 추가 가능 ${availableCount > 0 ? availableCount : 0}개)`);
e.target.value = '';
return;
}
// 30MB 초과 파일이나 잘못된 타입 파일만 있는 경우 중단
if (validFiles.length === 0 && (oversizedFiles.length > 0 || invalidTypeFiles.length > 0)) {
e.target.value = '';
return;
}
@@ -666,10 +649,26 @@ export default function AdminLessonsPage() {
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);
const uploadResponse = await apiService.uploadFiles(validFiles);
// 응답에서 fileKey 배열 추출
const fileKeys: string[] = [];
if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) {
uploadResponse.data.results.forEach((result: any) => {
if (result.ok && result.fileKey) {
fileKeys.push(result.fileKey);
}
});
}
if (fileKeys.length > 0) {
setCourseVideoFiles(prev => [...prev, ...validFiles.map(f => f.name)]);
setCourseVideoFileObjects(prev => [...prev, ...validFiles]);
setCourseVideoFileKeys(prev => [...prev, ...fileKeys]);
setCourseVideoCount(prev => prev + validFiles.length);
} else {
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
}
} catch (error) {
console.error('강좌 영상 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
@@ -703,6 +702,7 @@ export default function AdminLessonsPage() {
onClick={() => {
setCourseVideoFiles(prev => prev.filter((_, i) => i !== index));
setCourseVideoFileObjects(prev => prev.filter((_, i) => i !== index));
setCourseVideoFileKeys(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"
@@ -740,30 +740,47 @@ export default function AdminLessonsPage() {
if (!files) return;
const MAX_SIZE = 30 * 1024 * 1024; // 30MB
const MAX_COUNT = 10; // 최대 10개
const validFiles: File[] = [];
const oversizedFiles: string[] = [];
const invalidTypeFiles: string[] = [];
Array.from(files).forEach((file) => {
// zip 파일인지 확인
if (!file.name.toLowerCase().endsWith('.zip')) {
// ZIP 파일인지 확인
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (fileExtension !== '.zip') {
invalidTypeFiles.push(file.name);
return;
}
// 각 파일이 30MB 미만인지 검사
if (file.size < MAX_SIZE) {
// 각 파일이 30MB 이하인지 검사
if (file.size <= MAX_SIZE) {
validFiles.push(file);
} else {
oversizedFiles.push(file.name);
}
});
// 30MB 이상인 파일이 있으면 알림
// 파일 타입 오류
if (invalidTypeFiles.length > 0) {
alert(`다음 파일은 ZIP 형식만 가능합니다:\n${invalidTypeFiles.join('\n')}`);
}
// 30MB 초과 파일이 있으면 알림
if (oversizedFiles.length > 0) {
alert(`다음 파일은 30MB 미만이어야 합니다:\n${oversizedFiles.join('\n')}`);
alert(`다음 파일은 30MB 이하여야 합니다:\n${oversizedFiles.join('\n')}`);
}
// 파일 개수 제한 확인
if (vrContentCount + validFiles.length > 10) {
alert('VR 콘텐츠는 최대 10개까지 첨부할 수 있습니다.');
const totalCount = vrContentCount + validFiles.length;
if (totalCount > MAX_COUNT) {
const availableCount = MAX_COUNT - vrContentCount;
alert(`VR 콘텐츠는 최대 ${MAX_COUNT}개까지 첨부할 수 있습니다. (현재 ${vrContentCount}개, 추가 가능 ${availableCount > 0 ? availableCount : 0}개)`);
e.target.value = '';
return;
}
// 30MB 초과 파일이나 잘못된 타입 파일만 있는 경우 중단
if (validFiles.length === 0 && (oversizedFiles.length > 0 || invalidTypeFiles.length > 0)) {
e.target.value = '';
return;
}
@@ -771,10 +788,26 @@ export default function AdminLessonsPage() {
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);
const uploadResponse = await apiService.uploadFiles(validFiles);
// 응답에서 fileKey 배열 추출
const fileKeys: string[] = [];
if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) {
uploadResponse.data.results.forEach((result: any) => {
if (result.ok && result.fileKey) {
fileKeys.push(result.fileKey);
}
});
}
if (fileKeys.length > 0) {
setVrContentFiles(prev => [...prev, ...validFiles.map(f => f.name)]);
setVrContentFileObjects(prev => [...prev, ...validFiles]);
setVrContentFileKeys(prev => [...prev, ...fileKeys]);
setVrContentCount(prev => prev + validFiles.length);
} else {
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
}
} catch (error) {
console.error('VR 콘텐츠 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
@@ -808,6 +841,7 @@ export default function AdminLessonsPage() {
onClick={() => {
setVrContentFiles(prev => prev.filter((_, i) => i !== index));
setVrContentFileObjects(prev => prev.filter((_, i) => i !== index));
setVrContentFileKeys(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"
@@ -844,25 +878,59 @@ export default function AdminLessonsPage() {
<span></span>
<input
type="file"
accept=".csv"
accept=".csv,text/csv,application/vnd.ms-excel"
className="hidden"
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')) {
try {
// 단일 파일 업로드
await apiService.uploadFile(file);
setQuestionFileObject(file);
setQuestionFileCount(1);
} catch (error) {
console.error('학습 평가 문제 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
}
if (!file.name.toLowerCase().endsWith('.csv')) {
alert('CSV 파일 형식만 첨부할 수 있습니다.');
e.target.value = '';
return;
}
try {
// 단일 파일 업로드
const uploadResponse = await apiService.uploadFile(file);
// 응답에서 fileKey 추출
let fileKey: string | null = null;
if (uploadResponse.data?.fileKey) {
fileKey = uploadResponse.data.fileKey;
} else if (uploadResponse.data?.key) {
fileKey = uploadResponse.data.key;
} else if (uploadResponse.data?.id) {
fileKey = uploadResponse.data.id;
} else if (uploadResponse.data?.imageKey) {
fileKey = uploadResponse.data.imageKey;
} else if (uploadResponse.data?.fileId) {
fileKey = uploadResponse.data.fileId;
} else if (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) {
fileKey = uploadResponse.data.data.key || uploadResponse.data.data.fileKey;
} else if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results) && uploadResponse.data.results.length > 0) {
const result = uploadResponse.data.results[0];
if (result.ok && result.fileKey) {
fileKey = result.fileKey;
}
}
if (fileKey) {
setQuestionFileObject(file);
setQuestionFileKey(fileKey);
setQuestionFileCount(1);
} else {
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
}
} catch (error) {
console.error('학습 평가 문제 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
}
// input 초기화 (같은 파일 다시 선택 가능하도록)
e.target.value = '';
}}
/>
</label>