댓글 개요
댓글은 freeView에만 추가할 예정입니다.
댓글기능을 추가하기전 naver웹툰에 댓글을 참고합시다.
URL은 변화가 없는데 댓글부분의 보이는 화면이 다르다.
AJAX로 데이터를 변경했기 때문이다.
우리는 Spring 프로젝트 진행 중 freeView화면에서 AJAX로 요청을 하고
Controller에서 DB에 접근해서 댓글데이터를 얻을 것이다.
그리고 ReplyVO(댓글번호, 댓글작성자,댓글내용 등)에 저장할 것이다.
@ResponseBody를 붙인 메소드에서 return한 값은
그대로 AJAX succes함수의 파라미터로 온다.
이 ReplyVO를 AJAX요청한 곳에 return 하고
ReplyVO를 자바스크립트로 댓글형태의 태그를 생성한다.
Controller
//예시
@ResponseBody
@RequestMapping(value="/reply/replyList.wow")
public List<ReplyVO> replyList(){
List<ReplyVO> replyList=getReplyList();
return replyList;
}
script
//예시
$.ajax({
url : "<c:url value='replyList.wow'>"
,type : "post"
,success : function(data){
}
});//ajax
문제는 javascript에는 List<ReplyVO>라는 자료타입이 없다는 것이다.
https://brilliantdevelop.tistory.com/100에서 처럼
Spring은 return값을 response의 응답몸체로 변환해주어야 한다.
int,String등은 기본적으로 HttpMessageConverter 를 이용해서 응답몸체로 변환해주지만
List<ReplyVO>라는 객체를 바로 응답몸체로 변환 할 수 없다.
Spring은 자바객체를 반드시 적절한 형태로 바꾼 후에 응답몸체로 변환한다.
이 적절한 형태는 xml,csv,json 등이 있다.
이 글에서는 json 형태로 바꿔서 응답몸체로 변환하려한다.
이를 위해 jackson라이브러리를 추가하자.
<!-- json변환 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
jackson-databind 디펜던시를 추가하면 Spring의 MappingJackson2HttpMessageConver가
jackson lib를 이용해서 자바객체와 JSON간의 변환을 처리한다.
댓글 적용하기
댓글테이블
댓글 테이블을 만들자.
CREATE SEQUENCE seq_reply;
CREATE TABLE reply(
re_no NUMBER NOT NULL,
re_category VARCHAR2(30) NOT NULL,
re_parent_no NUMBER NOT NULL,
re_mem_id VARCHAR2(30) NOT NULL,
re_content VARCHAR2(4000) ,
re_ip VARCHAR2(30),
re_reg_date DATE DEFAULT SYSDATE,
re_mod_date DATE,
CONSTRAINT pk_reply PRIMARY KEY (re_no)
);
COMMENT ON table reply is '댓글정보 테이블';
COMMENT ON COLUMN reply.re_no IS '댓글번호';
COMMENT ON COLUMN reply.re_category IS '분류(BOARD, PDS, FREE, ...)';
COMMENT ON COLUMN reply.re_parent_no IS '부모 번호';
COMMENT ON COLUMN reply.re_mem_id IS '작성자ID';
COMMENT ON COLUMN reply.re_content IS '댓글 내용';
COMMENT ON COLUMN reply.re_ip IS 'IP';
COMMENT ON COLUMN reply.re_reg_date IS '댓글 등록일자';
COMMENT ON COLUMN reply.re_mod_date IS '댓글 수정일자';
re_no 컬럼은 primaryKey로서 댓글한개의 고유번호이다.
중요한건 re_parent_no와 re_category이다.
freeView 글 1개에 댓글이 여러개 일 수 있다.
그래서 free_board테이블과 reply테이블은 1:n 관계이다.
이를 위해 reply테이블에 re_parent_no컬럼이 있다.
또 아직 만들지는 않았지만 만약에 공지사항 게시판을 만들고
거기에 댓글을 여러개 달 수 있는 경우를 생각해보자.
공지사항과 reply테이블은 1:n 관계이다.
그런데 reply테이블입장에서 re_parent_no=1인 경우를 보면
공지사항 1번글의 댓글인지, freeBoard 1번글의 댓글인지 모른다.
이를 구분하기 위해 re_category 컬럼이 있다.
(물론 댓글테이블에 공지사항댓글,freeBoard댓글을 다 넣어서 이를 구별하기 위해
re_category 컬럼이 있지만 애초에 공지사항댓글테이블 reply_anounce, free_board댓글 테이블 reply_free_board를 따로 만들 수도 있다.)
ReplyVO
ReplyVO를 만듭시다.
package com.study.reply.vo;
import java.io.Serializable;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class ReplyVO {
private int reNo; /* 댓글번호 */
private String reCategory; /* 분류(BOARD, PDS, FREE, ...) */
private int reParentNo; /* 부모 번호 */
private String reMemId; /* 작성자ID */
private String reContent; /* 댓글 내용 */
private String reIp; /* IP */
private String reRegDate; /* 댓글 등록일자 */
private String reModDate; /* 댓글 수정일자 */
private String reMemName; //이름
// toString() 구현
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
//get/set
}
댓글부분 html태그
freeView.jsp에 다음의 댓글 html태그를 추가합시다.
<div class="container"> 로 시작하는 댓글목록 태그이므로
기존의 있던 <div class="container">태그의 밑에다 추가하면 되겠습니다
freeView.jsp
<!-- 기존 freeView태그
<div class="container">
</div>
-->
<div class="container">
<!-- reply container -->
<!-- // START : 댓글 등록 영역 -->
<div class="panel panel-default">
<div class="panel-body form-horizontal">
<form name="frm_reply" action="<c:url value='/reply/replyRegist' />"
method="post" onclick="return false;">
<input type="hidden" name="reParentNo" value="${freeBoard.boNo}">
<input type="hidden" name="reCategory" value="FREE"> <input
type="hidden" name="reMemId" value="${USER_INFO.userId }">
<input type="hidden" name="reIp"
value="<%=request.getRemoteAddr()%>">
<div class="form-group">
<label class="col-sm-2 control-label">댓글</label>
<div class="col-sm-8">
<textarea rows="3" name="reContent" class="form-control"
readonly='readonly'></textarea>
</div>
<div class="col-sm-2">
<button id="btn_reply_regist" type="button"
class="btn btn-sm btn-info">등록</button>
</div>
</div>
</form>
</div>
</div>
<!-- // END : 댓글 등록 영역 -->
<!-- // START : 댓글 목록 영역 -->
<div id="id_reply_list_area">
<div class="row">
<div class="col-sm-2 text-right">홍길동</div>
<div class="col-sm-6">
<pre>내용</pre>
</div>
<div class="col-sm-2">12/30 23:45</div>
<div class="col-sm-2">
<button name="btn_reply_edit" type="button"
class=" btn btn-sm btn-info" onclick="fn_modify()">수정</button>
<button name="btn_reply_delete" type="button"
class="btn btn-sm btn-danger">삭제</button>
</div>
</div>
<div class="row">
<div class="col-sm-2 text-right">그댄 먼곳만 보네요</div>
<div class="col-sm-6">
<pre> 롤링롤링롤링롤링</pre>
</div>
<div class="col-sm-2">11/25 12:45</div>
<div class="col-sm-2"></div>
</div>
</div>
<div class="row text-center" id="id_reply_list_more">
<a id="btn_reply_list_more"
class="btn btn-sm btn-default col-sm-10 col-sm-offset-1"> <span
class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span>
더보기
</a>
</div>
<!-- // END : 댓글 목록 영역 -->
<!-- START : 댓글 수정용 Modal -->
<div class="modal fade" id="id_reply_edit_modal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<form name="frm_reply_edit"
action="<c:url value='/reply/replyModify' />" method="post"
onclick="return false;">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">댓글수정</h4>
</div>
<div class="modal-body">
<input type="hidden" name="reNo" value="">
<textarea rows="3" name="reContent" class="form-control"></textarea>
<input type="hidden" name="reMemId" value="${USER_INFO.userId }">
</div>
<div class="modal-footer">
<button id="btn_reply_modify" type="button"
class="btn btn-sm btn-info">저장</button>
<button type="button" class="btn btn-default btn-sm"
data-dismiss="modal">닫기</button>
</div>
</form>
</div>
</div>
</div>
<!-- END : 댓글 수정용 Modal -->
</div>
<!-- reply container -->
<!-- 이 다음이 </body>태그 -->
freeView페이지를 보면 다음과 같이 나옵니다.
먼저 등록부분을 readonly로 했는데, 로그인한 사람만 댓글을 달 수 있도록 하려고한다.
마찬가지로 수정,삭제도 로그인한 사람만 달 수 있도록 하려고한다.
2개의 댓글예시는 하드코딩했지만, 나중엔 DB에 있는 댓글에 따라 보이는게 달라질 것이다.
첫번째 댓글의 수정,삭제버튼은 내가 그 글을 작성한 사람일 때만 보인다.
두번째 댓글은 로그인 안 했거나 내가 쓴 글이 아니어서 버튼이 보이지 않느다.
또 더보기 버튼을 누르면 댓글이 10개 씩 더 보이도록 할 것이다.
LoginCheckInterceptor
로그인한 사람만 등록,수정,삭제를 할 수 있도록 LoginCheckInterceptor를 수정하고
,mvc-servlet.xml에 mapping을 등록하자.
package com.study.common.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.study.login.vo.UserVO;
public class LoginCheckInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String ajax= request.getHeader("X-requested-with"); //요청이 ajax인지아닌지
HttpSession session=request.getSession();
UserVO user=(UserVO)session.getAttribute("USER_INFO");
if(user==null) {
//추가된부분
if(ajax!=null) { //ajax요청일 경우
response.sendError(401, "로그인안했어요"); //ajax error함수에서 login으로 이동하게 할거임.
return false;
}
response.sendRedirect(request.getContextPath()+"/login/login.wow");
return false;
}
return true;
}
}
요청을 ajax로 하기 때문에 response.sendRedirect()로 로그인페이지로 가는것이 아닌,
ajax로 error를 보낸 다음 ajax의 error함수에서 login페이지로 이동하도록 할 것이다.
mvc-servlet.xml
<interceptors>
<interceptor>
<mapping path="/mypage/*" /> <!-- mapping은 특정 url요청이 올때 무언가 하겠다 -->
<mapping path="/reply/*" /> <!--reply관련요청 인터셉트-->
<exclude-mapping path="/reply/replyList.wow"/>
<beans:bean
class="com.study.common.interceptor.LoginCheckInterceptor"></beans:bean> <!-- 무언가에 대한 내용 -->
</interceptor>
</interceptors>
이제 script에서 댓글관련 버튼을 누를 때마다 이벤트를 잘 등록해주기만 하면 된다.
sciript(ajax요청) , controller , service, dao,mapper.xml만 작성하면 된다.
Script
<script type="text/javascript">
// 댓글 데이터를 딱 10개만 가지고 오도록 하는 파라미터 모음
var params={"curPage":1, "rowSizePerPage" : 10
,"reCategory" : "FREE", "reParentNo": ${freeBoard.boNo} };
//ajax 요청해서 댓글리스트를 받아오는 함수.
function fn_reply_list(){
//아작스 호출해서 DB에 있는 reply 데이터 가지고 옵니다.
//가지고오면(success)하면 댓글 div 만들어줍니다.
//list를 가지고오니까 jquery 반복문 써서 div 여러개 만들어주면됩니다.
// 다 했으면 param의 curPage=2로 바꿔줍시다
}//function fn_reply_list
$(document).ready(function(){ //documnet가 준비될 때
//더보기 버튼
$("#id_reply_list_more").on("click",function(e){
//fn_reply_list에서 마지막에 curPage=2로 바꿔줍니다.
//그래서 그냥 fn_reply_list()하면 다음 댓글 10개 가져옵니다.
});
//등록버튼
$("#btn_reply_regist").on("click",function(e){
// form태그안에 input hidden으로 필요한거 넣기
//가장가까운 form찾은 후 ajax 호출(data는 form.serialize(), )
//성공 : 등록 글 내용부분 지우기, 댓글영역초기화( list_area.html('), curPage=1, fn_reply_list)
//실패 : error : req.status==401이면 login으로 location.href
});//등록버튼
//수정버튼 : 댓글 영역안에 있는 수정버튼만 이벤트 등록
$("#id_reply_list_area").on("click", 'button[name="btn_reply_edit"]'
,function(e){
//현재 버튼의 상위 div(한개 댓글) 찾기
//div에서 현재 댓글 내용을 modal에 있는 textarea에 복사
//div태그의 data-re-no 값을 modal에 있는 input name="reNo" 태그의 value값에 복사
//복사 후 .modal('show')
});//수정버튼
//모달창 저장 버튼
$("#btn_reply_modify").on("click", function(e){
//가장 가까운form 찾기 , ajax 호출( data:form.serialzie()
// 성공 : modal찾은 후 modal('hide')
// 현재 모달에 있는 reNo, reContent 찾기
// 댓글영역에서 re_no에 해당하는 댓글 찾은 후 안의 내용 re_content로 변경
});//모달창 저장버튼
//삭제버튼
$("#id_reply_list_area").on("click", 'button[name="btn_reply_delete"]'
,function(e){
//가장 가까운 div 찾기,
//reNo, reMemId(현재 로그인 한 사람의 id) 구하기
// ajax 호출(reNo, reMemeId보내기) reMemId는 본인이 쓴 글인지 확인하는데 쓰임 (BizAccessFailException)
//성공 후 해당 div.remove
}); //삭제버튼
});
</script>
Controller
서버단에서는 메소드이름만 보면 어떤식으로 구현할지 감이 올것이다.
ReplyController
package com.study.reply.web;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.study.exception.BizNotFoundException;
import com.study.reply.vo.ReplySearchVO;
import com.study.reply.vo.ReplyVO;
@RestController
public class ReplyController {
@RequestMapping(value = "/reply/replyList.wow")
public Map<String,Object> replyList(PagingVO paging,String reCategory,int reParentNo){
return null;
}
@RequestMapping(value = "/reply/replyRegist.wow")
public Map<String,Object> replyRegist(ReplyVO reply){
//result, msg (msg는 "등록성공", "등록실패"
return null;
}
@RequestMapping(value = "/reply/replyModify.wow")
public Map<String,Object> replyModify(ReplyVO reply){
//result, msg
return null;
}
@RequestMapping(value = "/reply/replyDelete.wow")
public Map<String,Object> replyDelete(ReplyVO reply){
//result, msg
return null;
}
}
Service
IReplyService
package com.study.reply.service;
import java.util.List;
import com.study.exception.BizAccessFailException;
import com.study.exception.BizNotFoundException;
import com.study.reply.vo.ReplySearchVO;
import com.study.reply.vo.ReplyVO;
public interface IReplyService {
/** 댓글 목록 조회 <br>
* <b>필수 : reCategory, reParentNo </b>
*/
public List<ReplyVO> getReplyListByParent(PagingVO paging, String reCategory, int reParentNo);
/** 댓글 수정 <br>
* 댓글이 존재하지 않으면 BizNotFoundException
* 댓글 작성자와 로그인 사용자가 다른 경우 BizAccessFailException
*/
public void modifyReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException;
/**
* 댓글 삭제 <br>
* 해당글의 존재하지 않으면 BizNotFoundException
* 댓글 작성자와 로그인 사용자가 다른 경우 BizAccessFailException
*/
public void removeReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException;
/** 댓글등록 */
public void registReply(ReplyVO reply) ;
}
또 혹시로그인을 했어도 현재 로그인ID가 수정할려는 댓글의 로그인ID랑 다를 때 수정이 안되도록 BizAccessFailException을 발생하려고 한다.
BizAccessFailException.java
package com.study.exception;
public class BizAccessFailException extends BizException {
public BizAccessFailException() {
}
public BizAccessFailException(String message) {
super(message);
}
public BizAccessFailException(Throwable cause) {
super(cause);
}
public BizAccessFailException(String message, Throwable cause) {
super(message, cause);
}
public BizAccessFailException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
DAO
IReplyDao.java
package com.study.reply.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.study.reply.vo.ReplySearchVO;
import com.study.reply.vo.ReplyVO;
@Mapper
public interface IReplyDao {
public int getReplyCountByParent(@Param("paging") PagingVO paging, @Param("reCategory") String reCategory,@Param("reParentNo") int reParentNo);
public List<ReplyVO> getReplyListByParent(@Param("paging")PagingVO paging, @Param("reCategory") String reCategory,@Param("reParentNo") int reParentNo);
public ReplyVO getReply(int reNo);
public int updateReply(ReplyVO reply);
public int deleteReply(ReplyVO reply);
public int insertReply(ReplyVO reply);
}
완성
script:
<script type="text/javascript">
var params={"curPage":1, "rowSizePerPage" : 10
,"reCategory" : "FREE", "reParentNo": ${freeBoard.boNo} };
function fn_reply_list(){
//아작스 호출해서 DB에 있는 reply 데이터 가지고 옵니다.
//가지고오면(success)하면 댓글 div 만들어줍니다.
//list를 가지고오니까 jquery 반복문 써서 div 여러개 만들어주면되겠죠?
$.ajax({
url : "<c:url value='/reply/replyList.wow' />"
,type: "POST"
,data : params
,dataType: 'JSON' //받을 때 data를 어떻게 받을지
, success: function(data){
console.log(data);
$.each(data.data, function(index, element) {
var str="";
str=str+'<div class="row" data-re-no="'+ element.reNo +'">'
+'<div class="col-sm-2 text-right" >'+element.reMemName+ '</div>'
+'<div class="col-sm-6"><pre>'+element.reContent+ '</pre></div>'
+'<div class="col-sm-2" >'+element.reRegDate +'</div>'
+'<div class="col-sm-2">';
if(element.reMemId=="${USER_INFO.userId}"){
str=str+ '<button name="btn_reply_edit" type="button" class=" btn btn-sm btn-info" >수정</button>'
+ '<button name="btn_reply_delete" type="button" class="btn btn-sm btn-danger" >삭제</button>';
}
str=str+'</div>'
+'</div>';
$('#id_reply_list_area').append(str);
});
params.curPage+=1;
}//success
}); //ajax
}//function fn_reply_list
$(document).ready(function(){ //documnet가 준비될 때
fn_reply_list(); //freeView처음에 댓글 10개 보여주기
// 등록버튼, 수정,삭제버튼, 모달의 등록버튼
//더보기 버튼
$("#id_reply_list_more").on("click",function(e){
e.preventDefault();
fn_reply_list();
});
//등록버튼
$("#btn_reply_regist").on("click",function(e){
e.preventDefault();
$form=$(this).closest("form[name='frm_reply']");
$.ajax({
url:"<c:url value='/reply/replyRegist.wow'/>"
,type : "POST"
,dataType :"JSON"
,data : $form.serialize()
,success: function(data){
console.log(data);
$form.find("textarea[name='reContent']").val('');
$("#id_reply_list_area").html('');
params.curPage=1;
fn_reply_list();
}
,error : function(req,st,err){
if(req.status==401){
location.href="<c:url value='/login/login.wow' />";
}
}
});//ajax
});//등록버튼
//수정버튼 function(){}은 동적으로 생긴 태그에도 적용이 되는거같아..
//$().on("click") 동적으로생긴 태그에 적용됨
$("#id_reply_list_area").on("click", 'button[name="btn_reply_edit"]'
,function(e){
//modal 아이디=id_reply_edit_modal
//현재 버튼의 상위 div(하나의 댓글 전체)를 찾으세요
// 그 div에서 현재 댓글의 내용 =modal에 있는 textarea에 복사
// 그 div태그의 data-re-no에 있는 값 $().data('re-no')
//=modal에 있는 < input name=reNo>태그에 값으로 복사
//2개 복사했으면 $('#id_reply_edit_modal').modal('show')
$btn=$(this); //수정버튼
$div=$btn.closest('div.row'); //버튼의 댓글 div
$modal=$('#id_reply_edit_modal'); //modal div
$pre=$div.find('pre');
var content=$pre.html();
$textarea=$modal.find('textarea');
$textarea.val(content);
var reNo=$div.data('re-no');
$modal.find('input[name=reNo]').val(reNo);
$modal.modal('show');
});//수정버튼
//모달창 저장 버튼
$("#btn_reply_modify").on("click", function(e){
e.preventDefault();
$form= $(this).closest('form[name="frm_reply_edit"]');
$.ajax({
url : "<c:url value='/reply/replyModify.wow' />"
,type : "POST"
,data : $form.serialize()
,dataType : "JSON"
,success: function(){
$modal=$('#id_reply_edit_modal');
$modal.modal('hide');
var reNo=$modal.find('input[name=reNo]').val();
var reContent=$modal.find('textarea').val();
$("#id_reply_list_area").find("div[data-re-no='"+reNo+"']").find("pre").html(reContent);
}
});//ajax
});//모달창 저장버튼
//삭제버튼
$("#id_reply_list_area").on("click", 'button[name="btn_reply_delete"]'
,function(e){
e.preventDefault();
$div=$(this).closest('.row');
reNo=$div.data('re-no');
reMemId="${USER_INFO.userId}";
$.ajax({
url : "<c:url value='/reply/replyDelete.wow' />"
,type : "POST"
,data : {"reNo" : reNo, "reMemId" : reMemId}
,dataType : 'JSON'
,success : function(){
$div.remove();
}
});//ajax
}); //삭제버튼
});
</script>
ReplyController.java
package com.study.reply.web;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.study.exception.BizAccessFailException;
import com.study.exception.BizNotFoundException;
import com.study.reply.service.IReplyService;
import com.study.reply.vo.ReplySearchVO;
import com.study.reply.vo.ReplyVO;
@RestController
public class ReplyController {
@Inject
IReplyService replyService;
@RequestMapping(value = "/reply/replyList.wow")
public Map<String,Object> replyList(PagingVO paging,String reCategory,int reParentNo){
List<ReplyVO> replyList=replyService.getReplyListByParent(paging,reCategory, reParentNo);
Map<String,Object> map=new HashMap<String, Object>();
map.put("result", true);
map.put("data", replyList);
map.put("size", replyList.size());
return map;
}
@RequestMapping(value = "/reply/replyRegist.wow")
public Map<String,Object> replyRegist(ReplyVO reply){
System.out.println(reply);
replyService.registReply(reply);
Map<String,Object> map=new HashMap<String, Object>();
map.put("result", true);
map.put("msg", "등록성공했어요");
return map;
}
@RequestMapping(value = "/reply/replyModify.wow")
public Map<String,Object> replyModify(ReplyVO reply){
Map<String, Object> map=new HashMap<String, Object>();
try{
replyService.modifyReply(reply);
map.put("result", true);
map.put("msg","수정성공");
} catch (BizAccessFailException e) {
map.put("result", false);
map.put("msg","당신은 댓글을 쓴 사람이 아닙니다.");
}catch (BizNotFoundException e) {
map.put("result", false);
map.put("msg","댓글이 없습니다.");
}
return map;
}
@RequestMapping(value = "/reply/replyDelete.wow")
public Map<String,Object> replyDelete(ReplyVO reply){
Map<String, Object> map=new HashMap<String, Object>();
try{
replyService.removeReply(reply);
map.put("result", true);
map.put("msg","삭제성공");
} catch (BizAccessFailException e) {
map.put("result", false);
map.put("msg","당신은 댓글을 쓴 사람이 아닙니다.");
}catch (BizNotFoundException e) {
map.put("result", false);
map.put("msg","댓글이 없습니다.");
}
return map;
}
}
혹시 한글 인코딩에 문제가 있을 땐 https://brilliantdevelop.tistory.com/110를 참고해라.
ReplyServiceImpl.java
package com.study.reply.service;
import java.util.List;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
import com.study.exception.BizAccessFailException;
import com.study.exception.BizNotFoundException;
import com.study.reply.dao.IReplyDao;
import com.study.reply.vo.ReplySearchVO;
import com.study.reply.vo.ReplyVO;
@Service
public class ReplyServiceImpl implements IReplyService{
@Inject
IReplyDao replyDao;
@Override
public List<ReplyVO> getReplyListByParent(PagingVO paging, String reCategory, int reParentNo) {
int totalRowCount=replyDao.getReplyCountByParent(paging,reCategory,reParentNo);
paging.setTotalRowCount(totalRowCount);
paging.pageSetting();
List<ReplyVO> replyList=replyDao.getReplyListByParent(paging,reCategory,reParentNo);
return replyList;
}
@Override
public void modifyReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException {
ReplyVO vo=replyDao.getReply(reply.getReNo()); //vo는 현재 DB에 있는 값
if(vo==null) throw new BizNotFoundException();
if(!vo.getReMemId().equals(reply.getReMemId())) throw new BizAccessFailException();
replyDao.updateReply(reply);
}
@Override
public void removeReply(ReplyVO reply) throws BizNotFoundException, BizAccessFailException {
ReplyVO vo=replyDao.getReply(reply.getReNo()); //vo는 현재 DB에 있는 값
if(vo==null) throw new BizNotFoundException();
if(!vo.getReMemId().equals(reply.getReMemId())) throw new BizAccessFailException();
replyDao.deleteReply(reply);
}
@Override
public void registReply(ReplyVO reply) {
replyDao.insertReply(reply);
}
}
reply.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.study.reply.dao.IReplyDao">
<select id="getReplyCountByParent"
resultType="int">
SELECT count(*)
FROM reply
WHERE re_parent_no=#{reParentNo}
AND re_category=#{reCategory}
</select>
<select id="getReplyListByParent"
resultType="com.study.reply.vo.ReplyVO">
SELECT *
FROM
(SELECT a.*,rownum AS rnum
FROM
(
SELECT
re_no, re_category, re_parent_no, re_mem_id
, re_content, re_ip
, to_char(re_reg_date,'YYYY-MM-DD') AS re_reg_date
, to_char(re_mod_date,'YYYY-MM-DD') AS re_mod_date
, b.mem_name AS re_mem_name
FROM
reply a, member b
WHERE a.re_mem_id=b.mem_id
AND re_parent_no=#{reParentNo}
AND re_category=#{reCategory}
ORDER by re_no desc
) a ) b
WHERE rnum between #{paging.firstRow} and #{paging.lastRow}
</select>
<insert id="insertReply" parameterType="com.study.reply.vo.ReplyVO">
INSERT INTO reply (
re_no , re_category, re_parent_no
, re_mem_id, re_content, re_ip
, re_reg_date, re_mod_date
) VALUES (
seq_reply.nextval, #{reCategory}, #{reParentNo}
,#{reMemId},#{reContent}, #{reIp}
,sysdate,null
)
</insert>
<select id="getReply" resultType="com.study.reply.vo.ReplyVO" parameterType="int">
SELECT
re_no , re_category, re_parent_no , re_mem_id
, re_content , re_ip , re_reg_date , re_mod_date
FROM
reply
WHERE re_no=#{reNo}
</select>
<update id="updateReply" parameterType="com.study.reply.vo.ReplyVO">
UPDATE reply
SET re_content=#{reContent}
WHERE re_no=#{reNo}
</update>
<delete id="deleteReply" parameterType="com.study.reply.vo.ReplyVO">
DELETE FROM reply
WHERE re_no=#{reNo}
</delete>
</mapper>
여기까지했으면 댓글기능은 완성되었다고 볼 수 있다.
주의사항
근데 여기서 댓글자체와는 큰 관련이없지만, 아래의 경우를 보자.
이 화면에서 로그인을 아직 안했는데 등록 버튼을 누르면 로그인 화면으로 가게된다.
로그인을 성공하고 나면 무조건 홈화면으로 오게 된다.
이게 왜 그런거냐면 LoginController에서 로그인 성공했을 시
무조건 홈화면으로 redirect하도록 했기 때문이다.
LoginController의
package com.study.login.web;
import java.net.URLEncoder;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.study.common.util.CookieUtils;
import com.study.login.service.ILoginService;
import com.study.login.service.LoginServiceImpl;
import com.study.login.vo.UserVO;
@Controller
public class LoginController {
@Inject
ILoginService loginService;
@GetMapping("/login/login.wow")
public String loginGet() {
return "login/login";
}
@PostMapping("/login/login.wow")
public String loginPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
// 사용자가 id,pass입력해서 로그인버튼 누름
String id = req.getParameter("userId");
String pw = req.getParameter("userPass");
String save_id = req.getParameter("rememberMe");
if (save_id == null) {
CookieUtils cookieUtils = new CookieUtils(req);
if (cookieUtils.exists("SAVE_ID")) {
Cookie cookie = CookieUtils.createCookie("SAVE_ID", id, "/", 0);
resp.addCookie(cookie);
}
save_id = "";
}
if ((id == null || id.isEmpty()) || (pw == null || pw.isEmpty())) {
return "redirect:" + "/login/login.wow?msg=" + URLEncoder.encode("입력안했어요", "utf-8");
} else {
UserVO user = loginService.getUser(id);
if (user == null) {
return "redirect:" + "/login/login.wow?msg="
+ URLEncoder.encode("아이디 또는 비번확인", "utf-8");
} else { // id맞았을때
if (user.getUserPass().equals(pw)) {// 다 맞는경우
if (save_id.equals("Y")) {
resp.addCookie(CookieUtils.createCookie("SAVE_ID", id, "/", 3600 * 24 * 7));
}
HttpSession session = req.getSession();
session.setAttribute("USER_INFO", user);
return "redirect:" + "/";
} else {// 비번만 틀린경우
return "redirect:" + "/login/login.wow?msg="
+ URLEncoder.encode("아이디 또는 비번확인", "utf-8");
}
}
}
}
@RequestMapping("/login/logout.wow")
public String logout( HttpSession session, HttpServletRequest req) {
session.removeAttribute("USER_INFO");
return "redirect:"+"/";
}
}
@PostMapping의 id,pw 다 맞는 경우를 보면
return "redirect:" +"/" 로 홈화면으로 redirect 하도록 되어있다.
그럼 어떻게 하면 freeView에서 로그인으로 이동했을 때
로그인성공하고나서 freeView로 다시 갈 수 있을까 ?
바로 request의 Header 중에 referer를 이용해 이전페이지 요청url을 얻을 수 있다.
LoginController에 @GetMapping메소드에서 request의 referer가 freeView가 된다.
그래서 다음과 같이 prePage를 출력하면
http://localhost:8080/study4/free/freeView.wow?boNo={글번호}를 얻을 수 있다.
@GetMapping("/login/login.wow")
public String loginGet(HttpServletRequest req) {
String prePage=req.getHeader("referer");
System.out.println(prePage);
return "login/login";
}
저 prePage 값을 같은 Controller에 @PostMapping으로 넘겨주면 된다는 얘기이다.
그러면 이제 로그인성공했을 시 바로 그 전 페이지로 이동할 수 있게 된다.
session을 통해 넘겨주면 다음과 같이 수정할 수 있다.
@GetMapping에서 session에 데이터 담고
@PostMapping의 다 맞는 경우 session에서 prePage를 얻어서 거기로 redirect했다.
@GetMapping("/login/login.wow")
public String loginGet(HttpServletRequest req,HttpSession session) {
String prePage=req.getHeader("referer");
session.setAttribute("PRE_PAGE",prePage );
System.out.println(prePage);
return "login/login";
}
@PostMapping("/login/login.wow")
public String loginPost(HttpServletRequest req, HttpServletResponse resp,HttpSession session) throws Exception {
// 사용자가 id,pass입력해서 로그인버튼 누름
String id = req.getParameter("userId");
String pw = req.getParameter("userPass");
String save_id = req.getParameter("rememberMe");
if (save_id == null) {
CookieUtils cookieUtils = new CookieUtils(req);
if (cookieUtils.exists("SAVE_ID")) {
Cookie cookie = CookieUtils.createCookie("SAVE_ID", id, "/", 0);
resp.addCookie(cookie);
}
save_id = "";
}
if ((id == null || id.isEmpty()) || (pw == null || pw.isEmpty())) {
return "redirect:" + "/login/login.wow?msg=" + URLEncoder.encode("입력안했어요", "utf-8");
} else {
UserVO user = loginService.getUser(id);
if (user == null) {
return "redirect:" + "/login/login.wow?msg="
+ URLEncoder.encode("아이디 또는 비번확인", "utf-8");
} else { // id맞았을때
if (user.getUserPass().equals(pw)) {// 다 맞는경우
if (save_id.equals("Y")) {
resp.addCookie(CookieUtils.createCookie("SAVE_ID", id, "/", 3600 * 24 * 7));
}
session.setAttribute("USER_INFO", user);
String prePage=(String)session.getAttribute("PRE_PAGE");
if(prePage!=null) {
return "redirect:"+prePage;
}
return "redirect:" + "/";
} else {// 비번만 틀린경우
return "redirect:" + "/login/login.wow?msg="
+ URLEncoder.encode("아이디 또는 비번확인", "utf-8");
}
}
}
}
※같은Controller의 있는 메소드2개니까 그냥 필드에 prePage선언해서 사용하면 되지않을까요???
왜 굳이 session에 담아서 사용했나요?
사용자는 여러명인데 모든 사용자의 요청을 LoginController 객체(빈) 1개가 처리합니다.
필드로 선언했을 경우 다음과 같은 문제가 있습니다.
@Controller
public class LoginController {
String prePage="";
@GetMapping("/login/login.wow")
public String loginGet(HttpServletRequest req) {
String prePage=req.getHeader("referer");
return "login/login";
}
@PostMapping("/login/login.wow")
public String loginPost(HttpServletRequest req, HttpServletResponse resp,HttpSession session) throws Exception {
//생략
//다 맞은 경우
return "redirect:"+prePage
}
만약 사용자1이 freeView에서 댓글 등록버튼을 눌렀습니다.
그리고 @GetMapping("/login/login.wow")메소드가
실행되고 이 때 LoginController 객체의 필드 prePage=freeView가 됩니다.
사용자1이 id,pw입력하기전에 사용자2가 MemberList에서 로그인버튼을 눌렀습니다.
이러면 필드prePage=memberList가 됩니다. 그리고 사용자1이 id,pw를 다 입력하면
짜잔..사용자1은 memberList로 이동하게 됩니다.
그래서 보통 빈객체에서는 데이터변경이 일어나는 변수를 필드로 선언하지않습니다.
주입관련한 빈은 필드에 선언해도 됩니다.