From 91d14fdabf4cd475692a3ec51dbf9d0fbd74c3bb Mon Sep 17 00:00:00 2001 From: wallace Date: Sat, 29 Nov 2025 13:41:13 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD,=20csv=20?= =?UTF-8?q?=ED=91=9C=EC=B6=9C=20=EC=9E=91=EC=97=851?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/lessons/[id]/edit/page.tsx | 260 +++++++++++--- src/app/admin/lessons/page.tsx | 178 +++++++++- .../admin/notices/NoticeValidationModal.tsx | 54 +++ src/app/admin/notices/[id]/page.tsx | 331 ++++++++++++++++++ src/app/admin/notices/page.tsx | 228 ++++++++++-- 5 files changed, 960 insertions(+), 91 deletions(-) create mode 100644 src/app/admin/notices/NoticeValidationModal.tsx create mode 100644 src/app/admin/notices/[id]/page.tsx diff --git a/src/app/admin/lessons/[id]/edit/page.tsx b/src/app/admin/lessons/[id]/edit/page.tsx index 73b27b2..3f93a36 100644 --- a/src/app/admin/lessons/[id]/edit/page.tsx +++ b/src/app/admin/lessons/[id]/edit/page.tsx @@ -37,6 +37,9 @@ export default function LessonEditPage() { const [questionFileObject, setQuestionFileObject] = useState(null); const [questionFileKey, setQuestionFileKey] = useState(null); const [existingQuestionFile, setExistingQuestionFile] = useState<{fileName: string, fileKey?: string} | null>(null); + const [csvData, setCsvData] = useState([]); + const [csvHeaders, setCsvHeaders] = useState([]); + const [csvRows, setCsvRows] = useState([]); // 원본 데이터 저장 (변경사항 비교용) const [originalData, setOriginalData] = useState<{ @@ -177,7 +180,7 @@ export default function LessonEditPage() { // 기존 평가 문제 파일 if (data.csvKey || data.csvUrl) { setExistingQuestionFile({ - fileName: '평가문제.csv', + fileName: data.csvFileName || data.csv_file_name || data.csvName || '평가문제.csv', fileKey: data.csvKey, }); setQuestionFileCount(1); @@ -935,68 +938,223 @@ export default function LessonEditPage() { if (!files || files.length === 0) return; const file = files[0]; - if (file.name.toLowerCase().endsWith('.csv')) { - 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?.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); - setExistingQuestionFile(null); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } - } catch (error) { - console.error('학습 평가 문제 업로드 실패:', error); - alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); - } + + // CSV 파일만 허용 + if (!file.name.toLowerCase().endsWith('.csv')) { + alert('CSV 파일 형식만 첨부할 수 있습니다.'); + e.target.value = ''; + 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'); + + // 단일 파일 업로드 + 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); + setExistingQuestionFile(null); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('학습 평가 문제 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + // input 초기화 (같은 파일 다시 선택 가능하도록) + e.target.value = ''; }} />
- {existingQuestionFile || questionFileObject ? ( -
-

- {existingQuestionFile ? `${existingQuestionFile.fileName} (기존)` : questionFileObject?.name} -

- -
- ) : ( + {csvData.length === 0 ? (

학습 평가용 문항 파일을 첨부해주세요.

+ ) : ( +
+ {/* 파일 정보 및 삭제 버튼 */} + {(existingQuestionFile || questionFileObject) && ( +
+

+ {existingQuestionFile ? `${existingQuestionFile.fileName} (기존)` : questionFileObject?.name} +

+ +
+ )} + {/* CSV 표 */} +
+
+ {/* 헤더 */} +
+ {csvHeaders.map((header, index) => { + const isLast = index === csvHeaders.length - 1; + return ( +
+
+

{header || `열 ${index + 1}`}

+
+
+ ); + })} +
+ + {/* 데이터 행 */} + {csvRows.map((row, rowIndex) => ( +
+
+ {csvHeaders.map((_, colIndex) => { + const isLast = colIndex === csvHeaders.length - 1; + const cellValue = row[colIndex] || ''; + return ( +
+

+ {cellValue} +

+
+ ); + })} +
+
+ ))} +
+
+
)}
diff --git a/src/app/admin/lessons/page.tsx b/src/app/admin/lessons/page.tsx index db8cf21..64f33e2 100644 --- a/src/app/admin/lessons/page.tsx +++ b/src/app/admin/lessons/page.tsx @@ -47,6 +47,9 @@ export default function AdminLessonsPage() { const [questionFileCount, setQuestionFileCount] = useState(0); const [questionFileObject, setQuestionFileObject] = useState(null); const [questionFileKey, setQuestionFileKey] = useState(null); + const [csvData, setCsvData] = useState([]); + const [csvHeaders, setCsvHeaders] = useState([]); + const [csvRows, setCsvRows] = useState([]); // 에러 상태 const [errors, setErrors] = useState<{ @@ -232,6 +235,9 @@ export default function AdminLessonsPage() { setQuestionFileCount(0); setQuestionFileObject(null); setQuestionFileKey(null); + setCsvData([]); + setCsvHeaders([]); + setCsvRows([]); setErrors({}); }; @@ -372,6 +378,9 @@ export default function AdminLessonsPage() { setQuestionFileCount(0); setQuestionFileObject(null); setQuestionFileKey(null); + setCsvData([]); + setCsvHeaders([]); + setCsvRows([]); // 토스트 팝업 표시 setShowToast(true); @@ -881,6 +890,83 @@ export default function AdminLessonsPage() { } 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'); + // 단일 파일 업로드 const uploadResponse = await apiService.uploadFile(file); @@ -923,10 +1009,94 @@ export default function AdminLessonsPage() { -
-

- 학습 평가용 문항 파일을 첨부해주세요. -

+
+ {csvData.length === 0 ? ( +
+

+ 학습 평가용 문항 파일을 첨부해주세요. +

+
+ ) : ( +
+ {/* 파일 정보 및 삭제 버튼 */} + {questionFileObject && ( +
+

+ {questionFileObject.name} +

+ +
+ )} + {/* CSV 표 */} +
+
+ {/* 헤더 */} +
+ {csvHeaders.map((header, index) => { + const isLast = index === csvHeaders.length - 1; + return ( +
+
+

{header || `열 ${index + 1}`}

+
+
+ ); + })} +
+ + {/* 데이터 행 */} + {csvRows.map((row, rowIndex) => ( +
+
+ {csvHeaders.map((_, colIndex) => { + const isLast = colIndex === csvHeaders.length - 1; + const cellValue = row[colIndex] || ''; + return ( +
+

+ {cellValue} +

+
+ ); + })} +
+
+ ))} +
+
+
+ )}
diff --git a/src/app/admin/notices/NoticeValidationModal.tsx b/src/app/admin/notices/NoticeValidationModal.tsx new file mode 100644 index 0000000..cb289db --- /dev/null +++ b/src/app/admin/notices/NoticeValidationModal.tsx @@ -0,0 +1,54 @@ +"use client"; + +import React from "react"; + +type NoticeValidationModalProps = { + open: boolean; + onClose: () => void; +}; + +/** + * 공지사항 작성 시 제목 또는 내용이 비어있을 때 표시되는 검증 모달 + */ +export default function NoticeValidationModal({ + open, + onClose, +}: NoticeValidationModalProps) { + if (!open) return null; + + return ( +
+ +
+ + + ); +} + diff --git a/src/app/admin/notices/[id]/page.tsx b/src/app/admin/notices/[id]/page.tsx new file mode 100644 index 0000000..15a893c --- /dev/null +++ b/src/app/admin/notices/[id]/page.tsx @@ -0,0 +1,331 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import Link from 'next/link'; +import AdminSidebar from '@/app/components/AdminSidebar'; +import BackCircleSvg from '@/app/svgs/backcirclesvg'; +import DownloadIcon from '@/app/svgs/downloadicon'; +import PaperClipSvg from '@/app/svgs/paperclipsvg'; +import apiService from '@/app/lib/apiService'; +import type { Notice } from '@/app/admin/notices/mockData'; + +type Attachment = { + name: string; + size: string; + url?: string; + fileKey?: string; +}; + +export default function AdminNoticeDetailPage() { + const params = useParams(); + const router = useRouter(); + const [notice, setNotice] = useState(null); + const [attachments, setAttachments] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchNotice() { + if (!params?.id) return; + + try { + setLoading(true); + setError(null); + + const response = await apiService.getNotice(params.id); + const data = response.data; + + // API 응답 데이터를 Notice 형식으로 변환 + const transformedNotice: Notice = { + id: data.id || data.noticeId || Number(params.id), + title: data.title || '', + date: data.date || data.createdAt || data.createdDate || new Date().toISOString().split('T')[0], + views: data.views || data.viewCount || 0, + writer: data.writer || data.author || data.createdBy || '관리자', + content: data.content + ? (Array.isArray(data.content) + ? data.content + : typeof data.content === 'string' + ? data.content.split('\n').filter((line: string) => line.trim()) + : [String(data.content)]) + : [], + hasAttachment: data.hasAttachment || data.attachment || false, + }; + + // 첨부파일 정보 처리 + if (data.attachments && Array.isArray(data.attachments)) { + setAttachments(data.attachments.map((att: any) => ({ + name: att.name || att.fileName || '', + size: att.size || att.fileSize || '', + url: att.url || att.downloadUrl, + fileKey: att.fileKey || att.key || att.fileId, + }))); + } else if (transformedNotice.hasAttachment && data.attachment) { + // 단일 첨부파일인 경우 + setAttachments([{ + name: data.attachment.name || data.attachment.fileName || '첨부파일', + size: data.attachment.size || data.attachment.fileSize || '', + url: data.attachment.url || data.attachment.downloadUrl, + fileKey: data.attachment.fileKey || data.attachment.key || data.attachment.fileId, + }]); + } + + if (!transformedNotice.title) { + throw new Error('공지사항을 찾을 수 없습니다.'); + } + + setNotice(transformedNotice); + } catch (err) { + console.error('공지사항 조회 오류:', err); + setError('공지사항을 불러오는 중 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + } + + fetchNotice(); + }, [params?.id]); + + const handleDownload = async (fileKey?: string, url?: string, fileName?: string) => { + if (url) { + // URL이 있으면 직접 다운로드 + const link = document.createElement('a'); + link.href = url; + link.download = fileName || 'download'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else if (fileKey) { + // fileKey가 있으면 API를 통해 다운로드 + try { + const fileUrl = await apiService.getFile(fileKey); + if (fileUrl) { + const link = document.createElement('a'); + link.href = fileUrl; + link.download = fileName || 'download'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } catch (err) { + console.error('파일 다운로드 실패:', err); + alert('파일 다운로드에 실패했습니다.'); + } + } + }; + + if (loading) { + return ( +
+
+
+
+ +
+
+
+
+

로딩 중...

+
+
+
+
+
+
+ ); + } + + if (error || !notice || !notice.content || notice.content.length === 0) { + return ( +
+
+
+
+ +
+
+
+
+ + + +

+ 공지사항 상세 +

+
+
+

+ {error || '공지사항을 찾을 수 없습니다.'} +

+
+
+
+
+
+
+ ); + } + + return ( +
+
+
+ {/* 사이드바 */} +
+ +
+ + {/* 메인 콘텐츠 */} +
+
+ {/* 상단 타이틀 */} +
+ + + +

+ 공지사항 상세 +

+
+ + {/* 카드 */} +
+
+ {/* 헤더 */} +
+
+
+

+ {notice.title} +

+
+
+ 작성자 + {notice.writer} +
+
+
+
+
+ 게시일 + + {notice.date.includes('T') + ? new Date(notice.date).toISOString().split('T')[0] + : notice.date} + +
+
+
+
+
+ 조회수 + {notice.views.toLocaleString()} +
+
+
+
+
+ + {/* 구분선 */} +
+ + {/* 본문 및 첨부파일 */} +
+ {/* 본문 */} +
+ {notice.content.map((p, idx) => ( +

+ {p} +

+ ))} +
+ + {/* 첨부파일 섹션 */} + {attachments.length > 0 && ( +
+
+
+
+

+ 첨부 파일 +

+
+
+ {attachments.map((attachment, idx) => ( +
+
+ +
+
+

+ {attachment.name} +

+

+ {attachment.size} +

+
+ +
+ ))} +
+
+ )} +
+
+ + {/* 버튼 영역 */} +
+ + +
+
+
+
+
+
+
+ ); +} + diff --git a/src/app/admin/notices/page.tsx b/src/app/admin/notices/page.tsx index 59587ae..e6e9be1 100644 --- a/src/app/admin/notices/page.tsx +++ b/src/app/admin/notices/page.tsx @@ -1,21 +1,90 @@ 'use client'; -import { useState, useMemo, useRef, ChangeEvent } from "react"; +import { useState, useMemo, useRef, ChangeEvent, useEffect } from "react"; +import { useRouter } from "next/navigation"; import AdminSidebar from "@/app/components/AdminSidebar"; import ChevronDownSvg from "@/app/svgs/chevrondownsvg"; import BackArrowSvg from "@/app/svgs/backarrow"; -import { MOCK_NOTICES, type Notice } from "@/app/admin/notices/mockData"; +import { type Notice } from "@/app/admin/notices/mockData"; import apiService from "@/app/lib/apiService"; +import NoticeValidationModal from "@/app/admin/notices/NoticeValidationModal"; export default function AdminNoticesPage() { - const [notices, setNotices] = useState(MOCK_NOTICES); + const router = useRouter(); + const [notices, setNotices] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [isWritingMode, setIsWritingMode] = useState(false); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [attachedFile, setAttachedFile] = useState(null); + const [fileKey, setFileKey] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isValidationModalOpen, setIsValidationModalOpen] = useState(false); const fileInputRef = useRef(null); + // 날짜를 yyyy-mm-dd 형식으로 포맷팅 + const formatDate = (dateString: string): string => { + if (!dateString) return ''; + + try { + const date = new Date(dateString); + if (isNaN(date.getTime())) { + // 이미 yyyy-mm-dd 형식인 경우 그대로 반환 + if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + return dateString; + } + return dateString; + } + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } catch { + return dateString; + } + }; + + // API에서 공지사항 목록 가져오기 + useEffect(() => { + async function fetchNotices() { + try { + setIsLoading(true); + const response = await apiService.getNotices(); + const data = response.data; + + // API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태) + let noticesArray: any[] = []; + if (Array.isArray(data)) { + noticesArray = data; + } else if (data && typeof data === 'object') { + noticesArray = data.items || data.notices || data.data || data.list || []; + } + + // API 응답 데이터를 Notice 형식으로 변환 + const transformedNotices: Notice[] = noticesArray.map((notice: any) => ({ + id: notice.id || notice.noticeId || 0, + title: notice.title || '', + date: notice.date || notice.createdAt || notice.createdDate || new Date().toISOString().split('T')[0], + views: notice.views || notice.viewCount || 0, + writer: notice.writer || notice.author || notice.createdBy || '관리자', + content: notice.content ? (Array.isArray(notice.content) ? notice.content : [notice.content]) : undefined, + hasAttachment: notice.hasAttachment || notice.attachment || false, + })); + + setNotices(transformedNotices); + } catch (error) { + console.error('공지사항 목록 조회 오류:', error); + // 에러 발생 시 빈 배열로 설정 + setNotices([]); + } finally { + setIsLoading(false); + } + } + + fetchNotices(); + }, []); + const totalCount = useMemo(() => notices.length, [notices]); const characterCount = useMemo(() => content.length, [content]); @@ -25,6 +94,11 @@ export default function AdminNoticesPage() { setTitle(''); setContent(''); setAttachedFile(null); + setFileKey(null); + // 파일 입력 초기화 + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } }; const handleFileAttach = () => { @@ -39,43 +113,116 @@ export default function AdminNoticesPage() { return; } try { + setIsLoading(true); // 단일 파일 업로드 - await apiService.uploadFile(file); - setAttachedFile(file); + const uploadResponse = await apiService.uploadFile(file); + + // 응답에서 fileKey 추출 + let extractedFileKey: string | null = null; + if (uploadResponse.data?.fileKey) { + extractedFileKey = uploadResponse.data.fileKey; + } else if (uploadResponse.data?.key) { + extractedFileKey = uploadResponse.data.key; + } else if (uploadResponse.data?.id) { + extractedFileKey = uploadResponse.data.id; + } else if (uploadResponse.data?.imageKey) { + extractedFileKey = uploadResponse.data.imageKey; + } else if (uploadResponse.data?.fileId) { + extractedFileKey = uploadResponse.data.fileId; + } else if (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) { + extractedFileKey = 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) { + extractedFileKey = result.fileKey; + } + } + + if (extractedFileKey) { + setFileKey(extractedFileKey); + setAttachedFile(file); + } else { + throw new Error('파일 키를 받아오지 못했습니다.'); + } } catch (error) { console.error('파일 업로드 실패:', error); alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + setAttachedFile(null); + setFileKey(null); + } finally { + setIsLoading(false); + // 파일 입력 초기화 + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } } } }; - const handleSave = () => { - if (!title.trim()) { - alert('제목을 입력해주세요.'); - return; - } - if (!content.trim()) { - alert('내용을 입력해주세요.'); + const handleSave = async () => { + if (!title.trim() || !content.trim()) { + setIsValidationModalOpen(true); return; } - // 새 공지사항 추가 - const newNotice: Notice = { - id: notices.length > 0 ? Math.max(...notices.map(n => n.id)) + 1 : 1, - title: title.trim(), - date: new Date().toISOString().split('T')[0], - views: 0, - writer: '관리자', // TODO: 실제 작성자 정보 사용 - content: content.split('\n'), - hasAttachment: attachedFile !== null, - }; - - setNotices([newNotice, ...notices]); - handleBack(); + try { + setIsLoading(true); + + // 공지사항 생성 API 호출 + const noticeData: any = { + title: title.trim(), + content: content.trim(), + }; + + // fileKey와 파일 정보가 있으면 attachments 배열로 포함 + if (fileKey && attachedFile) { + noticeData.attachments = [ + { + fileKey: fileKey, + filename: attachedFile.name, + mimeType: attachedFile.type || 'application/octet-stream', + size: attachedFile.size, + }, + ]; + } + + const response = await apiService.createNotice(noticeData); + + // API 응답 후 목록 새로고침 + const fetchResponse = await apiService.getNotices(); + const data = fetchResponse.data; + + // API 응답이 배열이 아닌 경우 처리 + let noticesArray: any[] = []; + if (Array.isArray(data)) { + noticesArray = data; + } else if (data && typeof data === 'object') { + noticesArray = data.items || data.notices || data.data || data.list || []; + } + + // API 응답 데이터를 Notice 형식으로 변환 + const transformedNotices: Notice[] = noticesArray.map((notice: any) => ({ + id: notice.id || notice.noticeId || 0, + title: notice.title || '', + date: notice.date || notice.createdAt || notice.createdDate || new Date().toISOString().split('T')[0], + views: notice.views || notice.viewCount || 0, + writer: notice.writer || notice.author || notice.createdBy || '관리자', + content: notice.content ? (Array.isArray(notice.content) ? notice.content : [notice.content]) : undefined, + hasAttachment: notice.hasAttachment || notice.attachment || !!notice.fileKey || false, + })); + + setNotices(transformedNotices); + handleBack(); + } catch (error) { + console.error('공지사항 저장 실패:', error); + alert('공지사항 저장에 실패했습니다. 다시 시도해주세요.'); + } finally { + setIsLoading(false); + } }; const handleCancel = () => { - if (title.trim() || content.trim() || attachedFile) { + if (title.trim() || content.trim() || attachedFile || fileKey) { if (confirm('작성 중인 내용이 있습니다. 정말 취소하시겠습니까?')) { handleBack(); } @@ -100,9 +247,14 @@ export default function AdminNoticesPage() { }, [sortedNotices, currentPage]); return ( -
- {/* 메인 레이아웃 */} -
+ <> + setIsValidationModalOpen(false)} + /> +
+ {/* 메인 레이아웃 */} +
{/* 사이드바 */}
@@ -190,10 +342,11 @@ export default function AdminNoticesPage() { - 저장하기 + {isLoading ? '저장 중...' : '저장하기'}
@@ -264,7 +418,7 @@ export default function AdminNoticesPage() {
{notices.length === 0 ? (
-

+

등록된 공지사항이 없습니다.
공지사항을 등록해주세요. @@ -298,7 +452,8 @@ export default function AdminNoticesPage() { return ( router.push(`/admin/notices/${notice.id}`)} + className="h-12 hover:bg-[#F5F7FF] transition-colors cursor-pointer" > {noticeNumber} @@ -307,7 +462,7 @@ export default function AdminNoticesPage() { {notice.title} - {notice.date} + {formatDate(notice.date)} {notice.views.toLocaleString()} @@ -416,6 +571,7 @@ export default function AdminNoticesPage() {

+ ); }