4/16/2018

(구버전) 스프링 게시판 만들기 #9. 댓글 작성 기능 구현

'(구버전) 스프링 게시판 만들기'는 내용이 부족하다고 판단하여
스프링 게시판 만들기를 새로 작성하였습니다.

링크 및 참조용으로 현재 게시물은 남겨두겠지만,
가급적이면 새로운 스프링 게시판 만들기를 참조해주시기 바랍니다.

게시물에 댓글을 작성하는 기능을 추가하겠습니다.

댓글은 특정 게시물에 종속되어있는 목록형태로 되어있습니다. 그렇기 때문에 게시물을 구분할 수 있어야하며, 댓글의 수정과 삭제 작업을 하기 위해서는 댓글도 구분할 수 있어야합니다.

create table myReply (
    bno       number            not null,
    rno       number            not null,
    content   varchar2(2000)    not null,
    writer    varchar2(30)      not null,
    regDate   date              default sysdate,
    primary key(bno, rno)
);

alter table myReply
    add constraint myReply_bno foreign key(bno)
    references myBoard(bno);

create sequence myReply_seq;

게시물 구분에 필요한 게시물 번호(bno), 댓글 구분에 필요한 게시물 번호(rno)가 있습니다. 댓글에 제목은 필수 사항이 아니므로, 작성자(writer)와 내용(content)과 작성일자(regDate)가 있습니다.

기본키(primary key)는 게시판과 다르게 게시물 번호(bno)와 댓글 번호(rno) 이렇게 2개로 구성되어있습니다. 기본키가 2개 이상인 경우, 이 요소들이 모두 중복되는걸 막을 수 있습니다. 즉, '첫번째 댓글'이라 할지라도 게시물에 의해서 구분된다는것 입니다.

다만, 이번엔 댓글 테이블에 하나의 시퀀스만 사용하기 때문에 댓글 번호(rno)가 겹칠 일은 없긴합니다.

댓글은 게시물이 있어야 작성할 수 있으므로, 댓글 테이블(myReply)에 있는 게시물 번호(bno)는 값을 추가하는게 아니라, 기존에 있는 게시판 테이블(myBoard)에 있는 게시물 번호(bno)를 따라가야합니다. 이것을 위해 참조키(foreign key)를 사용합니다. 참조키와 외래키는 같은 의미입니다.

참조키(foreign key)는 대상 테이블의 컬럼과 참조하는 테이블의 컬럼에 값이 존재하는지 확인하며, 참조하는 테이블에 값이 없을 경우 에러를 발생시킵니다.

먼저 글번호가 3109인 게시물에 오라클을 이용하여  더미용 댓글을 추가했습니다.

댓글을 불러오는 컬럼을 만들었고, 결과가 잘 나오는것을 확인한다음 commit; 명령을 이용하여 실제로 불러올 수 있도록 적용시킵니다.

댓글은 게시물과 더불어 자주 사용하며 그 형태가 고정되어있으므로, VO로 만들어서 domain패키지에 저장합니다.

src/main/resources → mappers에 새로운 매퍼 replyMapper.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="com.kuzuro.mappers.replyMapper">
   
 <!-- 댓글 조회 -->
 <select id="readReply" resultType="com.kuzuro.domain.ReplyVO">
  select
      rno, content, writer, regDate
  from myReply
      where bno = #{bno}
 </select>

</mapper>

이런식으로 각 기능별로 파일 자체를 분리해놓는것이 개발/관리하는데 있어서 용이하다고 생각합니다.

매퍼와 마찬가지로 DAO와 Service를 따로 작성합니다.

게시판 DAO와 Service, 댓글 DAO와 Service로 분류했습니다. 이렇게 분류한 이유도 개발/관리함에 있어서 용이하다고 생각하기 때문입니다.

게시물 컨트롤러(BoardController)에 댓글 서비스를 주입합니다.

DAO와 Service는 분리했는데 컨트롤러는 왜 같은걸 쓰냐면, 댓글은 게시물에 종속되기 때문입니다. 이 프로젝트가 냉장고라고 한다면, 게시물은 냉장실이고 댓글은 야채칸 같은 느낌입니다.

글 조회용 메서드에 댓글을 불러오는 코드를 추가합니다.

List<ReplyVO> repList = RepService.readReply(bno);
model.addAttribute("repList", repList);

read.jsp에 댓글 목록을 출력할 코드를 추가합니다.

<!-- 게시물 끝 -->
<div id="reply">
 <ol class="replyList">
 <c:forEach items="${repList}" var="repList">
 <li>
  <p>
   작성자 : ${repList.writer}<br />
   작성 날짜 :  <fmt:formatDate value="${repList.regDate}" pattern="yyyy-MM-dd" />
  </p>
  
  <p>${repList.content}</p>
 </li>
 </c:forEach>   
 </ol>
</div>

프로젝트를 실행하고, 더미 댓글을 추가한 3109번 게시물을 보면 댓글이 잘 출력되는걸 확인할 수 있습니다.

이제 댓글을 작성할 수 있도록 매퍼에 코드를 추가합니다.

<!-- 댓글 작성 -->
<insert id="writeReply">
 insert into
  myReply(bno, rno, content, writer)
    values(#{bno}, myReply_seq.nextval, #{content}, #{writer})
</insert>

리플 DAO와 Service에 코드를 추가합니다. 게시물 작성과 크게 다르지 않습니다.

컨트롤러에 댓글 작성 메서드를 추가합니다.

// 댓글 작성
@RequestMapping(value = "/replyWrite", method = RequestMethod.POST)
public String replyWrite(ReplyVO vo, SearchCriteria scri, RedirectAttributes rttr) throws Exception {
 logger.info("reply write");
 
 RepService.writeReply(vo);
 
 rttr.addAttribute("bno", vo.getBno());
 rttr.addAttribute("page", scri.getPage());
 rttr.addAttribute("perPageNum", scri.getPerPageNum());
 rttr.addAttribute("searchType", scri.getSearchType());
 rttr.addAttribute("keyword", scri.getKeyword());
 
 return "redirect:/board/read"; 
}

댓글 작성 메서드에서는 ReplyVO와 SearchCriteria를 받고있습니다. ReplyVO는 댓글 번호나 작성자, 내용이 있으니 필수이며, SearchCriteria는 현재 페이지를 저장할 목적으로 받고 있습니다.

RedirectAttributes에 SearchCriteria의 값을 넣어서 다음 댓글을 저장한 뒤 원래 페이지로 이동하게 됩니다.

이제 read.jsp에 댓글을 작성하는 폼을 추가합니다.

<section class="replyForm">
<form role="form" method="post" autocomplete="off">

 <input type="hidden" id="bno" name="bno" value="${read.bno}" readonly="readonly" />
 <input type="hidden" id="page" name="page" value="${scri.page}" readonly="readonly" />
 <input type="hidden" id="perPageNum" name="perPageNum" value="${scri.perPageNum}" readonly="readonly" />
 <input type="hidden" id="searchType" name="searchType" value="${scri.searchType}" readonly="readonly" />
 <input type="hidden" id="keyword" name="keyword" value="${scri.keyword}" readonly="readonly" />

 <p><label for="writer">작성자</label><input type="text" id="writer" name="writer" /></p>
 <p><label for="content">댓글 내용</label><textarea id="content" name="content"></textarea></p>
 <p>
  <button type="button" class="repSubmit">작성</button>
  <script>
  var formObj = $(".replyForm form[role='form']");
        
  $(".repSubmit").click(function(){
   formObj.attr("action", "replyWrite");
   formObj.submit();
  });
  </script>
 </p>
</form>
</section>

폼 안에 필요한 ReplyVO와 SearchCriteria가 들어가 있습니다. 스크립트에서는 폼의 액션 값을 설정하여, 작성 버튼을 눌렀을 때 replyWrite 메서드로 값이 갈 수 있도록 합니다.

프로젝트를 실행하고, 게시물에 댓글을 작성합니다.

작성 버튼을 클릭하면, 새로고침이 되면서 댓글이 추가되는것을 확인할 수 있습니다.

게시물 수정
  1. 안녕하세요 스프링을 공부하는 학생입니다.
    제가 댓글 수정 삭제 기능까지 확인해보고 게시글 삭제가 되는지 한번 확인해 봤더니
    violated - child record found : 정확한진 모르겠는데 bno가 상속관계라서 안 지워지는거 같더라고요..?
    이런건 어떻게 해결해야 할까요?

    답글삭제
    답글
    1. 안녕하세요? 음 먼저.. 죄송하다고 말씀드려야겠네요;

      지금 보고계시는 게시물은 제가 도중에 그만둬버려서 기능이 이것저것 빠져있다보니 이런 문제가 생기는거라서요..ㅠㅠ

      말씀하신 문제에 대해서는 현재 bno를 참조하는 테이블, 즉 댓글 테이블에서 bno를 조건으로 모두 지워주는 쿼리를 실행한 후 게시물 테이블에서 bno조건으로 지워주시면 됩니다.

      삭제
  2. 작성자가 댓글을 삭제했습니다.

    답글삭제
  3. 위와 같은 문제로 저도 검색해보아 해결책을 찾았습니다.
    초반 테이블 생성 시에 외래키지정부분을 다음과같이 하면 되더라구요.

    alter table myReply
    add constraint myReply_bno foreign key(bno)
    references myBoard(bno);
    ON DELETE CASCADE;


    이렇게하면 문제없이 댓글달린 게시글이 삭제됩니다!!



    블로그장님 제가 프로젝트 참고하고 공부하는 용도로 게시판글 열라게 보고있습니다
    자주와서 피드백 부탁드립니다 ㅠ,.ㅠㅋ

    답글삭제
  4. 딴거는 required 먹히는데 이상하게 댓글입력때는 required를 넣어놔도 전송되면서 에러발생하네요 ㅠㅠ 왜이러지;

    답글삭제