EditForm.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. 'use client';
  2. import '../style.scss';
  3. import { useState, useRef, useEffect } from 'react';
  4. import { useAuthContext } from '@/contexts/authProvider';
  5. import { fetchCommentUpdate } from '@/lib/api/forum/comment';
  6. import BoardResponse from '@/dtos/response/forum/board/boardResponse';
  7. import PostResponse from '@/dtos/response/forum/post/postResponse';
  8. import CommentUpdateRequest from '@/dtos/request/forum/comment/commentUpdateRequest';
  9. import { type CommentItem } from '@/types/forum/comment';
  10. import EmojiPicker from '@/app/component/EmojiPicker';
  11. import { BoardLayout, CommentConst } from '@/constants/forum';
  12. import { throwError } from '@/lib/utils/server';
  13. import Loading from '@/app/component/Loading';
  14. type Props = {
  15. board: BoardResponse,
  16. post: PostResponse,
  17. comment: CommentItem,
  18. onSuccess: () => void;
  19. onCancel?: () => void;
  20. };
  21. export default function EditForm({ board, post, comment, onSuccess, onCancel }: Props)
  22. {
  23. const { isAuthenticated } = useAuthContext();
  24. const boardMeta = board.boardMeta;
  25. const [error, setError] = useState<string|null>(null);
  26. const [loading, setLoading] = useState<boolean>(false);
  27. const [form, setForm] = useState<CommentUpdateRequest>({
  28. postID: post.id,
  29. commentID: comment.id,
  30. mention: comment.mention?.rawHandle ?? '',
  31. content: comment.content,
  32. isSecret: false
  33. });
  34. const textareaRef = useRef<HTMLTextAreaElement>(null);
  35. const contentRef = useRef<HTMLTextAreaElement>(null);
  36. const editorRef = useRef<any>(null);
  37. // 입력 창 높이 조절
  38. const resizeTextarea = () => {
  39. const textarea = textareaRef.current;
  40. if (textarea) {
  41. textarea.style.height = 'auto'; // 초기화
  42. textarea.style.height = `${textarea.scrollHeight}px`; // 높이 조정
  43. }
  44. };
  45. useEffect(() => {
  46. if (error) {
  47. alert(error);
  48. setError(null);
  49. }
  50. }, [error]);
  51. useEffect(() => {
  52. resizeTextarea();
  53. }, [form.content]);
  54. // 입력 내용 저장
  55. const handleChange = (key: keyof CommentUpdateRequest, value: any) => {
  56. setForm(prev => ({ ...prev, [key]: value }));
  57. };
  58. // 이모지 선택
  59. const handleEmoji = (emoji: string) => {
  60. handleChange("content", (form.content + emoji));
  61. };
  62. // Ctrl + Enter 최종 제출
  63. const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
  64. if (e.ctrlKey && e.key === 'Enter') {
  65. e.preventDefault();
  66. handleSubmit();
  67. }
  68. };
  69. // 댓글+답글 수정
  70. const handleSubmit = async () => {
  71. if (!isAuthenticated) {
  72. return setError('로그인 후 이용해주세요.');
  73. }
  74. if (!form.content.trim()) {
  75. return;
  76. }
  77. try {
  78. if (!form.content) {
  79. if (boardMeta.list.layout === BoardLayout.QnA) {
  80. editorRef.current!.editorInstance?.editing.view.focus();
  81. } else {
  82. contentRef.current!.focus();
  83. }
  84. throw new Error('내용을 입력해주세요.');
  85. } else if (!boardMeta.comment.enableEditor && !contentRef.current) {
  86. // 기본 textarea 사용 시 글자 수 검사
  87. if (form.content.length > CommentConst.MaxAllowedContentLength) {
  88. contentRef.current!.focus();
  89. throw new Error(`내용은 ${CommentConst.MaxAllowedContentLength}자 이내로 작성해주세요.`);
  90. }
  91. }
  92. const formData = new FormData();
  93. formData.append('postID', String(form.postID));
  94. formData.append('commentID', String(form.commentID));
  95. formData.append('isSecret', String(form.isSecret));
  96. if (form.content) {
  97. const doc = new DOMParser().parseFromString(form.content, 'text/html');
  98. doc.querySelectorAll('img[src]').forEach(img => {
  99. img.setAttribute('src', 'data:image/');
  100. });
  101. formData.append('content', doc.body.innerHTML);
  102. }
  103. // 이미지 정보
  104. if (boardMeta.list.layout === BoardLayout.QnA) {
  105. editorRef.current?.getImageStore().forEach((i: any) => {
  106. if (i.image?.size > 0 && i.name) {
  107. formData.append('images', i.image, i.name);
  108. }
  109. });
  110. // 미디어 정보
  111. editorRef.current!.getMediaStore().forEach((m: any) => {
  112. if (m.url) {
  113. formData.append('medias', m.url);
  114. }
  115. });
  116. // 첨부 파일
  117. editorRef.current!.getFileStore().forEach((f: any) => {
  118. if (f?.size > 0 && f.name) {
  119. formData.append('files', f.file, f.name);
  120. }
  121. });
  122. }
  123. const res = await fetchCommentUpdate(comment.id, formData);
  124. await throwError(res);
  125. onSuccess();
  126. } catch (err: any) {
  127. setError(err.message);
  128. } finally {
  129. setLoading(false);
  130. resetForm();
  131. }
  132. };
  133. // 입력 내용 초기화
  134. const resetForm = () => {
  135. setForm({
  136. postID: post.id,
  137. commentID: comment.id,
  138. mention: '',
  139. content: '',
  140. isSecret: false
  141. });
  142. if (contentRef.current) {
  143. contentRef.current.value = '';
  144. }
  145. if (textareaRef.current) {
  146. textareaRef.current.value = '';
  147. }
  148. resizeTextarea();
  149. };
  150. // 취소
  151. const handleCancel = () => {
  152. if (typeof onCancel === 'function') {
  153. onCancel();
  154. } else {
  155. resetForm();
  156. }
  157. };
  158. return (
  159. <>
  160. {loading ? (
  161. <Loading type={2} />
  162. ) : (
  163. <article className='edit-form'>
  164. <div>
  165. <textarea
  166. name='comment'
  167. rows={1}
  168. value={form.content}
  169. title='댓글 입력 창'
  170. placeholder=''
  171. ref={textareaRef}
  172. onChange={(e) => handleChange("content", e.target.value)}
  173. onKeyDown={handleKeyDown}
  174. disabled={loading}
  175. />
  176. </div>
  177. <div>
  178. {/* 비밀글, 이모지 */}
  179. <EmojiPicker onEmojiSelect={handleEmoji} />
  180. </div>
  181. <div>
  182. <button type='button' className='btn btn-default' onClick={handleCancel}>취소</button>
  183. <button type='button' className='btn btn-submit' onClick={handleSubmit}>확인</button>
  184. </div>
  185. </article>
  186. )}
  187. </>
  188. )
  189. };