diff --git a/frontend/src/AppContent.tsx b/frontend/src/AppContent.tsx index 9fcba7ad..5014cf1a 100644 --- a/frontend/src/AppContent.tsx +++ b/frontend/src/AppContent.tsx @@ -42,6 +42,7 @@ import SeminarEdit from './pages/Seminar/SeminarEdit'; import News from './pages/News/News/News'; import NewsDetail from './pages/News/News/NewsDetail'; import NewsCreate from './pages/News/News/NewsCreate'; +import NewsEdit from './pages/News/News/NewsEdit'; interface PageTransitionProps { children: React.ReactNode; @@ -303,6 +304,14 @@ function AppContent() { } /> + + + + } + /> } /> diff --git a/frontend/src/pages/News/News/NewsEdit.tsx b/frontend/src/pages/News/News/NewsEdit.tsx new file mode 100644 index 00000000..5b2a76c3 --- /dev/null +++ b/frontend/src/pages/News/News/NewsEdit.tsx @@ -0,0 +1,349 @@ +import React, { + useState, + useRef, + useMemo, + useCallback, + useContext, + useEffect, +} from 'react'; +import ReactQuill from 'react-quill-new'; +import 'react-quill-new/dist/quill.snow.css'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Modal, useModal } from '../../../components/Modal'; +import { AlertTriangle, CheckCircle } from 'lucide-react'; +import { + Container, + ContentWrapper, + Header, + FormSection, + FormGroup, + Label, + Input, + QuillWrapper, + ButtonGroup, + CancelButton, + SubmitButton, + FileInputLabel, + FileInput, + FileList, + FileItem, +} from './NewsCreateStyle'; // 기존 스타일 재사용 +import { apiEndpoints } from '../../../config/apiConfig'; +import { AuthContext } from '../../../context/AuthContext'; +import axios from 'axios'; + +interface NewsData { + id: number; + title: string; + content: string; + view: number; + createDate: string; + link: string; + image: string; +} + +const NewsEdit: React.FC = () => { + const { newsId } = useParams(); + const navigate = useNavigate(); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [link, setLink] = useState(''); + const [currentImage, setCurrentImage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [imageFile, setImageFile] = useState(null); + const quillRef = useRef(null); + const auth = useContext(AuthContext); + const { openModal } = useModal(); + + useEffect(() => { + const fetchNewsData = async () => { + try { + if (!newsId) return; + + const response = await fetch(apiEndpoints.news.get(newsId)); + if (!response.ok) { + if (response.status === 404) { + throw new Error('뉴스를 찾을 수 없습니다.'); + } + throw new Error('뉴스를 불러오는데 실패했습니다.'); + } + + const data: NewsData = await response.json(); + setTitle(data.title); + setContent(data.content); + setLink(data.link || ''); + setCurrentImage(data.image || ''); + } catch (error) { + console.error('Error fetching news:', error); + showErrorModal( + '데이터 로딩 실패', + '뉴스 데이터를 불러오는데 실패했습니다.', + ); + } finally { + setIsLoading(false); + } + }; + + fetchNewsData(); + }, [newsId]); + + const modules = useMemo( + () => ({ + toolbar: { + container: [ + [{ header: [1, 2, 3, 4, 5, 6, false] }], + ['bold', 'italic', 'underline', 'strike'], + [{ list: 'ordered' }, { list: 'bullet' }], + [{ color: [] }, { background: [] }], + ['link'], + ['clean'], + ], + }, + }), + [], + ); + + const formats = useMemo( + () => [ + 'header', + 'bold', + 'italic', + 'underline', + 'strike', + 'list', + 'bullet', + 'color', + 'background', + 'link', + ], + [], + ); + + const handleChange = useCallback((value: string) => { + setContent(value); + }, []); + + const showErrorModal = (title: string, message: string) => { + openModal( + <> + + + {title} + + +

{message}

+
+ + + + , + ); + }; + + const showSuccessModal = () => { + openModal( + <> + + + 수정 완료 + + +

뉴스가 성공적으로 수정되었습니다.

+
+ + navigate('/news')} /> + + , + ); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!title.trim() || !content.trim()) { + showErrorModal('입력 형식 오류', '제목과 내용은 필수 입력값입니다.'); + return; + } + + try { + setIsSubmitting(true); + + const newsReqDto = { + title: title.trim(), + content: content.trim(), + createDate: new Date().toISOString().split('T')[0], + link: link.trim() || undefined, + image: currentImage, + }; + + if (!newsId) { + throw new Error('뉴스 ID가 없습니다.'); + } + + if (imageFile) { + const formData = new FormData(); + formData.append( + 'newsReqDto', + new Blob([JSON.stringify(newsReqDto)], { + type: 'application/json', + }), + ); + formData.append('news_image', imageFile); + + const response = await axios.put( + apiEndpoints.news.update.url(newsId), + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + + if (response.status === 200) { + showSuccessModal(); + } + } else { + const response = await axios.post( + apiEndpoints.news.update.url(newsId), + newsReqDto, + ); + + if (response.status === 200) { + showSuccessModal(); + } + } + } catch (error: any) { + console.error('Error updating news:', error); + if (error.response?.status === 404) { + showErrorModal('뉴스 없음', '해당 뉴스를 찾을 수 없습니다.'); + } else if (error.response?.status === 400) { + showErrorModal('입력 오류', error.response.data.message); + } else { + showErrorModal('수정 실패', '뉴스 수정 중 오류가 발생했습니다'); + } + } finally { + setIsSubmitting(false); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const maxSize = 5 * 1024 * 1024; // 5MB + if (file.size > maxSize) { + showErrorModal( + '파일 크기 초과', + '이미지 크기는 5MB를 초과할 수 없습니다.', + ); + return; + } + + if (!file.type.startsWith('image/')) { + showErrorModal('잘못된 파일 형식', '이미지 파일만 업로드할 수 있습니다.'); + return; + } + + setImageFile(file); + }; + + if (isLoading) { + return ( + +
로딩 중...
+
+ ); + } + + return ( + + +
+

뉴스 수정

+
+ + + + + setTitle(e.target.value)} + required + /> + + + + + setLink(e.target.value)} + /> + + + + + {currentImage && ( +
+ 현재 이미지 +
+ )} + + 🖼️ 새 이미지 선택 + + + {imageFile && ( + + + {imageFile.name} + + + + )} +
+ + + + + + + + + + navigate('/news')}> + 취소 + + + {isSubmitting ? '수정 중...' : '뉴스 수정'} + + +
+
+
+ ); +}; + +export default NewsEdit;