티스토리나 네이버 블로그처럼, 어떤 글을 작성할 때는 글작성 전용 에디터가 필요하다. 에디터를 직접 만들어도 좋겠지만 그랬다가는 마음에 불화가 일어날 것 같아서 라이브러리를 사용하기로 했다. 누구나가 사용하는 위지윅 에디터를 사용했다면 속편했을텐데 Quill을 사용하겠다는 객기를 부려서 강제로 js실력이 업그레이드 되었다. (속터짐은 덤)
Quill은 에디터에 이미지를 삽입하면 base64로 이미지에 src를 저장한다. 그러나 base64는 1000자가 넘는 문자열이므로 그대로 db에 저장하기에는 너무나 거대하다. 그렇다고 일반적인 방식으로 handles: { image: imageHandler } 를 커스텀하여 사용하면, 이미지를 업로드 할 때마다 서버에 업로드하게 된다. 이럴 경우 에디터에서 필요없는 이미지를 지워도, 이미 서버에 이미지가 등록되어 한 자리를 차지하고 있기 때문에 서버가 무거워질 수 있다.
따라서 차라리 글을 완성할 때까지 기다린 다음, '저장하기' 버튼을 눌렀을 때 < img src = " base64문자열 " /> 의 "base64문자열" 부분만 정규식으로 추출하여, Blop 파일로 변환하고, FormData()로 치환해 백엔드 multer에 보내면 글쓴이가 실제로 사용할 이미지만 추출하여 서버에 저장할 수 있다. 과정은 아래와 같다.
<img src> 값들을 불러와서 => base64 를 Blop으로 변환 => Blop을 FormData로 변환 => 서버에 이미지 저장 => 서버는 이미지 경로 반환 => 프론트에서 반환한 경로와 base64값 바꾸기(img src="반환받은 경로"로 설정) => html 태그 저장 |
따라서 이번만 고생하고 다음에는 얌전히 Ckeditor5를 사용하기로 한다.
참고 : 리액트 에디터 순위 : https://ourcodeworld.com/articles/read/1065/top-15-best-rich-text-editor-components-wysiwyg-for-reactjs
Fontend
대망의 게시판 CRUD를 완성해본다. 나는 완성도를 높이고자 react-quill을 사용하였는데, Quill 글쓰기 에디터 라이브러리는 아래와 같이 생겼다. (https://www.npmjs.com/package/react-quill)
React-Quill 은 글 작성자가 에디터에 작성한 내용을 위와 같이 html로 저장한다. 우선 이 라이브러리를 사용하기 위한 설정을 해 주자.
Write.js
$ npm install react-quill
$ npm install quill-image-resize
터미널을 통해 라이브러리를 설치하고, import 해 준다.
그냥 react-quill만 사용할 경우에는 에디터 내에서 이미지 리사이즈(이미지 크기를 늘리거나 줄이는 것)가 불가능하기 때문에, 에디터 내에서 이미지 크기를 변경할 수 있는 라이브러리도 함께 사용해 주었다.
import React from "react";
import ReactQuill, { Quill } from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import ImageResize from 'quill-image-resize';
Quill.register('modules/ImageResize', ImageResize);
// ------------------------quill Modules------------------------
const modules = {
toolbar: [
[{ 'font': [] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline', 'strike','blockquote', 'code-block'], // toggled buttons
['link','image'],
[{ 'align': [] },{ 'color': [] },{ 'background': [] }], // dropdown with defaults from theme
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
],
ImageResize: {
parchment: Quill.import('parchment')
}
}
const [subject, setSubject] = useState('')
const [content, setContent] = useState('')
const onChagecontent = (e) => {
console.log(e)
setContent(e)
}
// ------------------------UI------------------------
return(
<div>
<div style={{width:'100%', height:'90vh'}}>
<div style={{width:'1000px', margin:'auto', borderRadius:'19px'}}>
<div style={{marginBottom:'20px',marginTop:'70px', fontSize:'20px', fontWeight:'bold'}} >공지사항</div>
{/* ======== Subject ======== */}
<input
className="Subject"
placeholder="제목을 입력해 주세요"
style={{padding:'7px', marginBottom:'10px',width:'100%',border:'1px solid lightGray', fontSize:'15px'}}
onChange={(e)=>{setSubject(e.target.value)}}
></input>
<div style={{height:'650px'}}>
{/* ======== Quill ======== */}
<ReactQuill
modules={modules}
placeholder='내용을 입력해 주세요'
onChange={onChagecontent}
style={{height: "600px"}}
/>
</div>
{/* ======== Button ======== */}
<div style={{float:'right'}}>
<Button variant="danger" style={{marginRight:'10px'}} >취소</Button>
<Button variant="dark"
onClick={()=>{
SaveBoard()
}}
>저장하기</Button>
</div>
</div>
</div>
</div>
)
modules에는 에디터의 상단 바에서 무슨 기능을 이용할 것인지에 대한 기능을 작성한다. html 단에서 ReactQuill의 moduels에 설정해 준 값을 넘기면 사용할 수 있다. onChange는 일반 input박스와 동일하게, 에디터 속의 내용이 변하면 변한 내용을 저장해준다. 겉을 650px로 감싸고 에디터의 크기를 600px로 설정하였다.
ui가 완성되었으니 기능을 만들어본다. 우선 에디터를 사용하였을 때, onChange안의 값을 보자. 만일 이미지와 텍스트를 함께 입력하면 어떻게 될까?
<p>"텍스트."</p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACbEAAAbsCAIAAACsxtsEAAAACXBIWXMAAC4jAAAuIwF4pT92AAAgAElEQVR4nOy93ZMdV3blt/c+J/Pe+kKBKBL8BAGSDT....
위와 같은 형식으로 몇천자가 출력되었는데, 무려 ShowMore이 붙어있다.
이것이 base64 형태로 저장된 이미지의 양식이다. 너무너무 길다.. 따라서 이대로 db에 저장하고 말았다가는 무료 에디션이 끝나고 말 것이다. 이것을 해결하고자 구글링을 해 보았는데, 다들 handles: { image: imageHandler } 을 사용해 아예 이미지를 업로드 할 때부터 커스텀하는 방식을 사용하고 있었다.
그러나 그것은 '이미지가 인풋 될 때 마다 서버에 저장해주세요~' 라는 것이기 때문에, 추후 에디터에서 이미지가 지워지더라도 서버에는 남아있게 된다. 쓸모없는 가비지 파일이 서버를 낭비하게 된다는 뜻이다. 굳이 이렇게 해야 하나 싶어 방도를 생각해 보았다.
사용자가 입력한 내용은 html 문서의 양식과 같지만, db에 저장될 때는 일반 String, 즉 문자열로 저장된다. 그 말은 그 문자열에 포함되어 있는 base64 문자열을, 정규식을 통해 추출해 낼 수 있다는 뜻이다.
정규직 삽질 과정이 길었기 때문에 쿨하게 답변을 공개한다.
< img src = "여기" /> 에서 "여기" 의 내용을 출력하는 정규식
/(<img[^>]*src\s*=\s*[\"']?([^>\"']+)[\"']?[^>]*>)/g
이 정규식을 통해 사용자가 '최종 글을 제출 할 때 전송받은 String에서, img의 src만 추출해 Blop으로 바꾸어 FormData에 넣고, 이를 백엔드에 보내면' 프로필 사진을 업로드 할 때와 똑같은 방식으로 편안하게 이미지를 업로드 할 수 있다. 따라서 과정은 다음과 같다.
1. 사용자가 작성 완료한 글을 받는다.
2. 작성 완료한 글에서 img 태그의 src만 추출한다.
3. 추출한 base64 형식의 src를 Blop 형태로 변환한다.
4. Blop으로 변환한 파일을 FormData에 넣어준다.
5. 프로필 사진 업로드 때와 똑같이, 이미지를 서버에 업로드한다.
6. 백엔드는 '서버에 업로드 된 이미지의 주소'를 반환한다.
7. 사용자가 작성 완료한 글의 img src를 기존의 base64에서, 서버에 저장한 이미지 주소로 바꾼다.
여기서 [1. 사용자가 작성 완료한 글을 받는다.] 는 위에서 onChage를 통해 content State에 저장해 주었다. 따라서 [저장하기] 버튼에 이벤트를 등록해주고, state를 받아오기만 하면 된다.
<div style={{float:'right'}}>
<Button variant="danger" style={{marginRight:'10px'}} >취소</Button>
<Button variant="dark"
onClick={()=>{
SaveBoard()
}}
>저장하기</Button>
</div>
버튼을 클릭하면 SaveBoard라는 함수가 실행된다. 이 함수 속에 2단계부터 7단계까지의 로직을 작성할 것이다.
■ 2. 작성 완료한 글에서 img 태그의 src만 추출한다.
// ------------------------정규식으로 src 추출------------------------
// src만 추출
const srcArray = []
// 최종 src url 저장할곳
const urlArray = []
const gainSource = /(<img[^>]*src\s*=\s*[\"']?([^>\"']+)[\"']?[^>]*>)/g
async function SaveBoard() {
// 이미지가 있을때만 아래 코드 실행(while)
// 이미지 처리
// 정규식으로 추출하여 배열에 저장
while(gainSource.test(content)){
console.log('이미지가 있을때만 진행함.')
let result = RegExp.$2
// console.log('src 추출 결과 : ',result)
srcArray.push(result)
console.log('srcArray 추가: ',srcArray)
javaScript의 .test를 통해, content에 저장된 문자열을 정규식을 통해 추출한다. RegExp.$2 옵션은 img src 뒤의 내용만 추출하여 반환해준다. 위와 같이 결과를 result에 담아주었고, 그것을 srcArray에 push하여 배열에 추가해 주었다. srcArray에는, 추출한 base64 형식의 초기 이미지 url이 들어있다. function을 선언할 때 async를 반드시 붙여줘야 한다. 빼먹어선 안된다. 가장 중요하다.
■ 3. 추출한 base64 형식의 src를 Blop 형태로 변환한다.
아래 글을 참고하였다.
// base64파일 Blop으로 바꾸기
// dataURL 값이 data:image/jpeg:base64,~~~~~~~ 이므로
//','를 기점으로 자른다.
const byteString = atob(result.split(",")[1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
// charCodeAt() 메서드는 주어진 인덱스에 대한 UTF-16 코드를 나타내는 0부터 65535 사이의 정수를 반환
// 비트연산자 & 와 0xff(255) 값은 숫자를 양수로 표현하기 위한 설정
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ia], { // base64 -> blob
type: "image/jpeg"
});
const file = new File([blob], "image.jpg");
■ 4. Blop으로 변환한 파일을 FormData에 넣어준다.
// 위 과정을 통해 만든 image폼을 FormData에 넣어준다.
const formData = new FormData();
formData.append("file", file);
console.log('formData: ',formData)
■ 5. 프로필 사진 업로드 때와 똑같이, 이미지를 서버에 업로드한다.
■ 6. 백엔드는 '서버에 업로드 된 이미지의 주소'를 반환한다.
// 백엔드로 보내서 urlArray에 돌려받은 url을 배열 형태로 push 해준다.
// 최상단 while문이 모든 사진을 추출해 하나씩 저장하여 push하므로
// 백엔드의 multer 패키지에 single로 저장을 요청한다.
const config = {
header : {'content-type': 'multipart/form-data'}
}
// TODO FormData 백엔드로 넘겨서 url 건네받아 저장하고, url 반환해서 urlArray에 저장하기
await axios.post('api/board/uploadImgFolder',formData, config )
.then(response => {
if(response.data.success) {
console.log('이미지 서버에 업로드 성공', response)
urlArray.push(response.data.url)
console.log('urlArray에 추가',urlArray)
} else {
console.log(response)
alert('이미지를 서버에 업로드하는데에 실패했습니다.')
}
})
api는 백엔드에서 작성해 줄 것이며, 프로필 사진과 완전히 동일하다. 이유는 whilte문이 'src를 하나씩 추출해서 진행하고, 하나를 찾고 과정을 완료한 후 다음 src를 찾는 형식' 이기 때문이다. 즉 한 번에 하나의 사진씩 처리하기 때문에, 굳이 배열에 저장했다가 한꺼번에 multer로 저장할 필요 없이 한 장씩 저장하면 된다.
이 코드에서 제일 중요한 것은 await 부분이다. 6. 백엔드는 '서버에 업로드 된 이미지의 주소'를 반환한다. 를 7번보다 반드시 먼저 실행하기 위해서는 '서버에 먼저 사진이 업로드 되어야 한다' 가 전제조건인데, 큰 작업은 뒤로 미루는 node.js의 비동기적 특성을 생각했을 때 이미지 저장 작업이 뒤로 밀릴 가능성이 있기 때문이다. 따라서 function 생성에서 async를 붙여준 것이다. '이미지를 서버에 업로드 한 후에, 뒤의 작업을 처리해 주세요~' 라고 요청하기 위해 axios.post에 await를 붙여주었다.
post를 통해 받아온 '서버에 저장된 이미지 url 정보' 를 urlArray에 push 해 주었으므로, 유저가 완성한 본문에서 base64 로 등록되어 있던 img의 src 태그 값만 바꾸어 주면 된다.
■ 7. 사용자가 작성 완료한 글의 img src를 기존의 base64에서, 서버에 저장한 이미지 주소로 바꾼다.
// 만일 이미지를 업로드 했다면, 첫번쨰 srcArray가 있는 부분을 첫번째 url로 바꾸는 식으로 계속 바꿔라.
if(srcArray.length > 0) {
console.log('실행은 됐음..')
for(let i = 0; i<srcArray.length; i++) {
console.log('실행중.. '+i+' 번째임')
console.log('srcArray[i]: ',srcArray[i],'urlArray[i]: ',urlArray[i])
let replace = endContent.replace(srcArray[i],urlArray[i])
endContent = replace
console.log('바뀌었는지 테스트',endContent)
}
} // 없다면 content=content
이미지를 업로드했을 경우에만 src 태그를 바꿔치기 해주기 위해, srcArray, 즉 추출한 src가 하나라도 있을 경우에만 실행하도록 로직을 구성한다. js가 기본으로 제공하는 메서드 replace를 사용하였다. content.replace(a, b)를 하면, content 속 a라는 내용을 b로 바꾸어 준다. srcArray에는 '처음 추출했던 base64의 내용'이 들어가 있고, urlArray에는 '서버에 업로드한 주소'가 들어있다. 따라서 이 내용을 서로 바꾸어 주면, 최종으로 저장될 글에서 base64가 아닌 '서버에 업로드 된 이미지 주소'가 연결되어 있을 것이다.
여기까지 길고 긴 이미지 작업이 끝났다. 이제 카테고리, 제목, 글쓴이, 최종적으로 이미지 주소를 변환한 본문 내용, 조회수, 좋아요 수를 백엔드로 보내준다. 더불어 urlArray에 저장한, 서버에 저장된 이미지 리스트를 전송하여 추후에 업데이트 또는 삭제할 때 빠르게 데이터를 찾을 수 있도록 해 주었다. 전체 코드는 최하단에 있다.
// 카테고리, 제목, 날짜, 글쓴이, 컨텐츠 내용, 조회수, 좋아요 백엔드에 보내기
let writeInform = {
category: category,
subject: subject,
content: endContent,
writer:redux.setUser.u_id,
imgList:urlArray,
view:0,
good:0
}
axios.post('/api/board/write', writeInform)
.then(response => {
if(response.data.success) {
console.log('업로드 성공')
console.log('저장한 데이터 : ',response)
} else {
alert('업로드에 실패하였습니다.')
}
})
드디어 기나긴 프론트엔드 작업이 모두 끝났다. 백엔드가 할 일은 간단하다. 백엔드 개발자가 되자고 결심하는 순간이다.
Backend
1. 이미지 파일을 받아 서버에 저장하고, 저장된 주소 돌려주기
2. 게시글 전체 내용을 받아, db에 저장하기
백엔드는 위의 두 내용만 수행하면 된다. 글은 board라는 새로운 db를 생성하여 그곳에 저장할 것이다. '누가, 언제, 무슨 내용을 작성했는지, 좋아요 수는 몇개인지, 조회수는 몇인지' 를 저장해 줄 것이다.
그런데 여기서 '누가' 를 저장하려면 어떻게 해야 할까? 유저의 정보를 모두 받아와서 저장해야 할까? msSql이나 mySql이라면 '누가' 를 저장할 때 Join을 했겠지만, 몽구스에서는 그렇지 않다. 몽구스는 자체적으로 ObjectId라는 것을 가지고 있고, ref라는 참조 유형을 제공한다.
writer: {
type: Schema.Types.ObjectId,
required: true,
ref: 'User',
},
Board라는 스키마에 writer 이라는 필드를 위와 같이 등록한다고 가정해 보자. 위의 코드는 writer 필드에 ref로 참조된 User 스키마의, 사용자 ObjectId가 들어간다는 뜻이다. 즉 유저의 정보를 모두 저장해줄 필요 없이, 프론트에서 보내준 _id값만 저장해주면 된다.
models/Board.js
이를 이용하여 모델 Board.js를 작성한다.
const mongoose = require('mongoose');
const Schema = mongoose.Schema
const moment = require("moment");
const boardSchema = mongoose.Schema({
// 이름, 이메일, 비밀번호, 닉네임, 유저 권한, 토큰(로그인 상태 관리)
category: {
type:String,
trim:true,
required: true,
},
subject: {
type:String,
maxlength: 50,
required: true,
},
content: {
type:String,
required: true,
},
writer: {
type: Schema.Types.ObjectId,
required: true,
ref: 'User',
},
imgList: {
type:[String]
},
view: {
type: Number,
default: 0
},
good : {
type:Number,
default: 0
},
createdAt: {
type:String,
default: moment().format("YYYY-MM-DD hh:mm:ss")
}
})
const Board = mongoose.model('Board', boardSchema);
module.exports = { Board }
글 작성 날짜는 프론트에서 보내는 것이 아닌, 백엔드에서 설정해 보내준다. 보통은 Date 자료형을 주고 Date.now를 사용하는데, 그렇게 되면 시간 형식이 우리나라에 맞지 않아 moment 라이브러리의 format을 이용해 주었다.
이제 저번에 사용했던 프로필 사진 업로드에서 코드를 긁어와, 사진을 저장해주고 글을 등록해주는 api만 작성하면 된다. routes 폴더에 board.js를 작성해주고, server.js에서 라우터를 require로 불러와 주었다.
사진을 저장하는 api는 /uploadImgFolder로, 글쓰기는 /write로 요청하였다.
board.js
const express = require('express')
const router = express.Router()
const multer = require('multer')
const path = require("path");
// -------------------------------------------------------
const { auth } = require('../middleware/auth')
const { Board } = require('../models/Board');
// -------------------------------------------------------
router.post('/write', function(req,res){
const board = new Board(req.body)
board.save((err,boardInfo) => {
if(err) return res.json({success:true,err})
return res.status(200).json({
success:true, boardInfo:boardInfo
})
})
})
const storage = multer.diskStorage({
// 받아온 file을 두번째 인자로 주어진 경로에 저장한다.
destination: (req, file, cb) => {
cb(null, "./client/public")
},
// 저장할 파일의 이름을 설정한다.
filename: (req, file, cb) => {
cb(null, `${Date.now()}_${file.originalname}`);
// 또는 const fileName = file.originalname.toLowerCase().split(' ').join('-');
// cb(null, v4() + '-' + fileName)
// (uuidv4 O) 7c7c98c7-1d46-4305-ba3c-f2dc305e16b0-통지서
// (uuidv4 X) 통지서
},
fileFilter: (req, file, cb) => {
// 파일 확장자를 출력하여 검사한다.
const ext = path.extname(file.originalname)
if(ext == "image/png"
|| file.mimetype == "image/jpg"
|| file.mimetype == "image/jpeg"
|| file.mimetype == "image/gif"){
cb(null, true);
} else {
cb(null, false);
return cb(new Error('Only .png .jpg .gif and .jpeg format allowed!'));
}
}
});
const upload = multer({storage:storage}).single("file")
router.post('/uploadImgFolder', (req,res)=>{
upload(req,res,err=> {
if(err){
return res.json({success:false, inText:'업로드 과정에서의 에러', err:err})
}
return res.json({success:true, inText:"폴더에 업로드 성공", url:res.req.file.path, fileName:res.req.file.filename})
})
})
module.exports = router
사진 업로드 부분은 프로필 파일 업로드와 완전히 동일하기 때문에, 설명을 생략한다. uuid로 무언가 해보려고 한 흔적이 있는데 라이브러리가 정상 작동하지 않는것을 나중에 깨달아서 저 부분은 무시해주면 된다... 이제 완료되었으니 테스트 해 보자.
결과물
React 전체 코드
Write.js
import React, { useState, useEffect } from "react";
import ReactQuill, { Quill } from 'react-quill';
import Button from 'react-bootstrap/Button';
import 'react-quill/dist/quill.snow.css';
import ImageResize from 'quill-image-resize';
import { useSelector } from "react-redux";
import axios from "axios";
Quill.register('modules/ImageResize', ImageResize);
function Write(){
// ------------------------State------------------------
// 리덕스에 _id저장해서 _id받아오기
let redux = useSelector((state)=>{return state})
// category의 경우, 추후 props로부터 건네받는다.
const category = 'notice'
let date = ''
const [subject, setSubject] = useState('')
const [content, setContent] = useState('')
// ------------------------get Content------------------------
const onChagecontent = (e) => {
console.log(e)
setContent(e)
}
// ------------------------정규식으로 src 추출------------------------
// src만 추출
const srcArray = []
// blopArray로 변환
const blopArray = []
// 최종 src url 저장할곳
const urlArray = []
const gainSource = /(<img[^>]*src\s*=\s*[\"']?([^>\"']+)[\"']?[^>]*>)/g
async function SaveBoard() {
// 이미지가 있을때만 아래 코드 실행(while)
// 이미지 처리
// 정규식으로 추출하여 배열에 저장
while(gainSource.test(content)){
console.log('이미지가 있을때만 진행함.')
let result = RegExp.$2
// console.log('src 추출 결과 : ',result)
srcArray.push(result)
console.log('srcArray 추가: ',srcArray)
// base64파일 Blop으로 바꾸기
// // dataURL 값이 data:image/jpeg:base64,~~~~~~~ 이므로 ','를 기점으로 잘라서 ~~~~~인 부분만 다시 인코딩
const byteString = atob(result.split(",")[1]);
// Blob를 구성하기 위한 준비, 이 내용은 저도 잘 이해가 안가서 기술하지 않았습니다.
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ia], {
type: "image/jpeg"
});
const file = new File([blob], "image.jpg");
// 위 과정을 통해 만든 image폼을 FormData에 넣어줍니다.
// 서버에서는 이미지를 받을 때, FormData가 아니면 받지 않도록 세팅해야합니다.
const formData = new FormData();
formData.append("file", file);
console.log('formData: ',formData)
// 백엔드로 보내서 urlArray에 돌려받은 url을 배열 형태로 push 해준다.
// 최상단 while문이 모든 사진을 추출해 하나씩 저장하여 push하므로
// 백엔드의 multer 패키지에 single로 저장을 요청한다.
const config = {
header : {'content-type': 'multipart/form-data'}
}
// TODO FormData 백엔드로 넘겨서 url 건네받아 저장하고, url 반환해서 urlArray에 저장하기
await axios.post('api/board/uploadImgFolder',formData, config )
.then(response => {
if(response.data.success) {
console.log('이미지 서버에 업로드 성공', response)
urlArray.push(response.data.url)
console.log('urlArray에 추가',urlArray)
} else {
console.log(response)
alert('이미지를 서버에 업로드하는데에 실패했습니다.')
}
})
// FormData의 key 확인
for (let key of formData.keys()) {
console.log('key',key);
}
// FormData의 value 확인
for (let value of formData.values()) {
console.log('value',value);
}
//
// ============================================================================================================================
}
// 게시글 내용 지정하기
console.log('서버 주소 저장된 어레이: ',urlArray)
let endContent = content
// 만일 이미지를 업로드 했다면, 첫번쨰 srcArray가 있는 부분을 첫번째 url로 바꾸는 식으로 계속 바꿔라.
if(srcArray.length > 0) {
console.log('실행은 됐음..')
for(let i = 0; i<srcArray.length; i++) {
console.log('실행중.. '+i+' 번째임')
console.log('srcArray[i]: ',srcArray[i],'urlArray[i]: ',urlArray[i])
let replace = endContent.replace(srcArray[i],urlArray[i])
endContent = replace
console.log('바뀌었는지 테스트',endContent)
}
} // 없다면 content=content
console.log('endContent:',endContent)
// 카테고리, 제목, 날짜, 글쓴이, 컨텐츠 내용, 조회수, 좋아요 백엔드에 보내기
let writeInform = {
category: category,
subject: subject,
content: endContent,
writer:redux.setUser.u_id,
imgList:urlArray,
view:0,
good:0
}
axios.post('/api/board/write', writeInform)
.then(response => {
if(response.data.success) {
console.log('업로드 성공')
console.log('저장한 데이터 : ',response)
} else {
alert('업로드에 실패하였습니다.')
}
})
console.log('최종 urlArray', urlArray)
console.log('최종 srcArray: ',srcArray)
console.log('최종 blopArray: ',blopArray)
}
// ------------------------quill Modules------------------------
const modules = {
toolbar: [
[{ 'font': [] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline', 'strike','blockquote', 'code-block'], // toggled buttons
['link','image'],
[{ 'align': [] },{ 'color': [] },{ 'background': [] }], // dropdown with defaults from theme
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
],
ImageResize: {
parchment: Quill.import('parchment')
}
}
// ------------------------UI------------------------
return(
<div>
<div style={{width:'100%', height:'90vh'}}>
<div style={{width:'1000px', margin:'auto', borderRadius:'19px'}}>
<div style={{marginBottom:'20px',marginTop:'70px', fontSize:'20px', fontWeight:'bold'}} >공지사항</div>
{/* ======== Subject ======== */}
<input
className="Subject"
placeholder="제목을 입력해 주세요"
style={{padding:'7px', marginBottom:'10px',width:'100%',border:'1px solid lightGray', fontSize:'15px'}}
onChange={(e)=>{setSubject(e.target.value)}}
></input>
<div style={{height:'650px'}}>
{/* ======== Quill ======== */}
<ReactQuill
modules={modules}
placeholder='내용을 입력해 주세요'
onChange={onChagecontent}
style={{height: "600px"}}
/>
</div>
{/* ======== Button ======== */}
<div style={{float:'right'}}>
<Button variant="danger" style={{marginRight:'10px'}} >취소</Button>
<Button variant="dark"
onClick={()=>{
SaveBoard()
}}
>저장하기</Button>
</div>
</div>
</div>
</div>
)
}
export default Write
'프로젝트 > 풀스택 프로젝트' 카테고리의 다른 글
4. [React + Express] 풀스택 보일러 플레이트 ( 회원가입, 로그인, 로그아웃, 회원 정보 수정, 게시글 CRUD ) (0) | 2022.12.18 |
---|---|
4-8. [React + Node.js Express] 게시판 - 게시글 리스트, 상세보기, 수정, 삭제 ( React-Quill ) CRUD (0) | 2022.12.15 |
4-6. [React + Node.js Express] 프로필 사진 업로드 (1) | 2022.12.10 |
4-5. [React + Node.js Express] 회원 정보 수정 기능 (0) | 2022.12.06 |
4-4. [React + Node.js Express] 로그아웃 기능 (0) | 2022.12.05 |
댓글