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

국비 지원 개발자 과정_Day87

by 루팽 2023. 4. 3.

스프링부트와 리액트 연동하기

  Front-end(부트스트랩, vue.js, react.js)와 Back-end(spring) 섞어 쓰기

 

JSP와 리액트의 공통점

  화면과 모델계층 사이에서 이어주는 역할(전달자)

 

  JSP

    → @Controller사용 → 페이지 이동처리 - 뷰솔루션 사용 시에는 필요 없는 부분

 

  리액트

    → @RestController - plain/test, 문자열 - 화면출력x(sendRedirect, forward대상이 아니다), 페이지이동은 없다

    useNavigate() 훅을 사용하여 화면 전환을 한다

    하나의 브라우저 세션[JSessionID - 쿠키에 저장 - 문자열]에서 처리된다

 

매개변수 - 객체주입 - 스프링 컨테이너 제공

  Model, ModelMap - 뷰계층

     @Controller일때만 필요하고 @RestController에선 필요 없기에 리액트를 사용하는 경우 배제

 

  @RequestParam

    VO는 사용 불가, String(첨부파일 이름), Map 가능

    → 리액트에선 params속성 사용

 

  @RequestBody

    VO, Map 가능

    → 리액트에선 data속성 사용

 

  원시형 타입은 파라미터에 정의하는 대로 스프링 부트에서 자동으로 담아준다

 

첨부파일 MultpartFile

  리액트에서 Post방식으로 아래의 코드와 같이 요청하는데, 표준 요청 객체로 사용자가 입력한 값을 읽을 수 없다

  그렇기에 스프링에서 요청을 받을 때 MultipartHttpServletRequest를 사용해야 한다

headers: {
          "Content-Type": "multipart/form-data",
        }

 

댓글형 게시판 - 리뷰게시판, 공지사항, QnA(복잡도 높음)

  전체, 일반, 결제, 양도, 회원, 수업 - qna_type - 게시글 목록처리 - 필터처리 - MyFilter.jsx 공통관리 - 재사용성 높음

 

조건검색기 - 리뷰게시판, 공지사항, QnA 공통기능 - SearchBar.jsx

 

게시판 구분 - id

  id: review 0 리뷰게시판

  id: qna - qna게시판

  id: notice - 공지사항 게시판

 

페이징처리

  각자 처리해 보기(분리해 보는 연습) - MyPagination.jsx - 리뷰게시판, 공지사항, QnA

 

인증과 인가

  인증 - 해당 사용자가 맞는지

  인가 - 어떤 접근 권한을 가지는지

 

화면처리

  상세 보기에서 하단에 댓글을 처리하는 형태

 

테이블 설계

  회원관리 - member230324 - mem_auth: 1이면 member, 2이면 teacher → 세션스토리지에 관리

 

유지의 문제

  쿠키

  세션

  로컬스토리지 - 세션아이디가 변해도 계속 유지

  세션스토리지 - 세션아이디가 변하면 유지되지 않음 → auth관리해야 함

 

QnA게시판 예시 - qna

댓글처리 - qna_comment

첨부파일처리 - mblof_file 테이블 설계(첨부파일이 필요한 게시판 정하기)

 

비밀글일 때와 아닐 때 처리

  qna_secret컬럼의 값을 true와 false로 저장

  화면에서 비밀글 여부 처리를 스위치 버튼으로 함

 

라우트 설정

{/*
	목록보기 &page=2 - 현재 내가 바라보는 페이지 번호
	조건검색 처리기 &condition=qna_title&content=키워드값 
		content 검색 키워드 값 / condition 제목, 내용, 작성자
	const search = window.location.search -> url 전체정보 검색
	search.split('&'_.filter((item) => return item.match('page')))[0]?.split('=')[1]
	0: ?page=1
	1: content=키워드
*/}
<Route path='/qna/list' exact={true} element={<KhQnAListPage/>} />

{/* 목록보기 &page=2 - 현재 내가 바라보는 페이지 번호 */}
<Route path='/qna/detail/*' element={<KhQnADetailPage />} />

<Route path='/qna/write' exact={true} element={<KhQnAWriteForm />} />

{/* 해시값 이용해 처리 - useParams() 훅 사용 */}
<Route path='/qna/update/:bno' element={<KhQnAUpdatePage />} />

 

dbLogic.js

  테이블

    qna

    qna_comment

    mblog_file: 첨부파일 - 이미지 여러 장

 

  목록보기일 때와 상세 보기일 때(조건 검색 포함)

    qnaListDB - GET방식 - @RequestParam

 

  글쓰기

    qnaInsertDB - POST방식 - @RequestBody

 

  글 수정

    qnaUpdateDB - POST방식 - @RequestBody

 

  글 삭제

    qnaDeleteDB - GET방식 - @RequestParam

 

<qna 조건검색, 정렬, 비밀글 - App.jsx>

import { Route, Routes, useNavigate } from 'react-router-dom';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
import HomePage from './components/page/HomePage';
import DeptPage from './components/page/DeptPage';
import DeptDetail from './components/dept/DeptDetail';
import RepleBoardPage from './components/page/RepleBoardPage';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';
import SignupPage from './components/auth/SignupPage';
import KhLoginPage from './components/auth/KhLoginPage';
import { onAuthChange } from './service/authLogic';
import { memberListDB } from './service/dbLogic';
import EmailVerifiedPage from './components/auth/EmailVerifiedPage';
import FindEmailPage from './components/auth/FindEmailPage';
import ResetPwdPage from './components/auth/ResetPwdPage';
import RepleBoardWriteForm from './components/repleboard/RepleBoardWriteForm';
import RepleBoardDetail from './components/repleboard/RepleBoardDetail';
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 />} />
          <Route path='/home' exact={true} element={<HomePage />} />
          <Route path='/auth/signup' exact={true} element={<SignupPage authLogic={authLogic} />} />
          <Route path='/auth/emailVerified' exact={true} element={<EmailVerifiedPage authLogic={authLogic} />} />
          <Route path='/auth/findEmail' exact={true} element={<FindEmailPage />} />
          <Route path='/auth/resetPwd' exact={true} element={<ResetPwdPage authLogic={authLogic} />} />
          <Route path='/reple/board' exact={true} element={<RepleBoardPage />} />
          <Route path='/reple/boarddetail/*' element={<RepleBoardDetail />} />
          <Route path='/reple/boardwrite' exact={true} element={<RepleBoardWriteForm />} />
          <Route path='/qna/list' exact={true} element={<KhQnAListPage />} />
          <Route path='/qna/detail/*' element={<KhQnADetailPage />} />
          <Route path='/qna/write' exact={true} element={<KhQnAWriteForm authLogic={authLogic} />} />
          <Route path='/qna/update/:bno' element={<KhQnAUpdatePage />} />
          <Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} />} />
          {/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
          <Route path='/deptdetail/:deptno' element={<DeptDetail imageUploader={imageUploader} />} />
          <Route path='/auth/kakao/callback' exact={true} element={<KakaoRedirectHandler />} />
          <Route path="/member" exact={true} element={<MemberPage imageUploader={imageUploader} />} />
          <Route path="/profile" exact={true} element={<Profile />} />
        </Routes>
      </div>
    </>
  );
}

export default App;

 

<qna 조건검색, 정렬, 비밀글 - 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(() => {
    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: item.QNA_SECRET,
        };
        list.push(obj);
      });
      setListBody(list);
    };
    qnaList();
  }, [setListBody, setTTitle, page, search]);

  //listItemsElements 클릭이벤트 처리시 사용
  const getAuth = (listItem) => {
    console.log(listItem);
  };

  const listHeaders = ["글번호", "분류", "제목", "작성자", "등록일", "조회수"];
  const HeaderWd = ["8%", "8%", "50%", "12%", "12%", "10%"];

  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 === "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>
              )}
              &nbsp;&nbsp;{listItem[key]}
              {listItem.comment ? (
                <span style={{ fontWeight: "bold" }}>
                  &nbsp;&nbsp;[답변완료]
                </span>
              ) : (
                <span>&nbsp;&nbsp;[미답변]</span>
              )}
              {listItem.secret && (
                <span>
                  &nbsp;&nbsp;<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;

 

<qna 조건검색, 정렬, 비밀글 - KhMyfilter.jsx>

import React from 'react';
import { Dropdown, DropdownButton } from 'react-bootstrap';
import { useLocation, useNavigate } from 'react-router-dom';


const KhMyFilter = ({types, type, id, title, handleTTitle}) => {
  console.log(id);//qna_type
  const navigate = useNavigate()
  const location = useLocation()

  const setPath = (oldItem, newItem, key) => {
    console.log(location.pathname)
    console.log(oldItem)
    console.log(newItem)
    console.log(key)
    let path= location.pathname+'?'+key+'='+newItem;
    return path;
  }

  return (
    <DropdownButton variant="" title={title} style={{border: '1px solid lightgray', borderRadius:'5px', height:'38px'}}>
      { 
        types.map((element, index)=>(
          <Dropdown.Item as="button" key={index} onClick={()=>{
            if(type){ 
              navigate(setPath(title,element,id)); 
            }
            handleTTitle(element); 
          }}>
            {element}
          </Dropdown.Item>
        )) 
      }
    </DropdownButton>
  );
};

export default KhMyFilter;

 

<qna 조건검색, 정렬, 비밀글 - KhSearchBar.jsx>

import React, { useCallback, useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { BButton } from "../styles/FormStyle";
import MyFilter from "./KhMyFilter";

const KhSearchBar = () => {
  const [content, setContent] = useState("");
  const [types] = useState(["제목", "내용", "작성자"]);
  const location = useLocation();
  const search = decodeURIComponent(location.search);
  console.log(search);
  const navigate = useNavigate();

  const [tTitle, setTTitle] = useState("제목");

  const handleTTitle = useCallback((e) => {
    console.log(e) // 사용자가 선택한 콤보박스명 제목,내용,작성자
    setTTitle(e);
  }, []);

  useEffect(() => {
    console.log("effect");
    search.split('&').forEach((item) => {
      console.log(item)
      // 요청 url에 담긴 condition 정보를 setTTitle에 초기화
      if(item.match('condition')) {
        setTTitle(item.split('=')[1])
      }
    })
  }, [search, setTTitle]); // 의존배열에 search를 사용했고 상태훅을 선택했으니 그 정보가 변경될때마다 호출

  const setPath = () => {
    console.log(tTitle, content)
    console.log(search)
    console.log(search.match('condition'))
    let path;
    // 앞에서 조회한 적이 있을 때 기존의 쿼리스트링 삭제 후 다시 만들어야함
    
    /*  if(search.match('condition')) {
      path = location.pathname + 
      search.replace(
        `&${search.split('&').filter((item)=>{return item.match('page')})}&${search.split('&').filter((item)=>{return item.match('content')})}`,
        `&condition=${tTitle}&content=${content}`
        ).replace(`&${search.split('&').filter((item)=>{return item.match('page')})}`,'&page=1&')
    } else {
      path = location.pathname + search + `?condition=${tTitle}&content=${content}`
    } */
    path = location.pathname + `?condition=${tTitle}&content=${content}`
    console.log(path);
    return path;
  };

  return (
    <div style={{ display: "flex", width: "100%", justifyContent: "center" }}>
      <MyFilter
        types={types}
        title={tTitle}
        id={"condition"}
        handleTTitle={handleTTitle}
      />
      <input
        type="text"
        value={content}
        style={{
          maxWidth: "600px",
          width: "40%",
          height: "40px",
          margin: "0px 10px 0px 10px",
          border: "1px solid lightgray",
          borderRadius: "10px",
        }}
        onChange={(e) => {
          setContent(e.target.value);
        }}
      />
      <BButton
        style={{ width: "70px", height: "40px", marginRight: "10px" }}
        onClick={() => {
          navigate(setPath());
        }}
      >
        검색
      </BButton>
      <BButton
        style={{ width: "70px", height: "40px" }}
        onClick={() => {
          navigate(`/qna/list?page=1`);
          setContent("");
        }}
      >
        초기화
      </BButton>
    </div>
  );
};

export default KhSearchBar;

 

<qna 조건검색, 정렬, 비밀글 - KhQnAWriteForm.jsx>

import React 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) => {
    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타입 출력
    const board = {
      qna_title: title,
      qna_content: content,
      qna_secret: (secret ? "true" : "false"),
      qna_type: tTitle,
      mem_no: sessionStorage.getItem('no'),
    } // 사용자가 입력한 값 넘기기 - @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;

 

<qna 조건검색, 정렬, 비밀글 - 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);
    }
  });
};

 

<qna 조건검색, 정렬, 비밀글 - RepleBoardController.java>

package com.example.demo.controller;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import com.example.demo.logic.RepleBoardLogic;
import com.google.gson.Gson;

@RestController
@RequestMapping("/reple/*")
public class RepleBoardController {
	Logger logger = LogManager.getLogger(RepleBoardController.class);
	
	@Autowired
	private RepleBoardLogic repleBoardLogic = null;
	
	// <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 result = 0;
		result = repleBoardLogic.qnaInsert(pMap);
		return String.valueOf(result);
	}
	
	@PostMapping("imageUpload")
	public Object imageUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="image", required=false) MultipartFile image) {
		logger.info("imageUpload 호출");
		// 사용자가 선택한 파일 이름 담기
		String filename = null;
		if(!image.isEmpty()) {
			filename = image.getOriginalFilename();
			String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
			String fullPath = saveFolder + "\\\\" + filename;
			try {
				// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
				File file = new File(fullPath);
				byte[] bytes = image.getBytes();
				// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
				// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
				// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
				bos.write(bytes);
				// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
				bos.close();
			} catch (Exception e) {
				
			}
		}
		// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
		String temp = filename;
		return temp;
	}
	
	@PostMapping("fileUpload")
	public Object fileUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="file_name", required=false) MultipartFile file_name) {
		logger.info("fileUpload 호출");
		// 사용자가 선택한 파일 이름 담기
		String filename = null;
		if(!file_name.isEmpty()) {
			filename = file_name.getOriginalFilename();
			String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
			String fullPath = saveFolder + "\\\\" + filename;
			try {
				// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
				File file = new File(fullPath);
				byte[] bytes = file_name.getBytes();
				// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
				// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
				// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
				bos.write(bytes);
				// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
				bos.close();
			} catch (Exception e) {
				
			}
		}
		// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
		String temp = filename;
		return temp;
	}
	
	@GetMapping("imageGet")
	public Object imageGet(HttpServletRequest req, HttpServletResponse res) {
		// imageName 정보는 공통코드로 제공된 QuillEditor.jsx에서 파라미터로 넘어오는 값임
		// imageUpload 메소드에서는 업로드된 파일 정보(파일명, 파일크기)가 리턴됨
		String b_file = req.getParameter("imageName"); // get방식으로 넘어옴
		logger.info("imageGet 호출 성공===>" + b_file); // XXX.png
		// 톰캣 서버의 물리적인 위지 정보
		String filePath = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
		String fname = b_file;
		logger.info("b_file: 8->euc" + b_file);
		// File은 내용까지 복제되는 것은 아니고 파일명만 객체화해주는 클래스이다
		File file = new File(filePath, b_file.trim());
		// 실제 업로드된 파일에 대한 마임타입을 출력해줌
		String mimeType = req.getServletContext().getMimeType(file.toString());
		logger.info(mimeType); // image, video, text
		if (mimeType == null) { // 마임타입이 널이면 아래의 속성값으로 마임타입을 설정
			// -> 브라우저는 해석이 가능한 마임타입은 페이지 로딩 처리,
			// 		해석이 불가능한 마임타입은 다운로드함
			// 강제로 다운로드 처리를 위한 마임타입 변경
			// -> 브라우저에서 해석가능한 마임타입의 경우 화면에 그대로 출력되니까 그걸 방지하기위해
			res.setContentType("application/octet-stream");
		}
		// 다운로드되는 파일 이름 담기
		String downName = null;
		// 위 File 객체에서 생성된 객체에 내용을 읽기위한 클래스 선언
		FileInputStream fis = null;
		// 응답으로 나갈 정보가 웹 서비스에 처리되어야 하기에 사용한 객체
		ServletOutputStream sos = null;
		try {
			if (req.getHeader("user-agent").indexOf("MSIE") == -1) {
				downName = new String(b_file.getBytes("UTF-8"), "8859_1");
			} else {
				downName = new String(b_file.getBytes("EUC-KR"), "8859_1");
			}
			// 응답 헤더에 다운로드 될 파읿명을 매핑하기
			res.setHeader("Content-Disposition", "attachment;filename=" + downName);
			// 위에서 생성된 파일 문자열 객체를 가지고 파일생성에 필요한 객체의 파라미터 넘김
			fis = new FileInputStream(file);
			sos = res.getOutputStream();
			// 파일 내용을 담을 byte배열을 생성
			byte b[] = new byte[1024 * 10];
			int data = 0;
			while ((data = (fis.read(b, 0, b.length))) != -1) {
				// 파일에서 읽은 내용을 가지고 실제 파일에 쓰기 처리함
				// 여기서 처리된 값은 브라우저를 통해서 내보내진다
				sos.write(b, 0, data);
			}
			// 처리한 내용이 버퍼에 있는데 이것을 모두 처리요청하기
			// 내보내고 버퍼를 비운다 - 버퍼는 크기가 작음(휘발성)
			sos.flush();
		} catch (Exception e) {
			logger.info(e.toString());
		} finally {
			try {
				if (sos != null)
					sos.close();
				if (fis != null)
					fis.close();
			} catch (Exception e2) {
				// TODO: handle exception
			}
		}
		// byte[] fileArray = boardLogic.imageDownload(imageName);
		// logger.info(fileArray.length);
		return null;
	}// end of imageGet
}

 

<qna 조건검색, 정렬, 비밀글 - 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 id="qnaInsert" parameterType="map">
		INSERT INTO QNA(
		      qna_bno
		    , mem_no
		    , qna_title
		    , qna_content
		    , qna_type
		    , qna_secret
		    , qna_hit
		    , qna_date
		    )
		    VALUES(
		      qna_seq.nextval
		    , #{mem_no}
		    , #{qna_title}
		    , #{qna_content}
		    , #{qna_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 dept
	 	<where>
	 		<if test='value!=null and value > 0'>
				AND deptno = #{value}
			</if>
	 	</where>
	</delete>
</mapper>

 

<qna 조건검색, 정렬, 비밀글 - 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>

댓글