'use client'; import '../style.scss'; import Image from 'next/image'; import { useState, useRef, useEffect } from 'react'; import { useMemberContext } from '@/contexts/memberProvider'; import { fetchCommentCreate } from '@/lib/api/forum/comment'; import BoardResponse from '@/dtos/response/forum/board/boardResponse'; import PostResponse from '@/dtos/response/forum/post/postResponse'; import CommentCreateRequest from '@/dtos/request/forum/comment/commentCreateRequest'; import EmojiPicker from '@/app/component/EmojiPicker'; import Loading from '@/app/component/Loading'; import { BoardLayout, CommentConst } from '@/constants/forum'; import { throwError } from '@/lib/utils/server'; import { CommentItem } from '@/types/forum/comment'; type Props = { board: BoardResponse, post: PostResponse, comment?: CommentItem; // null이면 새 댓글, 값이 있으면 답글 onSuccess: () => void; onCancel?: () => void; }; export default function WriteForm({ board, post, comment, onSuccess, onCancel }: Props) { const { member } = useMemberContext(); const boardMeta = board.boardMeta; const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [form, setForm] = useState({ postID: post.id, parentID: comment?.parentID ?? undefined, mention: '', content: '', isSecret: false }); const textareaRef = useRef(null); const contentRef = useRef(null); const editorRef = useRef(null); const initialContent = () => { if (comment && comment.parentID && member?.id != comment.writer.id) { const rawHandle = comment.mention?.rawHandle ?? `@${comment.writer.name ?? comment.writer.sid}`; const mentionText = rawHandle ? (rawHandle.endsWith(' ') ? rawHandle : `${rawHandle} `) : ''; setForm(prev => ({ ...prev, parentID: comment.id ?? undefined, mention: mentionText, content: mentionText + ' ' })); setTimeout(() => textareaRef.current?.focus(), 0); } else { // 새 글일 때는 mention 초기화 setForm(prev => ({ ...prev, parentID: undefined, mention: '' })); } }; useEffect(() => { initialContent(); }, [comment]); // 입력 창 높이 조절 const resizeTextarea = () => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = 'auto'; // 초기화 textarea.style.height = `${textarea.scrollHeight}px`; // 높이 조정 } }; useEffect(() => { if (error) { alert(error); setError(null); } }, [error]); useEffect(() => { resizeTextarea(); }, [form.content]); // 입력 내용 저장 const handleChange = (key: keyof CommentCreateRequest, value: any) => { setForm(prev => ({ ...prev, [key]: value })); }; // 이모지 선택 const handleEmoji = (emoji: string) => { handleChange("content", (form.content + emoji)); }; // Ctrl + Enter 최종 제출 const handleKeyDown = (e: React.KeyboardEvent) => { if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); handleSubmit(); } }; // 댓글+답글 등록 const handleSubmit = async () => { if (!form.content.trim()) { return; } try { if (!form.content) { if (boardMeta.list.layout === BoardLayout.QnA) { editorRef.current!.editorInstance?.editing.view.focus(); } else { contentRef.current!.focus(); } throw new Error('내용을 입력해주세요.'); } else if (!boardMeta.comment.enableEditor && !contentRef.current) { // 기본 textarea 사용 시 글자 수 검사 if (form.content.length > CommentConst.MaxAllowedContentLength) { contentRef.current!.focus(); throw new Error(`내용은 ${CommentConst.MaxAllowedContentLength}자 이내로 작성해주세요.`); } } const formData = new FormData(); formData.append('postID', String(form.postID)); formData.append('isSecret', String(form.isSecret)); if (form.parentID) { formData.append('parentID', String(form.parentID)); } // content에서 자동으로 붙은 @회원을 제거한다. const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (form.mention) { form.content = form.content.replace( new RegExp('^\\s*' + escapeRegExp(form.mention) + '\\s*', 'i') , ''); } // 이미지 정보 if (boardMeta.list.layout === BoardLayout.QnA) { if (form.content) { const doc = new DOMParser().parseFromString(form.content, 'text/html'); doc.querySelectorAll('img[src]').forEach(img => { img.setAttribute('src', 'data:image/'); }); form.content = doc.body.innerHTML; } editorRef.current?.getImageStore().forEach((i: any) => { if (i.image?.size > 0 && i.name) { formData.append('images', i.image, i.name); } }); // 미디어 정보 editorRef.current!.getMediaStore().forEach((m: any) => { if (m.url) { formData.append('medias', m.url); } }); // 첨부 파일 editorRef.current!.getFileStore().forEach((f: any) => { if (f?.size > 0 && f.name) { formData.append('files', f.file, f.name); } }); } formData.append('content', form.content); const res = await fetchCommentCreate(formData); await throwError(res); onSuccess(); } catch (err: any) { setError(err.message); } finally { setLoading(false); resetForm(); } }; // 입력 내용 초기화 const resetForm = () => { initialContent(); if (contentRef.current) { contentRef.current.value = ''; } if (textareaRef.current) { textareaRef.current.value = ''; } resizeTextarea(); }; // 취소 const handleCancel = () => { if (typeof onCancel === 'function') { onCancel(); } else { resetForm(); } }; return ( <> {loading ? ( ) : (
{member?.name