댓글 게시판 프로그램 (이어서)
분홍색 하이라이트 = 생소한 내용
하늘색 하이라이트 = 대비
상세 페이지
제목 클릭시 상세 페이지로 이동시 넘기는 값 (board_list.jsp 부분)
<!-- 제목 출력 부분 -->
<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
${b.board_subject}
</a>
- 상세페이지로 이동하려고 한다, "board_cont.do" 로 요청함
- 요청하면서 전달하는 값이 글 번호와 페이지 번호 외에도 state 라는 변수에 cont 라는 값을 저장해서 전달함
- 상세 페이지, 수정, 삭제 등 여러 기능을 1개의 요청으로 처리하기 위해서 state 값을 다르게 설정함

- Controller 클래스에서 "board_cont.do" 요청 처리 부분을 보자
/* 게시판 내용보기,삭제폼,수정폼,답변글폼 */
@RequestMapping(value = "/board_cont.do")
public String board_cont(@RequestParam("board_num") int board_num,
@RequestParam("page") String page,
@RequestParam("state") String state,
Model model) throws Exception {
if (state.equals("cont")) { // 내용보기일때만
boardService.hit(board_num); // 조회수 증가
}
BoardBean board = boardService.board_cont(board_num); // 상세 정보 구하기
model.addAttribute("bcont", board);
model.addAttribute("page", page);
if (state.equals("cont")) {// 내용보기일때
String board_cont = board.getBoard_content().replace("\n","<br/>");
model.addAttribute("board_cont", board_cont);
return "board/board_cont";// 내용보기 페이지 설정
// 글내용중 엔터키 친부분을 웹상에 보이게 할때 다음줄로 개행
} else if (state.equals("edit")) {// 수정폼
return "board/board_edit";
} else if (state.equals("del")) {// 삭제폼
return "board/board_del";
} else if (state.equals("reply")) {// 답변달기 폼
return "board/board_reply";
}
return null;
}
- 상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청을 모두 이 @RequestMapping("/board_cont.do") 에서 처리한다
- 글 번호, 페이지 번호, state 를 @RequestParam 으로 값을 바로 옆의 변수에 저장, state 값은 요청을 구분하기 위한 값
- state 값이 "cont" 인 경우, 즉 상세페이지 요청인 경우에만 조회수 값을 증가시키고 있다
- 상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청은 글 번호와 페이지 번호를 전달하는 등의 같은 형식으로 되어있고 1개 글에 대한 상세 정보를 구해오는 등의 같은 기능을 수행
- 그러므로 같은 요청으로 처리하고 다른 부분은 if-else if 문으로 state 값에 따른 다른 처리를 함
- 상세 페이지로 갈떄는 hit() 메소드로 조회수를 증가시키고, board_cont() 메소드로 상세 정보를 구해옴
<돌아온 후>
- 가져온 상세정보 객체 board 와 페이지 번호 page 를 Model 객체에 저장해서 각각의 View 페이지로 이동
- 글 번호는 객체 board 안에 있다
<조회수 증가>
- Service 클래스 BoardServiceImpl.java 에서 hit() 메소드 부분만
/* 조회수 증가 */
public void hit(int board_num) throws Exception {
boardDao.boardHit(board_num); // 조회수 증가
}
- DAO 클래스 BoardDaoImpl.java 에서 boardHit() 메소드 부분만
/* 게시판 조회수 증가 */
public void boardHit(int board_num) throws Exception {
sqlSession.update("Test.board_hit", board_num);
}
- Mapper 파일 board.xml 에서 id 가 "board_hit" 인 SQL문 부분만
<!-- 게시판 조회수 증가 -->
<update id="board_hit" parameterType="int">
update board53 set
board_readcount=board_readcount+1
where board_num=#{board_num}
</update>
<상세 정보 구하기>
- Service 클래스 BoardServiceImpl.java 에서 board_cont() 메소드 부분만
/* 상세정보 */
public BoardBean board_cont(int board_num) throws Exception {
BoardBean board = boardDao.getBoardCont(board_num);
return board;
}
- DAO 클래스 BoardDaoImpl.java 에서 getBoardCont() 메소드 부분만
/* 게시판 글내용보기 */
public BoardBean getBoardCont(int board_num) throws Exception {
return (BoardBean) sqlSession.selectOne("Test.board_cont",board_num);
}
- Mapper 파일 board.xml 에서 id 가 "board_cont" 인 SQL문 부분만
<!-- 게시판 내용보기 -->
<select id="board_cont" resultType="board"
parameterType="int">
select * from board53 where board_num=#{board_num}
</select>
- 상세 정보를 구해온다, 상세 페이지, 수정 폼, 삭제 폼, 답변달기 폼 에서 사용
- View 페이지를 보자
- board_cont.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시판 내용보기</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/board.css" />
</head>
<body>
<div id="boardcont_wrap">
<h2 class="boardcont_title">게시물 내용보기</h2>
<table id="boardcont_t">
<tr>
<th>제목</th>
<td>${bcont.board_subject}</td>
</tr>
<tr>
<th>글내용</th>
<td>
${board_cont}
<pre>${bcont.board_content}</pre>
</td>
</tr>
<tr>
<th>조회수</th>
<td>${bcont.board_readcount}</td>
</tr>
</table>
<div id="boardcont_menu">
<input type="button" value="수정" class="input_button"
onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=edit'" />
<input type="button" value="삭제" class="input_button"
onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=del'" />
<input type="button" value="답변" class="input_button"
onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=reply'" />
<input type="button" value="목록" class="input_button"
onclick="location='board_list.do?page=${page}'" />
</div>
</div>
</body>
</html>
- ${bcont.필드명} 으로 상세 정보를 가져와서 출력
버튼 처리
- '목록' 버튼 클릭시 "board_list.do" 로 요청하며 페이지 번호값을 가져감, 그래야 목록에서 원래 페이지로 돌아간다
- '수정', '삭제', '답변' 버튼 클릭시 동일한 요청 "board_cont.do" 로 요청한다, Controller 클래스로 가서는 state 값으로 구별
- 수정 폼, 삭제 폼, 답변작성 폼은 모두 글 번호, 페이지 번호가 필요하고, 상세정보를 돌려주므로 같은 요청으로 처리하는 것
- 답변작성 폼은 부모글에 대한 정보가 SQL문에 필요하므로 부모글의 상세정보를 가져가야 한다
ex) 부모글의 board_re_lev 보다 1 증가시킨 값이 들어가야한다
댓글 작성 폼
<input type="button" value="답변" class="input_button"
onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=reply'" />
- board_cont.jsp 에서 '답변' 버튼 클릭시 "board_cont.do" 로 요청한다, state 는 reply 이다
- 댓글 작성 폼은 부모글에 대한 정보가 SQL문에 필요하므로 부모글의 상세정보를 가져가야 한다
ex) 부모글의 board_re_lev 보다 1 증가시킨 값이 들어가야한다
- Controller, Service, DAO 로 가서 상세 정보를 가져온다, 그 부분은 생략
- Controller 의 "board_cont.do" 요청 부분은 상세 페이지를 설명할 때 했음
- View 페이지 board_reply.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시판 답변 달기</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/board.css" />
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="./js/board.js"></script>
</head>
<body>
<div id="boardreply_wrap">
<h2 class="boardreply_title">게시판 답변달기</h2>
<form method="post" action="board_reply_ok.do">
<input type="hidden" name="board_num" value="${bcont.board_num}" />
<input type="hidden" name="board_re_ref" value="${bcont.board_re_ref}" />
<input type="hidden" name="board_re_lev" value="${bcont.board_re_lev}" />
<input type="hidden" name="board_re_seq" value="${bcont.board_re_seq}" />
<input type="hidden" name="page" value="${page}" />
<table id="boardreply_t">
<tr>
<th>글쓴이</th>
<td>
<input name="board_name" id="board_name" size="14" class="input_box" />
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" name="board_pass" id="board_pass"
size="14" class="input_box" />
</td>
</tr>
<tr>
<th>글제목</th>
<td>
<input name="board_subject" id="board_subject" size="40"
class="input_box" value="Re:${bcont.board_subject}" />
</td>
</tr>
<tr>
<th>글내용</th>
<td>
<textarea name="board_content" id="board_content" rows="8"
cols="50" class="input_box" ></textarea>
</td>
</tr>
</table>
<div id="boardreply_menu">
<input type="submit" value="답변" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#board_name').focus();" />
</div>
</form>
</div>
</body>
</html>
- 글 번호값과 페이지 번호는 3번 전달되는데, 목록 페이지 -> 상세 페이지 -> 댓글작성폼(수정폼,삭제폼) -> 댓글작성/수정/삭제 로 넘어간다.
- hidden 으로 페이지 번호, 부모 글의 정보(글 번호, ref, seq, lev) 를 전달한다
- "board_reply_ok.do" 로 요청
- Controller 클래스 BoardController.java 에서 "board_reply_ok.do" 요청 부분만
/* 게시판 답변달기 저장 */
@RequestMapping(value = "/board_reply_ok.do", method = RequestMethod.POST)
public String board_reply_ok(@ModelAttribute BoardBean b,
@RequestParam("page") String page) throws Exception {
boardService.reply_ok(b);
return "redirect:/board_list.do?page=" + page;
}
- 답변달기 폼에서 사용자가 입력한 값들과 hidden 으로 넘겨준 부모글의 정보는 BoardBean 객체 b 에 저장
- 페이지 번호는 DTO 프로퍼티안에 저장할 수 있는 곳이 없으므로 따로 받아서 저장
<돌아온 후>
- 댓글을 단 후 reply_ok() 에서 돌아오면 View 에 출력하는 대신 바로 목록 페이지로 이동하기 위해 "redirect:board_list.do" 요청하고 페이지 번호를 전달한다
- Service 클래스 BoardServiceImpl.java 에서 reply_ok() 메소드 부분만
/* 게시판 댓글 달기 */
public void reply_ok(BoardBean b) throws Exception {
boardDao.refEdit(b); // 기존 댓글 board_re_seq값 1증가
b.setBoard_re_lev(b.getBoard_re_lev() + 1); // 부모보다 1증가된 값을 저장함
b.setBoard_re_seq(b.getBoard_re_seq() + 1);
boardDao.boardReplyOk(b);
}
댓글을 달때 수행하는 SQL문 2가지
1. Update SQL문으로 기존 댓글들의 board_re_seq 값을 1 증가
- 이후 부모글보다 1 증가한 board_re_lev, 부모글보다 1 증가한 board_re_seq 를 저장함
2. Insert SQL문으로 댓글 삽입
- DAO 클래스 BoardDaoImpl.java 에서 refEdit() 메소드 부분만
/* 답변글 레벨 증가 */
public void refEdit(BoardBean b) throws Exception {
sqlSession.update("Test.board_Level", b);
}
- Mapper 파일 board.xml 에서 id 가 "board_Level" 인 SQL문 부분만
<!-- 답변글 레벨 증가 -->
<update id="board_Level" parameterType="board">
update board53 set
board_re_seq=board_re_seq+1
where board_re_ref=#{board_re_ref} and
board_re_seq > #{board_re_seq}
</update>
- 부모글과 ref 값이 같고, 부모글보다 seq 값이 큰 글들만 board_re_seq 값을 1 증가시킴
<Service 클래스에서 돌아온 후>
- Service 클래스로 넘어온 DTO 객체 b는 작성할 댓글의 제목, 내용들을 저장하고 있지만, board_re_seq, board_re_ref, board_re_lev, board_num 은 부모 글의 값이다
- DTO 객체 b의 board_re_lev 컬럼의 값을 부모글의 board_re_lev 값보다 1 증가시킨 값을 넣는다
- DTO 객체 b의 board_re_seq 컬럼의 값을 부모글의 board_re_seq 값보다 1 증가시킨 값을 넣는다
- DAO 클래스 BoardDaoImpl.java 에서 boardReplyOkay() 메소드 부분만
/* 답변글 저장 */
public void boardReplyOk(BoardBean b) throws Exception {
sqlSession.insert("Test.board_reply", b);
}
- Mapper 파일 board.xml 에서 id 가 "board_reply" 인 SQL문 부분만
<!-- 답변글 저장 -->
<insert id="board_reply" parameterType="board">
insert into board53
(board_num,board_name,board_subject,board_content,
board_pass,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
values(board53_num_seq.nextval,#{board_name},#{board_subject},#{board_content},
#{board_pass},#{board_re_ref},#{board_re_lev},#{board_re_seq},0,SYSDATE)
</insert>
- board_num 은 원문 글이든 댓글이든 모두 시퀀스로 입력받음
- 댓글의 ref 값은 부모글의 ref 값과 같아야하므로 그대로 #{board_re_ref} 값을 저장한다
- lev, seq 값은 이미 부모글에서 1 증가시킨 값이므로 그대로 넣음
글 수정 폼
<input type="button" value="수정" class="input_button"
onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=edit'" />
- board_cont.jsp 에서 '수정' 버튼 클릭시 "board_cont.do" 로 요청한다, state 는 edit 이다
- 수정을 위해 글 번호 필요
- 수정 성공 후 원래 페이지로 돌아가야하므로 페이지 번호가 필요
- Controller, Service, DAO 로 가서 상세 정보를 가져온다, 그 부분은 생략
- Controller 의 "board_cont.do" 요청 부분은 상세 페이지를 설명할 때 했음
- View 페이지 board_edit.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시판 수정폼</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/bbs.css" />
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="<%=request.getContextPath() %>/js/board.js"></script>
</head>
<body>
<div id="bbswrite_wrap">
<h2 class="bbswrite_title">게시판 수정폼</h2>
<form method="post" action="board_edit_ok.do" onSubmit="return board_check()">
<input type="hidden" name="board_num" value="${bcont.board_num}" />
<input type="hidden" name="page" value="${page}" />
<table id="bbswrite_t">
<tr>
<th>글쓴이</th>
<td>
<input name="board_name" id="board_name" size="14" class="input_box"
value="${bcont.board_name}" />
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input type="password" name="board_pass" id="board_pass" size="14"
class="input_box" />
</td>
</tr>
<tr>
<th>글제목</th>
<td>
<input name="board_subject" id="board_subject" size="40"
class="input_box" value="${bcont.board_subject}" />
</td>
</tr>
<tr>
<th>글내용</th>
<td>
<textarea name="board_content" id="board_content" rows="8" cols="50"
class="input_box">${bcont.board_content}</textarea>
</td>
</tr>
</table>
<div id="bbswrite_menu">
<input type="submit" value="수정" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#board_name').focus();" />
</div>
</form>
</div>
</body>
</html>
- 앞에서 페이지 번호와 글 상세 정보가 넘어왔다
- 수정폼에 정보 입력 후 "수정" 클릭시 "board_edit_ok.do" 로 요청하며 페이지 번호, 글 번호를 hidden 객체로 전달
- value 속성으로 글 상세 정보를 수정폼에 뿌려주고 있다
- 사용자가 입력한 값 4가지도 또한 전달된다
글 수정
- 수정폼 board_edit.jsp 에 정보 입력 후 "수정" 클릭시 "board_edit_ok.do" 로 요청
- 페이지 번호, 글 번호를 hidden 객체로 전달, 사용자가 입력한 값들도 넘어온다
- Controller 클래스 BoardController.java 에서 "board_edit_ok.do" 요청 부분만
/* 게시판 수정 */
@RequestMapping(value = "/board_edit_ok.do", method = RequestMethod.POST)
public String board_edit_ok(@ModelAttribute BoardBean b,
@RequestParam("page") String page,
Model model) throws Exception {
// 수정 메서드 호출
BoardBean board = boardService.board_cont(b.getBoard_num());
int result = 0;
if (!board.getBoard_pass().equals(b.getBoard_pass())) {// 비번 불일치
result = 1;
model.addAttribute("result", result);
return "board/updateResult";
} else {
// 수정 메서드 호출
boardService.edit(b);
}
return "redirect:/board_cont.do?board_num=" + b.getBoard_num()
+ "&page=" + page + "&state=cont";
}
- 사용자가 수정폼에 입력한 값과 hidden 으로 넘어온 값 중 글 번호를 DTO 객체 b 에 받아서 저장한다
- hidden 으로 넘어온 페이지 번호는 따로 받아서 저장한다
- 즉 객체 b 안에는 글 번호와 사용자가 수정폼에 입력한 4가지 정보가 있다
수정할때 수행하는 SQL문 2가지
1. Select SQL문으로 상세정보를 가져와서 비번이 일치하는지 비교
2. Update SQL문으로 글 수정
<돌아온 후>
- 수정 후 View 대신 상세페이지로 바로 가기 위해 "board_cont.do" 로 요청하며 글 번호,페이지 번호 전달
- state 값은 cont 로 설정해야 상세 페이지를 요청함
+ 각 요청마다 필요한 값들이 있음, 여기서 "board_cont.do" 요청에는 글 번호, 페이지 번호, state 가 필요
<상세 정보 구하기>
- Service 클래스 BoardServiceImpl.java 에서 board_cont() 메소드는 상세 정보를 구할때도 사용했으므로 설명 생략
- 비번이 일치하면 수정 메소드 edit() 를 호출
<글 수정>
- Service 클래스 BoardServiceImpl.java 에서 edit() 메소드 부분만
/* 게시판 수정 */
public void edit(BoardBean b) throws Exception {
boardDao.boardEdit(b);
}
- DAO 클래스 BoardDaoImpl.java 에서 boardEdit() 메소드 부분만
/* 게시물 수정 */
public void boardEdit(BoardBean b) throws Exception {
sqlSession.update("Test.board_edit", b);
}
- Mapper 파일 board.xml 에서 id 가 "board_edit" 인 SQL문 부분만
<!-- 게시물 수정 -->
<update id="board_edit" parameterType="board">
update board53 set
board_name=#{board_name},
board_subject=#{board_subject},
board_content=#{board_content}
where board_num=#{board_num}
</update>
- 수정이 끝나고 다시 상세페이지로 가게된다, 조회수도 1 증가함
글 삭제 폼
<input type="button" value="삭제" class="input_button"
onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=del'" />
- board_cont.jsp 에서 '삭제' 버튼 클릭시 "board_cont.do" 로 요청한다, state 는 del 이다
- 삭제를 위해 글 번호 필요
- 삭제 성공 후 원래 페이지로 돌아가야하므로 페이지 번호가 필요
- Controller, Service, DAO 로 가서 상세 정보를 가져온다, 그 부분은 생략
- Controller 의 "board_cont.do" 요청 부분은 상세 페이지를 설명할 때 했음
- View 페이지 board_del.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>게시물 삭제</title>
<link rel="stylesheet" type="text/css" href="./css/board.css" />
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
function del_check(){
if($.trim($("#pwd").val())==""){
alert("삭제 비번을 입력하세요!");
$("#pwd").val("").focus();
return false;
}
}
</script>
</head>
<body>
<div id="boarddel_wrap">
<h2 class="boarddel_title">게시물 삭제</h2>
<form method="post" action="board_del_ok.do"
onsubmit="return del_check()">
<input type="hidden" name="board_num" value="${bcont.board_num}" />
<input type="hidden" name="page" value="${page}" />
<table id="boarddel_t">
<tr>
<th>삭제 비밀번호</th>
<td>
<input type="password" name="pwd" id="pwd" size="14"
class="input_box" />
</td>
</tr>
</table>
<div id="boarddel_menu">
<input type="submit" value="삭제" class="input_button" />
<input type="reset" value="취소" class="input_button"
onclick="$('#pwd').focus();" />
</div>
</form>
</div>
</body>
</html>
- 앞에서 페이지 번호와 글 상세 정보가 넘어왔다
- 삭제 폼에서는 글의 상세 정보에서 글 번호만 가져와서 사용함
- 삭제 폼에 정보 입력 후 "삭제" 클릭시 "board_del_ok.do" 로 요청하며 페이지 번호, 글 번호를 hidden 객체로 전달
- 사용자가 입력한 비밀번호도 전달된다
글 삭제
- 삭제폼 board_del.jsp 에 비번 입력 후 "삭제" 클릭시 "board_del_ok.do" 로 요청
- 페이지 번호, 글 번호를 hidden 객체로 전달, 사용자가 입력한 값들도 넘어온다
- 비번을 가져올때, 삭제를 할때 글 번호가 필요하고 삭제 후 원래 페이지로 돌아가기 위해 페이지 번호가 필요하다
- Controller 클래스 BoardController.java 에서 "board_del_ok.do" 요청 부분만
/* 게시판 삭제 */
@RequestMapping(value = "/board_del_ok.do", method = RequestMethod.POST)
public String board_del_ok(@RequestParam("board_num") int board_num,
@RequestParam("page") int page,
@RequestParam("pwd") String board_pass,
Model model) throws Exception {
BoardBean board = boardService.board_cont(board_num);
int result=0;
if (!board.getBoard_pass().equals(board_pass)) { // 비번 불일치
result = 1;
model.addAttribute("result", result);
return "board/deleteResult";
} else { // 비번 일치
boardService.del_ok(board_num);
}
return "redirect:/board_list.do?page=" + page;
}
- 사용자가 삭제폼에 입력한 비밀번호와 hidden 으로 넘어온 글 번호와 페이지 번호를 @RequestParam 으로 따로 받음
삭제할때 수행하는 SQL문 2가지
1. Select SQL문으로 상세정보를 가져와서 비번이 일치하는지 비교
2. Delete SQL문으로 글 삭제
<돌아온 후>
- 수정 후 목록 페이지로 이동하기 위해 "board_list.do" 로 요청하고 페이지 번호를 전달
<상세 정보 구하기>
- Service 클래스 BoardServiceImpl.java 에서 board_cont() 메소드는 상세 정보를 구할때도 사용했으므로 설명 생략
- 비번 비교를 하기 위해 상세 정보를 가져온다
- 비번이 일치하면 삭제 메소드 del_ok() 호출
<글 삭제>
- Service 클래스 BoardServiceImpl.java 에서 del_ok() 메소드 부분만
/* 게시판 삭제 */
public void del_ok(int board_num) throws Exception{
boardDao.boardDelete(board_num);
}
- DAO 클래스 BoardDaoImpl.java 에서 boardDelete() 메소드 부분만
/* 게시물 삭제 */
public void boardDelete(int board_num) throws Exception {
sqlSession.delete("Test.board_del", board_num);
}
- Mapper 파일 board.xml 에서 id 가 "board_del" 인 SQL문 부분만
<!-- 게시물 삭제 -->
<delete id="board_del" parameterType="int">
delete from board53 where
board_num=#{board_num}
</delete>
ajax 활용 댓글 게시판
실습 준비
- 클라우드의 board1 프로젝트를 STS 에 import
이전 프로젝트와 달라진 점
1. 원문을 작성하는 양식과 댓글을 작성하는 양식이 같이 되어있다, 하나로 처리함
- 이때는 Sequence 를 사용하지 못한다
- Sequence 를 사용하지 못하는 이유 : 원문의 컬럼 ref 는 시퀀스로 들어가야하고, 댓글의 컬럼 ref 는 시퀀스로 들어가면 안된다
2. 검색 기능 포함
- Mapper 파일을 보면 SQL문들이 동적 SQL문으로 되어있다
- SQL문의 LIKE 연산자, 와일드카드 % 를 사용해야 한다, 동적 SQL문을 써야 그걸 처리할 수 있음
- 검색된 결과의 데이터 개수를 구하는 SQL문, 검색된 결과의 리스트를 구하는 SQL문 등이 있다
- 나중에 검색 기능 설명 할 것
3. 삭제시 상태값만 변경하고 목록에서 제목 대신 "삭제된 데이터입니다" 표시
4. 동적 SQL문 사용, when 태그, if 태그 등
5. 환경설정 중 달라진 부분
- jdbc.properties 에 DB 연동 정보를 입력하고 그 파일을 root-context.xml 에서 읽어서 처리
- jdbc.properties
jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@127.0.0.1:1521:xe
jdbc.username=spring
jdbc.password=spring123
jdbc.maxPoolSize=20
- root-context.xml 부분
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
</bean>
- resouces 폴더 안에 jdbc.properties 파일이 있으므로 classpath: 를 붙이기
- jdbc.properties 파일안의 변수값들을 불러서 사용
테이블 생성
- board1.sql 에서 spring 계정으로 연결 후 테이블 생성
- board1.sql
-- 게시판
select * from tab;
select * from board;
create table board (
num number primary key, -- key
writer varchar2(20) not null, -- 작성자
subject varchar2(50) not null, -- 제목
content varchar2(500) not null, -- 본문
email varchar2(30) , -- 이메일
readcount number default 0, -- 읽은 횟수
passwd varchar2(12) not null, -- 암호
ref number not null, -- 답변글끼리 그룹
re_step number not null, -- ref내의 순서
re_level number not null, -- 들여쓰기
ip varchar2(20) not null, -- 작성자 ip
reg_date date not null, -- 작성일
del char(1)
);
update board set readcount = 51 where num = 250;
- 테이블 board 생성
- 원문을 작성하는 양식과 댓글을 작성하는 양식이 같을때는 Sequence 를 사용하지 못한다!
- 그래서 컬럼 num 에는 시퀀스로 값을 입력 가능하지만, ref 에 시퀀스로 값을 입력할 수 없음, 즉 같이 시퀀스를 사용 불가
- 컬럼 num 에는 그룹함수 max 를 사용해서 num 컬럼에 들어가있는 값 중 최대값을 구한 후 새로운 데이터를 insert 시킬땐 그 최대값에 1 을 증가한 값을 넣음
- 처음 글을 작성할떄는 컬럼 num 에 1 입력
테이블 board 컬럼 설명
- ip : 작성자 ip 를 저장할 것
- del : 글을 삭제하면 컬럼 del 에 "y" 란 글로 상태값을 바꿀 것
- 글을 삭제하더라도 실제로 delete SQL문으로 삭제시키지 않고, update SQL문으로 컬럼 del 을 "y" 로 수정, 그러면 목록페이지에서 글 제목에 링크가 걸리지 않게 됨
페이징 처리를 위해서 필요한 값
- 전체 목록을 구할때는 총 데이터 개수 ( 전체 게시물 개수 ) 가 필요
- 검색된 데이터 목록에는 페이징 처리를 위해 검색된 데이터 총 개수가 필요함
흐름 보기
- 프로젝트 실행
- 글을 작성해보자
- 검색 기능을 보자
- 내가 검색한 단어를 포함한 글만 목록에 출력해준다
- 위의 select-option 에서 어떤 컬럼을 기준으로 검색할건지 선택 가능하다
- 제목 + 내용은 OR 로 처리함
A가 포함된 단어를 검색하는 SQL문
select * from emp where ename like '%A%'
- 고정된 값이 아니라서 동적 SQL문이라고 한다
- 제목으로 검색한다면 제목을 가진 컬럼명이 ename 자리에 들어간다
- 내요응로 검색한다면 내용을 가진 컬럼명이 ename 자리에 들어간다
- 검색어가 A 자리에 들어간다, 사용자가 입력양식에 입력한 값을 A 자리에 넣음
- select-option 으로 이 양식을 만드는데, select 는 변수명이 되고 option 의 value 속성은 해당 컬럼(제목, 내용 등) 이 됨
<form action="list.do">
<input type="hidden" name="pageNum" value="1">
<select name="search">
<option value="subject" <c:if test="${search=='subject'}">selected="selected" </c:if>>제목</option>
<option value="content" <c:if test="${search=='content'}">selected="selected" </c:if>>내용</option>
<option value="writer" <c:if test="${search=='writer'}">selected="selected" </c:if>>작성자</option>
<option value="subcon" <c:if test="${search=='subcon'}">selected="selected" </c:if>>제목+내용</option>
</select>
<input type="text" name="keyword">
<input type="submit" value="확인">
</form>
- 선택하는 값이 따라 value 값을 달리해서 option 의 value 에 컬럼명을 넣는다
ex) '제목' 선택시 subject 컬럼이 value 에 들어감, 즉 그게 select 의 name 인 search 의 값이 된다
- 즉 사용자가 옵션에서 선택한 값이 search 가 되고, 검색어창에 입력한 값이 keyword 가 된다
- 그럼 그 값들이 폼 -> Controller -> Service -> DAO -> Mapper 로 가서 SQL문 안에 들어감!
- DTO Board 클래스에 search 와 keyword 를 저장할 수 있는 프로퍼티를 추가했다!
+ 테이블엔 search, keyword 컬럼이 없다!
- form 태그로 감싸져 있다, 즉 이부분만 다시 list.do 로 요청해서 검색창에서 "확인" 을 누를때마다 다시 목록을 가져옴
- Board.java 부분 (DTO)
추가된 프로퍼티 설명
- Mapper 파일로 값을 1개만 전달 가능하므로 page 번호만 전달해서 거기서 startRow, endRow 를 계산했었다, 여기서는 DTO Board 객체의 startRow, endRow 프로퍼티에 값을 담아서 Mapper 파일에 전달할 것
- select 의 name 값이 search, 검색어 입력양식의 name 값이 keyword 이다, 이 값들을 DTO 객체에 저장해서 Mapper 파일로 전달
- 글의 상세페이지에서 "답변" 을 눌러 댓글을 달아보자
- 댓글 작성폼과 원문 작성폼이 같고, 내부 처리가 같다
- 원문인지 댓글인지 구별을 내부적으로 해야한다
- 글을 삭제해보자
- 목록 페이지 list.jsp 에서 삭제된 데이터값은 조건식으로 구별해서 제목 대신 "삭제된 데이터 입니다" 메세지 뿌림
- 링크도 걸지 않는다
검색 목록 출력
- 전체 목록과 검색 목록을 출력을 같은 곳에서 하고 있다
- list.jsp 에서 전체 목록의 페이징 처리와 검색을 했을 경우의 페이징 처리 를 따로 만들고 if 태그로 구분한다
페이징 처리
- 페이징 처리를 하는 클래스를 따로 만들어뒀다
- 기본변수와 파생변수를 여기서 정의하고 getter / setter 메소드들이 있다
- 이 클래스의 필드값들이 기본변수, 파생변수이고 이 PagingPgm 객체를 만들어서 기본변수, 파생변수를 저장 가능
- src/main/java/board1/service 하위의 PagingPgm.java 파일
- PagingPgm.java 부분
- 값 전달시에 PagingPgm 객체를 전달하며 한번에 기본변수, 파생변수들을 전달시킬수있다
- getter / setter 메소드로 값을 돌려줌
- Controller 클래스에서 PagingPgm 클래스를 사용하는 코드
- 일부 변수들을 생성 한 후 PaginPgm 객체를 생성하며 생성자로 넘겨주면 PagingPgm 에서 나머지 변수들의 값을 구해줌
- Model 객체에 한번에 PaginPgm 객체를 저장해서 View 페이지로 넘길수도 있다
코드 설명
글 작성 기능
- 일단 시작부터 흐름을 간략 설명
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript">
location.href = "list.do";
//location.href="adminMail";
</script>
</body>
</html>
- Controller 의 "list.do" 요청 부분만
- 여기 갔다가 View 로 갔다가 list.jsp 로 이동
- list.jsp 부분
- "글 입력" 클릭시 "insertForm.do" 로 요청
- Controller 클래스에서 "insertForm.do" 요청 부분 만
@RequestMapping("insertForm.do") // 글작성 폼 (원문, 답변글)
public String insertForm(String nm, String pageNum, Model model) {
int num = 0, ref = 0, re_level = 0, re_step = 0; // 원문
if (nm != null) { // 답변글
num = Integer.parseInt(nm);
Board board = bs.select(num); // 부모글 정보 구해오기
ref = board.getRef();
re_level = board.getRe_level();
re_step = board.getRe_step();
}
model.addAttribute("num", num);
model.addAttribute("ref", ref);
model.addAttribute("re_level", re_level);
model.addAttribute("re_step", re_step);
model.addAttribute("pageNum", pageNum);
return "insertForm";
}
원문 글과 댓글 구별
- 원문 글 양식과 댓글 양식을 같은걸 쓰는 프로젝트이므로 여기서 원문 글 작성 양식과 댓글 작성 양식을 구분해야함
- 특정 글의 상세 페이지 하단에서 "답변" 버튼을 눌렀을때 글 번호를 변수 nm 에 저장해서 전달한다
- 그러므로 nm 값이 없다면 원문 글, nm 값이 있다면 댓글임을 구별 가능
- 댓글 작성 폼으로 갈때는 원문 글 번호를 저장한 nm 값을 넘겨준다 (아래)
- 또한 원문 글 작성폼으로 갈때는 pageNum 이 넘어오지 않으므로 null 이 되지만 댓글 작성 폼으로 갈때는 pageNum 이 넘어온다
<댓글 작성시>
- 부모글 번호를 저장한 변수 nm 을 int 로 변환하고 select() 메소드를 호출해서 부모글의 상세 정보를 가져온다
- 부모글의 ref, level, step 등의 정보가 필요하므로 부모글의 상세 정보를 가져오는 것임
- 부모글의 ref, level, step 정보를 저장후 Model 객체에 저장해서 전달, 부모 글 번호 num 과 페이지 번호 pageNum 도 전달
<원문 작성시>
- num, ref, level, step 값을 0 으로 초기값으로 설정함, 이 값들은 원문 작성할때 필요한 값이다
<가져가는 값>
- 폼으로 가기 위해 num, ref, re_level, re_step, pageNUm 값들을 가지고 글 작성 폼인 insertForm.jsp 로 간다
- 댓글 작성시에는 원문 작성할때와 다른 num, ref, re_level, re_step 값들을 가지고 insertForm.jsp 로 간다
- 지금은 원문을 작성하는 중이다
- insertForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div class="container" align="center">
<h2 class="text-primary">게시판 글쓰기</h2>
<form action="insert.do" method="post">
<input type="hidden" name="num" value="${num}">
<input type="hidden" name="ref" value="${ref}">
<input type="hidden" name="re_step" value="${re_step}">
<input type="hidden" name="re_level" value="${re_level}">
<input type="hidden" name="pageNum" value="${pageNum}">
<table class="table table-striped">
<tr>
<td>제목</td>
<td><input type="text" name="subject" required="required"></td>
</tr>
<tr>
<td>작성자</td>
<td><input type="text" name="writer" required="required"></td>
</tr>
<tr>
<td>이메일</td>
<td><input type="email" name="email" required="required"></td>
</tr>
<tr>
<td>암호</td>
<td><input type="password" name="passwd" required="required"></td>
</tr>
<tr>
<td>내용</td>
<td><textarea rows="5" cols="30" name="content"
required="required"></textarea></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="확인"></td>
</tr>
</table>
</form>
</div>
</body>
</html>
- 입력양식에서 4개의 값을, hideen 객체로 5개의 값(부모의 num, ref, re_step, re_level 과 pageNum) 을 "insert.do" 로 요청하며 전달
- 현재는 원문글을 작성하므로 부모가 없다, num, ref, re_step, re_level 은 0 이고 pageNum 은 null 이다
- Controller 클래스에서 "insert.do" 요청 부분만
@RequestMapping("insert.do") // 글 작성
public String insert(Board board, Model model, HttpServletRequest request) {
int num = board.getNum();
int number = bs.getMaxNum();
if (num != 0) { // 답변글
bs.updateRe(board);
board.setRe_level(board.getRe_level() + 1);
board.setRe_step(board.getRe_step() + 1);
} else // 원문
board.setRef(number); // else 문 끝
board.setNum(number);
String ip = request.getRemoteAddr();
board.setIp(ip);
int result = bs.insert(board);
model.addAttribute("result", result);
return "insert";
}
- 넘어온 값들을 DTO Board 객체 board 로 받아서 저장한다
- 원문인 경우 board 객체의 num 값은 0 이다, 댓글인 경우 board 객체의 num, ref, re_level, re_step 값은 부모의 값이다
- 즉 num 이 0 이면 원문, num 이 0 이 아니면 댓글이다
<공통적으로 적용>
시퀀스를 쓰지 않고 컬럼 num 에 값 넣기
- 먼저 Service 클래스의 getMaxNum() 을 호출해서, 컬럼 num 중 최대값을 구한 뒤 1을 증가시켜, 변수 number 에 돌려줌
ex) 현재 DB의 데이터들 중 가장 큰 num 값을 구해와서 1 을 더함, 이게 새로 입력할 글의 num 값이 된다
* getMaxNum() 메소드는 아래에서 설명
<댓글인 경우>
- 댓글인 경우 ref 값은 부모의 ref 값과 같아야하므로, updateRe() 메소드를 호출해서 부모의 ref 와 같은 ref 이면서 부모의 re_step 보다 큰 re_step 을 가진 글들의 step 값을 1 증가시킴
- 이후 객체 board 에는 작성할 글의 정보가 들어가야하므로 부모의 re_level, re_step 에서 1 증가한 값을 Setter 메소드로 DTO 객체 board 에 저장
<원문인 경우>
- 원문인 경우 num 과 ref 값이 같은 값이 들어가야하므로 DTO 의 Setter 메소드로 글의 ref 컬럼의 값을 number 로 설정
+ else 문에 괄호가 없으므로 board.setRef(number) 한줄만 적용됨
+ 원문일때 객체 board 안의 seq, level 값이 0 이므로 그대로 둔다
<공통적으로 적용>
- 컬럼 num 의 값을 최대값보다 1 증가된 값인 number 로 설정
- 글을 작성한 사람의 IP 주소를 구하기 위해 request.getRemoteAddr() 메소드 사용하고 Setter 메소드로 객체 board 에 세팅
- Servic 클래스의 insert() 메소드로 실제 글 작성(삽입)
* insert() 메소드 아래에서 설명
- Model 객체에 받은 result 저장 후 insert.jsp 로 이동
- Service 클래스 BoardServiceImpl.java 에서 getMaxNum() 메소드 부분만
public int getMaxNum() {
return bd.getMaxNum();
}
- DAO 클래스 BoardDaoImpl.java 에서 getMaxNum() 메소드 부분만
public int getMaxNum() {
return sst.selectOne("boardns.getMaxNum");
}
- 그룹함수 max 는 결과가 1개이므로 selectOne() 메소드 사용
- Mapper 파일 Board.xml 에서 id 가 "getMaxNum" 인 SQL문 부분만
<!-- num 번호중 최대값 구하기 : 첫번째 글은 1번으로 설정 -->
<select id="getMaxNum" resultType="int">
select nvl(max(num),0) + 1 from board
</select>
- 테이블 board 에서 가장 큰 num 값을 구해온다
- 처음으로 글을 작성할땐 max(num) 은 아무 데이터 없이 null 이 나온다
- nvl() 함수를 사용해서 null 값인 경우 0 으로 바꿔준다
- 그 후 구한 컬럼 num 의 최대값에서 1 을 더해서 int 형으로 돌려준다
- Service 클래스 BoardServiceImpl.java 에서 insert() 메소드 부분만
public int insert(Board board) {
return bd.insert(board);
}
- DAO 클래스 BoardDaoImpl.java 에서 insert() 메소드 부분만
public int insert(Board board) {
return sst.insert("boardns.insert",board);
}
- Mapper 파일 Board.xml 에서 id 가 "insert" 인 SQL문 부분만
<insert id="insert" parameterType="board">
<!--<selectKey keyProperty="num"
order="BEFORE" resultType="int">
select nvl(max(num),0) + 1 from board
</selectKey> -->
insert into board values (#{num},#{writer},#{subject},
#{content},#{email},0,#{passwd},#{ref},
#{re_step},#{re_level},#{ip},sysdate,'n')
</insert>
- 나중엔 주석을 풀 것, 나중에 설명할 것
- 글 작성(삽입) SQL문이므로 컬럼 del 에는 "n" 을 넣어줌
+ 삭제 시 "y" 를 넣음
- View 페이지 insert.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<c:if test="${result > 0 }">
<script type="text/javascript">
alert("입력 성공");
location.href = "list.do";
</script>
</c:if>
<c:if test="${result <= 0 }">
<script type="text/javascript">
alert("입력 실패");
history.go(-1);
</script>
</c:if>
</body>
</html>
- 넘어온 result 값으로 입력 성공 / 실패 처리를 한다
- 입력 성공시 목록 페이지로 이동, 이 프로젝트 전체 목록을 구할때는 페이지값을 넘겨주지 않아도 된다
- 즉, 글 작성 / 댓글 작성 성공시 "list.do" 로 요청한다
목록 페이지
- 글 작성 / 댓글 작성 성공시 "list.do" 로 요청한다
- 검색 목록을 요청할때도 "확인" 버튼 클릭시 다시 "list.do" 로 요청한다
- 이때는 hidden 객체로 pageNum 에 1 을 저장해서 넘어옴
- 그러므로 전체 목록인지 검색 목록인지를 Controller 클래스 "list.do" 요청 부분에서 구분해서 처리해야한다
- Controller 클래스에서 "list.do" 요청 부분만
@RequestMapping("list.do") // 전체 목록, 검색 목록
public String list(String pageNum, Board board, Model model) {
final int rowPerPage = 10; // 화면에 출력할 데이터 갯수
if (pageNum == null || pageNum.equals("")) {
pageNum = "1";
}
int currentPage = Integer.parseInt(pageNum); // 현재 페이지 번호
// int total = bs.getTotal();
int total = bs.getTotal(board); // 검색 (데이터 갯수)
int startRow = (currentPage - 1) * rowPerPage + 1;
int endRow = startRow + rowPerPage - 1;
PagingPgm pp = new PagingPgm(total, rowPerPage, currentPage);
board.setStartRow(startRow);
board.setEndRow(endRow);
// List<Board> list = bs.list(startRow, endRow);
int no = total - startRow + 1; // 화면 출력 번호
List<Board> list = bs.list(board);
model.addAttribute("list", list);
model.addAttribute("no", no);
model.addAttribute("pp", pp);
// 검색
model.addAttribute("search", board.getSearch());
model.addAttribute("keyword", board.getKeyword());
return "list";
}
<넘어오는 값>
- 글 작성 후 "list.do" 로 요청해서 넘어왔을때는 pageNum 이 넘어오지 않는다, null 이 된다
- 검색 창에서 "확인" 을 눌러서 "list.do" 로 요청해서 넘어왔을때는 pageNum 값이 넘어온다
- 검색을 했을때 넘어오는 search , keyword 값들이 매개변수에 선언한 DTO Board 객체 board 에 저장되게 된다
- 즉 전체 목록을 구하고자 할때는 search, keyword 값이 넘어오지 않을 것이고, 검색 목록을 구하고자 할때는 search, keyword 값이 넘어올 것이다
- search, keyword 값 (DTO Board 객체 board) 이 넘어오는지 유무 에 따라 전체 목록을 구해올지, 부분 목록을 구해올지 판별 가능
+ DTO Board 클래스 안에 search, keyword 컬럼도 만들어져 있음
<기본 변수 & 파생변수>
- 페이지 번호 pageNum 가 전달되지 않았을떄는 pageNum 을 "1" 로 설정해줌
- 기본변수 rowPerPage : 화면에 출력할 데이터 개수
- 기본변수 currentPage : 현재 페이지 번호
- 기본변수 total : 총 데이터 개수 또는 검색된 데이터 총 개수, getTotal() 메소드를 호출해서 그룹함수 count 로 구함
<getTotal() 로 총 데이터 개수 구하기, 검색된 데이터 총 개수 구하기 구별하는 법>
- 전체 데이터 개수도 이 변수 total 에 저장되고, 검색된 데이터 개수도 이 변수 total 에 저장해야한다
- 같은 위치에서 전체 데이터 목록도 구하고, 검색시엔 검색된 데이터 목록을 구하기때문에 이렇게 처리해야함
- 그러므로 전체 데이터 개수를 구하는 것과 검색된 데이터 개수를 구하는 경우가 구분되어야함
- getTotal() 메소드를 호출하면서 search, keyword 를 저장한 DTO 객체 board 를 매개변수로 전달함
- 그러면, 총 데이터 개수를 구할땐 객체 board 가 null 이고 검색한 데이터 총 개수를 구할땐 객체 board 에 keyword, search 값이 존재한다
- Mapper 파일의 데이터 개수 구하는 SQL문 에서 동적 SQL문을 쓰고 있다
* getTotal( ) 메소드 아래에서 설명
<getTotal() 에서 돌아온 후>
- startRow, endRow 변수 값을 구한다
- PagingPgm 클래스 객체 pp 를 생성하면서 기본변수 3개 total, rowPerPage, currentPage 를 생성자의 매개변수로 전달
* PaginPgm 클래스 아래에 설명
<목록 구하기>
- 목록을 잘라주기 위해 startRow, endRow 를 매개변수에서 만들어진 DTO Board 객체 board 에 Setter 메소드로 세팅
+ DTO Board 클래스 안에 startRow, endRow 컬럼도 만들어져 있음
+ 화면 출력번호를 구해서 변수 no 에 저장
- 검색을 했을때는 DTO 객체 board 안에 search, keyword, startRow, endRow 값이 저장되어있다
- 검색을 하지 않았을때는 DTO 객체 board 안에 startRow, endRow 값만 저장되어있게 된다
- 목록을 구하기 위한 Service 클래스의 list() 메소드를 호출하며 board 를 매개변수로 전달
* list() 메소드 아래에 설명
<list() 에서 돌아온 후>
- 구한 목록을 list 에 저장 후 Model 객체에 저장해서 list.jsp 에 전달
- 페이징 처리에 필요한 값들을 저장한 PaginPgm 객체 pp 를 Model 객체에 저장해서 list.jsp 에 전달, View 에서 pp의 Getter 메소드로 변수들을 불러옴
- 화면 출력번호 no 도 Model 객체에 저장해서 list.jsp에 전달
- 검색했을때 검색된 리스트에서 페이징 처리를 하려면 search 와 keyword 가 필요하므로 search, keyword 도 Model 객체에 저장해서 list.jsp 에 전달
+ list.jsp 로 search, keyword 를 전달해야하는 이유
- list.jsp 로 이동할때는 search, keyword 가 필요함
- 검색했을때 검색된 리스트에서 페이징 처리를 하려면 search 와 keyword 가 필요하다
- list.jsp 에서 search, keyword 를 쓰는 코드 (아래)
- ${search} 로 가져오고 있다
- 또한 list.jsp 에서 search, keyword 를 쓰는 코드 (아래)
- keyword 값이 empty 면 검색을 하지 않은 경우, keyword 값이 empty 가 아니면 검색을 한 경우 로 if 태그로 나눠서 처리
- 즉, 전체 데이터 목록도 페이징 처리를 따로 하고, 검색한 데이터도 페이징 처리를 따로 해야하므로 keyword 필요
- 페이징 처리 = [1] [2] [3] 같은 페이지 선택할 수 있는 메뉴바 만들고 원하는 페이지를 클릭할 수 있게 하는 것
- 전체 데이터 목록 출력시에는 페이지 번호만 가지고 가지만 검색된 데이터를 구할때는 "list.do" 로 요청하면서 search, keyword 를 가져감
- search, keyword 를 가져가야만 Controller 의 "list.do" 처리 부분에서, 검색된 데이터 목록 요청인지 전체 데이터 목록 요청인지 판별 가능
* 위에서 설명했음
- Service 클래스 BoardServiceImpl.java 에서 getTotal() 메소드 부분만
public int getTotal(Board board) {
return bd.getTotal(board);
}
- DAO 클래스 BoardDaoImpl.java 에서 getTotal() 메소드 부분만
public int getTotal(Board board) {
return sst.selectOne("boardns.getTotal",board);
}
- Mapper 파일 Board.xml 에서 id 가 "getTotal" 인 SQL문 부분만
<select id="getTotal" parameterType="board" resultType="int">
select count(*) from board
<where>
<if test="keyword != null and search !='subcon'">
${search} like '%'||#{keyword}||'%'
</if>
<if test="keyword != null and search=='subcon'">
subject like '%'||#{keyword}||'%' or
content like '%'||#{keyword}||'%'
</if>
</where>
</select>
- 가장 위의 select 문은 count(*) 그룹함수를 사용해서 총 데이터 개수를 구하는 코드이다
- where 절 대신 where 태그를 사용해서 동적 SQL문으로 작성되었다
<전체 데이터 총 개수를 가져올때>
- 전체 데이터를 가져올때는 keyword 도 null 이고 search 도 null 이므로 where 태그 안에 만족하는 조건이 없게 된다
- select count(*) from board 만 적용되어 전체 데이터의 개수를 가져오게 된다
<where 태그, if 태그 사용>
- 동적 SQL문 이다
- where 태그와 if 태그로 특정 조건을 만족할때만 where 절을 추가하는 것과 같은 효과
<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>
- 즉 검색 대상이 제목, 내용, 작성자 인 경우
- search 에 저장된 값은 subject, content, writer 등이 될 수 있다, 이처럼 가변적인 값인 경우 #{search} 가 아닌 ${search} 로 작성해야한다
- keyword 에 저장된 값을 포함하는 데이터의 개수를 검색하게 된다
+ || 로 문자열 연결
<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>
- 즉 검색 대상이 제목 + 내용 인 경우
- or 연산자로 연결해서 subject, content 컬럼에서 특정 키워드를 포함한 데이터의 개수를 검색하게 된다
- 설명보다는 코드를 보기
+ JSTL 의 if 태그와 비슷
+ 동적 SQL문
- JSTL 의 태그들과 비슷하다, if, choose, when, otherwise, foreach 태그 등
- 검색기능을 구현할때 사용
- where 태그와 if 태그로 특정 조건을 만족할때만 where 절을 추가하는 것과 같은 효과
- 나중에 찾아보고 공부하기
PagingPgm 클래스
- PagingPgm.java
package board1.service;
public class PagingPgm {
private int total; // 데이터 갯수
private int rowPerPage; // 화면에 출력할 데이터 갯수
private int pagePerBlk = 10; // 블럭당 페이지 갯수 (1개의 블럭당 10개의 페이지)
private int currentPage; // 현재 페이지 번호
private int startPage; // 각 블럭의 시작 페이지
private int endPage; // 각 블럭의 끝 페이지
private int totalPage; // 총 페이지 수
public PagingPgm(int total, int rowPerPage, int currentPage) {
this.total = total;
this.rowPerPage = rowPerPage;
this.currentPage = currentPage;
totalPage = (int) Math.ceil((double) total / rowPerPage);
startPage = currentPage - (currentPage - 1) % pagePerBlk; // 1, 11, 21...
endPage = startPage + pagePerBlk - 1; // 10, 20, 30...
if (endPage > totalPage)
endPage = totalPage;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getRowPerPage() {
return rowPerPage;
}
public void setRowPerPage(int rowPerPage) {
this.rowPerPage = rowPerPage;
}
public int getPagePerBlk() {
return pagePerBlk;
}
public void setPagePerBlk(int pagePerBlk) {
this.pagePerBlk = pagePerBlk;
}
public int getCurrentPage() {
return currentPage;
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
public int getStartPage() {
return startPage;
}
public void setStartPage(int startPage) {
this.startPage = startPage;
}
public int getEndPage() {
return endPage;
}
public void setEndPage(int endPage) {
this.endPage = endPage;
}
public int getTotalPage() {
return totalPage;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
}
- 기본변수, 파생변수들이 필드이다
- 기본변수 total, rowPerPage, currentPage
- 파생변수 startPage, endPage, totalPage
- pagePerBlk : 블럭 당 페이지 개수, 즉 1 개 블럭 당 10개의 페이지로 설정했다
- 생성자 매개변수로 기본변수 3개를 전달받아서 파생 변수 값들을 구해준다
- 아래쪽엔 Getter / Setter 메소드로 만들어져 있다
- 이런식으로 따로 클래스를 만들어서 페이징 처리를 하는 경우도 많다
- Mapper 파일 Board.xml 에서 id 가 "list" 인 SQL문 부분만
<!-- <select id="list" parameterType="hashMap" resultMap="boardResult"> -->
<select id="list" parameterType="board" resultMap="boardResult">
select * from (select a.*,rowNum rn from (
select * from board
<where>
<if test="keyword != null and search!='subcon'">
${search} like '%'||#{keyword}||'%'
</if>
<if test="keyword != null and search=='subcon'">
subject like '%'||#{keyword}||'%' or
content like '%'||#{keyword}||'%'
</if>
</where>
order by ref desc,re_step) a )
where rn between #{startRow} and #{endRow}
</select>
- 첫번째 서브쿼리는 rowNum 컬럼에 대한 별칭을 rn 으로 지정하는 역할
<where 태그 시작>
<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>
- 즉 검색 대상이 제목, 내용, 작성자 인 경우
- search 에 저장된 값은 subject, content, writer 등이 될 수 있다, 이처럼 가변적인 값인 경우 #{search} 가 아닌 ${search} 로 작성해야한다
- keyword 에 저장된 값을 포함하는 데이터의 개수를 검색하게 된다
+ || 로 문자열 연결
<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>
- 즉 검색 대상이 제목 + 내용 인 경우
- or 연산자로 연결해서 subject, content 컬럼에서 특정 키워드를 포함한 데이터의 개수를 검색하게 된다
- 즉 해당 검색어를 포함한 제목이 있거나 해당 검색어를 포함한 내용이 있다면 그 데이터들만 가져옴
<where 태그 끝난 후>
- 검색을 먼저하고 정렬을 나중에 해야한다, 그러므로 where 조건 후 order by 가 와야함
- 두번째 서브쿼리에서 정렬을 해야할때, ref 로 내림차순, re_step 으로 오름차순 정렬
- 객체 board 에 저장되어 넘어온 startRow, endRow 값을 where 절에 사용
+ between A and B = A 이상 B 이하
+ 해당 SQL문에 resultMap 이 있지만 지금은 board 테이블 컬럼과 DTO Board 의 프로퍼티 명이 같으므로 쓰지 않아도 됨
- DTO Board 에 테이블 board 에는 없는 프로퍼티가 있는 경우여도 이름이 같으므로 모두 자동 매핑 되므로 resultMap 을 쓰지 않아도 된다
- View 페이지 list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div class="container" align="center">
<h2 class="text-primary">게시판 목록</h2>
<table class="table table-striped">
<tr>
<td>번호</td>
<td>제목</td>
<td>작성자</td>
<td>작성일</td>
<td>조회수</td>
</tr>
<c:if test="${empty list}">
<tr>
<td colspan="5">데이터가 없습니다</td>
</tr>
</c:if>
<c:if test="${not empty list}">
<c:set var="no1" value="${no }"></c:set>
<c:forEach var="board" items="${list }">
<tr>
<td>${no1}</td>
<c:if test="${board.del =='y' }">
<td colspan="4">삭제된 데이터 입니다</td>
</c:if>
<c:if test="${board.del !='y' }">
<td><a href="view.do?num=${board.num}&pageNum=${pp.currentPage}"
class="btn btn-default">
<c:if test="${board.re_level >0 }">
<img alt="" src="images/level.gif" height="2"
width="${board.re_level *5 }">
<img alt="" src="images/re.gif">
</c:if> ${board.subject}
<c:if test="${board.readcount > 30 }">
<img alt="" src="images/hot.gif">
</c:if></a></td>
<td>${board.writer}</td>
<td>${board.reg_date}</td>
<td>${board.readcount}</td>
</c:if>
</tr>
<c:set var="no1" value="${no1 - 1}"/>
</c:forEach>
</c:if>
</table>
<form action="list.do">
<input type="hidden" name="pageNum" value="1">
<select name="search">
<option value="subject" <c:if test="${search=='subject'}">selected="selected" </c:if>>제목</option>
<option value="content" <c:if test="${search=='content'}">selected="selected" </c:if>>내용</option>
<option value="writer" <c:if test="${search=='writer'}">selected="selected" </c:if>>작성자</option>
<option value="subcon" <c:if test="${search=='subcon'}">selected="selected" </c:if>>제목+내용</option>
</select>
<input type="text" name="keyword">
<input type="submit" value="확인">
</form>
<ul class="pagination">
<!-- 검색 했을 경우의 페이징 처리 -->
<c:if test="${not empty keyword}">
<c:if test="${pp.startPage > pp.pagePerBlk }">
<li><a
href="list.do?pageNum=${pp.startPage - 1}&search=${search}&keyword=${keyword}">이전</a></li>
</c:if>
<c:forEach var="i" begin="${pp.startPage}" end="${pp.endPage}">
<li <c:if test="${pp.currentPage==i}">class="active"</c:if>><a
href="list.do?pageNum=${i}&search=${search}&keyword=${keyword}">${i}</a></li>
</c:forEach>
<c:if test="${pp.endPage < pp.totalPage}">
<li><a
href="list.do?pageNum=${pp.endPage + 1}&search=${search}&keyword=${keyword}">다음</a></li>
</c:if>
</c:if>
<!-- 전체 목록의 페이징 처리 -->
<c:if test="${empty keyword}">
<c:if test="${pp.startPage > pp.pagePerBlk }">
<li><a href="list.do?pageNum=${pp.startPage - 1}">이전</a></li>
</c:if>
<c:forEach var="i" begin="${pp.startPage}" end="${pp.endPage}">
<li <c:if test="${pp.currentPage==i}">class="active"</c:if>>
<a href="list.do?pageNum=${i}">${i}</a></li>
</c:forEach>
<c:if test="${pp.endPage < pp.totalPage}">
<li><a href="list.do?pageNum=${pp.endPage + 1}">다음</a></li>
</c:if>
</c:if>
</ul>
<div align="center">
<a href="insertForm.do" class="btn btn-info">글 입력</a>
</div>
</div>
</body>
</html>
- 위의 list.jsp 코드가 길므로 나눠서 캡처하면서 설명
<목록 출력>
- if 태그를 사용해서 list 가 있는 경우, list 를 출력
- 이때 Model 객체에 넘어온 화면 출력번호 no 를 가져와서 변수 no 에 저장
- 이제 list 를 forEach 의 items 에 넣어서 변수 board 로 받아서 하나씩 글을 출력, 이때 각 글의 del 컬럼이 "y" 인지 "n" 인지에 따라 나눔
- 삭제된 글은 del 컬럼이 "y" 이므로 "삭제된 데이터입니다" 를 두번째 td 자리에 출력
- 삭제된 글이 아니면 del 컬럼이 "n" 이므로 정상적으로 이미지, 제목등을 출력하고 링크를 걸어서 상세페이지로 가기 위해 "view.do" 로 요청, 이때 글 번호와 페이지 번호 가져감
- 댓글이 경우 이미지를 출력시켜 댓글임을 알림
- 조회수 값이 특정 값 이상이면 특정 이미지(hot) 를 불러와서 인기있는 글이라고 알려줌
- 현재 list 에는 검색된 목록이 있을 수도, 전체 목록이 있을수도 있다, 그러므로 그냥 list 를 출력하면 됨
- 하지만 페이징 처리는 다르다
<검색 창 부분>
- 선택한 select 와 사용자가 입력한 값을 가지고 다시 "list.do" 로 요청한다
- 이렇게 "list.do" 로 요청하면 Controller 에서는 search, keyword 값이 저장된 DTO 객체가 null 이 아니게 되므로 검색 목록요청인지 전체 목록 요청인지 구별 가능하다
- 페이징 처리는 검색된 목록, 전체 목록 나눠서 페이징 처리를 해야한다! * 이유는 아래에서 설명
<페이징 처리 : 검색된 목록>
- 검색 목록인 경우엔 Controller 에서 Model 로 keyword 를 전달할때 이미 keyword 값이 존재했으므로 keyword 가 null 이 아니다, 그러므로 위의 코드가 실행됨
- Controller 에서 "list.do" 요청 처리 부분 코드를 보면, 전체 데이터 목록을 원할땐 search, keyword 값이 없고, 검색된 데이터 목록을 원할땐 search, keyword 값이 넘어오도록 처리했다
- 그래서 검색한 리스트 목록을 출력하는 경우는 페이지 번호 뿐 아니라 search, keyword 도 같이 전달하며 "list.do" 로 요청해야한다
<페이징 처리 : 전체 목록>
- 전체 목록인 경우엔 Controller 에서 Model 로 keyword 를 전달할때 keyword 값이 없었으므로 keyword 가 null 이다, 그러므로 위의 코드가 실행됨
- 전체 데이터 목록을 가져올때 페이지 메뉴에서 특정 페이지 클릭시 View 에서 "list.do" 로 요청하면서 페이지 번호를 전달함
- Model 로 전달된 PagingPgm 객체 pp 로 각 변수들을 가져와서 페이징 처리를 한다
- 첫번째 블럭은 pp.startPage 가 1 이고, pp.pagePerBlk 가 10이므로 만족하지 않으므로 '이전' 메뉴가 없다
- 존재하는 페이지까지만 forEach 문을 통해 페이지 번호를 출력하고 있다
- 이떄 페이지 번호가 현재 페이지와 같을때는 class="active" 로 부트스트랩을 적용해서 디자인 적용
- 전체 목록 페이지 처리이므로 "list.do" 로 요청하면서 search, keyword 를 전달하지 않음, 그래야 Controller 에서 전체 목록 페이지 처리로 인식한다
페이징 처리를 전체 목록, 검색 목록 따로 처리하는 이유 2가지
1. Controller 클래스에서 글의 전체 개수를 저장한 total 값이 다르기 때문에
2. 클릭하면 가지고 가는 값도 다르기 때문에
ex) 전체 목록 페이징 처리에서 이전, 다음 같은 메뉴를 누르면 "list.do" 로 요청하면서 페이지 번호만 가지고 감
ex) 검색 목록 페이징 처리에서 이전, 다음 같은 메뉴를 누르면 "list.do" 로 요청하면서 페이지 번호 뿐 아니라 search, keyword 도 가져가야만함