본문 바로가기
개발/Spring

[SpringBoot] React + TypeScript + JPA 프로젝트 (14) - Quill 적용하기

by 코딩하는 흰둥이 2024. 11. 25.

이전글

https://greed-yb.tistory.com/326

 

[SpringBoot] React + TypeScript + JPA 프로젝트 (13) - Chart.js 적용하기

이전글https://greed-yb.tistory.com/325 [SpringBoot] React + TypeScript + JPA 프로젝트 (12) - 로그아웃 하기이전글https://greed-yb.tistory.com/324 [SpringBoot] React + TypeScript + JPA 프로젝트 (11) - 게시판 만들기(삭제하기)

greed-yb.tistory.com

 

 

 

 

 

 

 

Bootstrap - forms-editors 에 있는

editor 를 적용해보려고 한다

 

 

 

Quill - npm 설치
// React 에서 Quill editor 를 쉽게 사용하게 해주는 라이브러리
// 커스텀 할때 제한이 있을 수 있음
npm install react-quill


// Quill 자체 라이브러리
// Quill 모든 기능을 직접 제어 할 수 있지마 설정해야하는 부분이 번거롭다
npm install quill
npm install --save-dev @types/quill

가볍게 다뤄보기 위해서 react-quill 로 설치하였다

 

 

public/index.html 에 

css 를 적용하거나

 

적용하려는 페이지에 css 를 적용해야 된다

 

 

 

 

 

   
   ...
   ...
   // 작성 글 변수
    const [content, setContent] = useState('');
    
    // editor 옵션
    const modules = {
        toolbar: [
            [{ font: [] }],
            [{ header: [1, 2, 3, 4, 5, 6, false] }],
            ['bold', 'italic', 'underline', 'strike'],
            [{ color: [] }, { background: [] }],
            [{ script: 'sub' }, { script: 'super' }],
            [{ list: 'ordered' }, { list: 'bullet' }],
            [{ indent: '-1' }, { indent: '+1' }],
            [{ align: [] }],
            ['link', 'image', 'video'],
            ['clean'], // Clear formatting
        ],
    };

    const handleChange = (value: string) => {
        setContent(value);
    };

    const axiosTest = () => {
        console.log("232323 : " + content);
    }
    
    return (
    	    <div>
               <h5 className="card-title">Quill Editor Full</h5>
               <ReactQuill
                   value={content}
                   onChange={handleChange}
                   modules={modules}
                   theme="snow"
               />
            </div>

    )

 

예전에 만들었던 글 작성 페이지에 추가를 해보았다

 

 

잘 나오고 있다

내용부분으로 교체해주고 editor 크기도 키워보자

 

 

 

 

적용되었다

 

 

 

boardWrite.tsx - 글 작성 페이지
import React, {useEffect, useState} from "react";
import axios from "axios";
import {Link} from "react-router-dom";
import Cookies from 'js-cookie';
import ReactQuill from "react-quill";


// 게시판 변수 타입 설정
interface BoardWrite {
    title: string;
    contents: string;
}

function BoardWrite() {

    const [data, setData] = useState<BoardWrite>({
        title: "",
        contents: ""
    });

    // 태그의 name 을 인식해서 값을 가져온다
    const dataCheck = (event: { target: { value: string; name: string; }; }) => {
        const { value, name } = event.target;
        setData({
            ...data,
            [name] : value,
        })
    }

    const axiosSave : () => Promise<void> = async () => {

        const token = document.cookie;
        if (!token){
            // token 이 없다면 로그인 화면으로
            window.location.href = '/login';
        }else{
            // npm install js-cookie
            // npm install @types/js-cookie
            // Cookies 에서 바로 token 을 가져올 수 있게 js-cookie 를 설치해서 사용하였다
            let checkToken : string | undefined = "";
            if(!Cookies.get('refreshToken')){
                // refreshToken 이 없다면 로그인 화면으로
                window.location.href = '/login';
            }else{
                checkToken = Cookies.get('refreshToken')
            }

            await axios.post("http://localhost:7172/api/boardSave" , data ,{
                params: {checkToken}
                }
            ).then((response) => {
                if(response.data == "success"){
                    alert("저장하였습니다.");
                    window.location.href = '/board';
                }
            }).catch((error) => {
                alert("저장 실패하였습니다.");
            })
        }
    }
    

    // editor 옵션
    const modules = {
        toolbar: [
            [{ font: [] }],
            [{ header: [1, 2, 3, 4, 5, 6, false] }],
            ['bold', 'italic', 'underline', 'strike'],
            [{ color: [] }, { background: [] }],
            [{ script: 'sub' }, { script: 'super' }],
            [{ list: 'ordered' }, { list: 'bullet' }],
            [{ indent: '-1' }, { indent: '+1' }],
            [{ align: [] }],
            ['link', 'image', 'video'],
            ['clean'], // Clear formatting
        ],
    };

    // Quill editor 데이터 담기
    const handleChange = (value: string) => {
        setData({
            ...data,
            contents : value,
        })
    };
    

    return (
        <main id="main" className="main">

            <div className="pagetitle">
                <h1>게시판</h1>
                <nav>
                    <ol className="breadcrumb">
                        <li className="breadcrumb-item"><Link to="/">Home</Link></li>
                        <li className="breadcrumb-item">Forms</li>
                        <li className="breadcrumb-item active">Layouts</li>
                    </ol>
                </nav>
            </div>


            <section className="section">
                <div className="row">
                    <div>
                        <div className="card">
                            <div className="card-body">
                                <h5 className="card-title">글 작성</h5>

                                <form className="g-3">
                                    <div>
                                        <label htmlFor="inputNanme4" className="form-label">제목</label>
                                        <input type="text" className="form-control" id="title" name="title" onChange={dataCheck}/>
                                    </div>
                                    <div style={{ position: 'relative', marginBottom: '60px' }}>
                                        <label htmlFor="contents" className="form-label">내용</label>
                                        <ReactQuill
                                            value={data.contents}
                                            onChange={handleChange}
                                            modules={modules}
                                            theme="snow"
                                            style={{height: '300px' , width: '100%'}}
                                        />
                                    </div>
                                    <div className="text-center">
                                        <button type="button" className="btn btn-primary" onClick={axiosSave}>저장</button>
                                        <Link to="/board" className="btn btn-secondary">뒤로</Link>
                                    </div>
                                </form>

                            </div>
                        </div>

                    </div>
                </div>
            </section>
        </main>

    );
}

export default BoardWrite;

 

 

<input> 태그의 title 과 <textarea> 태그의 contents 는 dataCheck 를 사용하였으나

ReactQuilldataCheck 로 바로 연동할 수 없어서 data 의 contents업데이트 해줘야 된다고 한다

 

setData 로 contents 데이터만 업데이트 시킨다

 

 

 

글 작성이 정상적으로 동작하고 저장도 성공하였다

 

 

 


 

 

 

 

 

boardRead.tsx - 게시판 상세 보기

글 상세 보기에 들어가면 <textarea> 태그로 되어 있어서 태그가 그대로 나온다

 

 

 

첫 번째 방법

<div dangerouslySetInnerHTML={{__html: board.contents}} />

 

dangerouslySetInnerHTML 을 사용한다

Javascript 에서 .html()

thymeleaf 에서 utext() 

와 같은 기능을 한다

 

 

<textarea> 또는 <ReactQuill> 을 사용하지 않아서

card shadow CSS 로 구분을 하였다

 

 

 

 

 

 

두 번째 방법

Quill editor 를 사용한다

 

 

...
...
    // editor 옵션
    const modules = {
        toolbar: [
            [{ font: [] }],
            [{ header: [1, 2, 3, 4, 5, 6, false] }],
            ['bold', 'italic', 'underline', 'strike'],
            [{ color: [] }, { background: [] }],
            [{ script: 'sub' }, { script: 'super' }],
            [{ list: 'ordered' }, { list: 'bullet' }],
            [{ indent: '-1' }, { indent: '+1' }],
            [{ align: [] }],
            ['link', 'image', 'video'],
            ['clean'], // Clear formatting
        ],
    };


    // Quill editor 데이터 담기
    const handleChange = (value: string) => {
        setBoarRead((prevBoard) => ({
            ...prevBoard,   // 이전 상태의 데이터를 유지
            contents : value,
        }));
    };
    
       return (
       ...
       ...
       ...
       ...
     
              <div className="col-12" style={{ position: 'relative', marginBottom: '60px' }}>
                 <label htmlFor="contents" className="form-label">내용</label>
                 <ReactQuill
                    value={board.contents}
                    onChange={handleChange}
                    modules={modules}
                    theme="snow"
                    style={{height: '300px' , width: '100%'}}
                    readOnly={readOnly.contents}
                 />
              </div>
       ...
       ...
       ...
       ...

 

 

데이터가 정상적으로 출력되고

다른 계정으로 접속해 보면

readOnly 가 적용되어 내용 수정 및 Quill 기능을 사용하지 못하지만

모듈 옵션이 나오는 것과 이미지 버튼 클릭 시 파일 찾기 창이 나오는 문제가 있다

파일 찾기 창은 아마도 Quill 에 readOnly 설정을 걸면 될 거 같은데

여기서는 넘어가겠다

 

 

import React, {useEffect, useState} from "react";
import axios from "axios";
import {Link, useNavigate, useParams} from "react-router-dom";
import Cookies from 'js-cookie';
import { useUserContext } from '../utils/UserInfo';
import ReactQuill from "react-quill";

// 게시판 변수 타입 설정
interface BoardRead {
    title: string;
    contents: string;
    writer: string;
}

function BoardRead() {

    // navigate 훅 사용
    const navigate = useNavigate();

    // 전역변수로 저장한 login 한 사용자 username
    const { username } = useUserContext();

    // Route path 로 받은 no 값을 받아온다
    const { no } = useParams();

    const [board, setBoarRead] = useState<BoardRead>({
        title: "",
        contents: "",
        writer: ""
    });

    // readOnly 제어하기
    const [ readOnly, setReadOnly ] = useState({
        title: true,
        writer: true,
        contents: true
    })
    // button display 제어하기
    const [ displayButton , setDisplayButton ] = useState(false);

    useEffect(() => {
        axios.get("/api/boardRead" , {
            params : {
                no
            }
        }).then((response => {
            setBoarRead(response.data);

            // 로그인한 사용자와 글 작성자가 같을때
            if(response.data.writer == username){
                setReadOnly((prevState) => ({
                    ...prevState,       // 이전 데이터를 유지
                    title: false,       // 글쓴이가 일치한다면 해제
                    writer: true,       // 글쓴이는 계속 true
                    contents: false,    // 글쓴이가 일치한다면 해제
                }));

                // 저장 버튼 display
                setDisplayButton(true);
            }

        })).catch((error) => {
            console.log(error)
        })

    }, [no , username]) // 글번호와 로그인한 유저정보가 변경될 경우


    // 태그의 name 을 인식해서 값을 가져온다
    // HTMLInputElement 와 HTMLTextAreaElement 으로 type 을 지정한다
    const dataCheck = ({ target: { value, name} }: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> ) => {
        // readOnly 가 해제 되었을때 setBoarRead 로 값을 동기화 해야 텍스트를 입력할 수 있다
        setBoarRead((prevBoard) => ({
            ...prevBoard,   // 이전 상태의 데이터를 유지
            [name]: value,  // 일치하는 데이터만 변경
        }));
    }


    const axiosSave : () => Promise<void> = async () => {

        const token = document.cookie;
        if (!token){
            // token 이 없다면 로그인 화면으로
            navigate('/login');
        }else{
            // npm install js-cookie
            // npm install @types/js-cookie
            // Cookies 에서 바로 token 을 가져올 수 있게 js-cookie 를 설치해서 사용하였다
            let checkToken : string | undefined = "";
            if(!Cookies.get('refreshToken')){
                // refreshToken 이 없다면 로그인 화면으로
                navigate('/login');
            }else{
                checkToken = Cookies.get('refreshToken')
            }

            await axios.post("http://localhost:7172/api/boardSave" , board ,{
                params: {checkToken}
                }
            ).then((response) => {
                if(response.data == "success"){
                    alert("저장하였습니다.");
                    navigate('/board');
                }
            }).catch((error) => {
                alert("저장 실패하였습니다.");
            })
        }
    }


    const axiosDelete : () => Promise<void> = async () =>{
        if(!confirm("삭제하시겠습니까?")){
            return;
        }

        await axios.delete("http://localhost:7172/api/boardDelete",{
            params: {no}    // useParam 으로 받아온 글 번호
        }).then((response)=> {
            if(response.data == "success"){
                alert("해당 게시글 삭제 완료");
                navigate('/board');
            }else {
                console.log("오류가 발생했습니다.");
            }
        }).catch((error)=>{
            console.log(error);
        })
    }


    //////////////////////////////////////////////////////
    /// Quill Editor
    const modules = {
        toolbar:  [
            [{ font: [] }],
            [{ header: [1, 2, 3, 4, 5, 6, false] }],
            ['bold', 'italic', 'underline', 'strike'],
            [{ color: [] }, { background: [] }],
            [{ script: 'sub' }, { script: 'super' }],
            [{ list: 'ordered' }, { list: 'bullet' }],
            [{ indent: '-1' }, { indent: '+1' }],
            [{ align: [] }],
            ['link', 'image', 'video'],
            ['clean'], // Clear formatting
        ]
    }

    // Quill editor 데이터 담기
    const handleChange = (value: string) => {
        setBoarRead((prevBoard) => ({
            ...prevBoard,   // 이전 상태의 데이터를 유지
            contents : value,
        }));
    };
    //////////////////////////////////////////////////////

    return (
        <main id="main" className="main">

            <div className="pagetitle">
                <h1>게시판</h1>
                <nav>
                    <ol className="breadcrumb">
                        <li className="breadcrumb-item"><Link to="/">Home</Link></li>
                        <li className="breadcrumb-item">Forms</li>
                        <li className="breadcrumb-item active">Layouts</li>
                    </ol>
                </nav>
            </div>


            <section className="section">
                <div className="row">
                    <div className="lg-6">
                        <div className="card">
                            <div className="card-body">
                                <h5 className="card-title">상세보기</h5>

                                <form className="row g-3">
                                    <div className="col-6">
                                        <label htmlFor="title" className="form-label">제목</label>
                                        <input type="text" className="form-control" id="title" name="title" value={board.title} onChange={dataCheck} readOnly={readOnly.title} />
                                    </div>
                                    <div className="col-6">
                                        <label htmlFor="writer" className="form-label">글쓴이</label>
                                        <input type="text" className="form-control" id="writer" name="writer" value={board.writer} onChange={dataCheck} readOnly={readOnly.writer} />
                                    </div>
                                    <div className="col-12" style={{ position: 'relative', marginBottom: '60px' }}>
                                        <label htmlFor="contents" className="form-label">내용</label>
                                        <ReactQuill
                                            value={board.contents}
                                            onChange={handleChange}
                                            modules={modules}
                                            theme="snow"
                                            style={{ height: '300px', width: '100%' }}
                                            readOnly={readOnly.contents}
                                        />
                                    </div>
                                    <div className="text-center">
                                        <button type="button" className="btn btn-primary" onClick={axiosSave} style={{display: displayButton ? `inline-block` : `none`}} >저장</button>
                                        <button type="button" className="btn btn-danger" onClick={axiosDelete} style={{display: displayButton ? `inline-block` : `none`}} >삭제</button>
                                        <Link to="/board" className="btn btn-secondary">뒤로</Link>
                                    </div>
                                </form>

                            </div>
                        </div>

                    </div>
                </div>
            </section>
        </main>

    );
}

export default BoardRead;

 

 

 

 


 

세 번째 방식으로

Quill Editor 를 사용하면서 module 이 사라져 있다가

수정 버튼을 클릭하면 module 이 보이고 글을 수정할 수 있도록 변경하려고 했는데

module 이 보이게 할 때마다 <ReactQuill> 태그 전체가 사라지는 문제가 생겨서

한참을 붙잡고 있다가 우선은 손을 뗐다

랜더링 문제를 겪을 때마다 너무 골치가 아프다

react...typescript...싫어진다 흐음...

 

 

댓글