플러그인 설치
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;
'[Spring Boot] > [JPA]' 카테고리의 다른 글
[JPA] Creative-stylish-portfolio (블로그 테마) 연습 예제 (1) | 2023.11.03 |
---|---|
[JPA] 벡엔드 (02_SimpleDMS_Page) 반응형 게시판 (1) | 2023.11.02 |
[JPA] Pilot Project 2 (29) | 2023.10.31 |
[JPA] Pilot_Project 블로그 테마별 알아보기 (1) | 2023.10.30 |
[JPA] JPQL (Java Persistence Query Language)이란 ? (2) | 2023.10.29 |