서비스 게시판 생각해보기
페이징 처리 - 각자 처리 해보기- 분리해보기 연습
스프링 부트와 리액트 연동하기가 목표이다.
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 - 제목, 내용, 작성자
해시값을 이용해서 처리할때는 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>}
{listItem[key]}
{listItem.comment?<span style={{fontWeight:"bold"}}> [답변완료]</span>:<span> [미답변]</span>}
{listItem.secret&&<span> <i className="fas fa-lock"></i></span>}</td>
:
<td key={index} style={{textAlign: 'center'}}>{listItem[key]}</td>
))}
</tr>
)
})
return (
<>
<BlogHeader authLogic={authLogic}/>
<ContainerDiv>
<HeaderDiv>
<h3 style={{marginLeft:"10px"}}>QnA 게시판</h3>
</HeaderDiv>
<FormDiv>
<div>
<div style={{display:"flex", justifyContent:"space-between", height:"40px"}}>
<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;
'학원수업 > 4월' 카테고리의 다른 글
04/11 국비학원 개발자 과정 - (0) | 2023.04.11 |
---|---|
04/07 국비학원 개발자과정 91회차수업 - Redux, CSS 로 네임카드 만들기 (0) | 2023.04.07 |
04/06 국비학원 개발자과정 90회차- Redux, YoutubeAPI (0) | 2023.04.06 |
04/05 국비과정 개발자 89회차 수업- QnA게시판 글 상세보기, 이미지 다운, 글수정 (0) | 2023.04.05 |
04/04 국비학원 개발자과정 - 88회차 React, Spring QnA게시판 (0) | 2023.04.04 |
댓글