본문 바로가기
학원수업/4월

04/03 국비학원 개발자과정 87회차- React와 Spring연결

by 코딩마스터^^ 2023. 4. 3.

서비스 게시판 생각해보기

 

페이징 처리 - 각자 처리 해보기- 분리해보기 연습

 

스프링 부트와 리액트 연동하기가 목표이다.

 

Front-end를 jsp로 하는경우 VS 리액트로 하는 경우

프론트엔드와 백앤드 연결해보기를 잘 해야한다.

부트 스트랩, Vew.js일수도 있기때문에 독립적으로 생각할 수 있어야한다.

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

           @RestController - > plain/text, 문자열 - 화면출력 - 페이지 이동은 없다

                                            sendRedirect, forward대상이 아니다.

 

둘이 구분해서 사용할수있니?

 

공통점

- 화면과 모델계층 사이에 이어주는 전달자

-페이지 이동 useNavigate()-화면전환 훅(함수형)

-하나의 브라우저 세션(JSessionID-쿠키-문자열)에서 처리가 된다.

 

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

Model, ModelMap

- 뷰계층, 리액트 사용하는 경우 배제 - @Controller일때만 필요함. 

-@RestController필요없음

 

@RequestParam(Get)

- VO는 사용불가, 첨부파일이름-string, Map

-React측에서는 params속성 사용

 

@RequestBody(Post)

- VO가능, Map가능

-React측 data속성 사용

 

원시형은....? 파라미터에 넣으면 다 담긴다. 스프링 부트에서 자동으로 담아준다.

 

html, easyUI, React

 

첨부파일

MultipartFile

post방식으로 처리

표준 요청객체로 사용자가 입력한 값을 읽을 수가 없다.

MultipartHttpServletRequest를 사용해야한다. 

스프링 부트에서 제공하는 요청 객체이다.

 

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

전체, 일반, 결제, 양도 , 회원, 수업 - qna_type으로 나눠보자

게시글 목록 처리 시 필터 처리가 필요하다.

Myfilter.jsx-공통관리 - 재사용성 높임 - 재사용 가능한 조각임

 

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

 

인증과 인가

mem_auth : 1이면 member 2이면 teacher 3이면 admin - 세션 스토리지 관리 해야되나? 된다 결정...

세션스토리지 안하면 어디서 가져오지? 생각해놔야한다.

세션과 쿠키에 무엇을 관리할것인지

 

유지의 문제

쿠키

세션

react.js, view.js, angular.js

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

세션스토리지 - 유지 안됨 - auth도 관리를 해야한다.

 

QnA게시판 예시 - qna

댓글처리 - qna_comment - 단순 CRUD - 직접해보기

첨부파일처리 - mblog_file 테이블 설계 고민 해보기 - 리뷰게시판 (필요한가? 아닌가?), QnA

 

 

비밀글일때와 아닐때 처리 - qna_secert 컬럼의 값을 true와 false로 저장할것

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

 

라우트 설정 - App.jsx

목록보기 - &page=2&condition=qna_title&content=키워드값(현재 내가 바라보는 페이지 번호)

조건 검색처리기

content - 검색키워드 값

condition - 제목, 내용, 작성자

<Route path='qna/list' exact={true} element={<KhQnAListPage />} />
 <Route path='qna/detail/*' element={<KhQnADetailPage />} />
<Route path='/qna/write' exact={true} element={<KhQnAWriteForm />} />
<Route path='/qna/update/:bno' exact={true} element={<KhQnAWriteForm />} />

해시값을 이용해서 처리할때는 useParams()훅을 사용

OR

필터를 사용해서 잘라올것인지 선택...

const search = window.location.search

search.split('&'_.filter((item)=>return item.match('page')))[0]?.split('=')[1]

0:?page=1

1: content=키워드

 

또다른 문제

dbLogic.js

qna

qna_comment

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

 

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

qnaListDB - Get방식 - @ResquestParam

 

글쓰기

qnaInsertDB - Post방식 - @RequestBody

 

글수정

qnaUpdateDB - Post방식 - @RequestBody

 

글삭제

qnaDeleteDB - Get방식 - @RequestParam

 

쿼리문 작성해서 등록하기

 

대소문자 구분하므로 반드시 맞춰 주어야 한다.

 

<?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="java.util.HashMap" resultType="map">
		SELECT
		q.qna_bno, q.qna_title, q.qna_content, q.qna_type,
		q.qna_hit, q.qna_date, m.mem_name, m.mem_no, q.qna_secret
		FROM qna q, MEMBER230324 m
		where q.mem_no = m.mem_no
		<if test="qna_type!=null and !qna_type.equals('전체')"><!-- KhMyFilter조건검색시 사용 -->
			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>
	</select>
	
	<!-- 
	
	@RequestParam-Map타입이 올 수 있다-get방식 요청 - 요청header담김-인터셉트-캐시에 있는 정보가 다시 출력될 수 있다.
	:문제점 :노출-URL-보안취약-조회
	@RequsetParam-type은 Map만 가능 vo안된다.
	
	@RequestBody- post방식 요청- 단위 테스트가 불가능하다-그러니까 postman으로 테스트 가능함-요청은 Body에 담긴다.-무조건 서버로 전달이 된다.
	:VO Map 원시형타입 모두 가능함
	
	
	
	
	질문해보기
			mem_no(member230324)는 어디서 오나요? - 인증을 어디서 하나요?
			qna_type 즉 질문타입은 상수로 양도를 줌
			qna_secret에는 비번을 입력받음
			비번이 널이면 공개 널이 아니면 비공개 처리 가능한가?
			생각해볼 문제
			화면에서 가져올 컬럼의 종류는 몇가지 인가요?
			세션이나 쿠키에서 또는 세션 스토리지에서 가져와야하는 컬럼이 있을까요?
			상수로 넣을 수 있는 (또는 넣어야 하는 ) 컬럼이 존재하나요?
			만일 존재한다면 어떤 컬럼인지 말해보세요
			하나 더 
			작성자는 입력 받도록 화면을 그려야할까요?
			아님 자동으로 결정할 수 있는 건가요?
		 -->
	<insert id="qnaInsert" parameterType="map">
		INSERT INTO qna (qna_bno, mem_no, qna_title, qna_content, qna_type, qna_secret, qna_hit, qna_date)
		VALUES (QNA_SEQ.nextval, #{mem_no}, #{qna_title}, #{qna_content}, '양도', #{qna_secret}, #{qna_hit},to_char(sysdate,'YYYY-MM-DD')) 
	</insert>
</mapper>

 

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


const KhMyFilter = ({types, type, id, title, handleTitle}) => {
  console.log(id);//qna_type
  const navigate = useNavigate();
  const location = useLocation();//URL정보를 가져오려면 필요

  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)); 
            }
            handleTitle(element); 
          }}>
            {element}
          </Dropdown.Item>
        )) 
      }
    </DropdownButton>
  );
};

export default KhMyFilter;
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 { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle';
import KhMyFilter from './KhMyFilter';
import KhSearchBar from './KhSearchBar.jxs';

const KhQnAListPage = ({authLogic}) => {
  //페이징 처리 시에 현재 내가 바라보는 페이지 정보 담기
  let page = 1  
  const navigate = useNavigate();
  const search = decodeURIComponent(useLocation().search);
  //오라클 서버에서 받아온 정보를 담기
  const [listBody,setListBody] = useState([]);
  //qna구분 상수값-라벨
  const[types]= useState(['전체','일반','결제','양도','회원','수업']);
  //qna 상태 관리위해 선언
  const [tTitle, setTTitle] = useState('전체') 
  //함수를 메모이제이션 해주는-useCallback->useMemo는 값을 메모이제이션
  const handleTTitle = useCallback((element) => {
    //파라미터로 받은 값을 저장 - tTitle
    setTTitle(element);
  },[]);//의존배열이 비었으므로 한번 메모이제이션 된 함수값을 계속 기억해둠

  useEffect(() => {
    const qnaList = async() =>{
      //콤보박스 내용 -> 제목, 내용, 작성자 중 하나
      //사용자가 입력한 키워드
      //http://localhost:3000/qna/list?condition=제목|내용|작성자&content=입력한값
      //첫번째 방[0]-?condition=제목|내용|작성자
      //두번째 방[1]-content=입력한값
      const condition = search.split('&').filter((item)=>{return item.match('condition')})[0]?.split('=')[1];
      console.log(condition);
      const content = search.split('&').filter((item)=>{return item.match('content')})[0]?.split('=')[1];
      console.log(content)
      const qna_type = search.split('&').filter((item)=>{return item.match('qna_type')})[0]?.split('=')[1];
      //http://localhost:3000/qna/list?page=1&qna_type=수업
      console.log(qna_type);//수업이 저장됨
      setTTitle(qna_type||'전체');//쿼리스트링이 없으면 그냥 전체가 담김
      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);//3번 출력된다
        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_hit:item.QNA_HIT,
          qna_date:item.QNA_DATE,
          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"}}>
              <KhMyFilter types={types} type={true} id={"qna_type"} title={tTitle} handleTitle={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'}/> */}
            <KhSearchBar />
          </div>
        </FormDiv>
      </ContainerDiv>
      <BlogFooter />	
    </>
  );
};

export default KhQnAListPage;
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 { 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 KhQnAWritePage = ({authLogic}) => {

  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() => {//post로 처리
    console.log('qnaInsert');
    console.log(secret);
    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.data)
    //성공시 페이지 이동 처리하기
    // window.location.replace('/qna/list?page=1')
  }
    
  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} handleTitle={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 KhQnAWritePage;
import React, { useCallback, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { BButton } from '../styles/FormStyle';
import KhMyFilter 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);//condition=제목
      //요청 url에 담긴 condition정보를 serTitle
      if(item.match('condition')){
        setTTitle(item.split('=')[1])//1번째가 밸류값 0번째는 키값
      }
    })
  },[search, setTTitle])//의존 배열의 search를 사용했고 상태 훅을 선택했으니 그 정보가 변경될때마다 호출됨.


  const setPath = () => {
    console.log(tTitle, content);
    console.log(search)
    //자바스크립트에서는 싱글 혹은 더블로 묶지 않으면 변수 취급함
    console.log(search.match('condition'));
    let path;
    //앞에서 조회한 적이 있을때 기존에 쿼리스트링 삭제 후 다시 쿼리스트링 만들어야함
    path = location.pathname + `?condition=${tTitle}&content=${content}`
    // 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}`
      
    // }
    console.log(path)///qna/list?condition=제목&page=1
    return path;
  }

  return (
    <div style={{display: 'flex', width: '100%', justifyContent: 'center'}}>
      <KhMyFilter types={types} title={tTitle} id={"condition"} handleTitle={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);}}
        />
      {/* <div>{setPath()}</div> */}
      <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;

댓글