From 8e898caa151a893f0fad879d3507ae35ce1dac3d Mon Sep 17 00:00:00 2001 From: Yeolyi Date: Sun, 10 Mar 2024 02:03:06 +0900 Subject: [PATCH] refactor: SunEditor lazy loading --- .../community/news/EditNewsPageContent.tsx | 6 +-- app/[locale]/community/news/create/page.tsx | 15 ++---- .../[id]/edit/EditNoticePageContent.tsx | 2 +- app/[locale]/community/notice/create/page.tsx | 8 +-- .../seminar/EditSeminarPageContent.tsx | 2 +- .../community/seminar/create/page.tsx | 2 +- app/[locale]/research/labs/page.tsx | 2 - components/editor/PostEditor.tsx | 50 +++++++++---------- ...{PostEditorProps.ts => PostEditorTypes.ts} | 8 ++- components/editor/SeminarEditor.tsx | 9 +++- components/editor/SeminarEditorProps.ts | 2 +- components/editor/common/FilePicker.tsx | 2 +- components/editor/common/FilePickerRow.tsx | 2 +- components/editor/common/ImagePicker.tsx | 2 +- .../editor/common/SunEditorFallback.tsx | 15 ++++++ components/editor/common/SunEditorWrapper.tsx | 46 ++++------------- utils/formValidation.ts | 2 +- utils/post.ts | 13 +++++ 18 files changed, 94 insertions(+), 94 deletions(-) rename components/editor/{PostEditorProps.ts => PostEditorTypes.ts} (94%) create mode 100644 components/editor/common/SunEditorFallback.tsx create mode 100644 utils/post.ts diff --git a/app/[locale]/community/news/EditNewsPageContent.tsx b/app/[locale]/community/news/EditNewsPageContent.tsx index da92bf633..ddbb3aa1a 100644 --- a/app/[locale]/community/news/EditNewsPageContent.tsx +++ b/app/[locale]/community/news/EditNewsPageContent.tsx @@ -12,8 +12,8 @@ import { isLocalFile, isLocalImage, isUploadedFile, - postEditorDefaultValue, -} from '@/components/editor/PostEditorProps'; + defaultContent, +} from '@/components/editor/PostEditorTypes'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { NEWS_TAGS } from '@/constants/tag'; @@ -30,7 +30,7 @@ export default function EditNewsPageContent({ id, data }: { id: number; data: Ne const router = useRouter(); const initialContent: PostEditorContent = { - ...postEditorDefaultValue, + ...defaultContent, title: data.title, titleForMain: data.titleForMain ?? '', diff --git a/app/[locale]/community/news/create/page.tsx b/app/[locale]/community/news/create/page.tsx index e5469c730..c64d9c352 100644 --- a/app/[locale]/community/news/create/page.tsx +++ b/app/[locale]/community/news/create/page.tsx @@ -6,7 +6,7 @@ import { useRouter } from '@/navigation'; import { postNews } from '@/apis/news'; import PostEditor from '@/components/editor/PostEditor'; -import { PostEditorContent, isLocalFile, isLocalImage } from '@/components/editor/PostEditorProps'; +import { PostEditorContent, isLocalFile, isLocalImage } from '@/components/editor/PostEditorTypes'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { NEWS_TAGS } from '@/constants/tag'; @@ -23,14 +23,8 @@ export default function NewsCreatePage() { const handleCancel = () => router.push(newsPath); const handleComplete = async (content: PostEditorContent) => { - // HTML 생성을 위한 로그 - console.log(content.description); validateNewsForm(content); - const mainImage = - content.mainImage && isLocalImage(content.mainImage) ? content.mainImage.file : null; - - const attachments = content.attachments.filter(isLocalFile).map((x) => x.file); await postNews({ request: { title: content.title, @@ -42,8 +36,9 @@ export default function NewsCreatePage() { tags: content.tags, date: content.date, }, - mainImage, - attachments, + mainImage: + content.mainImage && isLocalImage(content.mainImage) ? content.mainImage.file : null, + attachments: content.attachments.filter(isLocalFile).map((x) => x.file), }); revalidateNewsTag(); @@ -51,7 +46,7 @@ export default function NewsCreatePage() { }; return ( - + { validateNoticeForm(content); - const attachments = content.attachments.filter(isLocalFile).map((x) => x.file); await postNotice({ request: { title: content.title, @@ -37,14 +36,15 @@ export default function NoticeCreatePage() { isImportant: content.isImportant, tags: content.tags, }, - attachments, + attachments: content.attachments.filter(isLocalFile).map((x) => x.file), }); + revalidateNoticeTag(); router.replace(noticePath); }; return ( - + import('@/components/editor/common/SunEditorWrapper'), { + ssr: false, + loading: () => , +}); export default function PostEditor({ tags, @@ -31,23 +34,19 @@ export default function PostEditor({ initialContent, }: PostEditorProps) { const editorRef = useRef(); - // description(HTML)의 경우 useRef를 사용하기에 여기에 최신값이 반영되지 않음 주의 + + // description(HTML)는 useRef를 사용하기에 여기에 최신값이 반영되지 않음 주의 const [content, setContent] = useState({ - ...postEditorDefaultValue, + ...defaultContent, ...initialContent, }); - const getContentWithDescription = (): PostEditorContent => { - if (editorRef.current) { - if (isContentEmpty(editorRef.current)) { - return { ...content, description: '' }; - } else { - const description = editorRef.current.getContents(false); - return { ...content, description }; - } - } else { - return { ...content, description: '' }; - } + const getContent = (): PostEditorContent => { + let description = ''; + if (editorRef.current && !isContentEmpty(editorRef.current)) + description = editorRef.current.getContents(false); + + return { ...content, description }; }; const setContentByKey = @@ -56,13 +55,14 @@ export default function PostEditor({ setContent((content) => ({ ...content, [key]: value })); }; - const toggleCheck = (tag: string, isChecked: boolean) => { + const toggleTag = (tag: string, isChecked: boolean) => { let nextTags = [...content.tags]; if (isChecked) nextTags.push(tag); else nextTags = nextTags.filter((x) => x !== tag); setContentByKey('tags')(nextTags); }; + // 아래 설정들이 활성화되어있으면 비공개글일 수 없습니다. if (content.isPinned || content.isImportant || content.isSlide) { if (content.isPrivate) { setContentByKey('isPrivate')(false); @@ -99,13 +99,13 @@ export default function PostEditor({ />
-
+
{tags.map((tag) => ( ))}
@@ -161,12 +161,8 @@ export default function PostEditor({
- {actions.type === 'CREATE' && ( - - )} - {actions.type === 'EDIT' && ( - - )} + {actions.type === 'CREATE' && } + {actions.type === 'EDIT' && }
); diff --git a/components/editor/PostEditorProps.ts b/components/editor/PostEditorTypes.ts similarity index 94% rename from components/editor/PostEditorProps.ts rename to components/editor/PostEditorTypes.ts index 312d056c3..f97e13642 100644 --- a/components/editor/PostEditorProps.ts +++ b/components/editor/PostEditorTypes.ts @@ -1,5 +1,7 @@ import { CreateAction, EditAction } from './common/ActionButtons'; +// 첨부파일 + export type PostEditorFile = LocalFile | UploadedFile; export interface LocalFile { @@ -22,6 +24,8 @@ export const isLocalFile = (file: PostEditorFile): file is LocalFile => file.typ export const isUploadedFile = (file: PostEditorFile): file is UploadedFile => file.type === 'UPLOADED_FILE'; +// 이미지 + export type PostEditorImage = LocalImage | UploadedImage | null; export interface LocalImage { @@ -37,6 +41,8 @@ export interface UploadedImage { export const isLocalImage = (image: LocalImage | UploadedImage): image is LocalImage => image.type === 'LOCAL_IMAGE'; +// Content + export interface PostEditorContent { title: string; titleForMain: string; @@ -62,7 +68,7 @@ export interface PostEditorProps { initialContent?: PostEditorContent; } -export const postEditorDefaultValue: PostEditorContent = { +export const defaultContent: PostEditorContent = { title: '', titleForMain: '', description: '', diff --git a/components/editor/SeminarEditor.tsx b/components/editor/SeminarEditor.tsx index c6a7b8b66..3a29a67f5 100644 --- a/components/editor/SeminarEditor.tsx +++ b/components/editor/SeminarEditor.tsx @@ -1,16 +1,16 @@ 'use client'; +import dynamic from 'next/dynamic'; import { MutableRefObject, useRef, useState } from 'react'; import SunEditorCore from 'suneditor/src/lib/core'; -import SunEditorWrapper from '@/components/editor/common/SunEditorWrapper'; - import { CreateActionButtons, EditActionButtons } from './common/ActionButtons'; import BasicTextInput from './common/BasicTextInput'; import DateSelector from './common/DateSelector'; import Fieldset from './common/Fieldset'; import FilePicker, { FilePickerProps } from './common/FilePicker'; import ImagePicker, { ImagePickerProps } from './common/ImagePicker'; +import SunEditorFallback from './common/SunEditorFallback'; import { SeminarEditorContent, SeminarEditorProps, @@ -20,6 +20,11 @@ import { } from './SeminarEditorProps'; import Checkbox from '../common/form/Checkbox'; +const SunEditorWrapper = dynamic(() => import('@/components/editor/common/SunEditorWrapper'), { + ssr: false, + loading: () => , +}); + export default function SeminarEditor({ actions, initialContent }: SeminarEditorProps) { const summaryEditorRef = useRef(); const speakerIntroductionEditorRef = useRef(); diff --git a/components/editor/SeminarEditorProps.ts b/components/editor/SeminarEditorProps.ts index a2055f2a3..caa41e1a1 100644 --- a/components/editor/SeminarEditorProps.ts +++ b/components/editor/SeminarEditorProps.ts @@ -1,5 +1,5 @@ import { CreateAction, EditAction } from './common/ActionButtons'; -import { PostEditorFile, PostEditorImage } from './PostEditorProps'; +import { PostEditorFile, PostEditorImage } from './PostEditorTypes'; export interface SeminarEditorContent { title: string; diff --git a/components/editor/common/FilePicker.tsx b/components/editor/common/FilePicker.tsx index 385b9cef9..2bfa62d33 100644 --- a/components/editor/common/FilePicker.tsx +++ b/components/editor/common/FilePicker.tsx @@ -1,7 +1,7 @@ import { ChangeEventHandler } from 'react'; import FilePickerRow from './FilePickerRow'; -import { PostEditorFile } from '../PostEditorProps'; +import { PostEditorFile } from '../PostEditorTypes'; export interface FilePickerProps { files: PostEditorFile[]; diff --git a/components/editor/common/FilePickerRow.tsx b/components/editor/common/FilePickerRow.tsx index 9ff5f277e..e53668f25 100644 --- a/components/editor/common/FilePickerRow.tsx +++ b/components/editor/common/FilePickerRow.tsx @@ -2,7 +2,7 @@ import { MouseEventHandler } from 'react'; import ClearIcon from '@/public/image/clear_icon.svg'; -import { PostEditorFile } from '../PostEditorProps'; +import { PostEditorFile } from '../PostEditorTypes'; interface FileRowProps { file: PostEditorFile; diff --git a/components/editor/common/ImagePicker.tsx b/components/editor/common/ImagePicker.tsx index dcbc4236a..ac2ad5d00 100644 --- a/components/editor/common/ImagePicker.tsx +++ b/components/editor/common/ImagePicker.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import { ChangeEventHandler, MouseEventHandler, useEffect, useState } from 'react'; -import { LocalImage, PostEditorImage, UploadedImage } from '../PostEditorProps'; +import { LocalImage, PostEditorImage, UploadedImage } from '../PostEditorTypes'; export interface ImagePickerProps { file: PostEditorImage; diff --git a/components/editor/common/SunEditorFallback.tsx b/components/editor/common/SunEditorFallback.tsx new file mode 100644 index 000000000..9433141b6 --- /dev/null +++ b/components/editor/common/SunEditorFallback.tsx @@ -0,0 +1,15 @@ +export default function SunEditorFallback() { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/components/editor/common/SunEditorWrapper.tsx b/components/editor/common/SunEditorWrapper.tsx index 74b0d9b0f..8e85a7bb6 100644 --- a/components/editor/common/SunEditorWrapper.tsx +++ b/components/editor/common/SunEditorWrapper.tsx @@ -1,17 +1,18 @@ 'use client'; -import { MutableRefObject, lazy } from 'react'; +import { MutableRefObject, Suspense } from 'react'; import { ko } from 'suneditor/src/lang/'; import SunEditorCore from 'suneditor/src/lib/core'; import plugins from 'suneditor/src/plugins'; +import './suneditor.css'; +import './suneditor-contents.css'; +import SunEditor from 'suneditor-react'; + +import { BASE_URL } from '@/apis/network/common'; // TODO // 정말 왜그러는지 모르겠는데 lazy + typeof window 조합으로만 빌드가 됨 // 건들지 말..것.. -const SunEditor = lazy(() => import('suneditor-react')); - -import './suneditor.css'; -import './suneditor-contents.css'; export default function SunEditorWrapper({ editorRef, @@ -21,7 +22,7 @@ export default function SunEditorWrapper({ initialContent?: string; }) { return ( - typeof window !== 'undefined' && ( + (editorRef.current = x)} setDefaultStyle={`padding: 1rem;`} @@ -39,38 +40,9 @@ export default function SunEditorWrapper({ ['lineHeight', 'align', 'horizontalRule', 'list'], ['table', 'link', 'image', 'preview'], ], - // imageUploadUrl: `${BASE_URL}/file/upload`, + imageUploadUrl: `${BASE_URL}/file/upload`, }} /> - ) + ); } - -// Suspense 쓰도록 고칠 때까지 삭제 -// const SunEditorFallback = () => { -// return ( -//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// ); -// }; - -// https://github.com/JiHong88/SunEditor/issues/199 -export const isContentEmpty = (editor: SunEditorCore) => { - const wysiwyg = editor.core.context.element.wysiwyg; - if (!wysiwyg.textContent) return true; - - return ( - editor.util.onlyZeroWidthSpace(wysiwyg.textContent) && - !wysiwyg.querySelector('.se-component, pre, blockquote, hr, li, table, img, iframe, video') && - (wysiwyg.textContent.match(/\n/g) || '').length <= 1 - ); -}; diff --git a/utils/formValidation.ts b/utils/formValidation.ts index 3f3f8befe..b45bbe4a0 100644 --- a/utils/formValidation.ts +++ b/utils/formValidation.ts @@ -1,4 +1,4 @@ -import { PostEditorContent } from '@/components/editor/PostEditorProps'; +import { PostEditorContent } from '@/components/editor/PostEditorTypes'; import { SeminarEditorContent } from '@/components/editor/SeminarEditorProps'; export const validateNoticeForm = (content: PostEditorContent) => { diff --git a/utils/post.ts b/utils/post.ts new file mode 100644 index 000000000..fb52ad618 --- /dev/null +++ b/utils/post.ts @@ -0,0 +1,13 @@ +import SunEditorCore from 'suneditor/src/lib/core'; + +// https://github.com/JiHong88/SunEditor/issues/199 +export const isContentEmpty = (editor: SunEditorCore) => { + const wysiwyg = editor.core.context.element.wysiwyg; + if (!wysiwyg.textContent) return true; + + return ( + editor.util.onlyZeroWidthSpace(wysiwyg.textContent) && + !wysiwyg.querySelector('.se-component, pre, blockquote, hr, li, table, img, iframe, video') && + (wysiwyg.textContent.match(/\n/g) || '').length <= 1 + ); +};