[Spring Boot]/[JPA]
[JPA] 프론트엔드 (02_SimpleDMS_Page) 반응형 게시판
북방바다코끼리표범
2023. 11. 1. 22:38
플러그인 설치
CamelCase 단축키 (shift + alt + u)
프론트
ReplyBoardList.tsx -> app.tsx -> IReplyBoard.ts(벡엔드의 schema.sql 폴더 참고) -> ReplyBoardService.ts
ReplyBoardList.tsx
// ReplyBoardList.tsx : rfce import React, { useEffect, useState } from "react"; import TitleCom from "../../../components/common/TitleCom"; import { Link } from "react-router-dom"; import IReplyBoard from "../../../types/normal/IReplyBoard"; import ReplyBoardService from "../../../services/normal/ReplyBoardService"; import { Pagination } from "@mui/material"; function ReplyBoardList() { // todo: 변수 정의 // replyBoard(게시물+답변) 배열 변수 // 답변글 1개만 달리게 제한 const [replyBoard, setReplyBoard] = useState<Array<IReplyBoard>>([]); // 검색어 변수 const [searchBoardTitle, setSearchBoardTitle] = useState<string>(""); // todo: 공통 변수 : page(현재페이지번호), count(총페이지건수), pageSize(3,6,9 배열) const [page, setPage] = useState<number>(1); const [count, setCount] = useState<number>(1); const [pageSize, setPageSize] = useState<number>(3); // 1페이지당개수 // todo: 공통 pageSizes : 배열 (셀렉트 박스 사용) const pageSizes = [3, 6, 9]; // todo: 함수 정의 useEffect(() => { retrieveReplyBoard(); // 전체 조회 }, [page, pageSize]); // 전체조회 함수 const retrieveReplyBoard = () => { ReplyBoardService.getAll(searchBoardTitle, page - 1, pageSize) // 벡엔드 전체조회요청 .then((response: any) => { const { replyBoard, totalPages } = response.data; setReplyBoard(replyBoard); setCount(totalPages); console.log("response", response.data); }) .catch((e: Error) => { // 벡엔드 실패시 실행됨 console.log(e); }); }; // 검색어 수동 바인딩 함수 const onChangeSearchBoardTitle = (e: any) => { setSearchBoardTitle(e.target.value); }; // todo: handlePageSizeChange(공통) : pageSize 값 변경시 실행되는 함수 // select 태그 수동 바인딩 : 화면값 -> 변수에 저장 const handlePageSizeChange = (event: any) => { setPageSize(event.target.value); // 1페이지당 개수저장(3,6,9) setPage(1); // 현재페이지번호 : 1로 강제설정 }; // todo: Pagination 수동 바인딩(공통) // 페이지 번호를 누르면 => page 변수에 값 저장 const handlePageChange = (event: any, value: number) => { // value == 화면의 페이지번호 setPage(value); }; // --------------------------------------- // todo: 답변 변수 정의 // reply 객체 초기화 const initialReply = { bid: null, boardTitle: "", boardContent: "", boardWriter: "", viewCnt: 0, boardGroup: null, boardParent: 0, }; // 답변 글 입력 객체 const [reply, setReply] = useState(initialReply); // reply 버튼 클릭시 상태 저장할 변수 : true/false const [replyClicked, setReplyClicked] = useState(false); // todo: 답변 함수 정의 // input 수동 바인딩 함수 const handleInputChange = (event: any) => { const { name, value } = event.target; // 화면값 setReply({ ...reply, [name]: value }); // 변수저장 }; // 답변글 생성함수(insert) const saveReply = () => { // 임시 객체 let data = { boardTitle: reply.boardTitle, boardContent: reply.boardContent, boardWriter: reply.boardWriter, viewCnt: 0, // 그룹번호(부모글 == 자식글) // rule : 1) 부모글 최초생성 또는 답변글 없을때 0 저장 // 2) 답변글 생성이면 부모글 게시판번호(bid) 저장 boardGroup: reply.bid, // 부모글번호 : // rule : 1) 부모글 최초생성 또는 답변글 없을때 자신의 게시판번호(bid) 저장 // 2) 답변글 생성이면 부모글번호(bid) boardParent: reply.bid, }; ReplyBoardService.create(data) // 벡엔드 답변글 저장 요청 .then((response: any) => { alert("답변글이 생성되었습니다."); // 전체 재조회 retrieveReplyBoard(); console.log(response.data); }) .catch((e: Error) => { console.log(e); }); }; // 게시물 reply 버튼 클릭시 화면에 답변입력창 보이게 하는 함수 const newReply = (data: any) => { // 매개변수 데이터(객체) 수정 : boardContent: "" 수정 setReply({ ...data, boardContent: "" }); // 답변 입력창 화면보이기 : replyClicked = true setReplyClicked(true); }; // 답변 입력창 숨기기 const closeReply = () => { // 답변 입력창 화면숨기기 : replyClicked = false setReplyClicked(false); }; return ( // 여기 <div> {/* 제목 start */} <TitleCom title="Reply Board List" /> {/* 제목 end */} {/* search start(검색어 입력창) */} <div className="row mb-5 justify-content-center"> <div className="col-12 w-50 input-group mb-3"> <input type="text" className="form-control" placeholder="Search by title" value={searchBoardTitle} onChange={onChangeSearchBoardTitle} /> <button className="btn btn-outline-secondary" type="button" onClick={retrieveReplyBoard} > Search </button> </div> </div> {/* search end */} {/* page start(페이지 번호) */} <div className="mt-3"> {"Items per Page: "} <select onChange={handlePageSizeChange} value={pageSize}> {pageSizes.map((size) => ( <option key={size} value={size}> {size} </option> ))} </select> <Pagination className="my-3" count={count} page={page} siblingCount={1} boundaryCount={1} variant="outlined" shape="rounded" onChange={handlePageChange} /> </div> {/* page end */} {/* 게시판(폼1) + 답변글(폼2) */} <div className="col-md-12"> {/* table start(게시판) */} <table className="table"> <thead> <tr> <th scope="col">board No</th> <th scope="col">board Title</th> <th scope="col">board Content</th> <th scope="col">board Writer</th> <th scope="col">view Cnt</th> <th scope="col">reply</th> <th scope="col">Actions</th> </tr> </thead> <tbody> {replyBoard && replyBoard.map((data, index) => ( // 키값 추가 않하면 react 에서 경고를 추가 : 키는 내부적으로 리액트가 rerending 할때 체크하는 값임 <tr key={index}> <td>{data.bid}</td> <td>{data.boardTitle}</td> <td>{data.boardContent}</td> <td>{data.boardWriter}</td> <td>{data.viewCnt}</td> <td> {/* 클릭 : 아래 답변 폼이 열림 */} {data.boardParent == 0 && ( <Link to={"#"}> {/* 리액트 : onClick={함수명} : 매개변수없으면 */} {/* 리액트 : onClick={()=>함수명(매개변수)} : 매개변수있으면 */} <span className="badge bg-warning" onClick={() => newReply(data)} > Reply </span> </Link> )} </td> <td> {/* 클릭 : 상세화면 이동 */} <Link to={ "/reply-board/bid/" + data.bid + "/boardParent/" + data.boardParent } > <span className="badge bg-success">Edit</span> </Link> </td> </tr> ))} </tbody> </table> {/* table end */} {/* reply form start(답변글) */} <div> {/* 변수명 && 태그 : 변수명 = true 태그가 보이고 */} {/* 변수명 && 태그 : 변수명 = false 태그가 안보임 */} {replyClicked && ( <div className="col-md-12 row"> <div className="col-md-12 row mt-2"> <label htmlFor="bid" className="col-md-2 col-form-label"> bid </label> <div className="col-md-10"> <input type="text" className="form-control-plaintext" id="bid" placeholder={reply.bid || ""} disabled name="bid" /> </div> </div> <div className="col-md-12 row mt-2"> <label htmlFor="boardTitle" className="col-md-2 col-form-label"> board Title </label> <div className="col-md-10"> <input type="text" className="form-control-plaintext" id="boardTitle" disabled placeholder={reply.boardTitle} name="boardTitle" /> </div> </div> <div className="col-md-12 row mt-2"> <label htmlFor="boardContent" className="col-md-2 col-form-label" > board Content </label> <div className="col-md-10"> <input type="text" className="form-control" id="boardContent" required value={reply.boardContent} onChange={handleInputChange} name="boardContent" /> </div> </div> <div className="col-md-12 row mt-2"> <label htmlFor="boardWriter" className="col-md-2 col-form-label" > board Writer </label> <div className="col-md-10"> <input type="text" className="form-control" id="boardWriter" required value={reply.boardWriter} onChange={handleInputChange} name="boardWriter" /> </div> </div> <div className="row px-4 mt-2"> <button onClick={saveReply} className="btn btn-success mt-3 col-md-5" > Submit </button> <div className="col-md-2"></div> <button onClick={closeReply} className="btn btn-danger mt-3 col-md-5" > Close </button> </div> </div> )} </div> {/* reply form end */} </div> </div> ); } export default ReplyBoardList;
app.tsx
import React from "react"; // app css import import "./assets/css/app.css"; import HeaderCom from "./components/common/HeaderCom"; import { Route, Routes } from "react-router-dom"; import Home from "./pages/Home"; import Login from "./pages/auth/Login"; import Register from "./pages/auth/Register"; import ForgotPassword from "./pages/auth/ForgotPassword"; import NotFound from "./pages/common/NotFound"; import DeptList from "./pages/basic/dept/DeptList"; import EmpList from "./pages/basic/emp/EmpList"; import AddDept from './pages/basic/dept/AddDept'; import AddEmp from "./pages/basic/emp/AddEmp"; import Dept from "./pages/basic/dept/Dept"; import Emp from "./pages/basic/emp/Emp"; import QnaList from "./pages/basic/qna/QnaList"; import CustomerList from "./pages/basic/customer/CustomerList"; import AddQna from "./pages/basic/qna/AddQna"; import AddCustomer from "./pages/basic/customer/AddCustomer"; import Qna from "./pages/basic/qna/Qna"; import Customer from "./pages/basic/customer/Customer"; import FaqList from "./pages/normal/faq/FaqList"; import CinemaFaqList from "./pages/normal/cinema/CinemaFaqList"; import AddFaq from "./pages/normal/faq/AddFaq"; import AddCinemaFaq from './pages/normal/cinema/AddCinemaFaq'; import Faq from "./pages/normal/faq/Faq"; import CinemaFaq from "./pages/normal/cinema/CinemaFaq"; import ReplyBoardList from './pages/normal/reply-board/ReplyBoardList'; import ThreadBoardList from "./pages/normal/thread-board/ThreadBoardList"; import AddReplyBoard from "./pages/normal/reply-board/AddReplyBoard"; import AddThreadBoard from "./pages/normal/thread-board/AddThreadBoard"; import ReplyBoard from "./pages/normal/reply-board/ReplyBoard"; import ThreadBoard from "./pages/normal/thread-board/ThreadBoard"; function App() { return ( <div className="App"> <HeaderCom /> {/* <!-- 구분 막대 시작 --> */} <div className="gutter text-center text-muted fade-in-box"> <div>클론 코딩 예제 사이트에 오신 것을 환영합니다.</div> </div> {/* <!-- 구분 막대 끝 --> */} <div id="content-wrapper"> {/* 라우터 정의 시작 */} <Routes> {/* login */} <Route path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/forgot-password" element={<ForgotPassword />} /> {/* dept */} <Route path="/dept" element={<DeptList />} /> <Route path="/add-dept" element={<AddDept />} /> <Route path="/dept/:dno" element={<Dept />} /> {/* emp(연습) */} <Route path="/emp" element={<EmpList />} /> <Route path="/add-emp" element={<AddEmp />} /> <Route path="/emp/:eno" element={<Emp />} /> {/* qna */} <Route path="/qna" element={<QnaList />} /> <Route path="/add-qna" element={<AddQna />} /> <Route path="/qna/:qno" element={<Qna />} /> {/* customer */} <Route path="/customer" element={<CustomerList />} /> <Route path="/add-customer" element={<AddCustomer />} /> <Route path="/customer/:cid" element={<Customer />} /> {/* faq */} <Route path="/faq" element={<FaqList />} /> <Route path="/add-faq" element={<AddFaq />} /> <Route path="/faq/:no" element={<Faq />} /> {/* cinema faq */} <Route path="/cinema-faq" element={<CinemaFaqList />} /> <Route path="/add-cinema-faq" element={<AddCinemaFaq />} /> <Route path="/cinema-faq/:cfno" element={<CinemaFaq />} /> {/* reply-board */} <Route path="/reply-board" element={<ReplyBoardList />} /> <Route path="/add-reply-board" element={<AddReplyBoard />} /> {/* 정리 : boardParent = 0 이면 부모글을 클릭 */} {/* 정리 : boardParent = 0 아니면 자식글을 클릭 */} <Route path="/reply-board/bid/:bid/boardParent/:boardParent" element={<ReplyBoard />} /> {/* thread-board */} <Route path="/thread-board" element={<ThreadBoardList />} /> <Route path="/add-thread-board" element={<AddThreadBoard />} /> {/* 정리 : tparent = 0 이면 부모글을 클릭 */} {/* 정리 : tparent = 0 아니면 자식글을 클릭 */} <Route path="/thread-board/tid/:tid/tparent/:tparent" element={<ThreadBoard />} /> {/* NotFound */} <Route path="*" element={<NotFound />} /> </Routes> {/* 라우터 정의 끝 */} </div> </div> ); } export default App;
IReplyBoard.ts
// IReplyBoard.ts : 인터페이스 타입 export default interface IReplyBoard { bid?: any | null, boardTitle: string, boardContent: string, boardWriter: string, viewCnt: number, boardGroup: any|null, boardParent: any|null }
ReplyBoardService.ts
// ReplyBoardService.ts : axios 공통 crud 함수 import http from "../../utils/http-common"; import IReplyBoard from './../../types/normal/IReplyBoard'; // 전체 조회 + like 검색(paging 기능 : page(현재페이지), size(1페이지당개수)) const getAll = (boardTitle:string, page:number, size:number) => { return http.get<Array<IReplyBoard>>(`/normal/reply-board?boardTitle=${boardTitle}&page=${page}&size=${size}`); }; // 상세 조회 const get = (bid:any) => { return http.get<IReplyBoard>(`/normal/reply-board/${bid}`); }; // 저장함수 : 게시물 생성(부모글) const createBoard = (data:IReplyBoard) => { return http.post<IReplyBoard>("/normal/reply-board", data); }; // 저장함수 : 답변글 생성(자식글) const create = (data:IReplyBoard) => { return http.post<IReplyBoard>("/normal/reply", data); }; // 수정함수 const update = (bid:any, data:IReplyBoard) => { return http.put<any>(`/normal/reply-board/${bid}`, data); }; // 삭제함수 : 게시물(부모글) + 답변글(자식글) 모두 삭제 // 그룹번호 : 부모글과 자식글은 모두 그룹번호가 같음 const removeBoard = (boardGroup:any) => { return http.delete<any>(`/normal/reply-board/deletion/${boardGroup}`); }; // 삭제함수 : 답변글만 삭제 const remove = (bid:any) => { return http.delete<any>(`/normal/reply/deletion/${bid}`); }; const ReplyBoardService = { getAll, get, createBoard, create, update, removeBoard, remove, }; export default ReplyBoardService;