RepleBoardPage.jsx
글쓰기버튼 → 라우트 /reple/boardwrite → RepleBoardWriteForm에 연결됨
RepleBoardWriteForm - files[]
QuillEditor → 이벤트 여기서 일어나는데 WriteForm에 있는 files[]에 담음(버블링) - 핵심 키워드 props(얕은 복사, 원본이 넘어오는 것이기에 거기에 담아준다)
RepleBoardFileInsert - props 가져야 함 - files[] → map으로 꺼내서 처리해 줌
버블링
자식으로부터 부모로 올라가는 것
한 요소에 이벤트가 발생하면 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작하고 최상단의 부모 요소를 만날 때까지 반복되면서 핸들러가 동작하는 현상
캡쳐링
부모로부터 자식으로 내려오는 것
버블링과 반대 방향으로 진행되는 이벤트 전파 방식
<Quill 이미지, 파일 첨부 - App.jsx>
import { Route, Routes, useNavigate } from 'react-router-dom';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
import HomePage from './components/page/HomePage';
import DeptPage from './components/page/DeptPage';
import DeptDetail from './components/dept/DeptDetail';
import RepleBoardPage from './components/page/RepleBoardPage';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';
import SignupPage from './components/auth/SignupPage';
import KhLoginPage from './components/auth/KhLoginPage';
import { onAuthChange } from './service/authLogic';
import { memberListDB } from './service/dbLogic';
import EmailVerifiedPage from './components/auth/EmailVerifiedPage';
import FindEmailPage from './components/auth/FindEmailPage';
import ResetPwdPage from './components/auth/ResetPwdPage';
import RepleBoardWriteForm from './components/repleboard/RepleBoardWriteForm';
import RepleBoardDetail from './components/repleboard/RepleBoardDetail';
function App({authLogic, imageUploader}) {
const navigate = useNavigate()
const dispatch = useDispatch()
const ssg = sessionStorage;
const toastStatus = useSelector(state => state.toastStatus)
useEffect(() => {
const asyncDB = async() => {
const auth = authLogic.getUserAuth()
// 현재 인증된 사용자 정보를 가져온다
const user = await onAuthChange(auth)
// 사용자가 있을 경우(userId가 존재) - 구글 로그인으로 사용자 정보를 가지고 있을 때
// user정보가 있으면 sessionStorage에 담는다 - email
if(user) {
console.log('user정보가 있을 때')
ssg.setItem('email', user.email)
const res = await memberListDB({mem_uid: user.uid, type: 'auth'})
console.log(res.data)
//오라클 서버의 회원집합에 uid가 존재할 경우 - 세션스토리지에 값을 담는다
if(res.data !== 0) { // 스프링 부트 - RestMemberController - memberList() -> 0 혹은 [{}]
const temp = JSON.stringify(res.data)
const jsonDoc = JSON.parse(temp)
ssg.setItem('nickname', jsonDoc[0].MEM_NICKNAME)
ssg.setItem('status', jsonDoc[0].MEM_STATUS)
ssg.setItem('auth', jsonDoc[0].MEM_AUTH)
ssg.setItem('no', jsonDoc[0].MEM_NO)
//navigate("/")
return // 렌더링이 종료됨
}
// 구글 계정이 아닌 다른 계정으로 로그인 시도를 했을 땐 user.emailVerified가 없다
// -> undefined
if(!user.emailVerified) {
navigate('./auth/emailVerified')
}
//오라클 서버의 회원집합에 uid가 존재하지 않을 경우
else {
console.log('가입되지 않은 구글 계정입니다.')
//navigate('/auth/signup')
}
}
// 사용자 정보가 없을 경우
else {
console.log('user정보가 없을 때')
if(ssg.getItem('email')){
// 세션 스토리지에 있는 값 모두 삭제하기
ssg.clear()
window.location.reload()
} // end of inner if
} // end of else
}
asyncDB()
}, [dispatch])
return (
<>
<div style={{height: '100vh'}}>
{toastStatus.status && <Toast />}
<Routes>
<Route path='/login' exact={true} element={<KhLoginPage authLogic={authLogic} />} />
<Route path='/' exact={true} element={<HomePage />} />
<Route path='/home' exact={true} element={<HomePage />} />
<Route path='/auth/signup' exact={true} element={<SignupPage authLogic={authLogic} />} />
<Route path='/auth/emailVerified' exact={true} element={<EmailVerifiedPage authLogic={authLogic} />} />
<Route path='/auth/findEmail' exact={true} element={<FindEmailPage />} />
<Route path='/auth/resetPwd' exact={true} element={<ResetPwdPage authLogic={authLogic} />} />
<Route path='/reple/board' exact={true} element={<RepleBoardPage />} />
<Route path='/reple/boarddetail/*' element={<RepleBoardDetail />} />
<Route path='/reple/boardwrite' exact={true} element={<RepleBoardWriteForm />} />
<Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} />} />
{/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
<Route path='/deptdetail/:deptno' element={<DeptDetail imageUploader={imageUploader} />} />
<Route path='/auth/kakao/callback' exact={true} element={<KakaoRedirectHandler />} />
<Route path="/member" exact={true} element={<MemberPage imageUploader={imageUploader} />} />
<Route path="/profile" exact={true} element={<Profile />} />
</Routes>
</div>
</>
);
}
export default App;
<Quill 이미지, 파일 첨부 - RepleBoardPage.jsx>
import React from 'react'
import { Button, Table } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom'
import BlogFooter from '../include/BlogFooter'
import BlogHeader from '../include/BlogHeader'
const RepleBoardPage = () => {
const navigate = useNavigate()
const boardSearch = () => {
}
return (
<>
<BlogHeader />
<div className='container'>
<div className="page-header">
<h2>댓글형 게시판 <i className="fa-solid fa-angles-right"></i> <small>글목록</small></h2>
<hr />
</div>
<div className="row">
<div className="col-3">
<select id="gubun" className="form-select" aria-label="분류선택">
<option defaultValue>분류선택</option>
<option value="b_no">글번호</option>
<option value="b_title">글제목</option>
<option value="b_content">내용</option>
</select>
</div>
<div className="col-6">
<input type="text" id="keyword" className="form-control" placeholder="검색어를 입력하세요"
aria-label="검색어를 입력하세요" aria-describedby="btn_search" />
</div>
<div className="col-3">
<Button variant='danger' id="btn_search" onClick={boardSearch}>검색</Button>
</div>
</div>
<div className='book-list'>
<Table striped bordered hover>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>댓글형 게시판 구현</td>
<td>관리자</td>
<td>2023-03-23</td>
<td>17</td>
</tr>
</tbody>
</Table>
<hr />
<div className='booklist-footer'>
<Button variant="warning" >
전체조회
</Button>
<Button variant="success" onClick={() => navigate('/reple/boardwrite')}>
글쓰기
</Button>
</div>
</div>
</div>
{/* ========================== [[ 글쓰기 Modal ]] ========================== */}
{/* ========================== [[ 글쓰기 Modal ]] ========================== */}
<BlogFooter />
</>
)
}
export default RepleBoardPage
<Quill 이미지, 파일 첨부 - RepleBoardWriteForm.jsx>
import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { boardInsertDB, uploadFileDB, uploadImageDB } from '../../service/dbLogic'
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle'
import QuillEditor from './QuillEditor'
import RepleBoardFileInsert from './RepleBoardFileInsert'
const RepleBoardWriteForm = () => {
const navigate = useNavigate()
const[title, setTitle] = useState("")
const[writer, setWriter] = useState("")
const[pw, setPw] = useState("")
const[content, setcontent] = useState("")
const[bsfile, setBsFile] = useState("")
const[bssize, setBsSize] = useState("")
const[fileName, setFileName] = useState("")
// QuillEditor이미지 선택하면 imageUploadDB타고 스프링 플젝 pds에 해당 이미지 업로드
// pds에 업로드된 파일을 읽어서 Editor안에 보여줌 imageGet?imageName=XXX.png
const[files, setFiles] = useState([])
const quillRef = useRef()
const fileRef = useRef()
const handleTitle = useCallback((e) => {
setTitle(e)
}, [])
const handleWriter = useCallback((e) => {
setWriter(e)
}, [])
const handlePW = useCallback((e) => {
setPw(e)
}, [])
const handleContent = useCallback((e) => {
setcontent(e)
}, [])
const boardInsert = async() => {
const board = {
bm_title: title,
bm_content: content,
bm_writer: writer,
bm_pw: pw,
bs_file: bsfile,
bs_size: bssize,
}
const res = await boardInsertDB(board)
console.log(res)
navigate("/board")
}
const handleClick = (event) => {
event.preventDefault()
document.querySelector("#file-input").click((event)=>{
console.log(event.currentTarget.value);
})
}
const handleChange = async (event) => {
console.log('첨부파일 선택'+event.target.value);
//console.log(fileRef.current.value);
//fileRef에서 가져온 값중 파일명만 담기
const str = fileRef.current.value.split('/').pop().split('\\\\').pop()
setFileName(str)
console.log(str);
//선택한 파일을 url로 바꾸기 위해 서버로 전달할 폼데이터 만들기
const formData = new FormData()
const file = document.querySelector("#file-input").files[0]
formData.append("file_name", file)// 아래 "image"와 스프링 컨트롤러 메소드의 value 맞춰야함!
const res = await uploadFileDB(formData)
console.log(res.data)
const fileinfo = res.data.split(',')
console.log(fileinfo)
setBsFile(fileinfo[0])
setBsSize(fileinfo[1])
}
const handleFiles = () => {
}
return (
<>
<ContainerDiv>
<HeaderDiv>
<h3 style={{marginLeft:"10px"}}>공지사항 글작성</h3>
</HeaderDiv>
<FormDiv>
<div style={{width:"100%", maxWidth:"2000px"}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>제목</h3>
<BButton onClick={()=>{boardInsert()}}>글쓰기</BButton>
</div>
<input id="dataset-title" type="text" maxLength="50" placeholder="제목을 입력하세요."
style={{width:"100%",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>작성자</h3>
</div>
<input id="dataset-writer" type="text" maxLength="50" placeholder="작성자를 입력하세요."
style={{width:"200px",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleWriter(e.target.value)}}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>비밀번호</h3>
</div>
<input id="dataset-pw" type="text" maxLength="50" placeholder="비밀번호를 입력하세요."
style={{width:"200px",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handlePW(e.target.value)}}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>첨부파일</h3>
</div>
<input id="file-input" name='file_name' ref={fileRef} type="file" maxLength="50" className="visuallyhidden" onChange={handleChange}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
</div>
<h3>상세내용</h3>
<hr style={{margin:'10px 0px 10px 0px'}}/>
<QuillEditor value={content} handleContent={handleContent} quillRef={quillRef} files={files} handleFiles={handleFiles}/>
<RepleBoardFileInsert files={files}/>
</div>
</FormDiv>
</ContainerDiv>
</>
)
}
export default RepleBoardWriteForm
<Quill 이미지, 파일 첨부 - RepleBoardFileInsert.jsx>
import React from 'react';
const RepleBoardFileInsert = ({files}) => {
console.log(files);
return (
<div style={{display:'block', border:'1px solid lightGray', borderRadius:'10px', minHeight:'60px', padding:'5px'}}>
<div style={{textAlign:"left", padding: "2px 5px 2px 5px"}}>첨부사진</div>
{
files.map((item, index)=>(
<div type='text' id='fileUpload' style={{padding:"5px"}} key={index}>{item}</div>
))
}
</div>
);
};
export default RepleBoardFileInsert;
<Quill 이미지, 파일 첨부 - QuillEditor.jsx>
import React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import ReactQuill, { Quill } from 'react-quill';
import { uploadImageDB } from '../../service/dbLogic';
const QuillEditor = ({ value, handleContent, quillRef, files, handleFiles}) => {
console.log(files);
console.log(Array.isArray(files)); // array
//const dispatch = useDispatch();
const imageHandler = useCallback(() => {
console.log(files);
if(files.length > 2){
return "이미지는 3장까지 업로드 가능합니다.";
}
const formData = new FormData(); // 이미지를 url로 바꾸기위해 서버로 전달할 폼데이터 만들기
const input = document.createElement("input"); // input 태그를 동적으로 생성하기
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*"); // 이미지 파일만 선택가능하도록 제한
input.setAttribute("name", "image");
input.click();
// 파일 선택창에서 이미지를 선택하면 실행될 콜백 함수 등록
input.onchange = async () => {
const file = input.files[0];
const fileType = file.name.split('.');
console.log(fileType);
if(!fileType[fileType.length-1].toUpperCase().match('JPG')&&
!fileType[fileType.length-1].toUpperCase().match('PNG')&&
!fileType[fileType.length-1].toUpperCase().match('JPEG')) {
console.log("jpg png jpeg형식만 지원합니다.");
}
// 아래 "image"와 스프링 컨트롤러 메소드의 value 맞춰야함! - 415(미디어타입 주의!)
formData.append("image", file); // 위에서 만든 폼데이터에 이미지 추가
for (let pair of formData.entries()) {
console.log(pair[0], pair[1]);
}
// 폼데이터를 서버에 넘겨 multer로 이미지 URL 받아오기
const res = await uploadImageDB(formData);
files.push(res.date)
console.log(res.data); // 리턴받는 파일명
if (!res.data) {
console.log("이미지 업로드에 실패하였습니다.");
}
const url = process.env.REACT_APP_SPRING_IP+`reple/imageGet?imageName=${res.data}`;
const quill = quillRef.current.getEditor();
/* ReactQuill 노드에 대한 Ref가 있어야 메서드들을 호출할 수 있으므로
useRef()로 ReactQuill에 ref를 걸어주자.
getEditor() : 편집기를 지원하는 Quill 인스턴스를 반환함
여기서 만든 인스턴스로 getText()와 같은 메서드를 사용할 수 있다.*/
const range = quill.getSelection().index;
//getSelection()은 현재 선택된 범위를 리턴한다. 에디터가 포커싱되지 않았다면 null을 반환한다.
if (typeof (range) !== "number") return;
/*range는 0이 될 수도 있으므로 null만 생각하고 !range로 체크하면 잘못 작동할 수 있다.
따라서 타입이 숫자이지 않을 경우를 체크해 리턴해주었다.*/
quill.setSelection(range, 1);
/* 사용자 선택을 지정된 범위로 설정하여 에디터에 포커싱할 수 있다.
위치 인덱스와 길이를 넣어주면 된다.*/
quill.clipboard.dangerouslyPasteHTML(
range,
`<img src=${url} style="width: 100%; height: auto;" alt="image" />`);
//handleFiles(res.data, `${Math.floor(file.size/(1024*1024)*10)/10}MB`);
} //주어진 인덱스에 HTML로 작성된 내용물을 에디터에 삽입한다.
}, [quillRef, files]);
const modules = useMemo(
() => ({
toolbar: { // 툴바에 넣을 기능들을 순서대로 나열하면 된다.
container: [
[{ 'header': [1, 2, 3, 4, 5, 6, false] }, { color: [] }, { 'align': [] }, { 'background': [] }],
["bold", "italic", "underline", "strike", "blockquote"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
['clean'],
['link', "image"],
],
handlers: { // 위에서 만든 이미지 핸들러 사용하도록 설정
image: imageHandler,
},
},
/* ImageResize: {
modules: ['Resize']
} */
}), [imageHandler])
useEffect(()=>{
console.log('QuillEditor useEffect');
},[])
const formats = [
//'font',
'header',
'bold', 'italic', 'underline', 'strike', 'blockquote',
'list', 'bullet', 'indent',
'link', 'image',
'align', 'color', 'background',
]
return (
<div style={{height: "550px", display: "flex", justifyContent: "center", padding:"0px"}}>
<ReactQuill
ref={quillRef}
style={{height: "470px", width: "100%"}}
theme="snow"
placeholder= "본문 입력"
modules={modules}
formats={formats}
value={value}
onChange={(content, delta, source, editor) => {handleContent(editor.getHTML());}} />
</div>
)
}
export default QuillEditor
<Quill 이미지, 파일 첨부 - dbLogic.js>
import axios from "axios";
export const uploadImageDB = (file) => {
console.log(file);
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/imageUpload",
headers: {
"Content-Type": "multipart/form-data",
},
processData: false,
contentType: false,
data: file,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const uploadFileDB = (file) => {
console.log(file);
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/fileUpload",
headers: {
"Content-Type": "multipart/form-data",
},
processData: false,
contentType: false,
data: file,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const boardInsertDB = (board) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/boardInsert",
headers: {
"Content-Type": "multipart/form-data",
},
data: board,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const memberListDB = (member) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "member/memberList",
params: member,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const memberInsertDB = (member) => {
return new Promise((resolve, reject) => {
console.log(member)
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "member/memberInsert",
data: member, // post방식 data
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const memberUpdateDB = (member) => {
return new Promise((resolve, reject) => {
console.log(member)
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "member/memberUpdate",
data: member, // post방식 data
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const memberDeleteDB = (member) => {
return new Promise((resolve, reject) => {
console.log(member)
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "member/memberDelete",
params: member,
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const deptInsertDB = (dept) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "dept/deptInsert",
data: dept, // post방식 data
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const deptUpdateDB = (dept) => {
return new Promise((resolve, reject) => {
console.log(dept)
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "dept/deptUpdate",
data: dept, // post방식 data
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const deptDeleteDB = (dept) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "dept/deptDelete",
params: dept,
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const deptListDB = (dept) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "dept/deptList",
params: dept, // 쿼리스트링은 header에 담김 - get방식 params
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
<Quill 이미지, 파일 첨부 - RepleBoardController.java>
package com.example.demo.controller;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.example.demo.logic.RepleBoardLogic;
import com.google.gson.Gson;
@RestController
@RequestMapping("/reple/*")
public class RepleBoardController {
Logger logger = LogManager.getLogger(RepleBoardController.class);
@Autowired
private RepleBoardLogic repleBoardLogic = null;
@GetMapping("boardList")
public String boardList(@RequestParam Map<String, Object> pMap) {
logger.info("boardList 호출");
List<Map<String, Object>> bList = null;
bList = repleBoardLogic.boardList(pMap);
Gson g = new Gson();
String temp = g.toJson(bList);
return temp;
}
@PostMapping("boardInsert")
public String boardInsert(@RequestBody Map<String, Object> pMap) {
logger.info("boardInsert 호출");
int result = 0;
result = repleBoardLogic.boardInsert(pMap);
return String.valueOf(result);
}
@PostMapping("imageUpload")
public Object imageUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="image", required=false) MultipartFile image) {
logger.info("imageUpload 호출");
// 사용자가 선택한 파일 이름 담기
String filename = null;
if(!image.isEmpty()) {
filename = image.getOriginalFilename();
String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
String fullPath = saveFolder + "\\\\" + filename;
try {
// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
File file = new File(fullPath);
byte[] bytes = image.getBytes();
// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bos.write(bytes);
// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
bos.close();
} catch (Exception e) {
}
}
// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
String temp = filename;
return temp;
}
@PostMapping("fileUpload")
public Object fileUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="file_name", required=false) MultipartFile file_name) {
logger.info("fileUpload 호출");
// 사용자가 선택한 파일 이름 담기
String filename = null;
if(!file_name.isEmpty()) {
filename = file_name.getOriginalFilename();
String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
String fullPath = saveFolder + "\\\\" + filename;
try {
// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
File file = new File(fullPath);
byte[] bytes = file_name.getBytes();
// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bos.write(bytes);
// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
bos.close();
} catch (Exception e) {
}
}
// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
String temp = filename;
return temp;
}
@GetMapping("imageGet")
public Object imageGet(HttpServletRequest req, HttpServletResponse res) {
// imageName 정보는 공통코드로 제공된 QuillEditor.jsx에서 파라미터로 넘어오는 값임
// imageUpload 메소드에서는 업로드된 파일 정보(파일명, 파일크기)가 리턴됨
String b_file = req.getParameter("imageName"); // get방식으로 넘어옴
logger.info("imageGet 호출 성공===>" + b_file); // XXX.png
// 톰캣 서버의 물리적인 위지 정보
String filePath = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
String fname = b_file;
logger.info("b_file: 8->euc" + b_file);
// File은 내용까지 복제되는 것은 아니고 파일명만 객체화해주는 클래스이다
File file = new File(filePath, b_file.trim());
// 실제 업로드된 파일에 대한 마임타입을 출력해줌
String mimeType = req.getServletContext().getMimeType(file.toString());
logger.info(mimeType); // image, video, text
if (mimeType == null) { // 마임타입이 널이면 아래의 속성값으로 마임타입을 설정
// -> 브라우저는 해석이 가능한 마임타입은 페이지 로딩 처리,
// 해석이 불가능한 마임타입은 다운로드함
// 강제로 다운로드 처리를 위한 마임타입 변경
// -> 브라우저에서 해석가능한 마임타입의 경우 화면에 그대로 출력되니까 그걸 방지하기위해
res.setContentType("application/octet-stream");
}
// 다운로드되는 파일 이름 담기
String downName = null;
// 위 File 객체에서 생성된 객체에 내용을 읽기위한 클래스 선언
FileInputStream fis = null;
// 응답으로 나갈 정보가 웹 서비스에 처리되어야 하기에 사용한 객체
ServletOutputStream sos = null;
try {
if (req.getHeader("user-agent").indexOf("MSIE") == -1) {
downName = new String(b_file.getBytes("UTF-8"), "8859_1");
} else {
downName = new String(b_file.getBytes("EUC-KR"), "8859_1");
}
// 응답 헤더에 다운로드 될 파읿명을 매핑하기
res.setHeader("Content-Disposition", "attachment;filename=" + downName);
// 위에서 생성된 파일 문자열 객체를 가지고 파일생성에 필요한 객체의 파라미터 넘김
fis = new FileInputStream(file);
sos = res.getOutputStream();
// 파일 내용을 담을 byte배열을 생성
byte b[] = new byte[1024 * 10];
int data = 0;
while ((data = (fis.read(b, 0, b.length))) != -1) {
// 파일에서 읽은 내용을 가지고 실제 파일에 쓰기 처리함
// 여기서 처리된 값은 브라우저를 통해서 내보내진다
sos.write(b, 0, data);
}
// 처리한 내용이 버퍼에 있는데 이것을 모두 처리요청하기
// 내보내고 버퍼를 비운다 - 버퍼는 크기가 작음(휘발성)
sos.flush();
} catch (Exception e) {
logger.info(e.toString());
} finally {
try {
if (sos != null)
sos.close();
if (fis != null)
fis.close();
} catch (Exception e2) {
// TODO: handle exception
}
}
// byte[] fileArray = boardLogic.imageDownload(imageName);
// logger.info(fileArray.length);
return null;
}// end of imageGet
}
Promise
실패 reject → callback
이행 fulfilled → 결괏값 resolve()
실패했을 경우 catch(e)
비동기메소드에서 동기메소드처럼 값을 반환할 수 있음 → 다만 미래의 어떤 시점에 결과를 제공하겠다는 약속을 반환함
<qna리스트 출력 - App.jsx>
import { Route, Routes, useNavigate } from 'react-router-dom';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
import HomePage from './components/page/HomePage';
import DeptPage from './components/page/DeptPage';
import DeptDetail from './components/dept/DeptDetail';
import RepleBoardPage from './components/page/RepleBoardPage';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';
import SignupPage from './components/auth/SignupPage';
import KhLoginPage from './components/auth/KhLoginPage';
import { onAuthChange } from './service/authLogic';
import { memberListDB } from './service/dbLogic';
import EmailVerifiedPage from './components/auth/EmailVerifiedPage';
import FindEmailPage from './components/auth/FindEmailPage';
import ResetPwdPage from './components/auth/ResetPwdPage';
import RepleBoardWriteForm from './components/repleboard/RepleBoardWriteForm';
import RepleBoardDetail from './components/repleboard/RepleBoardDetail';
function App({authLogic, imageUploader}) {
// 화면 전환시킬때
// window.location.href - 새로고침 요청 발생 - 가상돔 사용하지 않음
// useNavigate - 가상돔 사용됨
const navigate = useNavigate()
const dispatch = useDispatch() // 허브 - action.type(switch-선택), action.payload(내용)
const ssg = sessionStorage;
const toastStatus = useSelector(state => state.toastStatus) // store에 값을 접근할때
useEffect(() => { // 의존성배열 - 의존성 배열에 있는 변수, 함수, 훅이 변할때마다 다시 호출 가능함
const asyncDB = async() => { // 함수선언 - memberListDB호출
console.log('asyncDB 호출')
const auth = authLogic.getUserAuth()
// 현재 인증된 사용자 정보를 가져온다
const user = await onAuthChange(auth)
// 사용자가 있을 경우(userId가 존재) - 구글 로그인으로 사용자 정보를 가지고 있을 때
// user정보가 있으면 sessionStorage에 담는다 - email
if(user) {
console.log('user정보가 있을 때')
// 세션스토리지에 이메일 주소가 등록됨 - 단, 구글 로그인이 되어있는 상태일때만
ssg.setItem('email', user.email)
const res = await memberListDB({mem_uid: user.uid, type: 'auth'})
console.log(res.data)
//오라클 서버의 회원집합에 uid가 존재할 경우 - 세션스토리지에 값을 담는다
if(res.data !== 0) { // 스프링 부트 - RestMemberController - memberList() -> 0 혹은 [{}]
const temp = JSON.stringify(res.data)
const jsonDoc = JSON.parse(temp)
ssg.setItem('nickname', jsonDoc[0].MEM_NICKNAME)
ssg.setItem('status', jsonDoc[0].MEM_STATUS)
ssg.setItem('auth', jsonDoc[0].MEM_AUTH)
ssg.setItem('no', jsonDoc[0].MEM_NO)
//navigate("/")
return // 렌더링이 종료됨
}
// 구글 계정이 아닌 다른 계정으로 로그인 시도를 했을 땐 user.emailVerified가 없다
// -> undefined
if(!user.emailVerified) {
navigate('./auth/emailVerified')
}
//오라클 서버의 회원집합에 uid가 존재하지 않을 경우
else {
console.log('가입되지 않은 구글 계정입니다.')
//navigate('/auth/signup')
}
}
// 사용자 정보가 없을 경우
else {
console.log('user정보가 없을 때')
if(ssg.getItem('email')){
// 세션 스토리지에 있는 값 모두 삭제하기
ssg.clear()
window.location.reload()
} // end of inner if
} // end of else
}
asyncDB() // 함수호출
}, [dispatch]) // dispatch가 바뀔때마다 호출
return (
<>
<div style={{height: '100vh'}}>
{toastStatus.status && <Toast />}
<Routes>
<Route path='/login' exact={true} element={<KhLoginPage authLogic={authLogic} />} />
<Route path='/' exact={true} element={<HomePage />} />
<Route path='/home' exact={true} element={<HomePage />} />
<Route path='/auth/signup' exact={true} element={<SignupPage authLogic={authLogic} />} />
<Route path='/auth/emailVerified' exact={true} element={<EmailVerifiedPage authLogic={authLogic} />} />
<Route path='/auth/findEmail' exact={true} element={<FindEmailPage />} />
<Route path='/auth/resetPwd' exact={true} element={<ResetPwdPage authLogic={authLogic} />} />
<Route path='/reple/board' exact={true} element={<RepleBoardPage />} />
<Route path='/reple/boarddetail/*' element={<RepleBoardDetail />} />
<Route path='/reple/boardwrite' exact={true} element={<RepleBoardWriteForm />} />
<Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} />} />
{/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
<Route path='/deptdetail/:deptno' element={<DeptDetail imageUploader={imageUploader} />} />
<Route path='/auth/kakao/callback' exact={true} element={<KakaoRedirectHandler />} />
<Route path="/member" exact={true} element={<MemberPage imageUploader={imageUploader} />} />
<Route path="/profile" exact={true} element={<Profile />} />
</Routes>
</div>
</>
);
}
export default App;
<qna리스트 출력 - RepleBoardWriteForm.jsx>
import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { boardInsertDB, qnaInsertDB, uploadFileDB, uploadImageDB } from '../../service/dbLogic'
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle'
import QuillEditor from './QuillEditor'
import RepleBoardFileInsert from './RepleBoardFileInsert'
const RepleBoardWriteForm = () => {
const navigate = useNavigate()
const[title, setTitle] = useState("") // 사용자가 입력한 제목 담기
const[secret, setSecret] = useState("") // 사용자가 입력한 비번 담기
const[content, setContent] = useState("") // 사용자가 입력한 내용 담기(태그포함된 값들)
const[file_name, setFilename] = useState("") // 첨부파일 이름 담기
const[file_size, setFileSize] = useState("") // 첨부파일 크기 담기
// QuillEditor이미지 선택하면 imageUploadDB타고 스프링 플젝 pds에 해당 이미지 업로드
// pds에 업로드된 파일을 읽어서 Editor안에 보여줌 imageGet?imageName=XXX.png
const[files, setFiles] = useState([])
const quillRef = useRef()
const fileRef = useRef()
const handleTitle = useCallback((e) => {
setTitle(e)
}, [])
const handleSecret = useCallback((e) => {
setSecret(e)
}, [])
const handleContent = useCallback((e) => { // QuillEditor에서 담김 - 태그포함 정보
setContent(e)
}, [])
const boardInsert = async() => {
const no = sessionStorage.getItem(no)
const board = {
qna_bno: 0, // 오라클 자동채번하는 시퀀스 사용
qna_title: title,
qna_content: content,
qna_type: '양도',
qna_secret: secret,
qna_hit: 0,
mem_no: no,
}
const res = await qnaInsertDB(board)
console.log(res)
navigate("/reple/board")
}
const handleChange = async (event) => {
console.log('첨부파일 선택'+event.target.value);
//console.log(fileRef.current.value);
//fileRef에서 가져온 값중 파일명만 담기
const str = fileRef.current.value.split('/').pop().split('\\\\').pop()
setFilename(str)
console.log(str);
//선택한 파일을 url로 바꾸기 위해 서버로 전달할 폼데이터 만들기
const formData = new FormData()
const file = document.querySelector("#file-input").files[0]
formData.append("file_name", file)// 아래 "image"와 스프링 컨트롤러 메소드의 value 맞춰야함!
const res = await uploadFileDB(formData)
console.log(res.data)
const fileinfo = res.data.split(',')
console.log(fileinfo)
setFilename(fileinfo[0])
setFileSize(fileinfo[1])
}
const handleFiles = () => {
}
return (
<>
<ContainerDiv>
<HeaderDiv>
<h3 style={{marginLeft:"10px"}}>공지사항 글작성</h3>
</HeaderDiv>
<FormDiv>
<div style={{width:"100%", maxWidth:"2000px"}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>제목</h3>
<BButton onClick={()=>{boardInsert()}}>글쓰기</BButton>
</div>
<input id="dataset-title" type="text" maxLength="50" placeholder="제목을 입력하세요."
style={{width:"100%",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>비밀번호</h3>
</div>
<input id="dataset-pw" type="text" maxLength="50" placeholder="비밀번호를 입력하세요."
style={{width:"200px",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleSecret(e.target.value)}}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>첨부파일</h3>
</div>
<input id="file-input" name='file_name' ref={fileRef} type="file" maxLength="50" className="visuallyhidden" onChange={handleChange}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
</div>
<h3>상세내용</h3>
<hr style={{margin:'10px 0px 10px 0px'}}/>
<QuillEditor value={content} handleContent={handleContent} quillRef={quillRef} files={files} handleFiles={handleFiles}/>
<RepleBoardFileInsert files={files}/>
</div>
</FormDiv>
</ContainerDiv>
</>
)
}
export default RepleBoardWriteForm
<qna리스트 출력 - RepleBoardPage.jsx>
import React from 'react'
import { Button, Table } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom'
import BlogFooter from '../include/BlogFooter'
import BlogHeader from '../include/BlogHeader'
import RepleBoardList from '../repleboard/RepleBoardList'
const RepleBoardPage = () => {
const navigate = useNavigate()
const boardSearch = () => {
}
return (
<>
<BlogHeader />
<div className='container'>
<div className="page-header">
<h2>댓글형 게시판 <i className="fa-solid fa-angles-right"></i> <small>글목록</small></h2>
<hr />
</div>
<div className="row">
<div className="col-3">
<select id="gubun" className="form-select" aria-label="분류선택">
<option defaultValue>분류선택</option>
<option value="b_no">글번호</option>
<option value="b_title">글제목</option>
<option value="b_content">내용</option>
</select>
</div>
<div className="col-6">
<input type="text" id="keyword" className="form-control" placeholder="검색어를 입력하세요"
aria-label="검색어를 입력하세요" aria-describedby="btn_search" />
</div>
<div className="col-3">
<Button variant='danger' id="btn_search" onClick={boardSearch}>검색</Button>
</div>
</div>
<div className='book-list'>
<Table striped bordered hover>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
</thead>
<tbody>
<RepleBoardList />
</tbody>
</Table>
<hr />
<div className='booklist-footer'>
<Button variant="warning" >
전체조회
</Button>
<Button variant="success" onClick={() => navigate('/reple/boardwrite')}>
글쓰기
</Button>
</div>
</div>
</div>
{/* ========================== [[ 글쓰기 Modal ]] ========================== */}
{/* ========================== [[ 글쓰기 Modal ]] ========================== */}
<BlogFooter />
</>
)
}
export default RepleBoardPage
<qna리스트 출력 - RepleBoardList.jsx>
import React, { useEffect, useState } from 'react'
import { qnaListDB } from '../../service/dbLogic'
import RepleBoardRow from './RepleBoardRow'
// 고려사항 - 상위 컴포넌트에서 하위 컴포넌트로만 props 전달이 가능한 점
// 초급 - 가급적 상위 컴포넌트에 두는 것을 추천
// 중급 - 하위 컴포넌트에서 일어난 상태 변화를 상위 컴포넌트에 반영
// 리렌더링되는 3가지 - 부모컴포넌트, state, props
// useEffect, useMemo(값 메모이제이션), useCallback(함수 메모이제이션) -> 의존성배열을 가짐
// 의존성배열 [] - 맨 처음 딱 한 번 호출
// 의존성배열 없음 - 코딩할때마다 호출, 무한루프
// 의존성배열 [값] - 의존성배열에 입력한 변수가 바뀔때마다 호출 - 다중처리(주의: 전역변수만 가능)
const RepleBoardList = () => {
const [boards, setBoards] = useState([{}])
const [board, setBoard] = useState({
cb_gubun: "qna_title",
keyword: "PT10회권 양도",
})
// useEffect의 의존성 배열을 통해서 렌더링 시점을 제어할 수 있다
useEffect(()=>{
const qnaList = async() => { // 비동기처리
const res = await qnaListDB(board) // async가 있을때 await 사용 가능
console.log(res.data)
setBoards(res.data)
}
qnaList() // 호출
},[board])
return (
<>
{boards.map((board) => (
<RepleBoardRow board={board} />
))}
</>
) // 리렌더링 조건을 수렴할때마다 return()그룹이 재호출됨
}
export default RepleBoardList
<qna리스트 출력 - RepleBoardRow.jsx>
import React from 'react'
const RepleBoardRow = ({board}) => {
return (
<>
<tr key={board.QNA_BNO}>
<td>{board.QNA_BNO}</td>
<td>{board.QNA_TITLE}</td>
<td>{board.MEM_NAME}</td>
<td>{board.QNA_DATE}</td>
<td>{board.QNA_HIT}</td>
</tr>
</>
)
}
export default RepleBoardRow
<qna리스트 출력 - dbLogic.js>
import axios from "axios";
export const qnaListDB = (board) => {
return new Promise((resolve, reject) => {
try {
console.log(board)
// axios 비동기요청 처리(ajax - fetch[브라우저, 클라이언트사이드] - axios[NodeJS-오라클 서버연동, 서버사이드])
const response = axios({ // 3000번 서버에서 8000서버로 요청함 - 네트워크(다른서버-CORS이슈, 지연발생)
method: "get",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaList",
params: board,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const qnaInsertDB = (board) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaInsert",
headers: {
"Content-Type": "multipart/form-data",
},
data: board,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const uploadImageDB = (file) => {
console.log(file);
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/imageUpload",
headers: {
"Content-Type": "multipart/form-data",
},
processData: false,
contentType: false,
data: file,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const uploadFileDB = (file) => {
console.log(file);
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/fileUpload",
headers: {
"Content-Type": "multipart/form-data",
},
processData: false,
contentType: false,
data: file,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const memberListDB = (member) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "member/memberList",
params: member,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const memberInsertDB = (member) => {
return new Promise((resolve, reject) => {
console.log(member)
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "member/memberInsert",
data: member, // post방식 data
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const memberUpdateDB = (member) => {
return new Promise((resolve, reject) => {
console.log(member)
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "member/memberUpdate",
data: member, // post방식 data
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const memberDeleteDB = (member) => {
return new Promise((resolve, reject) => {
console.log(member)
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "member/memberDelete",
params: member,
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const deptInsertDB = (dept) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "dept/deptInsert",
data: dept, // post방식 data
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const deptUpdateDB = (dept) => {
return new Promise((resolve, reject) => {
console.log(dept)
try {
const response = axios({
method: "post", // @RequestBody
url: process.env.REACT_APP_SPRING_IP + "dept/deptUpdate",
data: dept, // post방식 data
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const deptDeleteDB = (dept) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "dept/deptDelete",
params: dept,
});
resolve(response); // 요청 처리 성공했을 때
} catch (error) {
reject(error); // 요청 처리 실패했을 때
}
});
};
export const deptListDB = (dept) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "dept/deptList",
params: dept, // 쿼리스트링은 header에 담김 - get방식 params
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
<qna리스트 출력 - RepleBoardController.java>
package com.example.demo.controller;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.example.demo.logic.RepleBoardLogic;
import com.google.gson.Gson;
@RestController
@RequestMapping("/reple/*")
public class RepleBoardController {
Logger logger = LogManager.getLogger(RepleBoardController.class);
@Autowired
private RepleBoardLogic repleBoardLogic = null;
@GetMapping("qnaList")
public String qnaList(@RequestParam Map<String, Object> pMap) {
logger.info("qnaList 호출");
List<Map<String, Object>> bList = null;
bList = repleBoardLogic.qnaList(pMap);
Gson g = new Gson();
String temp = g.toJson(bList);
return temp;
}
@PostMapping("qnaInsert")
public String qnaInsert(@RequestBody Map<String, Object> pMap) {
logger.info("qnaInsert 호출"); // 해당 메소드 호출 여부 찍어보기
logger.info(pMap); // 넘어온 파라미터값 찍어보기
int result = 0;
result = repleBoardLogic.qnaInsert(pMap);
return String.valueOf(result);
}
@PostMapping("imageUpload")
public Object imageUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="image", required=false) MultipartFile image) {
logger.info("imageUpload 호출");
// 사용자가 선택한 파일 이름 담기
String filename = null;
if(!image.isEmpty()) {
filename = image.getOriginalFilename();
String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
String fullPath = saveFolder + "\\\\" + filename;
try {
// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
File file = new File(fullPath);
byte[] bytes = image.getBytes();
// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bos.write(bytes);
// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
bos.close();
} catch (Exception e) {
}
}
// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
String temp = filename;
return temp;
}
@PostMapping("fileUpload")
public Object fileUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="file_name", required=false) MultipartFile file_name) {
logger.info("fileUpload 호출");
// 사용자가 선택한 파일 이름 담기
String filename = null;
if(!file_name.isEmpty()) {
filename = file_name.getOriginalFilename();
String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
String fullPath = saveFolder + "\\\\" + filename;
try {
// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
File file = new File(fullPath);
byte[] bytes = file_name.getBytes();
// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bos.write(bytes);
// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
bos.close();
} catch (Exception e) {
}
}
// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
String temp = filename;
return temp;
}
@GetMapping("imageGet")
public Object imageGet(HttpServletRequest req, HttpServletResponse res) {
// imageName 정보는 공통코드로 제공된 QuillEditor.jsx에서 파라미터로 넘어오는 값임
// imageUpload 메소드에서는 업로드된 파일 정보(파일명, 파일크기)가 리턴됨
String b_file = req.getParameter("imageName"); // get방식으로 넘어옴
logger.info("imageGet 호출 성공===>" + b_file); // XXX.png
// 톰캣 서버의 물리적인 위지 정보
String filePath = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
String fname = b_file;
logger.info("b_file: 8->euc" + b_file);
// File은 내용까지 복제되는 것은 아니고 파일명만 객체화해주는 클래스이다
File file = new File(filePath, b_file.trim());
// 실제 업로드된 파일에 대한 마임타입을 출력해줌
String mimeType = req.getServletContext().getMimeType(file.toString());
logger.info(mimeType); // image, video, text
if (mimeType == null) { // 마임타입이 널이면 아래의 속성값으로 마임타입을 설정
// -> 브라우저는 해석이 가능한 마임타입은 페이지 로딩 처리,
// 해석이 불가능한 마임타입은 다운로드함
// 강제로 다운로드 처리를 위한 마임타입 변경
// -> 브라우저에서 해석가능한 마임타입의 경우 화면에 그대로 출력되니까 그걸 방지하기위해
res.setContentType("application/octet-stream");
}
// 다운로드되는 파일 이름 담기
String downName = null;
// 위 File 객체에서 생성된 객체에 내용을 읽기위한 클래스 선언
FileInputStream fis = null;
// 응답으로 나갈 정보가 웹 서비스에 처리되어야 하기에 사용한 객체
ServletOutputStream sos = null;
try {
if (req.getHeader("user-agent").indexOf("MSIE") == -1) {
downName = new String(b_file.getBytes("UTF-8"), "8859_1");
} else {
downName = new String(b_file.getBytes("EUC-KR"), "8859_1");
}
// 응답 헤더에 다운로드 될 파읿명을 매핑하기
res.setHeader("Content-Disposition", "attachment;filename=" + downName);
// 위에서 생성된 파일 문자열 객체를 가지고 파일생성에 필요한 객체의 파라미터 넘김
fis = new FileInputStream(file);
sos = res.getOutputStream();
// 파일 내용을 담을 byte배열을 생성
byte b[] = new byte[1024 * 10];
int data = 0;
while ((data = (fis.read(b, 0, b.length))) != -1) {
// 파일에서 읽은 내용을 가지고 실제 파일에 쓰기 처리함
// 여기서 처리된 값은 브라우저를 통해서 내보내진다
sos.write(b, 0, data);
}
// 처리한 내용이 버퍼에 있는데 이것을 모두 처리요청하기
// 내보내고 버퍼를 비운다 - 버퍼는 크기가 작음(휘발성)
sos.flush();
} catch (Exception e) {
logger.info(e.toString());
} finally {
try {
if (sos != null)
sos.close();
if (fis != null)
fis.close();
} catch (Exception e2) {
// TODO: handle exception
}
}
// byte[] fileArray = boardLogic.imageDownload(imageName);
// logger.info(fileArray.length);
return null;
}// end of imageGet
}
<qna리스트 출력 - RepleBoardLogic.java>
package com.example.demo.logic;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.dao.RepleBoardDao;
@Service
public class RepleBoardLogic {
Logger logger = LogManager.getLogger(RepleBoardLogic.class);
@Autowired
private RepleBoardDao repleBoardDao = null;
public List<Map<String, Object>> qnaList(Map<String, Object> pMap) {
logger.info("qnaList 호출");
List<Map<String, Object>> bList = null;
bList = repleBoardDao.qnaList(pMap);
return bList;
}
public int qnaInsert(Map<String, Object> pMap) {
logger.info("qnaInsert 호출");
int result = 0;
result = repleBoardDao.qnaInsert(pMap);
return result;
}
}
<qna리스트 출력 - RepleBoardDao.java>
package com.example.demo.dao;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class RepleBoardDao {
Logger logger = LogManager.getLogger(RepleBoardDao.class);
@Autowired
private SqlSessionTemplate sqlSessionTemplate = null;
public List<Map<String, Object>> qnaList(Map<String, Object> pMap) {
logger.info("qnaList 호출");
List<Map<String, Object>> bList = null;
bList = sqlSessionTemplate.selectList("qnaList", pMap);
return bList;
}
public int qnaInsert(Map<String, Object> pMap) {
logger.info("qnaInsert 호출");
int result = 0;
// insert는 반환값이 object여서 update를 사용함
result = sqlSessionTemplate.update("qnaInsert", pMap);
return result;
}
}
<qna리스트 출력 - repleboard.xml>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo">
<!-- 게시글 조회 -->
<select id="qnaList" parameterType="map" resultType="map">
SELECT q.qna_bno
, q.qna_title
, q.qna_content
, q.qna_type
, q.qna_hit
, q.qna_date
, m.mem_name
FROM qna q, member230324 m
WHERE q.mem_no = m.mem_no
</select>
<!--
@RequestParam - Map타입이 올 수 있다 - GET방식 요청 - 요청 header에 담긴다 - 인터셉트 가능성(캐시에 있는 정보가 다시 출력될 가능성)
문제점: URL에 노출됨 - 보안에 취약 - 조회할때 사용
@RequestBody - POST방식 요청 - 단위테스트 불가함 - postman사용해 테스트 가능 - 요청 body에 담긴다 - 인터셉트 불가(무조건 서버로 전달)
VO Map 원시형타입 모두 가능
qna_type 질문 타입은 상수로 양도를 줌
qna_secret은 비밀번호를 입력받음
비밀번호가 null이면 공개, null이 아니면 비공개처리
생각해볼 문제
mem_no는 어디서 가져오고 인증인 어디서 하는가? - App.jsx의 useEffect가
화면에서 가져올 컬럼의 종류는 몇가지인가?
세션이나 쿠키 또는 세션스토리지에서 가져와야하는 컬럼이 있는가?
상수로 넣을 수 있는 (또는 넣어야하는) 컬럼이 존재하는가?
만약 존재한다면 어떤 컬럼인가?
작성자는 입력받도록 화면을 그려야 하는가 혹은 자동으로 결정될 수 있는가?
-->
<!-- 게시글 입력 -->
<insert id="qnaInsert" parameterType="map">
INSERT INTO QNA(
qna_bno
, mem_no
, qna_title
, qna_content
, qna_type
, qna_secret
, qna_hit
, qna_date
)
VALUES(
qna_seq.nextval
, #{mem_no}
, #{qna_title}
, #{qna_content}
, '양도'
, #{qna_secret}
, #{qna_hit}
, to_char(sysdate, 'YYYY-MM-DD')
)
</insert>
<!-- 게시글 수정 -->
<update id="qnaUpdate" parameterType="map">
UPDATE dept
SET dname = #{dname}
,loc = #{loc}
<if test="filename != null">
,filename = #{filename}
</if>
<if test="fileurl != null">
,fileurl = #{fileurl}
</if>
<where>
<if test='deptno!=null and deptno > 0'>
AND deptno = #{deptno}
</if>
</where>
</update>
<!-- 게시글 삭제 -->
<delete id="qnaDelete" parameterType="int">
DELETE from dept
<where>
<if test='value!=null and value > 0'>
AND deptno = #{value}
</if>
</where>
</delete>
</mapper>
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day88 (0) | 2023.04.04 |
---|---|
국비 지원 개발자 과정_Day87 (0) | 2023.04.03 |
국비 지원 개발자 과정_Day85 (0) | 2023.03.30 |
국비 지원 개발자 과정_Day84 (0) | 2023.03.29 |
국비 지원 개발자 과정_Day83 (1) | 2023.03.28 |
댓글