반응형

네이버 카페 등의 게시판을 주로 보면 질문에 대한 답글이 게시되어 있는 기능을 종종 볼 수 있습니다.
이번에는 답글을 달 수 있는 게시판을 제작해 보겠습니다.
단, 간단한 예제를 위해 답글은 1번만 달 수 있도록 제한하고 테이블 설계 및 코딩을 하도록 하겠습니다.

요소 기술 및 테이블 설계는 아래와 같습니다.

요소 기술 :
- 프로젝트 키워드 : ReplyBoard
- 프론트엔드 : 리액트 & 타입스크립트
- 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c)


프론트 작업

1) 답변형 게시판 TYPE 정하기

IReplyBoard.ts 파일을 types 폴더에 저장합니다.

export default interface IReplyBoard {
  bid?: any | null,					// 게시글 번호
  boardTitle: string,				// 게시글 제목
  boardContent: string,				// 게시글 내용
  boardWriter: string,				// 게시글 작성자
  viewCnt: number,					// 조회수
  boardGroup: any | null,			// 게시글 그룹 번호
  boardParent: any | null			// 부모 게시글 번호
}

2) 답변형 게시판 axios CRUD 서비스 코딩

ReplyBoardService.ts 파일을 service 폴더에 저장합니다.

여기서 생성(저장)함수는 부모 게시글 생성, 답변글 생성 함수를 2개 작성해야하며, 삭제 함수의 경우도 부모+답변글 모두 삭제 기능 함수와, 답변 삭제 기능 함수 총 2개를 작성하여야 합니다.

 

2-1) ReplyBoard type import / axios 공통 파일 import

import IReplyBoard from "../../types/normal/IReplyBoard";
import http from "../../utils/http-common";

2-2) 전체조회함수 + like 검색(페이징 기능)

// 전체 조회 + like 검색(paging 기능 : page(현재페이지), size(한 페이지당 개수))
const getAll = (boardTitle:string, page:number, size:number) => {
  return http.get<Array<IReplyBoard>>(`/normal/reply-board?boardTitle=${boardTitle}&page=${page}&size=${size}`);
};

2-3) 상세조회

// 상세 조회
const get = (bid:any) => {
  return http.get<IReplyBoard>(`/normal/reply-board/${bid}`);
};

2-4) 저장함수 : 부모 게시글 저장

// 저장 함수 : 게시물 생성(부모글)
const createBoard = (data:IReplyBoard) => {
  return http.post<IReplyBoard>("/normal/reply-board", data);
};

2-5) 저장함수 : 답변글 저장

// 저장 함수 : 답변글 생성(자식글)
const create = (data:IReplyBoard) => {
    return http.post<IReplyBoard>("/normal/reply", data);
  };

2-6) 수정함수

// 수정 함수
const update = (bid:any, data:IReplyBoard) => {
  return http.put<any>(`/normal/reply-board/${bid}`, data);
};

2-7) 삭제함수 : 부모글 + 답변글 모두 삭제

그룹번호 : 부모글과 자식글은 모두 그룹번호가 같음

// 삭제 함수 : 게시물(부모글) + 답변글(자식글) 모두 삭제
// 그룹 번호 : 부모글과 자식글은 모두 그룹 번호가 같음
const removeBoard = (boardGroup:any) => {
  return http.delete<any>(`/normal/reply-board/deletion/${boardGroup}`);
};

2-8) 삭제함수 : 답변글만 삭제

// 삭제 함수 : 답변글만 삭제
const remove = (bid:any) => {
    return http.delete<any>(`/normal/reply/deletion/${bid}`);
  };

2-9) 함수 내보내기

const ReplyBoardService = {
  getAll,
  get,
  createBoard,
  create,
  update,
  removeBoard,
  remove,
};

export default ReplyBoardService;

 

3) 답변형 게시글 리스트 페이지 작성

ReplyBoardList.tsx 파일을 reply-board 폴더에 저장합니다.

더보기
import React, { useEffect, useState } from "react";
import TitleCom from "../../../components/common/TitleCom";
import { Pagination } from "@mui/material";
import { Link } from "react-router-dom";
import IReplyBoard from "../../../types/normal/IReplyBoard";
import ReplyBoardService from "../../../services/normal/ReplyBoardService";

function ReplyBoardList() {
  // todo ) 변수정의
  // 답변형 게시판(게시물+답변) 배열 변수
  // 답변글 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); // 한 페이지당 개수
  // todo 공통 pageSizes : 배열 (selectbox에 사용)
  const pageSizes = [3, 6, 9];

  // todo ) 함수정의

  //  1) 컴포넌트가 mounted 될때 한번만 실행됨 : useEffect(() => {},[])
  //  2) 컴포넌트의 변수값이 변할 때 실행됨 : useEffect(() => {실행문},[감시할 변수])
  useEffect(() => {
    retrieveReplyBoard(); // 전체 조회
  }, [page, pageSize]);

  // 전체 조회 함수
  const retrieveReplyBoard = () => {
    // 벡엔드 매개변수 전송 : 현재페이지(page), 1페이지당 개수(pageSize)
    ReplyBoardService.getAll(searchBoardTitle, page - 1, pageSize) // 벡엔드 전체조회요청
      .then((response: any) => {
        // 벡엔드 성공시 실행됨
        // es6(모던js) 문법 : 객체 분해 할당
        // 원래 코드
        // const dept =  response.data.dept;  // 부서배열
        // const totalPage = response.data.totalPages;  // 전체페이지수
        const { replyBoard, totalPages } = response.data;
        // dept 저장
        setReplyBoard(replyBoard);
        setCount(totalPages);
        // 로그 출력
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        // 벡엔드 실패시 실행됨
        console.log(e);
      });
  };

  // 검색어 수동 바인딩 함수
  const onChangeSearchBoardTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchBoardTitle(e.target.value);
  };

  // todo handlePageSizeChange(공통) : pageSize 값 변경시 실행되는 함수
  // select 태그 수동바인딩 : 화면 값을 변수에 저장하는 것
  const handlePageSizeChange = (e: any) => {
    setPageSize(e.target.value); // 1 페이지당 개수 저장(3, 6, 9)
    setPage(1); // 현재페이지 번호 : 1로 강제설정
  };

  // todo Pagination 수동바인딩(공통)
  // page 번호를 누르면 => page 변수에 값 저장
  const handlePageChange = (e: any, value: number) => {
    // value == 화면의 페이지번호
    setPage(value);
  };

  // -------------------------------------------------------

  // todo :: 답변 변수 정의
  // 1) reply 객체 초기화
  const initialReply = {
    bid: null,
    boardTitle: "",
    boardContent: "",
    boardWriter: "",
    viewCnt: 0,
    boardGroup: null,
    boardParent: 0,
  };

  // 2) 답변 글 입력 객체 변수
  const [reply, setReply] = useState(initialReply);
  // 3) reply 버튼 클릭시 상태 저장할 변수 : true/false
  const [replyClicked, setReplyClicked] = useState(false);

  // todo :: 답변 함수 정의
  // 1) input 수동 바인딩 함수
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target; // 화면값
    setReply({ ...reply, [name]: value }); // 변수저장
  };

  // 2) 답변글 생성 함수(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);
      });
  };

  // 3) 게시물 reply 버튼 클릭시 화면에 답변입력창 보이게 하는 함수
  const newReply = (data: any) => {
    // 매개변수 데이터(객체) 수정 : boardContent: "" 으로 수정
    setReply({ ...data, boardContent: "" });
    // 답변 입력창 화면 보이기 : replyClicked : true
    setReplyClicked(true);
  };

  // 4) 답변 입력창 숨기기
  const closeReply = () => {
    // 답변 화면창 숨기기
    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;

4) 프론트 서버 시작 후 화면 확인

 

백엔드 작업

1) ReplyBoart 엔티티 생성

package com.example.simpledms.model.entity.normal;

import com.example.simpledms.model.common.BaseTimeEntity;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

/**
 * packageName : com.example.simpledms.model.entity.normal
 * fileName : ReplyBoard
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@Entity
@Table(name="TB_REPLY_BOARD")
@SequenceGenerator(
        name = "SQ_REPLY_BOARD_GENERATOR"
        , sequenceName = "SQ_REPLY_BOARD"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_REPLY_BOARD SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE BID = ?")
public class ReplyBoard extends BaseTimeEntity {
    // 속성 추가
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_REPLY_BOARD_GENERATOR")
    private Integer bid;                    // 기본키, 시퀀스

    private String boardTitle;              // 제목

    private String boardContent;            // 내용

    private String boardWriter;             // 작성자

    private Integer viewCnt;                // 조회수

    private Integer boardGroup;             // 트리구조 최상위 부모 노드( 부모가 있을 경우 : 부모번호, 없을 경우 : 자신의 게시판번호 ) (정렬)

    private Integer boardParent;            // 자신의 부모 노드 ( 부모가 있을 경우 : 부모번호, 없을 경우 : 0 ) (핵심)

}

2) ReplyBoard Repository 생성 및 ReplyBoard DTO 생성

 

📝 ReplyBoardRepository.java

package com.example.simpledms.repository.normal;

import com.example.simpledms.model.dto.normal.ReplyBoardDto;
import com.example.simpledms.model.entity.normal.ReplyBoard;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.simpledms.repository.normal
 * fileName : ReplyBoardRepository
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@Repository
public interface ReplyBoardRepository extends JpaRepository<ReplyBoard, Integer> {
    //    계층형 조회(특수) 쿼리 : @Query(, nativeQuery=true)
    @Query(value = "SELECT BID           AS bid " +
            "     , LPAD(' ', (LEVEL-1))|| board_title   AS BoardTitle " +
            "     , board_content AS boardContent " +
            "     , board_writer  AS boardWriter " +
            "     , view_cnt      AS viewCnt " +
            "     , board_group   AS boardGroup " +
            "     , board_parent  AS boardParent " +
            "FROM TB_REPLY_BOARD " +
            "WHERE BOARD_TITLE LIKE '%'|| :boardTitle ||'%' " +
            "AND   DELETE_YN = 'N' " +
            "START WITH BOARD_PARENT = 0    " +
            "CONNECT BY PRIOR BID = BOARD_PARENT  " +
            "ORDER SIBLINGS BY BOARD_GROUP DESC", nativeQuery = true)
    Page<ReplyBoardDto> selectByConnectByPage(
            @Param("boardTitle") String boardTitle,
            Pageable pageable);

}

답변형 게시판의 경우 레포지토리 생성시 계층형 쿼리를 이용하여 코딩합니다.

-- 계층형 쿼리 : level 의사(가상)컬럼(예 : 부모 1 ~ 2,3,4 ...)
-- START WITH BOARD_PARENT(부모컬럼) = 0 (최초시작값: 부모)
-- CONNECT BY PRIOR BID(게시판 번호) = BOARD_PARENT(부모 번호)
SELECT BID          AS bid
    , LPAD('└', (LEVEL-1))|| board_title   AS BoardTitle
    , board_content AS BoardContent
    , board_writer  AS BoardWriter
    , view_cnt      AS viewCnt
    , board_group   AS boardGroup
    , board_parent  AS boardParent
FROM TB_REPLY_BOARD
WHERE BOARD_TITLE LIKE '%%'
AND   DELETE_YN = 'N'
START WITH BOARD_PARENT = 0 
CONNECT BY PRIOR BID = BOARD_PARENT 
ORDER SIBLINGS BY BOARD_GROUP DESC;

SQL Developer 에서 계층형 쿼리 실행 결과

댓글의 경우 └제목 으로 표시가 된것을 확인할 수 있음

 

📝 ReplyBoardDto.java

package com.example.simpledms.model.dto.normal;

/**
 * packageName : com.example.simpledms.model.dto.normal
 * fileName : ReplyBoardDto
 * author : GGG
 * date : 2023-10-26
 * description : 계층형 쿼리 DTO
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
public interface ReplyBoardDto {
//    속성 x => getter 함수


    public Integer getBid();
    public String getBoardTitle();
    public String getBoardContent();
    public String getBoardWriter();
    public Integer getViewCnt();
    public Integer getBoardGroup();
    public Integer getBoardParent();

}

3) ReplyBoardService 생성

package com.example.simpledms.service.normal;

import com.example.simpledms.model.dto.normal.ReplyBoardDto;
import com.example.simpledms.repository.normal.ReplyBoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

/**
 * packageName : com.example.simpledms.service.normal
 * fileName : ReplyBoardService
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@Service
public class ReplyBoardService {

    @Autowired
    ReplyBoardRepository replyBoardRepository;

    // 계층형 쿼리 조회(DTO) : like 검색
    public Page<ReplyBoardDto> selectByConnectByPage(String boardTitle, Pageable pageable){
        Page<ReplyBoardDto> page
                = replyBoardRepository.selectByConnectByPage(boardTitle, pageable);
        return page;
    }


}

 

4) ReplyBoardController 생성

package com.example.simpledms.controller.normal;

import com.example.simpledms.model.dto.normal.ReplyBoardDto;
import com.example.simpledms.model.entity.basic.Dept;
import com.example.simpledms.service.normal.ReplyBoardService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * packageName : com.example.simpledms.controller.normal
 * fileName : ReplyBoardController
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@RestController
@Slf4j
@RequestMapping("/api/normal")
public class ReplyBoardController {
    @Autowired
    ReplyBoardService replyBoardService;

    // 전체 조회(계층형, DTO) : LIKE 검색
    @GetMapping("/reply-board")
    public ResponseEntity<Object> selectByConnectByPage(
            @RequestParam(defaultValue = "") String boardTitle,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "3") int size

    ) {
        try {
            Pageable pageable = PageRequest.of(page, size);

            Page<ReplyBoardDto> replyBoardDtoPage
                    = replyBoardService
                    .selectByConnectByPage(boardTitle, pageable);

            Map<String, Object> response = new HashMap<>();
            response.put("replyBoard", replyBoardDtoPage.getContent());
            response.put("currentPage", replyBoardDtoPage.getNumber());
            response.put("totalItems", replyBoardDtoPage.getTotalElements());
            response.put("totalPages", replyBoardDtoPage.getTotalPages());

//            신호 보내기
            if (replyBoardDtoPage.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }


        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

 

5) API 테스트

더보기
GET http://localhost:8000/api/normal/reply-board

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 26 Oct 2023 07:13:59 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "totalItems": 8,
  "replyBoard": [
    {
      "boardWriter": "작성자",
      "boardParent": 0,
      "bid": 8,
      "boardTitle": "제목",
      "boardContent": "내용",
      "viewCnt": 0,
      "boardGroup": 8
    },
    {
      "boardWriter": "작성자",
      "boardParent": 0,
      "bid": 7,
      "boardTitle": "제목",
      "boardContent": "내용",
      "viewCnt": 0,
      "boardGroup": 7
    },
    {
      "boardWriter": "작성자",
      "boardParent": 0,
      "bid": 5,
      "boardTitle": "제목",
      "boardContent": "내용",
      "viewCnt": 0,
      "boardGroup": 5
    }
  ],
  "totalPages": 3,
  "currentPage": 0
}
응답 파일이 저장되었습니다.
> 2023-10-26T161359.200.json

Response code: 200; Time: 241ms (241 ms); Content length: 395 bytes (395 B)

6) 프론트 + 백엔드 연동테스트

반응형

+ Recent posts