Skip to content

Commit

Permalink
#244 feat: 뉴스 수정 컴포넌트 구현, 삭제 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
pillow12360 committed Jan 5, 2025
1 parent 0c3f433 commit 40d3f54
Show file tree
Hide file tree
Showing 2 changed files with 358 additions and 0 deletions.
9 changes: 9 additions & 0 deletions frontend/src/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -303,6 +304,14 @@ function AppContent() {
</ProtectedRoute>
}
/>
<Route
path="/news/edit/:newsId"
element={
<ProtectedRoute requireAuth requireAdmin>
<NewsEdit />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</PageTransition>
Expand Down
349 changes: 349 additions & 0 deletions frontend/src/pages/News/News/NewsEdit.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('');
const [content, setContent] = useState<string>('');
const [link, setLink] = useState<string>('');
const [currentImage, setCurrentImage] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [imageFile, setImageFile] = useState<File | null>(null);
const quillRef = useRef<ReactQuill>(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(
<>
<Modal.Header>
<AlertTriangle size={48} color="#E53E3E" />
{title}
</Modal.Header>
<Modal.Content>
<p>{message}</p>
</Modal.Content>
<Modal.Footer>
<Modal.CloseButton />
</Modal.Footer>
</>,
);
};

const showSuccessModal = () => {
openModal(
<>
<Modal.Header>
<CheckCircle size={48} color="#38A169" />
수정 완료
</Modal.Header>
<Modal.Content>
<p>뉴스가 성공적으로 수정되었습니다.</p>
</Modal.Content>
<Modal.Footer>
<Modal.CloseButton onClick={() => navigate('/news')} />
</Modal.Footer>
</>,
);
};

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<HTMLInputElement>) => {
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 (
<Container>
<div>로딩 중...</div>
</Container>
);
}

return (
<Container>
<ContentWrapper>
<Header>
<h1>뉴스 수정</h1>
</Header>

<FormSection onSubmit={handleSubmit}>
<FormGroup>
<Label>제목*</Label>
<Input
type="text"
placeholder="제목을 입력하세요"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</FormGroup>

<FormGroup>
<Label>링크</Label>
<Input
type="url"
placeholder="관련 링크를 입력하세요 (선택사항)"
value={link}
onChange={(e) => setLink(e.target.value)}
/>
</FormGroup>

<FormGroup>
<Label>대표 이미지</Label>
{currentImage && (
<div>
<img
src={`https://dibb-bucket.s3.ap-northeast-2.amazonaws.com/news/${currentImage}`}
alt="현재 이미지"
style={{ maxWidth: '200px', marginBottom: '10px' }}
/>
</div>
)}
<FileInputLabel>
🖼️ 새 이미지 선택
<FileInput
type="file"
onChange={handleFileChange}
accept="image/*"
/>
</FileInputLabel>
{imageFile && (
<FileList>
<FileItem>
{imageFile.name}
<button type="button" onClick={() => setImageFile(null)}>
×
</button>
</FileItem>
</FileList>
)}
</FormGroup>

<FormGroup>
<Label>내용*</Label>
<QuillWrapper>
<ReactQuill
ref={quillRef}
theme="snow"
value={content}
onChange={handleChange}
modules={modules}
formats={formats}
placeholder="내용을 입력하세요"
/>
</QuillWrapper>
</FormGroup>

<ButtonGroup>
<CancelButton type="button" onClick={() => navigate('/news')}>
취소
</CancelButton>
<SubmitButton type="submit" disabled={isSubmitting}>
{isSubmitting ? '수정 중...' : '뉴스 수정'}
</SubmitButton>
</ButtonGroup>
</FormSection>
</ContentWrapper>
</Container>
);
};

export default NewsEdit;

0 comments on commit 40d3f54

Please sign in to comment.