공지사항 삭제등록 등11

This commit is contained in:
2025-11-29 15:40:39 +09:00
parent 872a88866e
commit eb7871133d
10 changed files with 1663 additions and 412 deletions

View File

@@ -16,6 +16,9 @@ export default function LessonEditPage() {
const [isSaving, setIsSaving] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const videoFileInputRef = useRef<HTMLInputElement>(null);
const vrFileInputRef = useRef<HTMLInputElement>(null);
const csvFileInputRef = useRef<HTMLInputElement>(null);
const [courses, setCourses] = useState<Course[]>([]);
const [showToast, setShowToast] = useState(false);
@@ -184,11 +187,134 @@ export default function LessonEditPage() {
// 기존 평가 문제 파일
if (data.csvKey || data.csvUrl) {
const csvFileKey = data.csvKey || (data.csvUrl && data.csvUrl.startsWith('csv/') ? data.csvUrl.substring(4) : data.csvUrl);
setExistingQuestionFile({
fileName: data.csvFileName || data.csv_file_name || data.csvName || '평가문제.csv',
fileKey: data.csvKey,
fileKey: csvFileKey,
});
setQuestionFileCount(1);
// CSV 파일 다운로드 및 파싱
if (data.csvUrl || data.csvKey) {
try {
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://hrdi.coconutmeet.net';
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
let fileKey: string | null = null;
// csvUrl에서 fileKey 추출
if (data.csvUrl) {
// 전체 URL인 경우 fileKey 추출
if (data.csvUrl.startsWith('http://') || data.csvUrl.startsWith('https://')) {
// URL에서 /api/files/ 이후 부분을 fileKey로 사용
const filesIndex = data.csvUrl.indexOf('/api/files/');
if (filesIndex !== -1) {
const extractedKey = data.csvUrl.substring(filesIndex + '/api/files/'.length);
// URL 디코딩
fileKey = decodeURIComponent(extractedKey);
} else {
// URL에서 마지막 경로를 fileKey로 사용
const urlParts = data.csvUrl.split('/');
const lastPart = urlParts[urlParts.length - 1];
if (lastPart) {
fileKey = decodeURIComponent(lastPart);
}
}
} else if (data.csvUrl.startsWith('csv/')) {
// "csv/" 접두사 제거
fileKey = data.csvUrl.substring(4);
} else {
// 그 외의 경우 fileKey로 사용
fileKey = data.csvUrl;
}
} else if (data.csvKey) {
// csvKey가 있으면 fileKey로 사용
fileKey = data.csvKey;
}
if (!fileKey) {
return; // fileKey가 없으면 종료
}
// /api/files/{fileKey} 형태로 요청
const csvUrl = `${baseURL}/api/files/${encodeURIComponent(fileKey as string)}`;
const csvResponse = await fetch(csvUrl, {
method: 'GET',
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
});
// 404 에러는 파일이 없는 것으로 간주하고 조용히 처리
if (csvResponse.status === 404) {
console.warn('CSV 파일을 찾을 수 없습니다:', data.csvUrl || data.csvKey);
} else if (csvResponse.ok) {
const csvText = await csvResponse.text();
// CSV 파싱 함수
const parseCsv = (csvText: string): string[][] => {
const lines: string[][] = [];
let currentLine: string[] = [];
let currentField = '';
let inQuotes = false;
for (let i = 0; i < csvText.length; i++) {
const char = csvText[i];
const nextChar = csvText[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
currentField += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
currentLine.push(currentField.trim());
currentField = '';
} else if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && nextChar === '\n') {
i++;
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
currentLine = [];
currentField = '';
}
} else {
currentField += char;
}
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
}
return lines;
};
const parsed = parseCsv(csvText);
if (parsed.length > 0) {
const headers = parsed[0];
const rows = parsed.slice(1);
setCsvHeaders(headers);
setCsvRows(rows);
setCsvData(parsed);
}
} else if (!csvResponse.ok) {
console.error(`CSV 파일을 가져오는데 실패했습니다. (${csvResponse.status})`);
}
} catch (csvError) {
console.error('CSV 파일 파싱 실패:', csvError);
// CSV 파싱 실패해도 계속 진행
}
}
}
} catch (err) {
console.error('강좌 로드 실패:', err);
@@ -365,82 +491,94 @@ export default function LessonEditPage() {
const handleCsvFileUpload = async (file: File) => {
try {
// CSV 파일 파싱
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result as string;
if (!text) return;
// CSV 파일 파싱 (Promise로 감싸서 파일 읽기 완료 대기)
const parseCsvFile = (): Promise<string[][]> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result as string;
if (!text) {
reject(new Error('파일을 읽을 수 없습니다.'));
return;
}
try {
// CSV 파싱 함수
const parseCsv = (csvText: string): string[][] => {
const lines: string[][] = [];
let currentLine: string[] = [];
let currentField = '';
let inQuotes = false;
try {
// CSV 파싱 함수
const parseCsv = (csvText: string): string[][] => {
const lines: string[][] = [];
let currentLine: string[] = [];
let currentField = '';
let inQuotes = false;
for (let i = 0; i < csvText.length; i++) {
const char = csvText[i];
const nextChar = csvText[i + 1];
for (let i = 0; i < csvText.length; i++) {
const char = csvText[i];
const nextChar = csvText[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
currentField += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
currentLine.push(currentField.trim());
currentField = '';
} else if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && nextChar === '\n') {
i++;
if (char === '"') {
if (inQuotes && nextChar === '"') {
currentField += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
currentLine.push(currentField.trim());
currentField = '';
} else if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && nextChar === '\n') {
i++;
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
currentLine = [];
currentField = '';
}
} else {
currentField += char;
}
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
currentLine = [];
currentField = '';
}
} else {
currentField += char;
return lines;
};
const parsed = parseCsv(text);
if (parsed.length === 0) {
reject(new Error('CSV 파일이 비어있습니다.'));
return;
}
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
resolve(parsed);
} catch (parseError) {
reject(new Error('CSV 파일을 읽는 중 오류가 발생했습니다.'));
}
return lines;
};
const parsed = parseCsv(text);
if (parsed.length === 0) {
alert('CSV 파일이 비어있습니다.');
return;
}
reader.onerror = () => {
reject(new Error('파일을 읽는 중 오류가 발생했습니다.'));
};
// 첫 번째 줄을 헤더로 사용
const headers = parsed[0];
const rows = parsed.slice(1);
setCsvHeaders(headers);
setCsvRows(rows);
setCsvData(parsed);
} catch (parseError) {
console.error('CSV 파싱 오류:', parseError);
alert('CSV 파일을 읽는 중 오류가 발생했습니다.');
}
reader.readAsText(file, 'UTF-8');
});
};
reader.onerror = () => {
alert('파일을 읽는 중 오류가 발생했습니다.');
};
// CSV 파일 파싱
const parsed = await parseCsvFile();
// 첫 번째 줄을 헤더로 사용
const headers = parsed[0];
const rows = parsed.slice(1);
reader.readAsText(file, 'UTF-8');
setCsvHeaders(headers);
setCsvRows(rows);
setCsvData(parsed);
// 단일 파일 업로드
const uploadResponse = await apiService.uploadFile(file);
@@ -465,22 +603,24 @@ export default function LessonEditPage() {
}
} catch (error) {
console.error('학습 평가 문제 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
const errorMessage = error instanceof Error ? error.message : '파일 업로드에 실패했습니다. 다시 시도해주세요.';
alert(errorMessage);
}
};
// 파일 교체 확인 모달 핸들러
const handleFileReplaceConfirm = async () => {
if (!pendingFiles.length || !pendingFileType) return;
const handleFileReplaceConfirm = () => {
if (!pendingFileType) return;
setIsFileReplaceModalOpen(false);
if (pendingFileType === 'video') {
await handleVideoFileUpload(pendingFiles);
} else if (pendingFileType === 'vr') {
await handleVrFileUpload(pendingFiles);
} else if (pendingFileType === 'csv' && pendingFiles.length > 0) {
await handleCsvFileUpload(pendingFiles[0]);
// 파일 선택 다이얼로그 열기
if (pendingFileType === 'video' && videoFileInputRef.current) {
videoFileInputRef.current.click();
} else if (pendingFileType === 'vr' && vrFileInputRef.current) {
vrFileInputRef.current.click();
} else if (pendingFileType === 'csv' && csvFileInputRef.current) {
csvFileInputRef.current.click();
}
setPendingFiles([]);
@@ -634,9 +774,9 @@ export default function LessonEditPage() {
// 성공 시 토스트 표시
setShowToast(true);
// 토스트 표시 후 상세 페이지로 이동
// 토스트 표시 후 상세 페이지로 이동 (새로고침하여 최신 데이터 표시)
setTimeout(() => {
router.push(`/admin/lessons/${params.id}`);
router.push(`/admin/lessons/${params.id}?refresh=${Date.now()}`);
}, 1500);
} catch (error) {
console.error('강좌 수정 실패:', error);
@@ -858,9 +998,21 @@ export default function LessonEditPage() {
30MB
</span>
</div>
<label className="h-[32px] w-[62px] px-[4px] py-[3px] border border-[#8c95a1] rounded-[6px] bg-white text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap hover:bg-gray-50 transition-colors cursor-pointer flex items-center justify-center">
<label
className="h-[32px] w-[62px] px-[4px] py-[3px] border border-[#8c95a1] rounded-[6px] bg-white text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap hover:bg-gray-50 transition-colors cursor-pointer flex items-center justify-center"
onClick={(e) => {
// 기존 파일이나 새로 첨부한 파일이 있으면 확인 모달 표시
if (existingVideoFiles.length > 0 || courseVideoFiles.length > 0 || courseVideoFileKeys.length > 0) {
e.preventDefault();
setPendingFiles([]);
setPendingFileType('video');
setIsFileReplaceModalOpen(true);
}
}}
>
<span></span>
<input
ref={videoFileInputRef}
type="file"
multiple
accept=".mp4,video/mp4"
@@ -916,16 +1068,8 @@ export default function LessonEditPage() {
}
if (validFiles.length > 0) {
// 새로 첨부한 파일이 있으면 확인 모달 표시
if (courseVideoFiles.length > 0 || courseVideoFileKeys.length > 0) {
setPendingFiles(validFiles);
setPendingFileType('video');
setIsFileReplaceModalOpen(true);
e.target.value = '';
return;
}
// 새로 첨부한 파일이 없으면 바로 업로드
// 기존 파일 삭제 후 새 파일 업로드
setExistingVideoFiles([]);
handleVideoFileUpload(validFiles);
}
e.target.value = '';
@@ -948,7 +1092,7 @@ export default function LessonEditPage() {
className="h-[40px] px-[20px] py-[12px] flex items-center gap-[12px]"
>
<p className="flex-1 text-[15px] font-normal leading-[1.5] text-[#333c47]">
{file.fileName} ()
{file.fileName}
</p>
<button
type="button"
@@ -1002,9 +1146,21 @@ export default function LessonEditPage() {
30MB
</span>
</div>
<label className="h-[32px] w-[62px] px-[4px] py-[3px] border border-[#8c95a1] rounded-[6px] bg-white text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap hover:bg-gray-50 transition-colors cursor-pointer flex items-center justify-center">
<label
className="h-[32px] w-[62px] px-[4px] py-[3px] border border-[#8c95a1] rounded-[6px] bg-white text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap hover:bg-gray-50 transition-colors cursor-pointer flex items-center justify-center"
onClick={(e) => {
// 기존 파일이나 새로 첨부한 파일이 있으면 확인 모달 표시
if (existingVrFiles.length > 0 || vrContentFiles.length > 0 || vrContentFileKeys.length > 0) {
e.preventDefault();
setPendingFiles([]);
setPendingFileType('vr');
setIsFileReplaceModalOpen(true);
}
}}
>
<span></span>
<input
ref={vrFileInputRef}
type="file"
multiple
accept=".zip,application/zip"
@@ -1060,16 +1216,8 @@ export default function LessonEditPage() {
}
if (validFiles.length > 0) {
// 새로 첨부한 파일이 있으면 확인 모달 표시
if (vrContentFiles.length > 0 || vrContentFileKeys.length > 0) {
setPendingFiles(validFiles);
setPendingFileType('vr');
setIsFileReplaceModalOpen(true);
e.target.value = '';
return;
}
// 새로 첨부한 파일이 없으면 바로 업로드
// 기존 파일 삭제 후 새 파일 업로드
setExistingVrFiles([]);
handleVrFileUpload(validFiles);
}
e.target.value = '';
@@ -1092,7 +1240,7 @@ export default function LessonEditPage() {
className="h-[40px] px-[20px] py-[12px] flex items-center gap-[12px]"
>
<p className="flex-1 text-[15px] font-normal leading-[1.5] text-[#333c47]">
{file.fileName} ()
{file.fileName}
</p>
<button
type="button"
@@ -1147,9 +1295,21 @@ export default function LessonEditPage() {
</span>
</div>
<div className="flex items-center gap-[8px]">
<label className="h-[32px] w-[62px] px-[4px] py-[3px] border border-[#8c95a1] rounded-[6px] bg-white text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap hover:bg-gray-50 transition-colors cursor-pointer flex items-center justify-center">
<label
className="h-[32px] w-[62px] px-[4px] py-[3px] border border-[#8c95a1] rounded-[6px] bg-white text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap hover:bg-gray-50 transition-colors cursor-pointer flex items-center justify-center"
onClick={(e) => {
// 기존 파일이나 새로 첨부한 파일이 있으면 확인 모달 표시
if (questionFileObject || questionFileKey || existingQuestionFile) {
e.preventDefault();
setPendingFiles([]);
setPendingFileType('csv');
setIsFileReplaceModalOpen(true);
}
}}
>
<span></span>
<input
ref={csvFileInputRef}
type="file"
accept=".csv"
className="hidden"
@@ -1166,100 +1326,9 @@ export default function LessonEditPage() {
return;
}
try {
// CSV 파일 파싱
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result as string;
if (!text) return;
try {
// CSV 파싱 함수
const parseCsv = (csvText: string): string[][] => {
const lines: string[][] = [];
let currentLine: string[] = [];
let currentField = '';
let inQuotes = false;
for (let i = 0; i < csvText.length; i++) {
const char = csvText[i];
const nextChar = csvText[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
currentField += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
currentLine.push(currentField.trim());
currentField = '';
} else if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && nextChar === '\n') {
i++;
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
currentLine = [];
currentField = '';
}
} else {
currentField += char;
}
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
}
return lines;
};
const parsed = parseCsv(text);
if (parsed.length === 0) {
alert('CSV 파일이 비어있습니다.');
return;
}
// 첫 번째 줄을 헤더로 사용
const headers = parsed[0];
const rows = parsed.slice(1);
setCsvHeaders(headers);
setCsvRows(rows);
setCsvData(parsed);
} catch (parseError) {
console.error('CSV 파싱 오류:', parseError);
alert('CSV 파일을 읽는 중 오류가 발생했습니다.');
}
};
reader.onerror = () => {
alert('파일을 읽는 중 오류가 발생했습니다.');
};
reader.readAsText(file, 'UTF-8');
// 기존 파일이 있으면 확인 모달 표시
if (questionFileObject || questionFileKey) {
setPendingFiles([file]);
setPendingFileType('csv');
setIsFileReplaceModalOpen(true);
e.target.value = '';
return;
}
// 기존 파일이 없으면 바로 업로드
await handleCsvFileUpload(file);
} catch (error) {
console.error('학습 평가 문제 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
}
// input 초기화 (같은 파일 다시 선택 가능하도록)
// 기존 파일 삭제 후 새 파일 업로드
setExistingQuestionFile(null);
await handleCsvFileUpload(file);
e.target.value = '';
}}
/>
@@ -1279,7 +1348,7 @@ export default function LessonEditPage() {
{(existingQuestionFile || questionFileObject) && (
<div className="h-[40px] px-[20px] py-[12px] flex items-center gap-[12px] bg-white border-b border-[#dee1e6]">
<p className="flex-1 text-[15px] font-normal leading-[1.5] text-[#333c47]">
{existingQuestionFile ? `${existingQuestionFile.fileName} (기존)` : questionFileObject?.name}
{existingQuestionFile ? existingQuestionFile.fileName : questionFileObject?.name}
</p>
<button
type="button"