본문 바로가기
Spring

[웹프로젝트] 18. 게시글 댓글 작성 (상세페이지)

by moca7 2024. 10. 31.

 

 

ㅁ 비회원이면 textarea와 등록하기 버튼이 활성화 안되게끔 한다.

- 댓글 등록하고 다른 페이지로 이동하지 않고 그 페이지 그대로 보여줄 것이다. ajax로 요청한다.

 

 

ㅁ detail.jsp

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<style>
    #reply_area tbody>tr>th:nth-child(1){width:120px} /* 댓글쪽 스타일 */
</style>

</head>
<body>

    <div class="container p-3">

        <!-- Header, Nav start -->
        <jsp:include page="/WEB-INF/views/common/header.jsp"/>
        <!-- Header, Nav end -->
   
        <!-- Section start -->
        <section class="row m-3" style="min-height: 500px">
   
          <div class="container border p-5 m-4 rounded">
            <h2 class="m-4">게시글 상세</h2>
            <br>

            <a class="btn btn-secondary" style="float:right" href="${ contextPath }/board/list.do">목록으로</a>
            <br><br>
            <table align="center" class="table">
                <tr>
                    <th width="120">제목</th>
                    <td colspan="3">${ b.boardTitle }</td>
                </tr>
                <tr>
                    <th>작성자</th>
                    <td width="400">${ b.boardWriter }</td>
                    <th width="120">작성일</th>
                    <td>${ b.registDt }</td>
                </tr>
                <tr>
                    <th>첨부파일</th>
                    <td colspan="3">
                        <c:forEach var="at" items="${ b.attachList }">
                            <!-- a태그 하나당 첨부파일 하나다. 원본명을 노출시키고 다운로드도 원본명으로 다운받게 한다. -->
                        <a href="${ contextPath }${ at.filePath }/${ at.filesystemName }" download="${ at.originalName }">${ at.originalName }</a><br>
                      </c:forEach>  
                    </td>
                </tr>
                <tr>
                    <th>내용</th>
                    <td colspan="3"></td>
                </tr>
                <tr>
                    <td colspan="4">
                        <p style="height:150px">${ b.boardContent }</p>
                    </td>
                </tr>
            </table>
            <br>

            <!-- 수정하기, 삭제하기 버튼은 이글이 본인글일 경우만 보여져야됨 -->
            <div align="center">
                <a class="btn btn-primary" href="">수정하기</a>
                <a class="btn btn-danger" href="">삭제하기</a>
            </div>
            <br><br>

            <table id="reply_area" class="table" align="center">
                <thead>
                    <tr>
                   
                   
                        <c:choose>
                            <c:when test="${ empty loginUser }">
                            <th colspan="2" width="650">
                                <textarea class="form-control" rows="2" style="resize:none; width:100%" readonly>로그인 후 이용가능한 서비스 입니다.</textarea>
                            </th>
                            <th style="vertical-align: middle"><button class="btn btn-secondary disabled" id="reply_submit">등록하기</button></th>
                          </c:when>                        
                           
                          <c:otherwise>  
                            <th colspan="2" width="650">
                                <textarea class="form-control" id="reply_content" rows="2" style="resize:none; width:100%"></textarea>
                            </th>
                            <th style="vertical-align: middle"><button class="btn btn-secondary" onclick="fn_insertReply();">등록하기</button></th>
                                                </c:otherwise>
                      </c:choose>  
                       
                    </tr>
                    <tr>
                       <td colspan="3">댓글 (<span id="rcount">0</span>) </td>
                    </tr>
                </thead>
                <tbody>

                </tbody>
            </table>
           
            <script>
           
                $(document).ready(function(){
                    fn_replyList();
                })
               
                // 해당 게시글의 댓글 목록 조회용 (ajax) 함수
                function fn_replyList(){
                    $.ajax({
                        url: '${contextPath}/board/rlist.do',
                        data: "no=" + ${b.boardNo},
                        success: function(resData){
                            console.log(resData); // [{}, {}, ..]
                           
                            /*
                            <tr>
                        <th>user02</th>
                        <td>댓글입니다.너무웃기다앙</td>
                        <td>2020-04-10</td>
                    </tr>
                            */
                            $("#rcount").text(resData.length); // 댓글 수 출력
                                        let tr ="";
                            for(let i=0; i<resData.length; i++){
                                tr += "<tr>"
                                        +   "<th>" + resData[i].replyWriter + "</th>"
                                        +   "<td>" + resData[i].replyContent + "</td>"
                                        +   "<td>" + resData[i].registDt + "</td>"
                                        + "</tr>";
                            }
                           
                            $("#reply_area tbody").html(tr);
                                                   
                        }
                    })
                }
               
                // 댓글 등록용 (ajax) 함수
                function fn_insertReply(){
                    $.ajax({
                        url: '${contextPath}/board/rinsert.do',
                        type: 'post',
                        data: {
                            replyContent: $("#reply_content").val(),
                            refBoardNo: ${b.boardNo}
                        },
                       
                        success: function(resData){
                            if(resData == "SUCCESS"){
                                fn_replyList();
                                $("#reply_content").val("");
                            }else{
                                alert("댓글 작성 실패"); // 실패할 일은 거의 없지만 혹시 댓글이 너무 긴 경우
                            }
                        }
                    })
                }
            </script>
           
         
          </div>
   
        </section>
        <!-- Section end -->
   
        <!-- Footer start -->
        <jsp:include page="/WEB-INF/views/common/footer.jsp"/>
        <!-- Footer end -->
   
    </div>


   
</body>
</html>

 

- class에 disabled 주면 부트스트랩에 등록된 그게 된다. 

 

 

 

ㅁ BoardController

 

 
package com.br.spring.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.MemberDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;
import com.br.spring.service.BoardService;
import com.br.spring.util.FileUtil;
import com.br.spring.util.PagingUtil;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
@RequestMapping("/board")
@RequiredArgsConstructor
@Controller
public class BoardController {
 
  private final BoardService boardService;
  private final PagingUtil pagingUtil;
  private final FileUtil fileUtil;
 
 
  // 메뉴 바에 있는 메뉴 클릭시    /board/list.do => 1번 페이지 요청
  // 페이징 바에 있는 페이지 클릭시 /board/list.do?page=xx
  @GetMapping("/list.do")
  public void list(@RequestParam(value="page", defaultValue="1") int currentPage, Model model) {
   
    int listCount = boardService.selectBoardListCount();
   
    PageInfoDto pi = pagingUtil.getPageInfoDto(listCount, currentPage, 5, 5);
    List<BoardDto> list = boardService.selectBoardList(pi);
   
    model.addAttribute("pi", pi);
    model.addAttribute("list", list);
   
    // return "board/list"; 생략해도 됨.
   
  }
 
   
 
  @GetMapping("/search.do")
  public String search(@RequestParam(value="page") int currentPage
             , @RequestParam Map<String, String> search
             , Model model) {
    // 요청시 전달값을 매개변수를 둬서 받기
    // 지금은 무조건 page라는 key값으로 1번이 올거라 defaultValue를 안썼다. 써도 된다.
    // 요청하는 페이지 번호는 currentPage에 담긴다.
   
    // 지금 condition과 keyword 값을 받아줄 dto 커맨드 객체가 없다. String 형 변수 2개를 둬서 받아도 된다. 근데 어차피 넘길때는 Map에 담아서 넘기도록 서비스 impl를 설계했었다.
    // ajax때 했던 @RequestBody로 Map으로 바로 받을 수 있었다.
    // 아니면 @RequestParam Map<String, String> search로 해도 된다. 이렇게 해본다. 알아서 key과 value값이 담긴다.
    // Map<String, String> search => {condition=user_id|board_Title|board_content, keyword=란}
   
    int listCount = boardService.selectSearchListCount(search);
    PageInfoDto pi = pagingUtil.getPageInfoDto(listCount, currentPage, 5, 5);
    List<BoardDto> list = boardService.selectSearchList(search, pi);
   
    model.addAttribute("pi", pi);
    model.addAttribute("list", list);
    model.addAttribute("search", search); // list.jsp가 로드되는 경우가 2가지 경우다. /search.do라는 요청으로 로드될 때는 search라는 key값으로 Map이 담겨있다.
   
    return "board/list"; // 이미 만든 list.jsp로 포워딩
  }
 
 
 
  @GetMapping("/regist.do")
  public void registPage() {} // 메소드명은 별 의미 없다. 마음대로 작성.
 
  @PostMapping("/insert.do")
  public String regist(BoardDto board // 글제목, 글내용은 담겨있다.
             , List<MultipartFile> uploadFiles // 첨부파일 개수만큼 MultipartFile 객체가 담긴다.
             , HttpSession session // 로그인한 회원정보(회원번호)는 session에서 뽑아야 한다.
             , RedirectAttributes rdAttributes) {
   
    // board테이블에 insert할 데이터
    board.setBoardWriter( String.valueOf( ((MemberDto)session.getAttribute("loginUser")).getUserNo() ) );
   
    // 첨부파일 업로드 후에 attachment테이블에 insert할 데이터
    List<AttachDto> attachList = new ArrayList<>();
    for(MultipartFile file : uploadFiles) {
      if(file != null && !file.isEmpty()) {
        Map<String, String> map = fileUtil.fileupload(file, "board");
        attachList.add(AttachDto.builder()
                    .filePath(map.get("filePath"))
                    .originalName(map.get("originalName"))
                    .filesystemName(map.get("filesystemName"))
                    .refType("B")
                    .build() );
      }
    }
   
    board.setAttachList(attachList); // 제목, 내용, 작성자회원번호, 첨부파일들정보
   
    int result = boardService.insertBoard(board);
   
    if( attachList.isEmpty() && result == 1 || !attachList.isEmpty() && result == attachList.size() ) { // && (AND) 연산자가 || (OR) 연산자보다 우선순위가 높습니다.
     
      rdAttributes.addFlashAttribute("alertMsg", "게시글 등록 성공");
     
    }else {
     
      rdAttributes.addFlashAttribute("alertMsg", "게시글 등록 실패");
     
    }
   
   
    return "redirect:/board/list.do";
   
  }
 
 
   
 
 
  @GetMapping("/increase.do") // 조회수 증가용 (타인의 글일 경우 호출) => /board/detail.do 재요청

  public String increaseCount(int no) {
   
    boardService.updateIncreaseCount(no);
   
    return "redirect:/board/detail.do?no=" + no;
  }


 
  @GetMapping("/detail.do")
  public void detail(int no, Model model) { // 게시글 상세조회용 (내 글일 경우 이걸로 바로 호출)
    // 상세페이지에 필요한 데이터
    // 게시글(제목, 작성자, 작성일, 내용) 데이터, 첨부파일 데이터(원본명, 저장경로, 실제파일명)들
   
    // 게시글을 조회하는 쿼리 따로, 첨부파일 조회하는 쿼리 따로 담아서 상세페이지로 이동을 해도 된다.
    // 모든 데이터를 하나의 쿼리로 조회해서 하나의 BoardDto 객체에 담아서 상세페이지로 이동할 예정이다.
   
    BoardDto b = boardService.selectBoard(no);
    // boardNo, boardTitle, boardContent, boardWriter, registDt, attachList  
   
    model.addAttribute("b", b);
   
    log.debug("BoardDto:{}", b);
  }
 
 
  @ResponseBody // 기본적으로 응답 뷰로 인식하기 때문에 붙여야 한다.
  @GetMapping(value="/rlist.do", produces="application/json")
  public List<ReplyDto> replyList(int no) {
    return boardService.selectReplyList(no); // ajax로 응답데이터를 돌려줄 때는 그냥 return만 해주면 된다.
  }
 
  @ResponseBody
  @PostMapping("/rinsert.do")
  public String replyInsert(ReplyDto r, HttpSession session) { // ajax의 data에서 보낸 데이터의 key값과 매개변수 dto의 필드명이 같으면 바로 담긴다.
    r.setReplyWriter(String.valueOf( ((MemberDto)session.getAttribute("loginUser")).getUserNo()  ));
    int result = boardService.insertReply(r);
   
    return result > 0 ? "SUCCESS" : "FAIL";
  }
 
}

 

- 응답데이터가 한글 아니고 영문이어서 그 형식은 필요없다.

대신 응답 뷰 아니니 @ResponseBody 붙인다.

 

 

 

ㅁ BoardServiceImpl

 

 
package com.br.spring.service;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.br.spring.dao.BoardDao;
import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService {

  private final BoardDao boardDao;
 

  @Override
  public int selectBoardListCount() {
    return boardDao.selectBoardListCount();
  }

  @Override
  public List<BoardDto> selectBoardList(PageInfoDto pi) {
    return boardDao.selectBoardList(pi);
  }

  @Override
  public int selectSearchListCount(Map<String, String> search) {
    return boardDao.selectSearchListCount(search);
  }

  @Override
  public List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi) {
    return boardDao.selectSearchList(search, pi);
  }

  @Override
  public int insertBoard(BoardDto b) { // 컨트롤러에서 BoardDto에 작성자, 제목, 내용, 첨부파일 list도 담아서 넘겼다.
   
    int result = boardDao.insertBoard(b);
   
    List<AttachDto> list = b.getAttachList();
    if(result > 0 && !list.isEmpty()) {
      result = 0;
      for(AttachDto attach : list) {
        result += boardDao.insertAttach(attach);
      }
    }
   
    return result;
  }

  @Override
  public int updateIncreaseCount(int boardNo) {
    return boardDao.updateIncreaseCount(boardNo);
  }
 

  @Override
  public BoardDto selectBoard(int boardNo) {
    return boardDao.selectBoard(boardNo);
  }

  @Override
  public int deleteBoard(int boardNo) {
    return 0;
  }

  @Override
  public List<ReplyDto> selectReplyList(int boardNo) {
    return boardDao.selectReplyList(boardNo);
  }

  @Override
  public int insertReply(ReplyDto r) {
    return boardDao.insertReply(r);
  }
 
}

 

 

 

ㅁ BoardDao

 

 
package com.br.spring.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Repository
public class BoardDao {

  private final SqlSessionTemplate sqlSession;

  public int selectBoardListCount() {
    return sqlSession.selectOne("boardMapper.selectBoardListCount");
  }

  public List<BoardDto> selectBoardList(PageInfoDto pi) {
   
    RowBounds rowBounds = new RowBounds((pi.getCurrentPage() - 1) * pi.getBoardLimit(), pi.getBoardLimit());
   
    return sqlSession.selectList("boardMapper.selectBoardList", null, rowBounds);
  }

  public int selectSearchListCount(Map<String, String> search) {
    return sqlSession.selectOne("boardMapper.selectSearchListCount", search);
  }

  public List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi) {
   
    RowBounds rowBounds = new RowBounds((pi.getCurrentPage() - 1) * pi.getBoardLimit(), pi.getBoardLimit());
   
    return sqlSession.selectList("boardMapper.selectSearchList", search, rowBounds); // 쿼리를 완성시키기 위해 2번째 인자값으로 map객체를 넘김.
  }

  public int insertBoard(BoardDto b) {
    return sqlSession.insert("boardMapper.insertBoard", b);
  }

  public int insertAttach(AttachDto attach) {
    return sqlSession.insert("boardMapper.insertAttach", attach);
  }

  public BoardDto selectBoard(int boardNo) {
    return sqlSession.selectOne("boardMapper.selectBoard", boardNo); // 결국 우리는 하나의 객체로 받는다.
  }

  public int updateIncreaseCount(int boardNo) {
    return sqlSession.update("boardMapper.updateIncreaseCount", boardNo);
  }

  public List<ReplyDto> selectReplyList(int boardNo) {
    return sqlSession.selectList("boardMapper.selectReplyList", boardNo);
  }

  public int insertReply(ReplyDto r) {
    return sqlSession.insert("boardMapper.insertReply", r);
  }
   
}

 

 

 

ㅁ board-mapper.xml

 

 
<?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="boardMapper">

  <resultMap id="boardResultMap" type="BoardDto">
    <result column="board_no" property="boardNo" />
    <result column="board_title" property="boardTitle" />
    <result column="board_content" property="boardContent" />
    <result column="user_id" property="boardWriter" />
    <result column="count" property="count" />
    <result column="regist_date" property="registDt" />
    <result column="attach_count" property="attachCount" />
   
    <!-- has many 관계(한 객체에 여러객체(List)를 가지고 있는)일 경우 collection -->
    <!-- case 1) List 내의 객체를 매핑시켜주는 resultMap이 따로 존재하지 않을 경우
    <collection ofType="AttachDto" property="attachList">
      <result column="file_no" property="fileNo" />
      <result column="file_path" property="filePath" />
      <result column="filesystem_name" property="filesystemName" />
      <result column="original_name" property="originalName" />                
    </collection>-->
   
    <!-- case 2) List내의 객체를 매핑시켜주는 resultMap이 따로 존재할 경우 -->
    <collection resultMap="attachResultMap" property="attachList" />
  </resultMap>
 
  <resultMap id="attachResultMap" type="AttachDto">
    <result column="file_no" property="fileNo" />
    <result column="file_path" property="filePath" />
    <result column="filesystem_name" property="filesystemName" />
    <result column="original_name" property="originalName" />    
  </resultMap>

  <!-- 혹시라도 has a 관계(1:1)일 경우 => collection 대신에 association으로 사용. 그리고 type이 다름. -->



  <resultMap id="replyResultMap" type="ReplyDto">
    <result column="reply_no" property="replyNo" />
    <result column="user_id" property="replyWriter" />
    <result column="reply_content" property="replyContent" />
    <result column="regist_date" property="registDt" />
  </resultMap>




  <select id="selectBoardListCount" resultType="_int">
    select
           count(*)
      from board
     where status = 'Y'      
  </select>


  <select id="selectBoardList" resultMap="boardResultMap">
    select
           b.board_no
         , board_title
         , user_id
         , count
         , to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
         , (
             select count(*)
               from attachment
              where ref_type = 'B'
                and ref_no = b.board_no
            ) as "attach_count"
      from board b
      join member on (user_no = board_writer)
     where b.status = 'Y'
     order by board_no desc
  </select>
 
 
  <select id="selectSearchListCount" resultType="_int">
    select
           count(*)
      from board b
      join member on (user_no = board_writer)
     where b.status = 'Y'  
       and ${condition} like '%' || #{keyword} || '%'
  </select>
 
  <select id="selectSearchList" resultMap="boardResultMap">
    select
           b.board_no
         , board_title
         , user_id
         , count
         , to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
         , (
             select count(*)
               from attachment
              where ref_type = 'B'
                and ref_no = b.board_no
            ) as "attach_count"
      from board b
      join member on (user_no = board_writer)
     where b.status = 'Y'
       and ${condition} like '%' || #{keyword} || '%'
     order by board_no desc
  </select>
 
 
 
  <insert id="insertBoard">
    insert
      into board
      (
        board_no
      , board_title
      , board_writer
      , board_content
      )      
      values
      (
        seq_bno.nextval
      , #{boardTitle}
      , #{boardWriter}
      , #{boardContent}
      )
  </insert>
 
  <insert id="insertAttach">
    insert
      into attachment
      (
        file_no
      , file_path
      , filesystem_name
      , original_name
      , ref_type
      , ref_no  
      )
      values
      (
        seq_ano.nextval
      , #{filePath}
      , #{filesystemName}
      , #{originalName}
      , #{refType}
      , seq_bno.currval
      )
  </insert>
 
 
  <select id="selectBoard" resultMap="boardResultMap">
    select
            board_no
          , board_title
          , board_content
          , user_id
          , to_char(regist_date, 'YYYY-MM-DD') regist_date
          , file_no
          , file_path
          , filesystem_name
          , original_name
      from board b
      join member on (user_no = board_writer)
      left join attachment on (ref_type = 'B' and ref_no = board_no)
     where b.status = 'Y'
       and board_no = #{boardNo}
  </select>
 
 
  <update id="updateIncreaseCount">
    update
           board
       set count = count + 1
     where board_no = #{boardNo}      
       and status = 'Y'
  </update>
 
 
  <select id="selectReplyList" resultMap="replyResultMap">
    select
           reply_no
         , user_id
         , reply_content
         , to_char(regist_date, 'YYYY-MM-DD') regist_date
      from reply r
      join member on (user_no = reply_writer)
     where ref_bno = #{boardNo}
       and r.status = 'Y'
     order by reply_no desc
  </select>
 
  <insert id="insertReply">
    insert
      into reply
      (
        reply_no
      , reply_content
      , ref_bno
      , reply_writer
      )
      values
      (
        seq_rno.nextval
      , #{replyContent}
      , #{refBoardNo}
      , #{replyWriter}  
      )
  </insert>
 
</mapper>

 

 

 

ㅁ 서버 start

 

 

- 로그인하고 댓글 작성해보기.