Git · 코드 기록

Spring 게시판 댓글 기능 구현

짱코딩러 2022. 11. 7. 12:41

 

리뷰 게시판의 댓글 기능을 구현해 보았다.

 

replyno number pk 댓글 pk
reviewno number fk 댓글 단 게시글 번호
reply varchar2 nn 댓글 내용
replyer varchar2 fk 댓글을 단 사용자
nth number default 0 댓글 레벨(댓글=0, 리댓글=1)
reReplyno number nn 리댓글 단 댓글 번호
(nth =0 인 경우 그냥 본인의
pk가 들어가도록 함.)
reSum number default 0 달린 리댓글 개수
(댓글에만 들어감)
replyDate date default sysdate 작성일

 

 

1. 게시글마다 댓글(+리댓)을 뿌려준다.

  1-1. 처음에는 1페이지의 댓글을 뿌려줌

  1-2. 새로 댓글을 작성하면 마지막 페이지로 이동한다.

2. 댓글(nth = 0)에 리댓(nth = 1)을 달 수 있다.

  2-1.리댓에는 리리댓을 달 수 없다.

3. 작성한 댓글은 작성자 본인만 삭제할 수 있다.(작성자에게만 버튼이 보임)

  3-1. 리댓이 달려있는 댓글은 삭제할 수 없다.

 

1. 게시글마다 댓글을 뿌려준다.

  1-1. 처음에는 1페이지의 댓글을 뿌려줌

  1-2. 새로 댓글을 작성하면 마지막 페이지로 이동한다.

//댓글목록 가져오기
	function showList(page){
		console.log("show list "+page);
											//page(파라미터값)가 없을 때는 1로 설정
		replyService.getList({reviewno:reviewnoValue,page: page|| 1 }, 
			function(replyCnt, list){
				console.log("replyCnt: " +replyCnt);
				console.log(list)
				
				//만약에 page번호가 -1이면
				if(page == -1){
					//(찐)마지막페이지로 감		=>1-2
					pageNum = Math.ceil(replyCnt/10.0);
					showList(pageNum);
					return;
				}
				
				var str="";
				//가져올 댓글이 없으면 return
				if(list == null || list.length == 0){
					return;
				}
                //댓글이 있으면 있는 만큼 반복해서 가져옴
				for(var i =0, len =list.length || 0; i<len; i++){
						str +="<ul class='chat'><li class='clearfix' data-replyno='"+list[i].replyno+"'>";
					    str += list[i].replyer+"</li>"; 
					    str +="    <li class='reply-content'>"+list[i].reply;
					   //작성자 또는 관리자만 삭제할 수 있도록 설정
                        var id;
					    var admin;
					    <sec:authorize access="isAuthenticated()">
					    	id="<sec:authentication property='principal.username' />";
					    </sec:authorize>
					    <sec:authorize access="hasRole('ROLE_ADMIN')">
					    	admin="ture";
				    	</sec:authorize>
                        //삭제버튼
					    if((id == list[i].replyer) || admin){
					    	str +="<img id='modalRemoveBtn' data-rrr='"+list[i].reReplyno+"' data-re='"+list[i].reSum+"' data-replyer='"+list[i].replyer+"' data-replyno='"+list[i].replyno+"' src='/resources/image/delete.png'>";
					    }
					    str +="    <li class='pull-right text-muted'>"
					        +replyService.displayTime(list[i].replyDate)+"</li></ul><hr style='margin-top:7px'>";
				}
				
				replyUL.html(str); //태그에 넣어줌
				
				showReplyPage(replyCnt); //페이지 출력하는 함수
				
			});//end function
	}//end showList

 

+페이지 출력 함수

var pageNum = 1;
	var replyPageFooter = $(".panel-footer");
	
//댓글 페이지 번호 출력
	function showReplyPage(replyCnt){
		//현재 페이지가 13일때  1.3을 올림하면 2로 해서 20이 마지막 페이지가 됨.
		var endNum = Math.ceil(pageNum / 10.0) * 10;
		var startNum = endNum - 9;
		
		var prev = startNum != 1;
		var next = false;	//next버튼 막아둠
		
		//마지막 페이지(20)를 10곱한 값(200)이 총 댓글수보다 크면
		//빈 페이지가 있는 상태이므로
		if(endNum * 10 >= replyCnt){
			//총 댓글수를 10으로 나눠서 올림한 값을 맨끝 페이지로 정함.(그리고 next 못누름)
			endNum = Math.ceil(replyCnt/10.0);
		}
		
		//총 댓글수보다 작으면 next버튼 사용 가넝한.
		if(endNum * 10 < replyCnt){
			next = true;
		}
		
		var str = "<ul class='pagination pull-right'>";
		
		//이전 버튼이 true면 시작번호 -1해줌(1~10, 11~20 이 숫자 조절해주는거)
		if(prev){
			str+= "<li class='page-item'><a class='page-link' href='"+(startNum -1)+"'>Previous</a></li>";
		}
		
		//전체 페이지수 만큼 반복
		for(var i = startNum; i<=endNum; i++){
			//현재 페이지가 i이면 css클래스 붙여주자
			var active = pageNum == i? "bold":"";
			str+= "<li class='page-item "+active+" '><a class='page-link' href='"+i+"'>"+i+"&nbsp;</a></li>";
		}

		//next가 true면 끝번호 +1해줌(1~10, 11~20 이 숫자 조절해주는거)
		if(next){
			str+= "<li class='page-item'><a class='page-link' href='"+(endNum + 1)+"'>Next</a></li>";
		}
		
		str += "</ul></div>";
		
		console.log(str);
		
		replyPageFooter.html(str); // 태그안에 페이지 붙여줌
	}
	
//페이지 번호를 클릭했을 때 새로운 댓글을 가져옴
	replyPageFooter.on("click","li a", function(e){
		//a태그 기능 제거하고
		e.preventDefault();
		console.log("page click");
		
		//클릭한 값을 담아서
		var targetPageNum = $(this).attr("href");
		
		console.log("targetPageNum: "+ targetPageNum);
		
		//현재 페이지에 넣어줌
		pageNum = targetPageNum;
		
		//해당 페이지목록을 띄움
		showList(pageNum);
	})

 

 

2. 댓글(nth = 0)에 리댓(nth = 1)을 달 수 있다.

  2-1.리댓에는 리리댓을 달 수 없다.

	//댓글목록 가져오기(리댓 추가)
	function showList(page){
		console.log("show list "+page);
											//page(파라미터값)가 없을 때는 1로 설정
		replyService.getList({reviewno:reviewnoValue,page: page|| 1 }, 
			function(replyCnt, list){
				console.log("replyCnt: " +replyCnt);
				console.log(list)
				
				//만약에 page번호가 -1이면
				if(page == -1){
					//(찐)마지막페이지로 감		=>1-2
					pageNum = Math.ceil(replyCnt/10.0);
					showList(pageNum);
					return;
				}
				
				var str="";
				
				if(list == null || list.length == 0){
					//replyUL.html("");
					return;
				}
				for(var i =0, len =list.length || 0; i<len; i++){
                	//nth가 1이면 태그 추가(리댓 들어갈 곳)
					if(list[i].nth){
						str += "<span class='rechat'>";
					}
						str +="<ul class='chat'><li class='clearfix' data-replyno='"+list[i].replyno+"'>";
					//nth가 1이면 리댓 표시 추가(└)
                    if(list[i].nth){
						str += "<span>└</span>";
					}
					    str += list[i].replyer+"</li>"; 
					    str +="    <li class='reply-content'>"+list[i].reply;
					     //작성자 또는 관리자만 삭제할 수 있도록 설정
                            var id;
					    	var admin;
					    <sec:authorize access="isAuthenticated()">
					    	id="<sec:authentication property='principal.username' />";
					    </sec:authorize>
					    <sec:authorize access="hasRole('ROLE_ADMIN')">
					    	admin="ture";
				    	</sec:authorize>
                        //삭제버튼
					    if((id == list[i].replyer) || admin){
					    	str +="<img id='modalRemoveBtn' data-rrr='"+list[i].reReplyno+"' data-re='"+list[i].reSum+"' data-replyer='"+list[i].replyer+"' data-replyno='"+list[i].replyno+"' src='/resources/image/delete.png'>";
					    }
                        //리댓버튼(nth가 0일때만)
					    if(id && !list[i].nth){
					    	str +="<img id='modalRereplyBtn' data-nth='"+list[i].nth+"' data-replyno='"+list[i].replyno+"' data-replyer='"+list[i].replyer+"' data-replyno='"+list[i].replyno+"' src='/resources/image/rereply.png'></li>";
					    }
					    str +="    <li class='pull-right text-muted'>"
					        +replyService.displayTime(list[i].replyDate)+"</li></ul><hr style='margin-top:7px'>";
					 //nth가 1이면 태그 닫아줌
                     if(list[i].nth){
							str += "</span>";
					 }
				}
				
				replyUL.html(str);
				
				showReplyPage(replyCnt);//페이지 출력하는 함수 시행
				
			});//end function
	}//end showList

 

댓글 추가

	//리댓버튼 눌렀으면 input[name='reReplynum']에 누른 댓글의 번호를 담는다
	$('.panel-body').on("click", "#modalRereplyBtn", function(e){
		e.preventDefault();
		modal.find("input[name='reReplynum']").val($(this).data("replyno"));
        //입력 창에 누른 댓글의 아이디를 표시해 주었음
		$("#replyContent").attr("placeholder", "@"+$(this).data("replyer"));
	});

 

	//댓글 추가
	modalRegisterBtn.on("click",function(e){
		e.preventDefault();
        //리댓버튼 안눌렀으면(input[name='reReplynum']에 아무것도 안담겼으면)
		if(!reReplynum.val()){
			//댓글 정보 담아서
			var reply = {
				reply : inputReply.val(),
				replyer: inputReplyer.val(),
				reviewno: reviewnoValue,
				nth:"0"
			};
			//기본 댓글로 insert해줌
			replyService.add(reply, function(result){
				modal.find("textarea").val("");
				showList(-1);
			});
        //리댓 눌렀으면
		}else{
			//댓글 정보 담아서
			var reply = {
				reply : inputReply.val(),
				replyer: inputReplyer.val(),
				reviewno: reviewnoValue,
				nth:"1",
				reReplyno: reReplynum.val()
			};
			//리댓으로 insert해줌(reSum도 +1해주고 있음)
			replyService.add(reply, function(result){
            	//insert성공했으면 댓글창이랑 reReplynum 비워줌
				modal.find("textarea").val("");
				modal.find("input[name='reReplynum']").val("");
				$("#replyContent").attr("placeholder", "댓글을 입력해 주세요.");
				showList(pageNum);
			});
		}
	});

 

 

3. 작성한 댓글은 작성자 본인만 삭제할 수 있다.

  3-1. 리댓이 달려있는 댓글은 삭제할 수 없다.

	//삭제하기 버튼 눌렀으면
	$('.panel-body').on("click", "#modalRemoveBtn", function(e){
		var replyer = $(this).data("replyer");
		var replyno = $(this).data("replyno");
		var reReplyno = $(this).data("rrr");
		var reSum = $(this).data("re");
		console.log(replyer, replyno, reSum, reReplyno)
        //리댓이 있을 때는 삭제 불가
		if(reSum != 0){
			alert("답댓글이 달린 댓글은 삭제할 수 없습니다.");
		}else if(confirm('선택한 댓글을 삭제 하시겠습니까?')){
        	//삭제하고
			replyService.remove(replyno, function(result){} )
			var reply = {
				reReplyno: reReplyno
			};
            //reSum-1해줌
			replyService.removeDown(reply, function(result){showList(pageNum)} )
		}else
			return false;
	});

 

+연결해준 js


var replyService = (function(){
		
	//댓글 등록
	function add(reply, callback, error) {
		
		$.ajax({
			type : 'post',	//전송 방식
			url : '/replies/new',	//전송 페이지(action)
			data : JSON.stringify(reply),	//넘길 파라미터(전송할 데이터)//reply를 JSON 문자열로 변환
			contentType : "application/json; charset=utf-8", //보내는 데이터의 타입
			success : function(result, status, xhr) {	//전송 성공시 실행할 코드
				if (callback) {	//만약에 콜백함수가 값을 주면
					callback(result);	//실행해!
				}
			},
			error : function(xhr, status, er) {		//실패시 실행할 코드
				if (error) {
					error(er);
				}
			}
		})
	}
	//리댓글 등록
	function addRe(reply, callback, error) {

		$.ajax({
			type : 'post',	//전송 방식
			url : '/replies/new_re',	//전송 페이지(action)
			data : JSON.stringify(reply),	//넘길 파라미터(전송할 데이터)//reply를 JSON 문자열로 변환
			contentType : "application/json; charset=utf-8", //보내는 데이터의 타입
			success : function(result, status, xhr) {	//전송 성공시 실행할 코드
				if (callback) {	//만약에 콜백함수가 값을 주면
					callback(result);	//실행해!
				}
			},
			error : function(xhr, status, er) {		//실패시 실행할 코드
				if (error) {
					error(er);
				}
			}
		})
	}
	
	//댓글 목록
	function getList(param, callback, error){
		var reviewno = param.reviewno;
		var page = param.page || 1;
		
		//원하는 해당자료의 json데이터를 얻기위하여 호출
		$.getJSON("/replies/pages/"+reviewno+"/"+page+".json",		//[1]url
			function(data){										//[2]data
				if(callback){	//값이 오면
					//callback(data);	//댓글 목록만 가져오는 경우
					callback(data.replyCnt, data.list);		//댓글 숫자와 목록을 가져오는 경우
				}
			}).fail(function(xhr, status, err){					//[3]success
				if(error){
					error();
				}
			});
	}
	
	//삭제
	function remove(replyno, callback, error){
		$.ajax({
			type : 'delete',
			url : '/replies/'+replyno,
			success : function(deleteResult, status, xhr){
				if(callback){
					callback(deleteResult);
				}
			},
			error : function(xhr, status, er){
				if(error){
					error(er);
				}
			}
		});
	}
	function removeDown(reply, callback, error){
		$.ajax({
			type : 'put',
			url : '/replies/'+ reply.reReplyno,
			data : JSON.stringify(reply),
			contentType : "application/json; charest=utf-8",
			success : function(result, status, xhr){
				if(callback){
					callback(result);
				}
			},
			error : function(xhr, status, er){
				if(error){
					error(er);
				}
			}
		});
	}
    
	//시간 표기
	function displayTime(timeValue){
		//현재 시간(날짜)
		var today = new Date();
		//현재 시간과 작성 시간의 차이
		var gap = today.getTime() - timeValue;
		//작성 시간
		var dateObj = new Date(timeValue);
		var str="";
		
		//하루 차이보다 적으면 작성 시간 입력(시.분.초)
		if( gap < (1000*60*60*24)){
			var hh = dateObj.getHours();
			var mi = dateObj.getMinutes();
			var ss = dateObj.getSeconds();
			
			return [ (hh > 9 ? '' : '0') + hh,':', (mi > 9 ? '' : '0') + mi,
				':', (ss > 9 ? '' : '0') + ss ].join('');
		
		//하루가 넘으면 작성 날짜 입력(연.월.일)
		}else{
			var yy = dateObj.getFullYear();
			var mm = dateObj.getMonth() + 1;
			var dd = dateObj.getDate();
			
			return [ yy, '/', (mm > 9 ? '' : '0') + mm, '/',
				(dd > 9 ? '' : '0') + dd ].join('');
		}
	};
	
	return {
		add : add,
		getList : getList,
		remove : remove,
		removeDown : removeDown,
		displayTime : displayTime
	};
})();

 

+Controller

@RequestMapping("/replies/")
@RestController
@Log4j
@AllArgsConstructor
public class ReplyController {

	private ReplyService service;

	// 들어오는 데이터 타입 정의 			//반환하는 데이터 타입 정의 			=>JSON방식으로만 처리하도록 함
	@PostMapping(value = "/new", consumes = "application/json", produces = { MediaType.TEXT_PLAIN_VALUE })
	// JSON데이터를 ReplyVO타입으로 변환하도록 함(JSON->자바객체)
	public ResponseEntity<String> create(@RequestBody ReplyVO vo) {
		log.info("ReplyVO: " + vo);
		
		if(vo.getNth()==1) {
			log.info("ReplyVO: " + vo+ vo.getReReplyno());

			int insertCount = service.register(vo, vo.getReReplyno());

			log.info("Reply INSERT COUNT: " + insertCount);

			//insert성공 했으면 성공 뿌려주고, 아니면 에러 뿌려줘
			return insertCount == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
					: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
		}else {
			int insertCount = service.registerKey(vo);
			
			log.info("Reply INSERT COUNT: " + insertCount);
			
			//insert성공 했으면 성공 뿌려주고, 아니면 에러 뿌려줘
			return insertCount == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
					: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
		}
		
	}
	
	//목록
	@GetMapping(value = "/pages/{reviewno}/{page}",
			produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
	public ResponseEntity<ReplyPageDTO> getList(@PathVariable("page") int page, @PathVariable("reviewno") Long reviewno){
		Criteria cri = new Criteria(page, 10);
		log.info("get Reply List reviewno: "+reviewno);
		log.info("cri:"+cri);
		
		return new ResponseEntity<>(service.getListPage(cri, reviewno),HttpStatus.OK);
	}
	
	//삭제
	@DeleteMapping(value = "/{replyno}", produces = {MediaType.TEXT_PLAIN_VALUE})	//replyno값으로 데이터를 삭제
	public ResponseEntity<String> remove(@PathVariable("replyno") Long replyno){
		log.info("remove: "+replyno);
		
		return service.remove(replyno) == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
											: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
	}
	
}