이전글
https://greed-yb.tistory.com/326
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 를 사용하였으나
ReactQuill 은 dataCheck 로 바로 연동할 수 없어서 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...싫어진다 흐음...
'개발 > Spring' 카테고리의 다른 글
[SpringBoot] React + TypeScript + JPA 프로젝트 (13) - Chart.js 적용하기 (0) | 2024.11.24 |
---|---|
[SpringBoot] React + TypeScript + JPA 프로젝트 (12) - 로그아웃 하기 (0) | 2024.11.23 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (11) - 게시판 만들기(삭제하기) (0) | 2024.11.22 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (10) - 게시판 만들기(수정하기) (0) | 2024.11.21 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (9) - 게시판 만들기(상세보기) (0) | 2024.11.20 |
댓글