[SI] 프론트엔드 (02_SimpleDMS_Page) 반응형 게시판

플러그인 설치

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;