npm i @tinymce/tinymce-react
import React from 'react';
import { useFormContext } from 'react-hook-form';
import newsApi from '@apis/news/newsApi';
import { CONFIG } from '@config';
import { Editor } from '@tinymce/tinymce-react';
import { BlobInfo } from './editor.type';
function TinyEditor() {
const editorRef = React.useRef<Editor | null>(null);
const methods = useFormContext();
const handleImageUpload = async (blobInfo: BlobInfo) => {
const formData = new FormData();
const blob = await blobInfo.blob();
formData.append('image', blob, blobInfo.filename());
try {
console.log({ formData });
const data = await newsApi.uploadNewsImage(formData);
console.log('성공 시, 백엔드가 보내주는 데이터', data);
const IMG_URL = data.profileImageUrl;
return IMG_URL;
} catch (error) {
console.log(error);
console.log('실패했어요ㅠ');
return '';
}
};
return (
<Editor
ref={editorRef}
apiKey={CONFIG.EDITOR_KEY}
value={methods.watch('content')}
init={{
placeholder: '내용을 입력해 주세요...',
plugins:
'print preview paste importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists wordcount imagetools textpattern noneditable help charmap quickbars emoticons ',
menubar: 'none',
toolbar_sticky: true,
toolbar1: 'undo redo removeformat | image link | fullscreen preview',
toolbar2:
'h1 h2 h3 | fontsize fontfamily | bold italic underline strikethrough | forecolor backcolor | charmap emoticons',
toolbar3:
'alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist',
autosave_ask_before_unload: true,
autosave_interval: '30s',
autosave_prefix: '{path}{query}-{id}-',
autosave_restore_when_empty: false,
autosave_retention: '2m',
importcss_append: true,
quickbars_selection_toolbar:
'bold italic | quicklink h1 h2 h3 blockquote quickimage quicktable',
noneditable_noneditable_class: 'mceNonEditable',
toolbar_mode: 'sliding',
contextmenu: 'link image imagetools table',
content_style:
'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
width: '100%',
height: 600,
images_upload_handler: handleImageUpload,
}}
onEditorChange={(content) => {
methods.setValue('content', content);
}}
/>
);
}
export default TinyEditor;
onEditorChange : 에디터에 입력된 컨텐츠 내용을 감지하는 핸들러
images_upload_handler : 이미지 업로드 핸들러 (이미지 경로를 반환해야 이미지가 에디터 내부에 그려짐)
파일 등록 ⇒ 클라우드 저장소 저장 ⇒ 저장된 url 주소 반환
import { useEffect, useRef } from 'react';
interface Props {
html: string;
}
const IFrameViewer = ({ html }: Props) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const createIframe = () => {
const iframeDocument = iframeRef.current?.contentDocument;
if (!iframeDocument) return;
const blockquoteElements = iframeDocument.querySelectorAll('blockquote');
// 1️⃣ iframe 내부 bockquote 요소 스타일 지정
blockquoteElements.forEach((blockquoteElement) => {
blockquoteElement.style.borderLeft = '2px solid #ccc';
blockquoteElement.style.marginLeft = '1.5rem';
blockquoteElement.style.paddingLeft = '1rem';
});
// 2️⃣ iframe 높이 동적 설정
const contentHeight = iframeDocument.documentElement.scrollHeight;
iframeRef.current.style.height = `${contentHeight}px`;
};
// 모든 이미지가 로드되었는지 확인
const trackAllImageLoad = () => {
const iframeDocument = iframeRef.current?.contentDocument;
if (!iframeDocument) return;
const imgElements = iframeDocument.querySelectorAll('img');
const trackImageLoad = () => {
let allImagesLoaded = true;
imgElements.forEach((imgElement) => {
if (!imgElement.complete) {
allImagesLoaded = false;
}
});
if (allImagesLoaded) {
createIframe();
} else {
setTimeout(trackImageLoad, 100);
}
};
trackImageLoad();
};
if (!html) {
return null;
}
return (
<iframe
ref={iframeRef}
srcDoc={html}
onLoad={trackAllImageLoad}
style={{
width: '100%',
}}
/>
);
};
export default IFrameViewer;
1️⃣ iframe 내부 html 요소의 스타일 지정을 위해 css 파일을 head에 추가했으나 적용되지 않아 iframe 돔 요소에 직접 접근하여 style 지정
2️⃣ iframe의 높이를 동적으로 설정하기 위해서 iframe의 스크롤 높이(컨텐츠 길이)를 iframe의 높이로 설정