본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day89

by 루팽 2023. 4. 5.

dbLogic.js → qnaListDB → Back-end → spring boot

댓글형 게시판 - 여러 유형의 게시판 고려해 보기

id: notice, qna, tranfer

qna_type

 

전체목록 - n건 - 댓글테이블 고려대상 아니다

상세 보기 - 1건 - 댓글처리 고민 - QNA_COMMENT(select, insert, delete)

상세보기 조회 시에는 추가적으로 qna_comment를 붙여서 조회해야 한다

 

KhQnaListPage - 부모

  RepleBoardFileDetail - 자식

 

BlogHeader→ authLogic props로 필요

RepleBoardHeader → 수정, 삭제, 목록버튼

DetailPage → 상세정보 [{}]

FileDetail → 첨부사진 리스트

댓글처리(컴포넌트 따로 뺴지않고 Detail에 추가)

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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';
import KhQnAUpdatePage from './components/repleboard/KhQnAUpdatePage';
import KhQnAListPage from './components/repleboard/KhQnAListPage';
import KhQnADetailPage from './components/repleboard/KhQnADetailPage';
import KhQnAWriteForm from './components/repleboard/KhQnAWriteForm';

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 authLogic={authLogic} />}  />
          <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 authLogic={authLogic} />} />
          <Route path='/reple/boarddetail/*' element={<RepleBoardDetail />} />
          <Route path='/reple/boardwrite' exact={true} element={<RepleBoardWriteForm />} />
          <Route path='/qna/list' exact={true} element={<KhQnAListPage authLogic={authLogic} />}   />
          <Route path='/qna/detail/*' element={<KhQnADetailPage authLogic={authLogic} />} />
          <Route path='/qna/write' exact={true} element={<KhQnAWriteForm authLogic={authLogic} />} />
          <Route path='/qna/update/:bno' element={<KhQnAUpdatePage authLogic={authLogic}  />} />
          <Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} authLogic={authLogic} />} />
          {/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - 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;

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - BlogHeader.jsx>

import React, { useEffect, useState } from 'react'
import { Button, Container, Nav, Navbar } from 'react-bootstrap'
import { Link, useNavigate } from 'react-router-dom'
import { logout } from '../../service/authLogic'

const BlogHeader = ({authLogic}) => {
  const navigate = useNavigate()
  const auth = authLogic.getUserAuth()
  // 로그아웃 버튼을 추가하기
  // 일반변수가 아닌 상태훅에 담는 이유 -> 상태훅에 관리하면 화면에 즉시 반영되기에
  // 인증(front-end -> sessionStorage, back-end -> session:cpu cache메모리)과 인가를 구분
  // 동기화 처리 필요함 - 여려움
  const [email, setEmail] = useState()
  // 의존성 배열이란? - 실행문(변수선언, 제어문, 로직)이 재요청되는 기준
  // 빈 배열일땐 딱 한 번 호출됨
  // 빈 배열을 삭제하면(아무것도 넣지 않으면) 글자 하나만 입력해도 재요청이 일어남 - 비효율적
  // 리렌더링이 발생하는 경우 - 1)상태훅 2)props 3)부모컴포넌트가 변경됨 -> return 코드 블럭이 호출됨
  useEffect(() => {
    setEmail(sessionStorage.getItem("email"))
    // return; -> 
    // 인터셉트해서 뭔가 전처리가 필요할 때
    // return () => {
    // }
  }, []) // 의존성 배열 - useMemo, useCallback(메모이제이션-효율성)

  return (

    <>
      <Navbar bg="light" variant="light">
        <Container>
          <Link to="/" className='nav-link'>TerrGYM</Link>
          <Nav className="me-auto">
            <Link to="/" className='nav-link'>Home</Link>
            <Link to="/dept/0" className='nav-link'>부서관리</Link>
            <Link to="/reple/board" className='nav-link'>게시판</Link>
            <Link to="/qna/list" className='nav-link'>QnA</Link>
          </Nav>
          {/* js와 jsx 섞어쓰기 */}
          {/* null, undefined 조심 */}
          {email && <Button variant='primary' onClick={()=>{logout(auth); navigate('/login'); window.location.reload();}}>Logout</Button>}
        </Container>
      </Navbar>
    </>
  )
}

export default BlogHeader

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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, QnACommentArea } from '../styles/FormStyle';
import RepleBoardHeader from './RepleBoardHeader';
import { qnaDetailDB, qnaListDB } from '../../service/dbLogic';
import BlogHeader from '../include/BlogHeader';
import BlogFooter from '../include/BlogFooter';
import RepleBoardFileDetail from './RepleBoardFileDetail';
import { Button } from 'react-bootstrap';

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 qnaDetail = async() => {
      const board = {
        qna_bno : bno
      }
      // 상세보기 페이지에서는 첨부파일이 있는 경우에 fileList 호출해야함
      // qnaListDB에서는 qma_bno를 결정지을 수 없음
      const res = await qnaDetailDB(board);
      console.log(res.data);//빈배열만 출력됨
      const temp = JSON.stringify(res.data)
      const jsonDoc = JSON.parse(temp)
      console.log(jsonDoc[0]);
      console.log(jsonDoc[0].QNA_TITLE);
      console.log(jsonDoc[0].QNA_CONTENT);
      console.log(jsonDoc[0].MEM_NAME);
      console.log(jsonDoc[0].MEM_NO);
      console.log(jsonDoc[0].QNA_DATE);
      console.log(jsonDoc[0].QNA_HIT);
      console.log(JSON.parse(jsonDoc[0].QNA_SECRET));
      console.log(jsonDoc[0].QNA_TYPE);
      if(JSON.parse(jsonDoc[0].QNA_SECRET)){
        if(sessionStorage.getItem('auth')!=='3'&&sessionStorage.getItem('no')!==JSON.stringify(jsonDoc[0].MEM_NO)) {
          //navigate(`/qna/list?page=1`);
          //return dispatch(setToastMsg("권한이 없습니다.")); 
        }
      }
      // 이미지 파일 담을 배열 선언
      const list = []
      if(jsonDoc.length > 1) {
        for(let i=1; i<jsonDoc.length; i++) {
          const obj = {
            FILE_NAME: jsonDoc[i].FILE_NAME
          }
          list.push(obj)
        }
      }
      setFiles(list)
      setDetail({
        QNA_TITLE : jsonDoc[0].QNA_TITLE,
        QNA_CONTENT : jsonDoc[0].QNA_CONTENT,
        MEM_NAME : jsonDoc[0].MEM_NAME,
        MEM_NO : jsonDoc[0].MEM_NO,
        QNA_DATE : jsonDoc[0].QNA_DATE,
        QNA_HIT : jsonDoc[0].QNA_HIT,
        QNA_SECRET : JSON.parse(jsonDoc[0].QNA_SECRET),
        QNA_TYPE : jsonDoc[0].QNA_TYPE,
      });
    }
    qnaDetail();
  },[setDetail, setFiles , bno, dispatch, navigate])

  const commentInsert = () => {
    console.log('답변');
  }

  const commentUpdate = () => {
    console.log('수정');
  }
  
  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>
          <RepleBoardFileDetail files={files} />
          <hr style={{height:"2px"}}/>
          <div>
            <div style={{display:"flex", justifyContent: "space-between", marginBottom: "10px" }}>
                <h2>답변 대기중&nbsp;</h2>
              <div style={{display:"flex"}}>
                <Button onClick={commentInsert}>답변</Button>
                &nbsp;
                <Button onClick={commentUpdate}>수정</Button>
              </div>
            </div>
              <QnACommentArea />
          </div>
        </FormDiv>
      </ContainerDiv>
      <BlogFooter />
    </>
  );
};

export default KhQnADetailPage;

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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?page=1')
  }
  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

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - RepleBoardFileDetail.jsx>

import React from 'react'
import axios from 'axios';
import styled from 'styled-components';

const Dspan = styled.span`
  padding: 2px 5px 2px 5px;
  font-size: 14px;
  cursor: pointer;
  &:hover {
    border-bottom: 1px solid gray;
  }
`
const RepleBoardFileDetail = ({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 key={index}>
            <Dspan type='text' id='fileUpload' key={index}
              onClick={()=>{ axios({
                method: 'GET',
                url: process.env.REACT_APP_SPRING_IP+`reple/imageDownload?imageName=${item.FILE_NAME}`,                 
                responseType: 'blob' // 가장 중요함
              }).then(response =>{      
                console.log(response.data);  
                const url = window.URL.createObjectURL(new Blob([response.data], 
                  { type: response.headers['content-type'] }));
                const link = document.createElement('a');
                link.href = url;
                //link.setAttribute('download', 'img.jpg');
                //선택한 파일명으로 다운로드 파일명이 결정됨
                link.setAttribute('download', `${item.FILE_NAME}`);
                document.body.appendChild(link);
                link.click();
              })}}
            >
              {item.FILE_NAME}
            </Dspan>
          </div>
        ))
      }
    </div>
  );
};

export default RepleBoardFileDetail;

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - KhQnAUpdatePage.jsx>

import React from 'react'
import { useRef } from 'react';
import { useEffect } from 'react';
import { useCallback } from 'react';
import { useState } from 'react';
import { Form } from 'react-bootstrap';
import { json, useNavigate, useParams } from 'react-router-dom';
import { qnaDetailDB, qnaUpdateDB } from '../../service/dbLogic';
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle';
import MyFilter from './KhMyFilter';
import QuillEditor from './QuillEditor';

const KhQnAUpdatePage = () => {
  const navigate = useNavigate()
  // 해시값, 쿼리스트링, props로 가져오기
  const {bno} = useParams() // 해시값으로 가져오기
  console.log(bno);
  const[title, setTitle]= useState('');
  const[content, setContent]= useState('');
  const[files, setFiles]= useState([]);
  const[secret, setSecret]= useState(false);
  const[tTitle, setTTitle]= useState('일반');
  const[types]= useState(['일반','결제','양도','회원','수업']);
  const quillRef = useRef();

  useEffect(() => {
    const qnaDetail = async() => {
      const board = {
        qna_bno : bno
      }
      //한 건 가져오기
      const res = await qnaDetailDB(board)
      console.log(res.data);
      const temp = JSON.stringify(res.data)
      const jsonDoc = JSON.parse(temp)
      console.log(jsonDoc[0]);
      setTitle(jsonDoc[0].QNA_TITLE)
      setContent(jsonDoc[0].QNA_CONTENT)
      // 아래에서 parse는 "true"를 boolean true로 변경해줌
      setSecret(JSON.parse(jsonDoc[0].QNA_SECRET))
      setTTitle(jsonDoc[0].QNA_TYPE)
      // 글 작성자가 아닌 경우
      if(jsonDoc[0].MEM_NO !== sessionStorage.getItem("no")) {
        return console.log("작성자가 아닙니다!")
      }
    }
    qnaDetail()
  },[bno]);
  

  const handleContent = useCallback((value) => {
    console.log(value);
    setContent(value);
  },[]);

  const handleFiles = useCallback((value) => {
    setFiles([...files, value]);
  },[files]);

  const handleTitle = useCallback((e) => {
    setTitle(e);
  },[]); 

  const handleTTitle = useCallback((e) => {
    setTTitle(e);
  },[]);

  const boardUpdate = async() => {
    if(title.trim() === '' || content.trim() === '') return console.log('게시글이 작성(수정)되지 않음');
    const board = {
      qna_bno: bno,
      qna_title: title, // useState 훅이다
      qna_content: content,
      qna_secret: (secret ? 'true' : 'false'),
      qna_type: tTitle,
      fileNames: files
    }
    const res = await qnaUpdateDB(board)
    if(!res.data) return console.log('게시판 수정 실패');
    navigate('/qna/list')
  }

  return (
    <>
      <ContainerDiv>
        <HeaderDiv>
          <h3 style={{marginLeft:"10px"}}>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" checked={secret===true?true:false} readOnly
                    style={{paddingLeft: '46px'}} onClick={()=>{setSecret(!secret)}}/>
                </div>
                <MyFilter types={types} id={"qna_type"} title={tTitle} handleTitle={handleTTitle}></MyFilter>
                <BButton style={{marginLeft: '10px'}} onClick={()=>{boardUpdate()}}>글수정</BButton>
              </div>
            </div>
            <input id="dataset-title" type="text" placeholder="제목을 입력하세요." defaultValue={title}
              style={{width:"100%",height:'40px' , border:'1px solid lightGray'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
            <hr />
            <h3 style={{textAlign:"left", marginBottom:'10px'}}>상세내용</h3>
            <QuillEditor value={content} handleContent={handleContent} quillRef={quillRef} files={files} handleFiles={handleFiles}/>
          </div>
        </FormDiv>
      </ContainerDiv>
    </>
  );
};

export default KhQnAUpdatePage

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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 qnaDetailDB = (board) => {
  console.log(board)
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "reple/qnaDetail",
        params: board,
      });
      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);
    }
  });
};

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
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.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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;
	}
	
	@GetMapping("qnaDetail")
	public String qnaDetail(@RequestParam Map<String, Object> pMap) {
		logger.info("qnaDetail 호출");
		logger.info(pMap);
		List<Map<String, Object>> bList = null;
		bList = repleBoardLogic.qnaDetail(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);
	}
	
	@PostMapping("qnaUpdate")
	public String qnaUpdate(@RequestBody Map<String, Object> pMap) {
		logger.info("qnaUpdate 호출"); // 해당 메소드 호출 여부 찍어보기
		logger.info(pMap);
		if(pMap.get("qna_bno") != null) {
			// NumberFormatException 방어코드(값에 null이 들어가지 않도록!)
			int qna_bno = Integer.parseInt(pMap.get("qna_bno").toString());
			pMap.put("qna_bno", qna_bno);
		}
		int result = 0;
		result = repleBoardLogic.qnaUpdate(pMap);
		return String.valueOf(result);
	}
	
	// 글삭제 - pk값만 받아오면 되니 일단 get으로 처리
	@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
	
	//이미지 다운로드 처리
	@GetMapping("imageDownload")
	public ResponseEntity<Resource> imageDownload(@RequestParam(value="imageName") String imageName) {
		logger.info("imageDownload 호출");
		String fileFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
		try {
			File file = new File(fileFolder, URLDecoder.decode(imageName, "UTF-8"));
			HttpHeaders header = new HttpHeaders();
			header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment:filename" + imageName);
			header.add("Cache-Control", "no-cache, no-store, must-revalidate");
			header.add("Pragma", "no-cache");
			header.add("Expires", "0");
			Path path = Paths.get(file.getAbsolutePath());
			// 이미지 리소스를 읽어서 담기
			ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
			return ResponseEntity.ok() // 200
					.headers(header) // 헤더 설정
					.contentLength(file.length()) // 파일 크기
					.contentType(MediaType.parseMediaType("application/octet-stream")) // 이미지를 브라우저가 로딩하지 못하게함(무조건 다운로드)
					.body(resource);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	} // end if imageDownload
}

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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 List<Map<String, Object>> qnaDetail(Map<String, Object> pMap) {
		logger.info("qnaDetail 호출");
		logger.info(pMap);
		int qna_bno = 0;
		// 조건절에 들어오는 number값 주의! -> int로 바꿔주기!!
		if(pMap.get("qna_bno") != null) {
			qna_bno = Integer.parseInt(pMap.get("qna_bno").toString());
			pMap.put("qna_bno", qna_bno);
		}
		List<Map<String, Object>> bList = null;
		bList = repleBoardDao.qnaDetail(pMap);
		logger.info(bList);
		// 조회 결과가 있을 경우 조회수 올리기
		if(bList.size() > 0) {
			repleBoardDao.qnaHit(pMap);
		}
		// 댓글 처리 추가
		// insert here
		// 이미지 파일이 있는경우
		if(bList != null & bList.size() == 1) {
			List<Map<String, Object>> fileList = repleBoardDao.fileList(pMap);
			if(fileList != null && fileList.size() > 0) {
				bList.addAll(fileList);
			}
		}
		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 qnaUpdate(Map<String, Object> pMap) {
		logger.info("qnaUpdate 호출");
		int result = 0;
		result = repleBoardDao.qnaUpdate(pMap);
		// 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;
		result = repleBoardDao.qnaDelete(pMap);			
		int fileDel = repleBoardDao.fileDelete(pMap);
		logger.info("파일삭제 => " + fileDel);
		int commDel = repleBoardDao.commDelete(pMap);
		logger.info("댓글삭제 => " + commDel);
		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;
	}
}

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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 List<Map<String, Object>> qnaDetail(Map<String, Object> pMap) {
		logger.info("qnaDetail 호출");
		List<Map<String, Object>> bList = null;
		bList = sqlSessionTemplate.selectList("qnaDetail", pMap);
		return bList;
	}
	
	public void qnaHit(Map<String, Object> pMap) {
		logger.info("qnaHit 호출");
		logger.info(pMap);
		pMap.put("id", "qna");
		int result = sqlSessionTemplate.update("qnaHit", pMap);
		logger.info(result);
	}

	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 qnaUpdate(Map<String, Object> pMap) {
		int result = 0;
		result = sqlSessionTemplate.update("qnaUpdate", pMap);
		return result;
	}
	
	public int qnaDelete(Map<String, Object> pMap) {
		int result = 0;
		result = sqlSessionTemplate.update("qnaDelete", pMap);
		return result;
	}
	
	public List<Map<String, Object>> fileList(Map<String, Object> pMap) {
		logger.info("fileList 호출");
		List<Map<String, Object>> fList = null;
		fList = sqlSessionTemplate.selectList("fileList", pMap);
		return fList;
	}
	
	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;
	}

	public int fileDelete(Map<String, Object> pMap) {
		logger.info("fileDelete 호출");
		int result = 0;
		result = sqlSessionTemplate.update("fileDelete", pMap);
		return result;
	}

	public int commDelete(Map<String, Object> pMap) {
		logger.info("commDelete 호출");
		int result = 0;
		result = sqlSessionTemplate.update("commDelete", pMap);
		return result;
	}
}

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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>
	
	<select id="qnaDetail" 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
			     , c.comm_no
			  FROM qna q, member230324 m,
			       (SELECT COUNT (comm_no) comm_no, qna_bno
			            FROM qna_comment GROUP BY qna_bno) c
			 WHERE q.mem_no = m.mem_no
			   AND q.qna_bno = c.qna_bno(+)
			<!-- 회원고유번호별 조건검색시 사용 -->
			<if test="mem_no != null">
				AND m.mem_no = #{mem_no}
			</if>
			<!-- 글번호 조건검색시 사용 -->
			<!-- setNull(1,1111)에러타입, 부적합한 열유형 - Integer.parseInt필요 -->
			<if test="qna_bno != null">
				AND q.qna_bno = #{qna_bno}
			</if>
	</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 QNA
			 SET
			 		qna_upddate = TO_CHAR(sysdate, 'YYYY-MM-DD HH24:MI:SS')
				<if test="qna_title != null">
			 		, qna_title = #{qna_title}
				</if>
		 		<if test="qna_content != null">
			 		, qna_content = #{qna_content}
				</if>
		 		<if test="qna_type != null">
					, qna_type = #{qna_type}
				</if>
				<if test="qna_secret != null">
					, qna_secret = #{qna_secret}
				</if>
			WHERE qna_bno = #{qna_bno}
	</update>
	
	<!--
		아래 쿼리문은 언제 어디서 왜 호출되는가? - 게시판을 클릭해서 조회수를 업데이트 해야할 때
		Controller Logic Dao가 디 필요한 경우,
		Controller Logic까지만 필요한 경우,
		Dao 만 필요한 경우가 있다 - 조회수 증가가 여기에 해당
		언제 조회수 증가 함수를 호출하는가? - qnaDetail을 호출할 때
	 -->
	<update id="qnaHit" parameterType="map">
		<choose>
			<when test='id.equals("notice")'>
				UPDATE notice SET not_hit = not_hit + 1
				WHERE not_bno = #{not_bno}
			</when>
			<when test='id.equals("qna")'>
				UPDATE qna SET qna_hit = qna_hit + 1
				WHERE qna_bno = #{qna_bno}
			</when>
			<when test='id.equals("review")'>
				UPDATE review SET rev_hit = rev_hit + 1
				WHERE rev_hit = #{rev_hit}
			</when>
		</choose>
	</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>
	
	<!-- 댓글 삭제 -->
	<delete id="commDelete" parameterType="int">
		DELETE FROM qna_comment
		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>

 

<게시판 상세보기, 첨부파일 다운, 글 수정 - 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>

댓글