10/13/2019

스프링 게시판 만들기 #14. 댓글 기본 설정 및 조회 구현

지금도 '게시판'이라 보기 힘들지만, 댓글이 없으니 더욱 보기 힘들어집니다.

게시물을 조회하는 view.jsp 파일을 열고, 게시물 수정/게시물 삭제 링크 하단에 주석을 추가합니다.

지금이야 내용이 많지 않으니 상관없지만, 나중에는 내용이 많아져서(적어도 여기에서는 아닙니다) 어디에서 뭐가 시작되고 끝나는지 모르기 때문에 작업하기 전에 미리 표시해두었습니다.

<hr />

<ul>
	<li>첫번째 댓글</li>
	<li>두번째 댓글</li>
	<li>세번째 댓글</li>
</ul>

<div>
	<p>
		<label>댓글 작성자</label> <input type="text">
	</p>
	<p>
		<textarea rows="5" cols="50"></textarea>
	</p>
	<p>
		<button type="button">댓글 작성</button>
	</p>
</div>

화면에 어떻게 출력되는지 확인하기 위해 임시로 html코드를 넣었습니다.

이렇게보니, 작성자를 추가하지 않았군요

<ul>
	<li>
		<div>
			<p>첫번째 댓글 작성자</p>
			<p>첫번째 댓글</p>
		</div>
	</li>
	<li>
		<div>
			<p>두번째 댓글 작성자</p>
			<p>두번째 댓글</p>
		</div>
	</li>
	<li>
		<div>
			<p>세번째 댓글 작성자</p>
			<p>세번째 댓글</p>
		</div>
	</li>
</ul>

리스트 태그(<li> ~ </li>)의 내용을 수정해줍니다. 댓글 작성자와 작성한 댓글을 구분했습니다.

대충 저렇게 나오면 무난할것 같습니다. 작성자 뒤에 날짜까지 표시되면 더 좋겠군요.

CREATE TABLE tbl_reply (
	rno 		int 			not null auto_increment,
	bno 		int 			not null,
	writer 	varchar(30) not null,
	content 	text 			not null,
	regDate 	timestamp 	not null default now(),
	PRIMARY KEY(rno, bno),
	FOREIGN KEY(bno) REFERENCES tbl_board(bno)
);

commit;

select * from tbl_reply;

댓글을 저장하기 위한 테이블인 tbl_reply를 생성합니다.

rno는 댓글의 고유 번호, bno는 댓글이 작성된 게시물의 번호입니다. 댓글 테이블은 게시물 테이블에 종속된 하위 테이블이므로, 상위 테이블의 pk(Primary key) 컬럼인 bno를 fk(foreign key)로 설정해주어야합니다. 또한, 하나의 게시물에 여러개의 댓글이 달릴 수 있으므로, 단순히 rno를 pk로 설정하는게 아닌, rno, bno 2개의 컬름을 pk로 설정합니다.

테스트 데이터를 넣기전, 게시물 테이블의 데이터를 확인합니다.
위에서 말씀드렸듯 댓글 테이블은 게시물 테이블의 하위 테이블이므로, 댓글 테이블의 bno의 값은 게시물 테이블의 bno값과 일치해야합니다.

확인하기 용이하도록 bno가 823인.. 마지막 게시물에 댓글을 생성하겠습니다.

먼저 인서트(insert)문을 테스트해봅니다.

bno는 꼭 게시물 테이블(tbl_board)에 있는 bno를 따라가야합니다.

업데이트(update)문을 테스트해봅니다.

rno와 bno 2개의 컬럼을 조건으로 수정합니다.

델리트(delete)문을 테스트해봅니다.

업데이트와 마찬가지로 rno와 bno 2개의 컬럼을 조건으로 수정합니다.

insert into tbl_reply(bno, writer, content, regDate)
	value(823, '댓글 작성자', '댓글 내용', sysdate());

이제 게시물에서 댓글이 어떻게 보이는지 확인하기 위해, 다시 인서트문을 이용하여 데이터를 넣었습니다.

db에 테이블을 추가했으므로, 테이블 형태의 VO(Value Object)를 추가해야합니다.

com.board.domain 을 우클릭한 뒤 New → Class 를 선택합니다.

파일명은 지나가는 사람 붙잡고 물어봐도 댓글 관련으로 보이도록, ReplyVO로 해줍니다.

테이블을 생성한 쿼리문을 그대로 가져와서 주석 처리해주고, 컬럼을 보며 VO의 변수를 생성해줍니다.
쿼리문을 복사해서 오는 이유는, 첫번째로 오타를 줄이기 위함이고, 두번째는 다른 사람이 보았을 때 실제로 어떤 테이블을 사용하는지 알리는 용도입니다.

그런데, 21번 라인에 Date타입에서 에러가 발생했습니다.

컨트롤(Ctrl) + 쉬프트(Shift) + O (영문자) 를 누르면

필요한 패키지들을 편리하게 임포트할 수 있습니다.

게터(getter)/세터(setter)를 추가하기 위해
상단 메뉴에서 Source → Generate Getters and Setters 를 선택합니다.

우측의 select All 버튼을 클릭하여 모든 변수를 선택한 뒤, 생성(Generate) 버튼을 클릭합니다.

자동으로 게터/세터가 추가되었습니다.

위에서 테스트했던 쿼리문을 저장할 매퍼를 생성합니다.

mappers 폴더를 우클릭한뒤 New → Other 를 선택합니다.

'xml'로 검색한 뒤, XML File을 선택하고 다음(Next) 버튼을 클릭합니다.

생성될 경로를 확인하고, 파일 이름을 replyMapper.xml 을 입력한 뒤 완료(Finish) 버튼을 클릭합니다.

파일명은 ~매퍼(mapper)지만, 실제론 일반 xml파일이기 때문에 별 내용이 없습니다.

마이바티스 사이트에서도 참고할 수 있지만, 기존에 있는 boardMapper.xml에서 필요한 정보를 복사/붙여넣기 하여 사용합니다.

이때, mapper의 namespace를 알맞게 입력해주고, 마지막의 </mapper> 태그도 잊지말고 넣어줍니다.

<!-- 댓글 조회 -->
<select id="replyList" parameterType="int" resultType="com.board.domain.ReplyVO">
	select
		rno, bno, writer, content, regDate
	from tbl_reply
		where bno = #{bno}		
</select>

<!-- 댓글 작성 -->
<insert id="replyWrite" parameterType="com.board.domain.ReplyVO">
	insert into tbl_reply(bno, writer, content, regDate)
		value(#{bno}, #{writer}, #{content}, #{regDate})
</insert>
	
<!-- 댓글 수정 -->
<update id="replyModify" parameterType="com.board.domain.ReplyVO">
	update tbl_reply set
		writer = #{writer},
		content = #{content}
	where rno = #{rno}
		and bno = #{bno}	
</update>

<!-- 댓글 삭제 -->
<delete id="replyDelete" parameterType="com.board.domain.ReplyVO">
	delete from tbl_reply
	where rno = #{rno}
		and bno = ${bno}	
</delete>

위에서 테스트했던 각각의 쿼리문을 매퍼에 추가해줍니다.

댓글 조회는 게시물 번호(bno)만 이용하여 조회하되 결과는 ReplyVO의 형태로 반환되므로, 파라미터타입(parameterType)는 정수형(int)이며 리절트타입(resultType)은 ReplyVO입니다.

댓글 작성/수정/삭제는 게시물 번호(bno)와 댓글 번호(rno)가 모두 필요하며, 추가적으로 작성자(writer), 댓글 내용(content), 작성 날짜(regDate)가 필요하며, 반환되는 데이터는 없으므로 파라미터타입은 ReplyVO, 리절트타입은 없습니다.

매퍼를 만들었으면, 매퍼에 접속할 DAO(Data Access Object)를 생성해주어야합니다.

com.board.dao 패키지(폴더)를 우클릭한뒤 New → Interface 를 선택합니다.

이때, 댓글은 reply이므로, com.reply.dao에 만드는게 아닌가? 라는 의문이 들 수 있는데.. 현재 프로젝트명이 board 이므로(!) 하위의 dao들이 모이는 패키지명이 com.board.com 인겁니다. 새로 패키지를 만들어서 하셔도 무관하며, 저는 dao끼리 묶어둘 생각으로 com.board.dao 패키지에 생성하겠습니다.

파일명은 ReplyDAO 로 입력하고 완료(Finish) 버튼을 클릭합니다.

인터페이스의 구현부를 생성하기 위해, com.board.dao 를 우클한뒤 New → Class 를 선택합니다.

파일명은 ReplyDAOImpl 로 입력하고, 이전에 생성한 인터페이스를 추가하기 위해 Add 버튼을 클릭합니다.

replyDAO로 검색하면, 이전에 생성한 인터페이스가 결과에 나옵니다. 확인(OK) 버튼을 클릭합니다.

인터페이스가 추가된걸 확인하고, 완료(Finish) 버튼을 클릭합니다.

ReplyDAO 와 ReplyDAOImpl 가 생성되었습니다.

// 댓글 조회
public List<ReplyVO> list(int bno) throws Exception;

// 댓글 조회
public void write(ReplyVO vo) throws Exception;

// 댓글 수정
public void modify(ReplyVO vo) throws Exception;

// 댓글 삭제
public void delete(ReplyVO vo) throws Exception;
@Inject
private SqlSession sql;

private static String namespace = "com.board.mappers.reply";

// 댓글 조회
@Override
public List<ReplyVO> list(int bno) throws Exception {
	return sql.selectList(namespace + ".replyList", bno);
}

// 댓글 작성
@Override
public void write(ReplyVO vo) throws Exception {
	sql.insert(namespace + ".replyWrite", vo);
}

// 댓글 수정
@Override
public void modify(ReplyVO vo) throws Exception {
	sql.update(namespace + ".replyModify", vo);
}

// 댓글 삭제
@Override
public void delete(ReplyVO vo) throws Exception {
	sql.delete(namespace + ".replyDelete", vo);
}

ReplyDAO와 ReplyDAOImpl에 코드를 추가합니다.

ReplyVO가 임포트되지 않아서 빨간줄(에러)이 그어진다면
컨트롤 (Ctrl) + 쉬프트 (Shift) + O (영문자) 를 눌러서 ReplyVO를 임포트하면 됩니다.

또한, ReplyDAOImpl 12번째줄처럼 @Repository 어노테이션을 꼭 추가해줍니다.
이 어노테이션이 없으면 실행이 안되며 에러를 뱉으므로 꼭 추가해야합니다.

ReplyDAO, ReplyDAOImpl 와 같은 방법으로
인터페이스인 ReplyService, 그 구현부인 ReplyServiceImpl 를 생성합니다.

// 댓글 조회
public List<ReplyVO> list(int bno) throws Exception;

// 댓글 조회
public void write(ReplyVO vo) throws Exception;

// 댓글 수정
public void modify(ReplyVO vo) throws Exception;

// 댓글 삭제
public void delete(ReplyVO vo) throws Exception;
@Inject
private ReplyDAO dao;

// 댓글 조회
@Override
public List<ReplyVO> list(int bno) throws Exception {
	return dao.list(bno);
}

@Override
public void write(ReplyVO vo) throws Exception {
	dao.write(vo);
}

@Override
public void modify(ReplyVO vo) throws Exception {
	dao.modify(vo);
}

@Override
public void delete(ReplyVO vo) throws Exception {
	dao.delete(vo);
}

ReplyVO가 임포트되지 않아서 빨간줄(에러)이 그어진다면
컨트롤 (Ctrl) + 쉬프트 (Shift) + O (영문자) 를 눌러서 ReplyVO를 임포트하면 됩니다.

DAO때와 마찬가지로, ReplyServiceImpl 12번째줄처럼 @Service 어노테이션을 꼭 추가해줍니다.

다음은 댓글을 다루는 컨트롤러(Controller)를 생성해줍니다.

com.board.conmtroller 를 우클릭한뒤 New → Class 를 선택합니다.

누가봐도 뻔한 이름인 ReplyController로 입력해주고, 완료(Finish) 버튼을 클릭합니다.

이번 게시물에선 ReplyController 를 사용할 일이 없으므로, 기본적인 부분만 입력해줍니다.

@Controller 어노테이션을 추가하여, 스프링이 ReplyController가 컨트롤러라는걸 알 수 있도록 해주고
@RequestMapping("/reply/*") 어노테이션을 추가하여, 어떤 URL로 접속해야 ReplyController로 접근할 수 있는지 설정해줍니다.

다음으론 ReplyService를 주입(inject)해주고, 이후에 추가할 메서드들이 무엇인지 주석으로 대충 적어줍니다.
이때 ReplyService를 임포트해주어야하는데, 컨트롤 (Ctrl) + 쉬프트 (Shift) + O (영문자) 를 눌러서 임포트해줍니다.

@Inject
private ReplyService replyService;

BoardController에 replyService를 주입(inject)해줍니다.
컨트롤 (Ctrl) + 쉬프트 (Shift) + O (영문자) 를 눌러서 임포트해줍니다.

기껏 ReplyController를 만들어놓고, 왜 BoardController에 작업하냐면
아직까지는 ReplyController에서 다룰 필요가 없기 때문입니다.
지금은 단순하게, 게시물을 읽어오면서 댓글도 같이 읽어오는 방식으로 진행합니다.

// 댓글 조회
List<ReplyVO> reply = null;
reply = replyService.list(bno);
model.addAttribute("reply", reply);

게시물 조회 메서드(getView)에 코드를 추가했습니다.

현재 게시물 번호(bno)를 이용하여 Service → DAO → Mapper → DataBase 로 진행하여, 현재 게시물에 작성된 댓글을 역순서로 반환(return)합니다. 반환한 값은 모델(model)에 reply 라는 이름으로 뷰(view, jsp)로 보내줍니다.

예를들어, 823번 게시물을 조회한다면 최종적으로 위와같은 결과가 나올것입니다.
댓글 번호(rno), 게시물 번호(bno), 댓글 작성자(writer), 댓글 내용(content), 작성날짜(regDate)의 데이터가 있습니다.

초반에 임시로 작성한 댓글 태그에는 댓글 번호(rno), 게시물 번호(bno), 작성날짜(regDate)가 없지만 현재 댓글 번호와 게시물 번호는 굳이 표시할 필요가 없으므로, 댓글 작성자 뒤에 작성날짜만 추가해주면 될것 같습니다.

<c:forEach items="${reply}" var="reply">
<li>
	<div>
		<p>${reply.writer} / ${reply.regDate}</p>
		<p>${reply.content }</p>
	</div>
</li>	
</c:forEach>

기존 <li> ~ </li> 태그는 모두 주석을 걸어주고, jsp 반복문을 추가합니다.

게시물 목록에서했던것과 마찬가지로, reply의 행 갯수만큼 <c:forEach> ~ </c:forEach> 내부 코드가 반복되어 출력됩니다.

DB에 있는 데이터가 출력되는거 같긴한데...
작성자 이름이나 내용이 저래서는 구분하기가 조금 그렇습니다.

update tbl_reply set
	writer = '익명의 작성자',
	content = '미스테리한 댓글'
where bno = 823;
		
select
	rno, bno, writer, content, regDate
from tbl_reply
	where bno = 823;

업데이트(update)문을 사용하여 알아보기 쉽게(?) 수정합니다.

오오.. 수정한대로 출력이 됩니다만.. 저 날짜 표현식은 언제봐도 마음에 들지 않습니다.

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

view.jsp 상단에 jstl 라이브러리중 fmt(포맷) 라이브러리를 임포트합니다.

<fmt:formatDate value="${reply.regDate}" pattern="yyyy-MM-dd" />

${reply.regDate} 를 yyyy-MM-dd 포맷으로 출력하도록 수정했습니다.

이제 좀 편안해졌습니다.

게시물 수정
  1. 지금 최근 게시판 만들기 잘보구잇는데 그 다음에는 이제 안올라오는건가요??

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      위 게시물은 초안이 19년 10월 13일에 작성된거고.. 실제로는 최근에 작성된 게시물입니다. (??)
      결과만 말씀드리면, 계속 작성은 하고 있습니다^^;

      다만 주기적으로 하지 않는것 뿐이지요-_-;;

      삭제
  2. 익명7/08/2022

    저번처럼 '파일명은 지나가는 사람 붙잡고 물어봐도 댓글 관련으로 보이도록, ReplyVO로 해줍니다.' 쓰셨는데 아무도 안웃어줘서 아깝네요 ㅋㅋ

    답글삭제
    답글
    1. 익명7/08/2022

      게시글 보고 그대로 따라했는데도 오류가 뜨네요 ㅠㅠ 어떻게 해결해야할까요?

      HTTP 상태 500 – 내부 서버 오류
      타입 예외 보고

      메시지 java.lang.NumberFormatException: For input string: "writer"

      설명 서버가, 해당 요청을 충족시키지 못하게 하는 예기치 않은 조건을 맞닥뜨렸습니다.

      삭제
    2. 안녕하세요? 방문해주셔서 감사합니다.

      java.lang.NumberFormatException: For input string는... 숫자를 입력해야할곳에 문자열이 입력되었다는 에러 메시지로 보입니다. 형변환을 시켜주셔야할 것 같습니다.

      삭제
  3. replymapper쪽에 네임스페이스가 com.board.mappers.reply 이걸로 되어 있던데 replyMapper아닌가요? reply라는게 아예없는거같습니다

    답글삭제
  4. 익명7/27/2023

    안녕하세요 pom.xml 에 jstl 설정 해두었고 jsp 상단에 import 해주었는데 <c:forEach items="${reply}" var="reply">
    <li>
    <div>
    <p>${reply.writer} / ${reply.regDate}</p>
    <p>${reply.content }</p>
    </div>
    </li>
    </c:forEach> 이부분 화면에서 출력이 안됩니다 ㅠㅠ 왜그럴까요?

    답글삭제
  5. 익명8/22/2023

    감사합니다~~~~~~~~~~~~~~~!!!!

    답글삭제
  6. 익명4/11/2024

    감사합니당 ! 복 많이 받으세요!!

    답글삭제