본문 바로가기

Program/Spring

Clone_Project - 2(게시물 목록 요청 ,동적 SQL, 댓글)

22.10.31 - Clone_Project - 2(게시물 목록 요청 ,동적 SQL, 댓글)


SQL

mySQL

<select id="findAll2" resultMap="boardMap">
        SELECT * FROM tbl_board

        <if test="type == 'title'">
            WHERE title LIKE CONCAT('%'.#{keyword},'%')
        </if>
        <if test="type == 'writer'">
            WHERE writer LIKE CONCAT('%'.#{keyword},'%')
        </if>
        <if test="type == 'content'">
            WHERE content LIKE CONCAT('%'.#{keyword},'%')
        </if>
        <if test="type == 'content'">
            WHERE title LIKE CONCAT('%'.#{keyword},'%')
            OR content LIKE CONCAT('%'.#{keyword},'%')
        </if>

        ORDER BY board_no DESC
        LIMIT #{start},#{amount};
    </select>

Oracle

<select id="findAll2" resultMap="boardMap">
        SELECT  *
        FROM (
                SELECT ROWNUM rn, v_board.*
                FROM (
                        SELECT *
                        FROM tbl_board
                        <if test="type == 'title'">WHERE title LIKE '%' || #{keyword} || '%'</if>
                        <if test="type == 'writer'">WHERE writer LIKE '%' || #{keyword} || '%'</if>
                        <if test="type == 'content'">WHERE content LIKE '%' || #{keyword} || '%'</if>
                        <if test="type == 'tc'">
                            WHERE title LIKE '%' || #{keyword} || '%'
                                OR content LIKE '%' || #{keyword} || '%'
                        </if>
                        ORDER BY board_no DESC
                        ) v_board
              )
        WHERE rn BETWEEN (#{pageNum} - 1) * #{amount} + 1 AND (#{pageNum} * #{amount})
    </select>

동적 SQL 사용(MySql)


<!-- 동적 SQL 코드 재사용하기   -->
<sql id="search">
    <if test="type == 'title'">WHERE title LIKE CONCAT('%', #{keyword}, '%')</if>
    <if test="type == 'writer'">WHERE writer LIKE CONCAT('%', #{keyword}, '%')</if>
    <if test="type == 'content'">WHERE content LIKE CONCAT('%', #{keyword}, '%')</if>
    <if test="type == 'tc'">
        WHERE title LIKE CONCAT('%', #{keyword}, '%')
        OR  content LIKE CONCAT('%', #{keyword}, '%')
    </if>
</sql>

MySQL


<select id="findAll2" resultMap="boardMap">

    SELECT * FROM tbl_board
    <include refid="search" />
    ORDER BY board_no DESC
    LIMIT #{start}, #{amount}
</select>

<select id="getTotalCount2" resultType="int">
        SELECT COUNT(*)
        FROM tbl_board
        <include refid="search" />
</select>

resultType : 단일 컬럼 ⇒ 컬럼 한 줄만 조회될 때, 값이 여러개이면 list int와 같은 형태로 나옴, 형태를 지정 하고 싶을 때 지정할 수 있음

resultMap : 다중 컬럼 ⇒ 컬럼이 여러줄 나올 때

JSP

<!-- 검색창 영역 -->
<div class="search">
    <form action="/board/list" method="get">

        <select class="form-select" name="type" id="search-type">
            <option value="title">제목</option>
            <option value="content">내용</option>
            <option value="writer">작성자</option>
            <option value="tc">제목+내용</option>
        </select>

<!-- 아래의 코드에서 value는 방금 검색했던 검색어가 들어가도록 받는 것-->
        <input type="text" class="form-control" name="keyword" value="${s.keyword}">

        <button class="btn btn-primary" type="submit">
            <i class="fas fa-search"></i>
        </button>

    </form>
</div>

JS

// 옵션태그 고정
        function fixSearchOption() {
            const $select = document.getElementById('search-type');

            for (let $opt of [...$select.children]) {
                if ($opt.value === '${s.type}') {
                    $opt.setAttribute('selected', 'selected');
                    break;
                }
            }
        }

Service

// 게시물 전체 조회 요청 중간 처리 with searching
    public Map<String, Object> findAllService(Search search) {
        log.info("findAll service start");

        Map<String, Object> findDataMap = new HashMap<>();

        List<Board> boardList = repository.findAll2(search);
        // 목록 중간 데이터 처리
        processConverting(boardList);

        findDataMap.put("bList", boardList);
        findDataMap.put("tc", repository.getTotalCount2(search));

        return findDataMap;
    }

Controller

// 게시물 목록 요청
@GetMapping("/list")
public String list(@ModelAttribute("s") Search search, Model model) {
log.info("controller request /board/list GET! - search: {}", search);

    Map<String, Object> boardMap = boardService.findAllService(search);
log.debug("return data - {}", boardMap);

    // 페이지 정보 생성
    PageMaker pm = new PageMaker(
            new Page(search.getPageNum(), search.getAmount())
            , (Integer) boardMap.get("tc"));

    model.addAttribute("bList", boardMap.get("bList"));
    model.addAttribute("pm", pm);

    return "board/board-list";
}

댓글


SQL

CREATE TABLE tbl_reply (
   reply_no INT(10) AUTO_INCREMENT,
   reply_text VARCHAR(1000) NOT NULL,
   reply_writer VARCHAR(50) NOT NULL,
   reply_date DATETIME default current_timestamp,
   board_no INT(10),
   CONSTRAINT pk_reply PRIMARY KEY (reply_no),
   CONSTRAINT fk_reply_board
       FOREIGN KEY (board_no)
           REFERENCES tbl_board (board_no)
);

domain

package com.project.web_prj.reply.domain;

import lombok.*;

import java.util.Date;

@Setter @Getter @ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Reply {

    private Long replyNo; //댓글번호
    private String replyText; //댓글내용
    private String replyWriter; //댓글작성자
    private Date replyDate; //작성일자
    private Long boardNo; //원본 글번호
}

api

Rest개념

Rest개념

  • ReplyApiController

      package com.project.web_prj.reply.api;
    
      import com.project.web_prj.common.paging.Page;
      import com.project.web_prj.reply.domain.Reply;
      import com.project.web_prj.reply.service.ReplyService;
      import lombok.RequiredArgsConstructor;
      import lombok.extern.log4j.Log4j2;
      import org.springframework.web.bind.annotation.*;
    
      import java.util.Map;
    
      @RestController
      @RequiredArgsConstructor
      @Log4j2
      @RequestMapping("/api/v1/replies")
      public class ReplyApiController {
    
          private final ReplyService replyService;
    
          /*
              - 댓글 목록 조회요청 : /api/v1/replies - GET
              - 댓글 개별 조회요청 : /api/v1/replies/72 - GET
              - 댓글 쓰기 요청 : /api/v1/replies - POST
              - 댓글 수정 요청 : /api/v1/replies/72 - PUT
              - 댓글 삭제 요청 : /api/v1/replies/72 - DELETE
           */
    
          // 댓글 목록 요청
          @GetMapping("")
          public Map<String, Object> list(Long boardNo, Page page) {
      log.info("/api/v1/replies GET! bno={}, page={}", boardNo, page);
    
              Map<String, Object> replies = replyService.getList(boardNo, page);
    
              return replies;
          }
    
          // 댓글 등록 요청
          @PostMapping("")
          public String create(@RequestBody Reply reply) {
      log.info("/api/v1/replies POST! - {}", reply);
              boolean flag = replyService.write(reply);
              return flag ? "insert-success" : "insert-fail";
          }
    
          // 댓글 수정 요청
          @PutMapping("/{rno}")
          public String modify(@PathVariable Long rno, @RequestBody Reply reply) {
    
              reply.setReplyNo(rno);
      log.info("/api/v1/replies PUT! - {}", reply);
              boolean flag = replyService.modify(reply);
              return flag ? "mod-success" : "mod-fail";
          }
    
          // 댓글 삭제 요청
          @DeleteMapping("/{rno}")
          public String delete(@PathVariable Long rno) {
    
      log.info("/api/v1/replies DELETE! - {}", rno);
              boolean flag = replyService.remove(rno);
              return flag ? "del-success" : "del-fail";
          }
      }
    
  • RestBasicController

      package com.project.web_prj.common.api;
    
      import com.project.web_prj.board.domain.Board;
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.Setter;
      import lombok.ToString;
      import lombok.extern.log4j.Log4j2;
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.*;
      import org.springframework.web.servlet.ModelAndView;
    
      import javax.servlet.http.HttpServletResponse;
      import java.util.List;
    
      // jsp 뷰포워딩을 하지않고 클라이언트에게 JSON데이터를 전송함
      @RestController //@Controller+@ResponseBody
      @Log4j2
      public class RestBasicController {
    
          @Setter @Getter @ToString
          @AllArgsConstructor
          public static class BoardResponseDto {
              private Long bno;
              private String writer;
              private String content;
              private String title;
    
              public BoardResponseDto(Board b) {
                  this.bno = b.getBoardNo();
                  this.writer = b.getWriter();
                  this.content = b.getContent();
                  this.title = b.getTitle();
              }
          }
    
          @GetMapping("/api/hello")
          @ResponseBody
          public String hello() {
              return "hello!!!";
          }
          @GetMapping("/api/board")
          @ResponseBody
          public BoardResponseDto board() {
              Board board = new Board();
              board.setBoardNo(10L);
              board.setContent("할룽~");
              board.setTitle("메룽~~");
              board.setWriter("박영희");
    
              return new BoardResponseDto(board);
          }
    
          @GetMapping("/api/arr")
          public String[] arr() {
              String[] foods = {"짜장면", "레몬에이드", "볶음밥"};
              return foods;
          }
    
          // post 요청처리
          @PostMapping("/api/join")
          public String join(@RequestBody List<String> info) {
              log.info("/api/join POST!! - {}", info);
              return "POST-OK";
          }
          // put 요청처리
          @PutMapping("/api/join")
          public String joinPut(@RequestBody Board board) {
              log.info("/api/join PUT!! - {}", board);
              return "PUT-OK";
          }
          // delete 요청처리
          @DeleteMapping("/api/join")
          public String joinDelete() {
              log.info("/api/join DELETE!!");
              return "DEL-OK";
          }
    
          // RestController에서 뷰포워딩하기
          @GetMapping("/hoho")
          public ModelAndView hoho() {
              ModelAndView mv = new ModelAndView();
              mv.setViewName("index");
              return mv;
          }
    
      }

repository

package com.project.web_prj.reply.repository;

import com.project.web_prj.common.paging.Page;
import com.project.web_prj.reply.domain.Reply;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface ReplyMapper {

    //댓글 입력
    boolean save(Reply reply);

    //댓글 수정
    boolean modify(Reply reply);

    //댓글 삭제
    boolean remove(Long replyNo);

    //댓글 전체 삭제
    boolean removeAll(Long boardNo);

    //댓글 개별 조회
    Reply findOne(Long replyNo);

    //댓글 목록 조회
    List<Reply> findAll(@Param("boardNo") Long boardNo
            , @Param("page") Page page);

    // 댓글 수 조회
    int getReplyCount(Long boardNo);
}

service

package com.project.web_prj.reply.service;

import com.project.web_prj.common.paging.Page;
import com.project.web_prj.common.paging.PageMaker;
import com.project.web_prj.reply.domain.Reply;
import com.project.web_prj.reply.repository.ReplyMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

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

@Service
@RequiredArgsConstructor
public class ReplyService {

    private final ReplyMapper replyMapper;

    //댓글 목록 조회
    public Map<String, Object> getList(Long boardNo, Page page) {
        PageMaker maker
                = new PageMaker(page, getCount(boardNo));

        Map<String, Object> replyMap = new HashMap<>();
        replyMap.put("replyList", replyMapper.findAll(boardNo, page));
        replyMap.put("maker", maker);

        return replyMap;
    }
    //댓글 총 개수 조회
    public int getCount(Long boardNo) {
        return replyMapper.getReplyCount(boardNo);
    }
    //댓글 개별 조회
    public Reply get(Long replyNo) {
        return replyMapper.findOne(replyNo);
    }

    //댓글 쓰기 중간처리
    public boolean write(Reply reply) {
        return replyMapper.save(reply);
    }

    //댓글 수정 중간처리
    public boolean modify(Reply reply) {
        return replyMapper.modify(reply);
    }
    //댓글 삭제 중간처리
    public boolean remove(Long replyNo) {
        return replyMapper.remove(replyNo);
    }
}

resources

<?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.project.web_prj.reply.repository.ReplyMapper">

    <resultMap id="replyMap" type="com.project.web_prj.reply.domain.Reply">
        <result property="replyNo" column="reply_no" />
        <result property="replyText" column="reply_text" />
        <result property="replyWriter" column="reply_writer" />
        <result property="replyDate" column="reply_date" />
        <result property="boardNo" column="board_no" />
    </resultMap>

    <insert id="save">
        <!--        INSERT INTO tbl_reply-->
        <!--            (reply_no, reply_text, reply_writer, board_no)-->
        <!--        VALUES-->
        <!--            (seq_tbl_reply.nextval, #{replyText}, #{replyWriter}, #{boardNo})-->

        INSERT INTO tbl_reply
        (reply_text, reply_writer, board_no)
        VALUES
        (#{replyText}, #{replyWriter}, #{boardNo})
    </insert>

    <!--  댓글 수정  -->
    <update id="modify">
        UPDATE tbl_reply
        SET reply_text = #{replyText}
        WHERE reply_no = #{replyNo}
    </update>

    <!--  댓글 삭제  -->
    <delete id="remove">
        DELETE FROM tbl_reply
        WHERE reply_no = #{replyNo}
    </delete>

    <!--  댓글 전체 삭제  -->
    <delete id="removeAll">
        DELETE FROM tbl_reply
        WHERE board_no = #{boardNo}
    </delete>

    <!--  댓글 개별조회  -->
    <select id="findOne" resultMap="replyMap">
        SELECT * FROM tbl_reply
        WHERE reply_no = #{replyNo}
    </select>

    <!--  댓글 목록 조회  -->
    <select id="findAll" resultMap="replyMap">
        <!--        SELECT  *-->
        <!--        FROM (-->
        <!--            SELECT ROWNUM rn, v_reply.*-->
        <!--            FROM (-->
        <!--                SELECT *-->
        <!--                FROM tbl_reply-->
        <!--                WHERE board_no = #{boardNo}-->
        <!--                ORDER BY board_no DESC-->
        <!--            ) v_reply-->
        <!--        )-->
        <!--        WHERE rn BETWEEN (#{page.pageNum} - 1) * #{page.amount} + 1 AND (#{page.pageNum} * #{page.amount})-->

        SELECT  *
        FROM tbl_reply
        WHERE board_no = #{boardNo}
        ORDER BY reply_no
        LIMIT #{page.start}, #{page.amount}
    </select>

    <select id="getReplyCount" resultType="int">
        SELECT COUNT(*)
        FROM tbl_reply
        WHERE board_no=#{boardNo}
    </select>

</mapper>

## Client 댓글 처리

---

### board-detail.jsp

```html
<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="ko">

<head>
    <%@ include file="../include/static-head.jsp" %>

    <style>
        .content-container {
            width: 60%;
            margin: 150px auto;
            position: relative;
        }

        .content-container .main-title {
            font-size: 24px;
            font-weight: 700;
            text-align: center;
            border-bottom: 2px solid rgb(75, 73, 73);
            padding: 0 20px 15px;
            width: fit-content;
            margin: 20px auto 30px;
        }

        .content-container .main-content {
            border: 2px solid #ccc;
            border-radius: 20px;
            padding: 10px 25px;
            font-size: 1.1em;
            text-align: justify;
            min-height: 400px;
        }

        .content-container .custom-btn-group {
            position: absolute;
            bottom: -10%;
            left: 50%;
            transform: translateX(-50%);
        }

        /* 페이지 액티브 기능 */
        .pagination .page-item.p-active a {
            background: #333 !important;
            color: #fff !important;
            cursor: default;
            pointer-events: none;
        }

        .pagination .page-item:hover a {
            background: #888 !important;
            color: #fff !important;
        }
    </style>
</head>

<body>

    <div class="wrap">
        <%@ include file="../include/header.jsp" %>

        <div class="content-container">

            <h1 class="main-title">${b.boardNo}번 게시물</h1>

            <div class="mb-3">
                <label for="exampleFormControlInput1" class="form-label">작성자</label>
                <input type="text" class="form-control" id="exampleFormControlInput1" placeholder="이름" name="writer"
                    value="${b.writer}" disabled>
            </div>
            <div class="mb-3">
                <label for="exampleFormControlInput2" class="form-label">글제목</label>
                <input type="text" class="form-control" id="exampleFormControlInput2" placeholder="제목" name="title"
                    value="${b.title}" disabled>
            </div>
            <div class="mb-3">
                <label for="exampleFormControlTextarea1" class="form-label">내용</label>

                <p class="main-content">
                    ${b.content}

                </p>

            </div>

            <div class="btn-group btn-group-lg custom-btn-group" role="group">
                <button id="mod-btn" type="button" class="btn btn-warning">수정</button>
                <button id="del-btn" type="button" class="btn btn-danger">삭제</button>
                <button id="list-btn" type="button" class="btn btn-dark">목록</button>
            </div>

            <!-- 댓글 영역 -->

            <div id="replies" class="row">
                <div class="offset-md-1 col-md-10">
                    <!-- 댓글 쓰기 영역 -->
                    <div class="card">
                        <div class="card-body">
                            <div class="row">
                                <div class="col-md-9">
                                    <div class="form-group">
                                        <label for="newReplyText" hidden>댓글 내용</label>
                                        <textarea rows="3" id="newReplyText" name="replyText" class="form-control"
                                            placeholder="댓글을 입력해주세요."></textarea>
                                    </div>
                                </div>
                                <div class="col-md-3">
                                    <div class="form-group">
                                        <label for="newReplyWriter" hidden>댓글 작성자</label>
                                        <input id="newReplyWriter" name="replyWriter" type="text" class="form-control"
                                            placeholder="작성자 이름" style="margin-bottom: 6px;">
                                        <button id="replyAddBtn" type="button"
                                            class="btn btn-dark form-control">등록</button>
                                    </div>
                                </div>

                            </div>
                        </div>
                    </div> <!-- end reply write -->

                    <!--댓글 내용 영역-->
                    <div class="card">
                        <!-- 댓글 내용 헤더 -->
                        <div class="card-header text-white m-0" style="background: #343A40;">
                            <div class="float-left">댓글 (<span id="replyCnt">0</span>)</div>
                        </div>

                        <!-- 댓글 내용 바디 -->
                        <div id="replyCollapse" class="card">
                            <div id="replyData">
                                <!-- 
                                < JS로 댓글 정보 DIV삽입 > 
                            -->
                            </div>

                            <!-- 댓글 페이징 영역 -->
                            <ul class="pagination justify-content-center">
                                <!-- 
                                < JS로 댓글 페이징 DIV삽입 > 
                            -->
                            </ul>
                        </div>
                    </div> <!-- end reply content -->
                </div>
            </div> <!-- end replies row -->

            <!-- 댓글 수정 모달 -->
            <div class="modal fade bd-example-modal-lg" id="replyModifyModal">
                <div class="modal-dialog modal-lg">
                    <div class="modal-content">

                        <!-- Modal Header -->
                        <div class="modal-header" style="background: #343A40; color: white;">
                            <h4 class="modal-title">댓글 수정하기</h4>
                            <button type="button" class="close text-white" data-bs-dismiss="modal">X</button>
                        </div>

                        <!-- Modal body -->
                        <div class="modal-body">
                            <div class="form-group">
                                <input id="modReplyId" type="hidden">
                                <label for="modReplyText" hidden>댓글내용</label>
                                <textarea id="modReplyText" class="form-control" placeholder="수정할 댓글 내용을 입력하세요."
                                    rows="3"></textarea>
                            </div>
                        </div>

                        <!-- Modal footer -->
                        <div class="modal-footer">
                            <button id="replyModBtn" type="button" class="btn btn-dark">수정</button>
                            <button id="modal-close" type="button" class="btn btn-danger"
                                data-bs-dismiss="modal">닫기</button>
                        </div>
                    </div>
                </div>
            </div>

            <!-- end replyModifyModal -->

        </div>

        <%@ include file="../include/footer.jsp" %>
    </div>

    <!-- 게시글 상세보기 관련 script -->
    <script>
        const [$modBtn, $delBtn, $listBtn] = [...document.querySelector('div[role=group]').children];

        // const $modBtn = document.getElementById('mod-btn');
        //수정버튼
        $modBtn.onclick = e => {
            location.href = '/board/modify?boardNo=${b.boardNo}';
        };

        //삭제버튼
        $delBtn.onclick = e => {
            if (!confirm('정말 삭제하시겠습니까?')) {
                return;
            }
            location.href = '/board/delete?boardNo=${b.boardNo}';
        };
        //목록버튼
        $listBtn.onclick = e => {
            location.href = '/board/list?pageNum=${p.pageNum}&amount=${p.amount}';
        };
    </script>

    <!-- 댓글관련 script -->
    <script>
        //원본 글 번호
        const bno = '${b.boardNo}';
        // console.log('bno:', bno);

        // 댓글 요청 URL
        const URL = '/api/v1/replies';

        //날짜 포맷 변환 함수
        function formatDate(datetime) {
            //문자열 날짜 데이터를 날짜객체로 변환
            const dateObj = new Date(datetime);
            // console.log(dateObj);
            //날짜객체를 통해 각 날짜 정보 얻기
            let year = dateObj.getFullYear();
            //1월이 0으로 설정되어있음.
            let month = dateObj.getMonth() + 1;
            let day = dateObj.getDate();
            let hour = dateObj.getHours();
            let minute = dateObj.getMinutes();
            //오전, 오후 시간체크
            let ampm = '';
            if (hour < 12 && hour >= 6) {
                ampm = '오전';
            } else if (hour >= 12 && hour < 21) {
                ampm = '오후';
                if (hour !== 12) {
                    hour -= 12;
                }
            } else if (hour >= 21 && hour <= 24) {
                ampm = '밤';
                hour -= 12;
            } else {
                ampm = '새벽';
            }
            //숫자가 1자리일 경우 2자리로 변환
            (month < 10) ? month = '0' + month: month;
            (day < 10) ? day = '0' + day: day;
            (hour < 10) ? hour = '0' + hour: hour;
            (minute < 10) ? minute = '0' + minute: minute;
            return year + "-" + month + "-" + day + " " + ampm + " " + hour + ":" + minute;
        }

        // 댓글 페이지 태그 생성 렌더링 함수
        function makePageDOM(pageInfo) {
            let tag = "";
            const begin = pageInfo.beginPage;
            const end = pageInfo.endPage;
            //이전 버튼 만들기
            if (pageInfo.prev) {
                tag += "<li class='page-item'><a class='page-link page-active' href='" + (begin - 1) +
                    "'>이전</a></li>";
            }
            //페이지 번호 리스트 만들기
            for (let i = begin; i <= end; i++) {
                let active = '';
                if (pageInfo.page.pageNum === i) {
                    active = 'p-active';
                }

                tag += "<li class='page-item " + active + "'><a class='page-link page-custom' href='" + i +
                    "'>" + i + "</a></li>";
            }
            //다음 버튼 만들기
            if (pageInfo.next) {
                tag += "<li class='page-item'><a class='page-link page-active' href='" + (end + 1) +
                    "'>다음</a></li>";
            }

            // 페이지태그 렌더링
            const $pageUl = document.querySelector('.pagination');
            $pageUl.innerHTML = tag;

            // ul에 마지막페이지 번호 저장.
            $pageUl.dataset.fp = pageInfo.finalPage;

        }

        // 댓글 목록 DOM을 생성하는 함수
        function makeReplyDOM({ //=> (replyMap)으로 하면 뒤에 계속 붙여야 하니까 이거를 풀어서 쓴거
            replyList,
            count,
            maker
        }) {
            // 각 댓글 하나의 태그
            let tag = '';

            if (replyList === null || replyList.length === 0) {                
                tag += "<div id='replyContent' class='card-body'>댓글이 아직 없습니다! ㅠㅠ</div>";

            } else {
                for (let rep of replyList) {
                    tag += "<div id='replyContent' class='card-body' data-replyId='" + rep.replyNo + "'>" +
                        "    <div class='row user-block'>" +
                        "       <span class='col-md-3'>" +
                        "         <b>" + rep.replyWriter + "</b>" +
                        "       </span>" +
                        "       <span class='offset-md-6 col-md-3 text-right'><b>" + formatDate(rep.replyDate) +
                        "</b></span>" +
                        "    </div><br>" +
                        "    <div class='row'>" +
                        "       <div class='col-md-6'>" + rep.replyText + "</div>" +
                        "       <div class='offset-md-2 col-md-4 text-right'>" +
                        "         <a id='replyModBtn' class='btn btn-sm btn-outline-dark' data-bs-toggle='modal' data-bs-target='#replyModifyModal'>수정</a>&nbsp;" +
                        "         <a id='replyDelBtn' class='btn btn-sm btn-outline-dark' href='#'>삭제</a>" +
                        "       </div>" +
                        "    </div>" +
                        " </div>";
                }
            }

            // 댓글 목록에 생성된 DOM 추가
            document.getElementById('replyData').innerHTML = tag;

            // 댓글 수 배치
            document.getElementById('replyCnt').textContent = count;

            // 페이지 렌더링
            makePageDOM(maker);

        }

        // 댓글 목록을 서버로부터 비동기요청으로 불러오는 함수
        function showReplies(pageNum = 1) {

            fetch(URL + '?boardNo=' + bno + '&pageNum=' + pageNum)
                .then(res => res.json())
                .then(replyMap => {
                    // console.log(replyMap.replyList);
                    makeReplyDOM(replyMap);
                });
        }

        // 페이지 버튼 클릭이벤트 등록 함수
        function makePageButtonClickEvent() {
            // 페이지 버튼 클릭이벤트 처리
            const $pageUl = document.querySelector('.pagination');
            $pageUl.onclick = e => {
                if (!e.target.matches('.page-item a')) return;

                e.preventDefault();
                // 누른 페이지 번호 가져오기
                const pageNum = e.target.getAttribute('href');
                // console.log(pageNum);

                // 페이지 번호에 맞는 목록 비동기 요청
                showReplies(pageNum);
            };
        }

        // 댓글 등록 이벤트 처리 핸들러 등록 함수
        function makeReplyRegisterClickEvent() {

            document.getElementById('replyAddBtn').onclick = makeReplyRegisterClickHandler;
        }

        // 댓글 등록 이벤트 처리 핸들러 함수
        function makeReplyRegisterClickHandler(e) {

            const $writerInput = document.getElementById('newReplyWriter');
            const $contentInput = document.getElementById('newReplyText');

            // 서버로 전송할 데이터들
            const replyData = {
                replyWriter: $writerInput.value,
                replyText: $contentInput.value,
                boardNo: bno
            };

            // POST요청을 위한 요청 정보 객체
            const reqInfo = {
                method: 'POST',
                headers: {
                    'content-type': 'application/json'
                },
                body: JSON.stringify(replyData)
            };

            fetch(URL, reqInfo)
                .then(res => res.text())
                .then(msg => {
                    if (msg === 'insert-success') {
                        alert('댓글 등록 성공');
                        // 댓글 입력창 리셋
                        $writerInput.value = '';
                        $contentInput.value = '';
                        // 댓글 목록 재요청, 작성 후 마지막 댓글 페이지로 이동
                        showReplies(document.querySelector('.pagination').dataset.fp);
                    } else {
                        alert('댓글 등록 실패');
                    }
                });
        }

        // 댓글 수정화면 열기 상세처리
        function processModifyShow(e, rno) {

            // console.log('수정버튼 클릭함!! after');

            // 클릭한 버튼 근처에 있는 댓글 내용텍스트를 얻어온다.
            const replyText = e.target.parentElement.parentElement.firstElementChild.textContent;
            //console.log('댓글내용:', replyText);

            // 모달에 해당 댓글내용을 배치한다.
            document.getElementById('modReplyText').textContent = replyText;

            // 모달을 띄울 때 다음 작업(수정완료처리)을 위해 댓글번호를 모달에 달아두자.
            const $modal = document.querySelector('.modal');
            $modal.dataset.rno = rno;
        }

        // 댓글 삭제 상세처리
        function processRemove(rno) {
            if (!confirm('진짜로 삭제합니까??')) return;

            fetch(URL + '/' + rno, {
                    method: 'DELETE'
                })
                .then(res => res.text())
                .then(msg => {
                    if (msg === 'del-success') {
                        alert('삭제 성공!!');
                        showReplies(); // 댓글 새로불러오기
                    } else {
                        alert('삭제 실패!!');
                    }
                });
        }

        // 댓글 수정화면 열기, 삭제 처리 핸들러 정의
        function makeReplyModAndDelHandler(e) {

            const rno = e.target.parentElement.parentElement.parentElement.dataset.replyid;

            e.preventDefault();

            // console.log('수정버튼 클릭함!! before');
            if (e.target.matches('#replyModBtn')) {
                processModifyShow(e, rno);
            } else if (e.target.matches('#replyDelBtn')) {
                processRemove(rno);
            }
        }

        // 댓글 수정 화면 열기, 삭제 이벤트 처리
        function openModifyModalAndRemoveEvent() {

            const $replyData = document.getElementById('replyData');
            $replyData.onclick = makeReplyModAndDelHandler;
        }

        // 댓글 수정 비동기 처리 이벤트
        function replyModifyEvent() {

            const $modal = $('#replyModifyModal');

            document.getElementById('replyModBtn').onclick =
                e => {
                    // console.log('수정 완료 버튼 클릭!');

                    // 서버에 수정 비동기 요청 보내기
                    const rno = e.target.closest('.modal').dataset.rno;
                    // console.log(rno);

                    const reqInfo = {
                        method: 'PUT',
                        headers: {
                            'content-type': 'application/json'
                        },
                        body: JSON.stringify({
                            replyText: $('#modReplyText').val(),
                            replyNo: rno
                        })
                    };

                    fetch(URL + '/' + rno, reqInfo)
                        .then(res => res.text())
                        .then(msg => {
                            if (msg === 'mod-success') {
                                alert('수정 성공!!');
                                $modal.modal('hide'); // 모달창 닫기
                                showReplies(); // 댓글 새로불러오기
                            } else {
                                alert('수정 실패!!');
                            }
                        });
                };
        }

        // 메인 실행부
        (function () {

            // 초기 화면 렌더링시 댓글 1페이지 렌더링
            showReplies();

            // 댓글 페이지 버튼 클릭이벤트 처리
            makePageButtonClickEvent();

            // 댓글 등록 버튼 클릭이벤트 처리
            makeReplyRegisterClickEvent();

            // 댓글 수정 모달 오픈, 삭제 이벤트 처리
            openModifyModalAndRemoveEvent();

            // 댓글 수정 완료 버튼 이벤트 처리
            replyModifyEvent();

        })();
    </script>

</body>

</html>