250x250
๋ฐ˜์‘ํ˜•
arkhyeon
arkhyeon
arkhyeon
์ „์ฒด ๋ฐฉ๋ฌธ์ž
์˜ค๋Š˜
์–ด์ œ
  • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (88)
    • Spring (5)
    • Java (4)
    • React (25)
      • TypeScript (6)
      • JavaScript (1)
      • Jest (9)
    • NEXT (8)
    • SQL (1)
    • React native (1)
    • CSS (3)
    • Web (1)
    • Git (3)
    • ETC (6)
    • ๋น…๋ฐ์ดํ„ฐDB (8)
    • Docker (4)
    • Tool (1)

๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

  • ํ™ˆ
  • ํƒœ๊ทธ
  • ๋ฐฉ๋ช…๋ก

๊ณต์ง€์‚ฌํ•ญ

์ธ๊ธฐ ๊ธ€

ํƒœ๊ทธ

  • react
  • websocket
  • react typescript
  • react usetransition
  • WSS
  • usetransition
  • websocket server
  • Spring WebSocket
  • kudu
  • jest
  • javasciprt websocket
  • docker tomcat
  • react spring websocket
  • node WebSocket
  • react19
  • react websocket
  • javascript wss
  • HIVE
  • react jest
  • react loading

์ตœ๊ทผ ๋Œ“๊ธ€

์ตœ๊ทผ ๊ธ€

ํ‹ฐ์Šคํ† ๋ฆฌ

hELLO ยท Designed By ์ •์ƒ์šฐ.
arkhyeon

arkhyeon

React/TypeScript

React RichTextEditor Image Upload - reactQuill

2024. 3. 18. 10:19
728x90
๋ฐ˜์‘ํ˜•
๐Ÿ’ก RichTextEditor์—์„œ ์ด๋ฏธ์ง€ ์‚ฌ์šฉ ์‹œ ๋‚ด๋ถ€์— base64๋กœ ์ด๋ฏธ์ง€๊ฐ€ ์˜ฌ๋ผ๊ฐ€๊ฒŒ ๋˜๋Š”๋ฐ ์ด๋Š” ๋„ˆ๋ฌด ๊ธด ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๊ฐ€ ์—ฌ๋Ÿฌ์žฅ ์˜ฌ๋ผ๊ฐ„๋‹ค๋ฉด ๋” ํฐ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋จผ์ € ์„œ๋ฒ„์— ์˜ฌ๋ฆฌ๊ณ  ๊ทธ ์ฃผ์†Œ๋ฅผ ๋ฐ˜ํ™˜๋ฐ›์•„ ์ด๋ฏธ์ง€ ํƒœ๊ทธ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ์ฑ„ํƒํ–ˆ๋‹ค.
 

react-quill

The Quill rich-text editor as a React component.. Latest version: 2.0.0, last published: 2 years ago. Start using react-quill in your project by running `npm i react-quill`. There are 806 other projects in the npm registry using react-quill.

www.npmjs.com

Image Upload with ReactQuill

ReactQuill์˜ modules : { handlers : { image : imageHandler }} ์˜ต์…˜์œผ๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๋ฅผ ํ•ธ๋“ค๋ง ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ๋จผ์ € ์„œ๋ฒ„์— ์ €์žฅ ์š”์ฒญ์„ ํ•˜๊ณ  ๋ฐ˜ํ™˜๋ฐ›์€ ์ฃผ์†Œ๋กœ ์—๋””ํ„ฐ์˜ ํ˜„์žฌ ์ปค์„œ์— ์‚ฝ์ž…ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

// ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์š”์ฒญ API
const imageApi = async (img: File): Promise<APIUploadType> => {
    const formData = new FormData();
    formData.append('files', img);
    return client.post('board/upload', formData);
  };
// ์ด๋ฏธ์ง€ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜
const imageHandler = () => {
	// ๋นˆ fileInput์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›๋Š”๋‹ค.
  const input = document.createElement('input');
  input.setAttribute('type', 'file');
  input.setAttribute('accept', 'image/*');
  input.click();

  input.addEventListener('change', async () => {
    if (input.files === null) {
      DC.alert('ํŒŒ์ผ์ด ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
      return;
    }

    if (quillRef.current === null) {
      DC.alert('ํ…์ŠคํŠธ ์ž‘์„ฑ๊ธฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ฃผ์„ธ์š”.');
      return;
    }

    const file = input.files[0];

    try {
      //์ด๋ฏธ์ง€ ์—…๋กœ๋“œ API ์š”์ฒญ
      const res: APIUploadType = await imageApi(file);
      const imgUrl = '์„œ๋ฒ„ URL' + res.fileImg.split('/image/')[1];
      const editor = quillRef.current.getEditor();
      const range = editor.getSelection();
      if (range === null) {
        DC.alert('ํ…์ŠคํŠธ ์ž‘์„ฑ๊ธฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ฃผ์„ธ์š”.');
        return;
      }
      // ์—๋””ํ„ฐ์— ๋ฐ˜ํ™˜๋ฐ›์€ ์ฃผ์†Œ ๊ฐ’์˜ ์ด๋ฏธ์ง€ ์‚ฝ์ž…
      editor.insertEmbed(range.index, 'image', imgUrl);
      if (fileImg === '') {
        setFileImg(imgUrl);
      }
      // ์ปค์„œ๋ฅผ ์ด๋ฏธ์ง€ ๋’ค๋กœ ์ด๋™
      editor.setSelection({ ...range, index: range.index + 1 });
    } catch (error) {
      console.log(error);
    }
  });
}

์ „์ฒด ์ฝ”๋“œ

interface QuillStateType {
  content: string;
  setContent: React.Dispatch<React.SetStateAction<string>>;
  fileImg: string;
  setFileImg: (fileImg: string) => void;
}

function QuillCustom({ content, setContent, fileImg, setFileImg }: QuillStateType) {
  const quillRef = useRef(null) as RefObject<ReactQuill>;

  const imageApi = async (img: File): Promise<APIUploadType> => {
    const formData = new FormData();
    formData.append('files', img);
    return client.post('board/upload', formData);
  };

  const imageHandler = () => {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();

    input.addEventListener('change', async () => {
      if (input.files === null) {
        DC.alert('ํŒŒ์ผ์ด ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
        return;
      }

      if (quillRef.current === null) {
        DC.alert('ํ…์ŠคํŠธ ์ž‘์„ฑ๊ธฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ฃผ์„ธ์š”.');
        return;
      }

      const file = input.files[0];

      try {
        //์ด๋ฏธ์ง€ ์—…๋กœ๋“œ API
        const res: APIUploadType = await imageApi(file);
        console.log(res);
        const imgUrl = '์„œ๋ฒ„ URL' + res.fileImg.split('/image/')[1];
        const editor = quillRef.current.getEditor();
        const range = editor.getSelection();
        if (range === null) {
          DC.alert('ํ…์ŠคํŠธ ์ž‘์„ฑ๊ธฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ฃผ์„ธ์š”.');
          return;
        }
        editor.insertEmbed(range.index, 'image', imgUrl);
        if (fileImg === '') {
          setFileImg(imgUrl);
        }
        editor.setSelection({ ...range, index: range.index + 1 });
      } catch (error) {
        console.log(error);
      }
    });
  };

  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          [{ header: '1' }, { header: '2' }, { font: [] }],
          [{ size: [] }],
          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
          ['link', 'image'],
          ['clean'],
        ],
        handlers: { image: imageHandler },
      },
      clipboard: {
        matchVisual: false,
      },
    }),
    [],
  );

  const formats = [
    'header','font','size','bold','italic','underline','strike','blockquote',
    'list','bullet','indent','link','image','video',
  ];

  return (
    <ReactQuill
      ref={quillRef}
      value={content}
      onChange={setContent}
      modules={modules}
      formats={formats}
      theme="snow"
    />
  );
}

export default QuillCustom;
728x90
๋ฐ˜์‘ํ˜•

'React > TypeScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

React Typescript Interface Tuple Generic  (0) 2024.03.25
React TypeScript useInfiniteQuery Infinite Scroll  (0) 2024.03.15
React TypeScript Zustand Infinite Scroll with IntersectionObserver  (0) 2024.03.14
React Axios TypeScript with JWT Boilerplate  (0) 2024.03.14
React TypeSciprt - Community Project  (0) 2024.03.13
    'React/TypeScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
    • React Typescript Interface Tuple Generic
    • React TypeScript useInfiniteQuery Infinite Scroll
    • React TypeScript Zustand Infinite Scroll with IntersectionObserver
    • React Axios TypeScript with JWT Boilerplate
    arkhyeon
    arkhyeon

    ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”