FILE_NO 시퀀스
BOARD_TYPE - id:’qna’ id=’review’ id=’default’
FILE_PATH - fullPath
FILE_NAME - man.png
FILE_SIZE = 12.34mb
MASTER_BNO - 현재는 필요 없음
QNA_BNO - qnaInsert 결정됨 - 이미지 업로드 시에는 반영되지 않음
파일 업로드시에 insert문 처리하고
글쓰기 저장 시에 update문으로 처리한다
imageUpload - pds에 이미지 업로드 - 컨트롤러에만 있음 - 로직과 DAO에는 없다(메소드 선언이 안되어있다)
fileUpload - 파일 업로드
imageGet - 이미지 불러오기 - QuillEditor
언제 이미지 insert처리를 해야 할까?
→ 이미지 업로드할 때!
어떤 이미지가 몇 장이 선택되었는지는 언제 결정되는가?
→ QuillEdiotr에서 이미지를 선택할 때마다 파일명이 결정됨
→ 이미지를 선택하자마자 이미지 업로드가 진행됨
→ 글쓰기에서 저장버튼을 누른 시점이 아님!
이미지가 업로드될 때 qna_bno를 채번 할 수 없다면 언제 채번할 수 있는가?
→ 글쓰기 버튼을 눌러 qnaInsert를 했을 때 imageUpload처리 fileInsert
→ 단 qna_bno를 결정할 수 없다(아직 글쓰기 버튼을 누르지 않았기에)
→ qna_bno는 qnaInsert 할 때 반환값으로 가져와서 mblog_file에 업데이트해준다
<Quill 이미지 저장과 글쓰기 - KhQnAListPage.jsx>
import React from "react";
import { useCallback } from "react";
import { useEffect } from "react";
import { useState } from "react";
import { Table } from "react-bootstrap";
import { useLocation, useNavigate } from "react-router-dom";
import { qnaListDB } from "../../service/dbLogic";
import BlogFooter from "../include/BlogFooter";
import BlogHeader from "../include/BlogHeader";
import MyPagination from "../MyPagination";
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from "../styles/FormStyle";
import MyFilter from "./KhMyFilter";
import SearchBar from "./KhSearchBar";
const KhQnAListPage = ({ authLogic }) => {
// 페이징 처리시에 현재 내가 바라보는 페이지 정보 담기
let page = 1;
// 화면전환시 필요한 훅
const navigate = useNavigate();
// url주소에 한글있을때 사용
const search = decodeURIComponent(useLocation().search);
// 오라클 서버에서 받아온 정보를 담기
// {} - 객체리터럴 - 자바로따지면 클래스
const [listBody, setListBody] = useState([]); // 배열타입 [{}, {}, {}]
// qna_type 구분 상수값 - 라벨
const [types] = useState(["전체", "일반", "결제", "양도", "회원", "수업"]);
// qna_type 상태관리위해 선언
const [tTitle, setTTitle] = useState("전체");
// 함수 메모이제이션해줌(useCallback) -> useMamo는 값을 메모이제이션
const handleTTitle = useCallback((element) => {
// 파라미터로 받은 값을 저장 - tTitle
setTTitle(element);
}, []); // 의존배열이 비었으므로 한 번 메모이제이션된 함수값을 계속 기억해둠
// 일반함수로 정의하는 것과 useEffect에서 정의하는 것의 차이
// async가 있고 없고는 고려대상이 아니다 - 동기화에대한 문제는 별개
useEffect(() => {
const qnaList = async () => {
//콤보박스 내용 -> 제목, 내용, 작성자 중 하나
//사용자가 입력한 키워드
// <http://localhost:3000/qna/list?page=1&qna_type=수업>
const qna_type = search
.split("&")
.filter((item) => {
return item.match("qna_type");
})[0]
?.split("=")[1];
console.log(qna_type); // '수업' 저장됨
setTTitle(qna_type || "전체"); // 쿼리스트링이 없으면 그냥 전체가 담김
// <http://localhost:3000/qna/list?condition=제목|내용|작성자&content=입력값>
// [0] ?condition=제목|내용|작성자
// [1] content=입력값
const condition = search.split("&").filter((item) => {return item.match("condition")})[0]?.split("=")[1];
const content = search.split("&").filter((item) => {return item.match("content")})[0]?.split("=")[1];
const board = { // get방식 조건검색 - params속성에 들어갈 변수
page: page,
qna_type: qna_type,
condition: condition,
content: content
};
const res = await qnaListDB(board);
console.log(res.data);
const list = [];
const datas = res.data;
datas.forEach((item, index) => {
console.log(item);
const obj = {
qna_bno: item.QNA_BNO,
qna_type: item.QNA_TYPE,
qna_title: item.QNA_TITLE,
mem_name: item.MEM_NAME,
mem_no: item.MEM_NO,
qna_date: item.QNA_DATE,
qna_hit: item.QNA_HIT,
qna_secret: JSON.parse(item.QNA_SECRET), // 문자열 "false"가 실제 불리언 false가 됨
file: item.FILE_NAME,
comment: item.COMM_NO
};
list.push(obj);
});
// 데이터셋의 변화에따라 리렌더링할 것과 기존의 DOM을 그냥 출력하는 것 - 비교알고리즘
setListBody(list); // listBodt[1] - 일반변수로 선언하는 것과 훅을 선언하는 것의 차이점
};
/* return () => {
alert('return')
} */
qnaList();
}, [setListBody, setTTitle, page, search]);
//listItemsElements 클릭이벤트 처리시 사용
const getAuth = (listItem) => {
console.log(listItem);
console.log(listItem.qna_secret)
if(listItem.qna_secret === false) {
navigate(`/qna/detail?qna_bno=${listItem.qna_bno}`)
} else {
console.log('권한이 없습니다.')
}
};
const listHeaders = ["글번호", "분류", "제목", "작성자", "등록일", "조회수"];
const HeaderWd = ["8%", "8%", "50%", "12%", "12%", "10%"];
// listbody는 상태훅이다
const listHeadersElements = listHeaders.map((listHeader, index) =>
listHeader === "제목" ? (
<th key={index} style={{ width: HeaderWd[index], paddingLeft: "40px" }}>
{listHeader}
</th>
) : (
<th key={index} style={{ width: HeaderWd[index], textAlign: "center" }}>
{listHeader}
</th>
)
);
const listItemsElements = listBody.map((listItem, index) => {
console.log(listItem);
return (
<tr
key={index}
onClick={() => {
getAuth(listItem);
}}
>
{Object.keys(listItem).map((key, index) =>
key === "secret" ||
key === "no" ||
key === "file" ||
key === "comment" ? null : key === "date" ? (
<td key={index} style={{ fontSize: "15px", textAlign: "center" }}>
{listItem[key]}
</td>
) : key === "qna_title" ? (
<td key={index}>
{isNaN(listItem.file) && (
<span>
<i
style={{ width: "15px", height: "15px" }}
className={"fas fa-file-lines"}
></i>
</span>
)}
{!isNaN(listItem.file) && (
<span>
<i
style={{ width: "15px", height: "15px" }}
className={"fas fa-image"}
></i>
</span>
)}
{listItem[key]}
{listItem.comment ? (
<span style={{ fontWeight: "bold" }}>
[답변완료]
</span>
) : (
<span> [미답변]</span>
)}
{listItem.qna_secret && (
<span>
<i className="fas fa-lock"></i>
</span>
)}
</td>
) : (
<td key={index} style={{ textAlign: "center" }}>
{listItem[key]}
</td>
)
)}
</tr>
);
});
return (
<>
<BlogHeader authLogic={authLogic} />
<ContainerDiv>
<HeaderDiv>
<h3 style={{ marginLeft: "10px" }}>QnA 게시판</h3>
</HeaderDiv>
<FormDiv>
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
height: "40px",
}}
>
<MyFilter
types={types}
type={true}
id={"qna_type"}
title={tTitle}
handleTTitle={handleTTitle}
/>
{sessionStorage.getItem("auth") === "teacher" && (
<BButton
style={{ width: "80px", height: "38px" }}
onClick={() => {
navigate(`/qna/write`);
}}
>
글쓰기
</BButton>
)}
</div>
<Table responsive hover style={{ minWidth: "800px" }}>
<thead>
<tr>{listHeadersElements}</tr>
</thead>
<tbody>{listItemsElements}</tbody>
</Table>
</div>
<div
style={{
margin: "10px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<MyPagination page={page} path={"/qna/list"} />
<SearchBar />
</div>
</FormDiv>
</ContainerDiv>
<BlogFooter />
</>
);
};
export default KhQnAListPage;
<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(file)
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.data)
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 이미지 저장과 글쓰기 - KhQnAWriteForm.jsx>
import React, { useEffect } from 'react'
import { useCallback } from 'react';
import { useRef } from 'react';
import { useState } from 'react';
import { Form } from 'react-bootstrap';
import { useDispatch } from 'react-redux';
import { Navigate, useNavigate } from 'react-router-dom';
import { qnaInsertDB } from '../../service/dbLogic';
import BlogFooter from '../include/BlogFooter';
import BlogHeader from '../include/BlogHeader';
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle';
import MyFilter from './KhMyFilter';
import QuillEditor from './QuillEditor';
import RepleBoardFileInsert from './RepleBoardFileInsert';
const KhQnAWriteForm = ({authLogic}) => { // props로 넘어온 값 즉시 구조분해할당
const navigate = useNavigate()
const[title, setTitle]= useState(''); // 제목
const[content, setContent]= useState(''); // 내용작성
const[secret, setSecret]= useState(false); // 비밀글
const[tTitle, setTTitle]= useState('일반'); // qna_type
const[types]= useState(['일반','결제','양도','회원','수업']); // qna_type의 라벨값
const[files, setFiles]= useState([]); // 파일처리
const quillRef = useRef();
const handleContent = useCallback((value) => {
console.log(value);
setContent(value);
},[]);
const handleFiles = useCallback((value) => {
console.log(value);
setFiles([...files, value]); // 깊은복사
},[files]);
const handleTitle = useCallback((e) => {
setTitle(e);
},[]);
const handleTTitle = useCallback((e) => {
setTTitle(e);
},[]);
const qnaInsert = async() => {
console.log('qnaInsert');
console.log(secret) // true, 0이 아닌 것 모두
console.log(typeof secret) // boolean타입 출력
console.log(files)
const board = {
qna_title: title,
qna_content: content,
qna_secret: (secret ? "true" : "false"),
qna_type: tTitle,
mem_no: sessionStorage.getItem('no'),
fileNames: files
} // 사용자가 입력한 값 넘기기 - @RequestBody로 처리됨
const res = await qnaInsertDB(board)
console.log(res)
// window.location.replace('qna/list?page=1')
navigate('/qna/list')
}
return (
<>
<BlogHeader authLogic={authLogic} />
<ContainerDiv>
<HeaderDiv>
<h3>QNA 글작성</h3>
</HeaderDiv>
<FormDiv>
<div style={{width:"100%", maxWidth:"2000px"}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'10px'}}>
<h2>제목</h2>
<div style={{display: 'flex'}}>
<div style={{display: 'flex', flexDirection: 'column', marginRight:'10px', alignItems: 'center'}}>
<span style={{fontSize: '14px'}}>비밀글</span>
<Form.Check type="switch" id="custom-switch" style={{paddingLeft: '46px'}}
onClick={()=>{setSecret(!secret)}}/>
</div>
<MyFilter title={tTitle} types={types} handleTTitle={handleTTitle}></MyFilter>
<BButton style={{marginLeft:'10px'}}onClick={()=>{qnaInsert()}}>글쓰기</BButton>
</div>
</div>
<input id="dataset-title" type="text" maxLength="50" placeholder="제목을 입력하세요."
style={{width:"100%",height:'40px' , border:'1px solid lightGray'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
<hr style={{margin:'10px 0px 10px 0px'}}/>
<h3>상세내용</h3>
<QuillEditor value={content} handleContent={handleContent} quillRef={quillRef} files={files} handleFiles={handleFiles}/>
<RepleBoardFileInsert files={files}/>
</div>
</FormDiv>
</ContainerDiv>
<BlogFooter />
</>
);
};
export default KhQnAWriteForm;
<Quill 이미지 저장과 글쓰기 - RepleBoardHeader.jsx>
import React from 'react'
import { useNavigate } from 'react-router-dom';
import { qnaDeleteDB } from '../../service/dbLogic';
import { BButton } from '../styles/FormStyle';
const RepleBoardHeader = ({detail, bno}) => {
console.log(detail);
console.log(bno);
const navigate = useNavigate();
const boardDelete = async() => {
const board = {
qna_bno: bno
}
const res = await qnaDeleteDB(board)
console.log(res.data)
navigate('/qna/list')
}
const qnaList = () => {
navigate('/qna/list')
}
return (
<div>
<div style={{display: 'flex', flexDirection: 'column', width: '100%'}}>
<div style={{display: 'flex', justifyContent:"space-between"}}>
<div style={{overflow: "auto"}}>
<span style={{marginBottom:'15px', fontSize: "30px", display:"block"}}>
{detail.QNA_TITLE}
</span>
</div>
{
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<BButton style={{margin:'0px 10px 0px 10px'}} onClick={()=>{navigate(`/qna/update/${bno}`)}}>
수정
</BButton>
<BButton style={{margin:'0px 10px 0px 10px'}} onClick={()=>{boardDelete()}}>
삭제
</BButton>
<BButton style={{margin:'0px 10px 0px 10px'}} onClick={qnaList}>
목록
</BButton>
</div>
}
</div>
<div style={{display: 'flex', justifyContent: 'space-between', fontSize: '14px'}}>
<div style={{display: 'flex', flexDirection: 'column'}}>
<span>작성자 : {detail.MEM_NAME}</span>
<span>작성일 : {detail.QNA_DATE}</span>
</div>
<div style={{display: 'flex', flexDirection: 'column', marginRight:'10px'}}>
<div style={{display: 'flex'}}>
<span style={{marginRight:'5px'}}>조회수 :</span>
<div style={{display: 'flex', justifyContent: 'flex-end', width:'30px'}}>{detail.QNA_HIT}</div>
</div>
<div style={{display: 'flex'}}>
{detail.COMMENT?<>
<span style={{marginRight:'5px'}}>댓글수 :</span>
<div style={{display: 'flex', justifyContent: 'flex-end', width:'30px'}}>{detail.COMMENT}</div>
</>:<></>}
</div>
</div>
</div>
</div>
<hr style={{height: '2px'}}/>
</div>
)
}
export default RepleBoardHeader
<Quill 이미지 저장과 글쓰기 - KhQnADetailPage.jsx>
import React from 'react'
import { useEffect } from 'react';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle';
import RepleBoardHeader from './RepleBoardHeader';
import { qnaListDB } from '../../service/dbLogic';
import BlogHeader from '../include/BlogHeader';
import BlogFooter from '../include/BlogFooter';
const KhQnADetailPage = ({authLogic}) => {
const search = window.location.search;
console.log(search);
const page = search.split('&').filter((item)=>{return item.match('page')})[0]?.split('=')[1];
console.log(page);
const bno = search.split('&').filter((item)=>{return item.match('bno')})[0]?.split('=')[1];
console.log(bno);
const [detail, setDetail] = useState({});
const[files, setFiles]= useState([]);
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
const boardDetail = async() => {
const board = {
qna_bno : bno
}
// 상세보기 페이지에서는 첨부파일이 있는 경우에 fileList 호출해야함
// qnaListDB에서는 qma_bno를 결정지을 수 없음
const res = await qnaListDB(board);
console.log(res.data);//빈배열만 출력됨
//shift는 배열에서 첫 번째 요소를 제거하고, 제거된 요소를 반환합니다
const bTemp = res.data.shift();
console.log(bTemp);
console.log(bTemp);
console.log(bTemp.QNA_TITLE);
console.log(bTemp.QNA_CONTENT);
console.log(bTemp.MEM_NAME);
console.log(bTemp.MEM_NO);
console.log(bTemp.QNA_DATE);
console.log(bTemp.QNA_HIT);
console.log(JSON.parse(bTemp.QNA_SECRET));
if(JSON.parse(bTemp.QNA_SECRET)){
if(sessionStorage.getItem('auth')!=='3'&&sessionStorage.getItem('no')!==JSON.stringify(bTemp.MEM_NO)) {
//navigate(`/qna/list?page=1`);
//return dispatch(setToastMsg("권한이 없습니다."));
}
}
setDetail({
QNA_TITLE : bTemp.QNA_TITLE,
QNA_CONTENT : bTemp.QNA_CONTENT,
MEM_NAME : bTemp.MEM_NAME,
MEM_NO : bTemp.MEM_NO,
QNA_DATE : bTemp.QNA_DATE,
QNA_HIT : bTemp.QNA_HIT,
QNA_SECRET : JSON.parse(bTemp.QNA_SECRET),
QNA_TYPE : bTemp.QNA_TYPE,
});
}
boardDetail();
},[setDetail, setFiles , bno, dispatch, navigate])
return (
<>
<BlogHeader authLogic={authLogic}/>
<ContainerDiv>
<HeaderDiv>
<h3 style={{marginLeft:"10px"}}>QnA 게시글</h3>
</HeaderDiv>
<FormDiv>
<RepleBoardHeader detail={detail} bno={bno}/>
<section style={{minHeight: '400px'}}>
<div dangerouslySetInnerHTML={{__html:detail.QNA_CONTENT}}></div>
</section>
<hr style={{height:"2px"}}/>
</FormDiv>
</ContainerDiv>
<BlogFooter />
</>
);
};
export default KhQnADetailPage;
<Quill 이미지 저장과 글쓰기 - dbLogic.js>
import axios from "axios";
export const qnaListDB = (board) => {
console.log(board)
return new Promise((resolve, reject) => {
try {
// axios 비동기요청 처리(ajax - fetch[브라우저, 클라이언트사이드] - axios[NodeJS-오라클 서버연동, 서버사이드])
const response = axios({ // 3000번 서버에서 8000서버로 요청함 - 네트워크(다른서버-CORS이슈, 지연발생)
method: "get",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaList",
params: board, // 스프링 부트와 연동시 @RequestParam 사용
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const qnaInsertDB = (board) => {
console.log(board)
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaInsert",
data: board,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const qnaUpdateDB = (board) => {
// 대소문자 구분 어떻게 할 것인지 - 파라미터 소문자, 리턴값 대문자 혹은 둘다 대문자
console.log(board) // 사용자가 입력한 값 확인
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaUpdate",
data: board,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const qnaDeleteDB = (board) => {
console.log(board)
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaDelete",
params: 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, // 스프링 부트와 연동시 @RequestBody 사용
});
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);
}
});
};
<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;
// http://localhost:8000/reple/qnaList?content=질문&condition=제목
// http://localhost:8000/reple/qnaList?qna_type=매매
@GetMapping("qnaList")
public String qnaList(@RequestParam Map<String, Object> pMap) {
logger.info("qnaList 호출");
logger.info(pMap);
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 타입으로 변경하지 않으면 부적합한 열유형 111에러메시지
// Map, List: Object 주의할 것 - 부적합한 열유형 setNull(111)
// 클라이언트에서 가져오는 no값이 세션스토리지에서 꺼낸 값이기에 int로 변환해줌
if(pMap.get("mem_no") != null) {
// NumberFormatException 방어코드(값에 null이 들어가지 않도록!)
int mem_no = Integer.parseInt(pMap.get("mem_no").toString());
pMap.put("mem_no", mem_no);
}
int result = 0;
result = repleBoardLogic.qnaInsert(pMap);
return String.valueOf(result);
}
@GetMapping("qnaDelete")
public String qnaDelete(@RequestParam Map<String, Object> pMap) {
logger.info("qnaDelete 호출"); // 해당 메소드 호출 여부 찍어보기
logger.info(pMap);
if(pMap.get("qna_bno") != null) {
int qna_bno = Integer.parseInt(pMap.get("qna_bno").toString());
pMap.put("qna_bno", qna_bno);
}
int result = 0;
result = repleBoardLogic.qnaDelete(pMap);
return String.valueOf(result);
}
// QuillEditor에서 선택한 이미지를 mblog_file테이블에 insert하기
// -> myBatis에서 insert태그의 역할이 있다 - 채번한 숫자를 캐쉬에 담아준다
// 그런데 select가 아니라서 resultType을 사용할 수 없다!
// resultType은 불가하고 있는건 parameterType뿐이다 - 매개변수에 값을 담아준다
// 파라미터에 값을 담는다는 컨셉: TestParam.java -> HashMapBinder설계
@PostMapping("imageUpload")
public Object imageUpload(@RequestParam(value="image", required=false) MultipartFile image) {
logger.info("imageUpload 호출");
String filename = repleBoardLogic.imageUpload(image);
return filename;
}
@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
}
<Quill 이미지 저장과 글쓰기 - RepleBoardLogic.java>
package com.example.demo.logic;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
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 org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
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 호출");
// 여기서 result는 insert 성공유무를 나타내는 숫자(1성공, 0실패)가 아니라
// 글 등록시에 채번된 시퀀스 반환 값이다 -> qna_bno를 mblog_file에 업데이트 해주기위해
int result = 0;
result = repleBoardDao.qnaInsert(pMap); // qna_bno 넘어옴
// 위에서 돌려받은 시퀀스 값(qna_bno)를 pMap에 담아준다
pMap.put("qna_bno", result);
// Quill에서 선택한 이미지가 있는 경우
if(pMap.get("fileNames") != null) {
// 작성자가 선택한 이미지의 개수가 3개까지 올 수 있다
// -> 이미지 개수만큼, 3개에대한 업데이트가 3번 일어나야한다
// -> xml에서 forEach list로 받기에 해당 부분 처리가 필요함
result = repleBoardDao.fileUpdate(fileNames(pMap));
}
return result;
}
public int qnaDelete(Map<String, Object> pMap) {
logger.info("qnaDelete 호출");
int result = 0;
int fileDel = repleBoardDao.fileDelete(pMap);
if (fileDel > 0) {
result = repleBoardDao.qnaDelete(pMap);
}
return result;
}
private List<Map<String, Object>> fileNames(Map<String, Object> pMap) {
logger.info("fileNames");
List<Map<String, Object>> pList = new ArrayList<>();
// pMap.get("fileNames") 리턴형태는 배열 - ["man1.png", "man2.png"]
HashMap<String, Object> fMap = null;
String[] fileNames = pMap.get("fileNames").toString().substring(1, pMap.get("fileNames").toString().length()-1).split(",");
for(int i=0; i<fileNames.length; i++) {
fMap = new HashMap<>();
fMap.put("file_name", fileNames[i]);
fMap.put("qna_bno", pMap.get("qna_bno"));
pList.add(fMap);
}
return pList;
}
public String imageUpload(MultipartFile image) {
logger.info("imageUpload 호출");
// 이미지 업로드가 된 파일에대한 file_name, file_size, file_path 등을 결정해줌 - 서비스계층
Map<String, Object> pMap = new HashMap<>();
// 사용자가 선택한 파일 이름 담기
String filename = null;
String fullPath = null;
double d_size = 0.0;
if(!image.isEmpty()) {
// filename = image.getOriginalFilename();
// 같은 파일명으로 업로드되는 경우 덮어쓰기 되는 것을 방지하고자
// 오리지널 파일명 앞에 날짜와 시간 정보를 활용하여 절대 같은 이름이 발생하지 않도록 처리한다
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHMmmss");
Calendar time = Calendar.getInstance();
filename = sdf.format(time.getTime()) + "-" + image.getOriginalFilename().replaceAll(" ", "-");
String saveFolder = "D:\\workspace_sts\\mblog-1\\src\\main\\webapp\\pds";
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();
// 여기까지는 이미지 파일 쓰기 처리
// 아래부터는 mblog_file 테이블에 insert될 정보를 초기화해줌
d_size = Math.floor(file.length()/(1024.0)*10)/10;
pMap.put("file_name", filename);
pMap.put("file_size", d_size);
pMap.put("file_path", fullPath);
logger.info(d_size);
int result = repleBoardDao.fileInsert(pMap);
logger.info(result);
logger.info(filename);
logger.info(fullPath);
} catch (Exception e) {
e.printStackTrace();
logger.info(e.toString());
}
}
// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
String temp = filename;
return temp;
}
}
<Quill 이미지 저장과 글쓰기 - 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; // 입력이 성공했는지 유무를 담는 변수선언
int qna_bno = 0; // insert시에 시퀀스로 채번된 속성을 담을 변수 - qna_bno의 값
// insert는 반환값이 object
result = sqlSessionTemplate.insert("qnaInsert", pMap);
if(result == 1) {
if(pMap.get("qna_bno") != null) {
qna_bno = Integer.parseInt(pMap.get("qna_bno").toString());
}
}
logger.info("result => " + result);
logger.info("userGeneratedKeys 프로퍼티 속성값 => " + qna_bno);
return qna_bno;
}
public int qnaDelete(Map<String, Object> pMap) {
int result = 0;
result = sqlSessionTemplate.update("qnaDelete", pMap);
return result;
}
public int fileDelete(Map<String, Object> pMap) {
int result = 0;
result = sqlSessionTemplate.update("fileDelete", pMap);
return result;
}
public int fileInsert(Map<String, Object> pMap) {
logger.info("fileInsert 호출");
int result = 0;
result = sqlSessionTemplate.insert("fileInsert", pMap);
return result;
}
public int fileUpdate(List<Map<String, Object>> pList) {
logger.info("fileUpdate 호출");
logger.info(pList);
int result = 0;
result = sqlSessionTemplate.update("fileUpdate", pList);
return result;
}
}
<Quill 이미지 저장과 글쓰기 - 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
, q.qna_secret
, m.mem_name
, m.mem_no
FROM qna q, member230324 m
WHERE q.mem_no = m.mem_no
<!-- KhMyFilter 조건 검색시 사용 -->
<if test='qna_type != null and !qna_type.equals("전체")'>
AND qna_type = #{qna_type}
</if>
<!-- 회원고유번호별 조건검색시 사용 -->
<if test="mem_no != null">
AND m.mem_no = #{mem_no}
</if>
<!-- 글번호 조건검색시 사용 -->
<if test="qna_bno != null">
AND q.qna_bno = #{qna_bno}
</if>
<if test="content != null">
<choose>
<when test='condition != null and condition.equals("제목")'>
AND qna_title LIKE '%'||#{content}||'%'
</when>
<when test='condition != null and condition.equals("내용")'>
AND qna_content LIKE '%'||#{content}||'%'
</when>
<when test='condition != null and condition.equals("작성자")'>
AND mem_name LIKE '%'||#{content}||'%'
</when>
</choose>
</if>
ORDER BY q.qna_bno DESC
</select>
<!--
@RequestParam - Map타입이 올 수 있다 - GET방식 요청 - 요청 header에 담긴다 - 인터셉트 가능성(캐시에 있는 정보가 다시 출력될 가능성)
문제점: URL에 노출됨 - 보안에 취약 - 조회할때 사용
@RequestBody - POST방식 요청 - 단위테스트 불가함 - postman사용해 테스트 가능 - 요청 body에 담긴다 - 인터셉트 불가(무조건 서버로 전달)
VO Map 원시형타입 모두 가능
qna_type 질문 타입은 상수로 양도를 줌
qna_secret은 비밀번호를 입력받음
비밀번호가 null이면 공개, null이 아니면 비공개처리
생각해볼 문제
mem_no는 어디서 가져오고 인증인 어디서 하는가? - App.jsx의 useEffect가
화면에서 가져올 컬럼의 종류는 몇가지인가?
세션이나 쿠키 또는 세션스토리지에서 가져와야하는 컬럼이 있는가?
상수로 넣을 수 있는 (또는 넣어야하는) 컬럼이 존재하는가?
만약 존재한다면 어떤 컬럼인가?
작성자는 입력받도록 화면을 그려야 하는가 혹은 자동으로 결정될 수 있는가?
-->
<!-- 게시글 입력 - insert할때 qna_bno의 값이 파라미터에 저장됨 -->
<insert id="qnaInsert" parameterType="map" useGeneratedKeys="true" keyColumn="qna_bno" keyProperty="qna_bno">
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_type}
, #{qna_secret}
, 0
, 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 qna
WHERE qna_bno = #{qna_bno}
</delete>
<!-- 이미지 삭제 -->
<delete id="fileDelete" parameterType="int">
DELETE FROM mblog_file
WHERE qna_bno = #{qna_bno}
</delete>
<!-- ========== 첨부파일 가져오기 ========== -->
<select id="fileList" parameterType="map" resultType="map">
SELECT file_name, file_size
FROM mblog_file
WHERE qna_bno = #{qna_bno}
</select>
<!-- ========== 첨부파일 추가하기 ========== -->
<insert id="fileInsert" parameterType="map">
INSERT INTO mblog_file(file_no
, file_path
, file_name
, file_size
)
VALUES (mblog_file_seq.nextval
, #{file_path}
, #{file_name}
, #{file_size}
)
</insert>
<!-- ========== 첨부파일 추가하기 ========== -->
<update id="fileUpdate" parameterType="list">
<foreach collection="list" item="item" separator=";" open="DECLARE BEGIN" close="; END;">
UPDATE MBLOG_FILE
SET board_type = 'qna'
, qna_bno = #{item.qna_bno}
WHERE file_name = LTRIM(#{item.file_name})
</foreach>
</update>
</mapper>
<Quill 이미지 저장과 글쓰기 - member.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="getToday" resultType="string" parameterType="string">
SELECT
to_char(sysdate, 'YYYY-MM-DD') FROM dual
</select>
<select id="login" parameterType="map" resultType="string">
select mem_name from member230324
<where>
<if test='mem_uid!=null and mem_uid.length()>0'>
AND mem_uid = #{mem_uid}
</if>
<if test='mem_pw!=null and mem_pw.length()>0'>
AND mem_pw = #{mem_pw}
</if>
</where>
</select>
<!-- 회원 목록 - 닉네임 중복확인 -->
<select id="memberList" parameterType="map" resultType="map">
select mem_uid, mem_name, mem_email
, mem_no, mem_nickname, mem_status, mem_auth
from member230324
<where>
<if test='mem_uid!=null and mem_uid.length()>0'>
AND mem_uid = #{mem_uid}
</if>
<!--
<input type=text id="mem_nickname" value="" />
항상 무조건 빈문자열이다 - 폼전송하면 무조건 빈문자열이 있는 상태이다
아무것도 입력하지 않아도 null에 걸리지 않는다
잡아내려면 문자열>0까지 비교해야한다
-->
<if test='MEM_NICKNAME!=null and MEM_NICKNAME.length()>0'>
AND mem_nickname = #{MEM_NICKNAME}
</if>
<if test='mem_name != null and mem_name.length()>0'>
AND mem_name = #{mem_name}
</if>
<if test='mem_tel != null and mem_tel.length()>0'>
AND mem_tel = #{mem_tel}
</if>
</where>
</select>
<!-- 회원 정보 입력 -->
<insert id="memberInsert" parameterType="map">
INSERT INTO member230324(
mem_no
,mem_uid
,mem_pw
,mem_name
,mem_nickname
,mem_email
,mem_tel
,mem_gender
,mem_birthday
,mem_zipcode
,mem_addr
,mem_addr_dtl
,mem_auth
,mem_status
)
VALUES (seq_member_no.nextval
<if test="MEM_UID != null">
,#{MEM_UID}
</if>
<if test="MEM_PW != null">
,#{MEM_PW}
</if>
<if test="MEM_NAME != null">
,#{MEM_NAME}
</if>
<if test="MEM_NICKNAME != null">
,#{MEM_NICKNAME}
</if>
<if test="MEM_EMAIL != null">
,#{MEM_EMAIL}
</if>
<if test="MEM_TEL != null">
,#{MEM_TEL}
</if>
<if test="MEM_GENDER != null">
,#{MEM_GENDER}
</if>
<if test="MEM_BIRTHDAY != null">
,#{MEM_BIRTHDAY}
</if>
<if test="MEM_ZIPCODE != null">
,#{MEM_ZIPCODE}
</if>
<if test="MEM_ADDR != null">
,#{MEM_ADDR}
</if>
<if test="MEM_ADDR_DTL != null">
,#{MEM_ADDR_DTL}
</if>
<if test="MEM_AUTH != null">
,#{MEM_AUTH}
</if>
<if test="MEM_STATUS != null">
,#{MEM_STATUS}
</if>
)
</insert>
</mapper>
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day90 (0) | 2023.04.06 |
---|---|
국비 지원 개발자 과정_Day89 (0) | 2023.04.05 |
국비 지원 개발자 과정_Day87 (0) | 2023.04.03 |
국비 지원 개발자 과정_Day86 (0) | 2023.03.31 |
국비 지원 개발자 과정_Day85 (0) | 2023.03.30 |
댓글