'use client'; import './style.scss'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { useState, useEffect, useCallback, useRef, FormEvent } from 'react'; import Loading from '@/app/component/Loading'; import { BoardLayout, PostConst } from '@/constants/forum'; import { fetchBoard, fetchBoardList } from '@/lib/api/forum/board'; import { fetchPostCreate } from '@/lib/api/forum/post'; import { throwError } from '@/lib/utils/client'; import boardResponse from '@/dtos/response/forum/board/boardResponse'; import BoardListResponse from '@/dtos/response/forum/board/boardListResponse'; import Editor, { Handle } from '../_component/Editor'; import HeaderContent from '../_component/HeaderContent'; import FooterContent from '../_component/FooterContent'; import PostTagInput from '../_component/PostTagInput'; export default function View() { const router = useRouter(); const editorRef = useRef(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [isChanged, setIsChanged] = useState(false); const [board, setBoard] = useState(null); const [boardList, setBoardList] = useState([]); const [boardCode, setBoardCode] = useState(''); const [boardPrefix, setBoardPrefix] = useState(''); const [subject, setSubject] = useState(''); const [content, setContent] = useState(''); const [isSecret, setIsSecret] = useState(false); const [isNotice, setIsNotice] = useState(false); const [isSpeaker, setIsSpeaker] = useState(false); const [tags, setTags] = useState([]); const boardCodeRef = useRef(null); const boardPrefixRef = useRef(null); const subjectRef = useRef(null); const contentRef = useRef(null); useEffect(() => { if (error) { alert(error); setError(null); } }, [error]); useEffect(() => { const hash = window.location.hash; if (hash) { setBoardCode(hash.substring(1)); } }, []); useEffect(() => { if (boardCode) { setLoading(true); // 게시판 상세 정보 호출 fetchBoard(boardCode).then((res) => { if (res.success) { setBoard(res.data); } else { throw new Error('게시판을 조회할 수 없습니다.'); } }).catch((err) => { setError(err.message); }).finally(() => { setLoading(false); }); } }, [boardCode]); // 게시판 변경 시 useEffect(() => { if (board) { resetForm(); // 게시판 목록 호출 if (boardList.length <= 0) { fetchBoardList(board.boardGroup.code).then((res) => { if (res.success) { if (res.data && res.data.length >= 0) { setBoardList(res.data); } } else { throw new Error('게시판을 조회할 수 없습니다.'); } }).catch((err) => { setError(err.message); }).finally(() => { setLoading(false); }); } } }, [board]); // 게시글 초기화 const resetForm = () => { setError(''); setIsChanged(false); setBoardPrefix(''); setSubject(board?.boardMeta.write.defaultSubject || ''); setContent(board?.boardMeta.write.defaultContent || ''); setIsSecret(false); setIsNotice(false); setIsSpeaker(false); setTags([]); // Editor 초기화 if (editorRef.current?.editorInstance) { editorRef.current.editorInstance.setData(content); } }; // 게시판 선택 시 const handleBoardChange = (e: React.ChangeEvent) => { const code = e.target.value; if (isChanged) { if (!confirm('작성 중인 내용이 사라질 수 있습니다. 게시판을 변경하시겠습니까?')) { return } } setBoardCode(code); setIsChanged(false); }; // 제목, 내용, 말머리 변경 시 const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target as HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement; const checked = (e.target as HTMLInputElement).checked; switch (name) { case 'boardPrefix': setBoardPrefix(value); break; case 'isSecret': setIsSecret(checked); break; case 'isNotice': setIsNotice(checked); setIsSpeaker(false); break; case 'isSpeaker': setIsSpeaker(checked); setIsNotice(false); break; case 'subject': setSubject(value); break; case 'content': setContent(value); break; } setIsChanged(true); }; // CKEditor에서 내용 변경 시 const handleEditorChange = useCallback((data: string) => { setContent(data); setIsChanged(true); }, []); const validate = () => { if (!boardCode || !board) { boardCodeRef.current!.focus(); throw new Error('게시판을 선택해주세요.'); } if (board.boardMeta.write.allowPrefix && board.boardMeta.write.requiredPrefix && !boardPrefix) { boardPrefixRef.current!.focus(); throw new Error((board.boardMeta.list.layout === BoardLayout.QnA ? '분류' : '말머리') + '를 선택해주세요.'); } if (!subject) { subjectRef.current!.focus(); throw new Error('제목을 입력해주세요.'); } else if (subject.length > PostConst.MaxAllowedSubjectLength) { subjectRef.current!.focus(); throw new Error(`제목은 ${PostConst.MaxAllowedSubjectLength}자 이내로 작성해주세요.`); } if (!content) { if (board.boardMeta.write.allowEditor) { editorRef.current!.editorInstance?.editing.view.focus(); } else { contentRef.current!.focus(); } throw new Error('내용을 입력해주세요.'); } else if (!board.boardMeta.write.allowEditor) { // 기본 textarea 사용 시 글자 수 검사 if (content.length > PostConst.MaxAllowedContentLength) { contentRef.current!.focus(); throw new Error(`내용은 ${PostConst.MaxAllowedContentLength}자 이내로 작성해주세요.`); } } if (board.boardMeta.write.allowTag && tags.length > board.boardMeta.write.tagLimit) { throw new Error(`태그는 ${board.boardMeta.write.tagLimit}개 이내로 작성해주세요.`); } }; // 게시글 등록 처리 const handleSubmit = useCallback(async (e: FormEvent) => { e.preventDefault(); try { validate(); setLoading(true); if (!board) { throw new Error('게시판을 선택해 주세요.'); } const formData = new FormData(); formData.append('boardID', String(board.id)); formData.append('boardCode', boardCode); formData.append('boardPrefixID', boardPrefix); formData.append('isSecret', String(isSecret)); formData.append('isNotice', String(isNotice)); formData.append('isSpeaker', String(isSpeaker)); formData.append('subject', subject); if (content) { const doc = new DOMParser().parseFromString(content, 'text/html'); doc.querySelectorAll('img[src]').forEach(img => { img.setAttribute('src', 'data:image/'); }); formData.append('content', doc.body.innerHTML); } // 태그 if (board.boardMeta.write.allowTag) { tags.forEach(tag => formData.append('tags', tag)); } // 이미지 정보 if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowImage) { editorRef.current?.getImageStore().forEach(i => { if (i.image?.size > 0 && i.name) { formData.append('images', i.image, i.name); } }); } // 미디어 정보 if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowMedia) { editorRef.current!.getMediaStore().forEach((m) => { if (m.url) { formData.append('medias', m.url); } }); } // 첨부 파일 if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowFile) { editorRef.current!.getFileStore().forEach(f => { if (f?.size > 0 && f.name) { formData.append('files', f.file, f.name); } }); } const res = await fetchPostCreate(formData); if (res.success) { resetForm(); router.push(`/post/${res.data!.id}`); } else { throwError(res); } } catch (err: any) { setError(err.message); } finally { setLoading(false); } }, [boardCode, board, boardPrefix, subject, content, isSecret, isNotice, isSpeaker, tags]); return (
{loading && }

{board?.name} 글쓰기

{/* 상단 안내 */} {} {/* 게시판 선택, 말머리, 비밀글, 공지, 전체 공지 */}
{/* 게시판 선택 */}
{/* 말머리 */} {board?.boardMeta.write.allowPrefix && (
)}
{/* 비밀글 */} {board?.boardMeta.write.allowSecret && ( <> )} {/* 해당 게시판 공지 */} {/* 게시판 전체 공지 */}
{/* 제목 */}
{/* 내용 */}
{board?.boardMeta.write.allowEditor ? ( ) : ( )}
{/* 태그 */} {board?.boardMeta.write?.allowTag && (
)} {/* 하단 안내 */} {}
취소
); }