[Spring_demo프로젝트] 4. 댓글 기능 (REST방식)

jngyoon·2023년 11월 10일
0

혼공일기

목록 보기
23/24

#231016 수업 내용 복습

📣 REST방식 설계(Ajax 통신, 돔 구현)

REST(Representational State Transfer)
"하나의 URI는 하나의 고유한 리소스를 대표한다"
하나의 주소에 하나의 리소스를 매칭시키며 설계한다.

CRUD를 아래 방식으로 type 지정
POST(생성), GET(읽어오기), DELETE(삭제), PUT(생성/수정), PATCH(수정)

/board/get?boardnum=3
/board/3 - GET

/board/remove?boardnum=3
/board/3 - DELETE

댓글 기능

get.html 중 댓글 부분

<div class="reply_line">
			<a href="#" class="regist">댓글 등록</a>
			<div class="replyForm row">
				<div style="width:20%">
					<h4>작성자</h4>
					<input type="text" name="userid" th:value="${session.loginUser}" readonly style="text-align: center;">
				</div>
				<div style="width:65%">
					<h4>내 용</h4>
					<textarea name="replycontents" placeholder="Contents" style="resize:none;"></textarea>
				</div>
				<div style="width:15%">
					<a href="#" class="button finish" style="margin-bottom:1rem;">등록</a>
					<a href="#" class="button cancel">취소</a>
				</div>
			</div>
  			<!-- 댓글 띄워주는 부분 -->
			<ul class="replies"></ul>
			<!-- 댓글 페이징 -->
  			<div class="page"></div>
		</div>
<!-- 댓글 기능 부분 javascript -->
<script th:inline="javascript">
	const loginUser = /*[[${session.loginUser}]]*/'';
	const boardnum = /*[[${board.boardnum}]]*/'';
	const replies = $(".replies")
	const page = $(".page")
	let pagenum = 0;
	
	$(document).ready(function(){
		$(".replyForm").hide();	//댓글 입력하는 부분은 처음에는 보이지 않게 hide
		nowpage = 1;
		showList(1);
		
	})
  
  	//댓글등록(class=regist)
	$(".regist").on("click",function(e){
		e.preventDefault();		//댓글등록을 클릭했을 때 페이지이동 없도록 기능 막기(a태그)
	$(".replyForm").show();	//hide했던 replyForm 보여주기
	$(this).hide();			//클릭했던 '댓글등록' 버튼 hide
	})
  	
  	//등록(class=finish) (작성한 댓글 데이터를 POST방식으로 백에 보내주어야함)
  	//'등록' 버튼을 누르면 댓글작성폼은 없어지고, '댓글등록' 버튼 다시 보여져야함
	$(".finish").on("click",function(e){
		e.preventDefault();		//등록을 클릭했을 때 페이지이동 없도록 기능 막기(a태그)
		let replycontents = $("[name='replycontents']").val(); //댓글내용 작성값 변수에 담기
		
  		//'자바스크립트->자바'로 데이터 전송하기 위해 JSON 형태로 보내줌
  		//REST방식으로 설계하기 위해 Ajax통신 코드 작성, 결과 돔 구현 코드 작성을 해야함
  		//Ajax, 돔 분리하여 모듈화
		replyService.add(
			{"boardnum":boardnum,"userid":loginUser, "replycontents":replycontents},
			function(result){
				alert("등록!");
  				showList(1);
				
			}
		)
		
		
	})

static/board/js/reply.js 자바스크립트 생성(Ajax 통신 코드)

//Ajax 통신
/*
function f(){
	return {};
}
const replyService = f();
이걸 줄인게 밑에거
*/
const replyService = (function(){
  
  	//외부에서 ajax 통신만 함
  	//댓글 등록
	function insert(reply, callback){	//reply 객체와 callback 함수 받아옴(reply : ajax통신할 데이터 / callback함수 : 어떤 돔이 구현되어 있는 함수)
		$.ajax({	//어떤식으로 통신할지 정의해서 객체로 넘겨줌
			type:"POST",	//전송방식
			url:"/reply/regist",	//url
			data:JSON.stringify(reply),	//보낼 데이터
			contentType:"application/json;charset=utf-8",	//보내고 있는 내용의 형태
			success:function(result,status,xhr){		//성공시
				callback(result);
			}
		});
	}
	
 	//댓글목록 띄우는 함수
	function selectAll(data,callback){
		let boardnum = data.boardnum;
		let pagenum = data.pagenum;
		
		$.getJSON(
			"/reply/pages/"+boardnum+"/"+pagenum,
			function(data){
				//data : {replyCnt:댓글개수, list:[....]}
				callback(data.replyCnt, data.list);
			}
		)
	}
	
  	//댓글 삭제
	function drop(replynum,callback){
		$.ajax({
			type:"DELETE",
			url:"/reply/"+replynum,
			success:function(result,status,xhr){
				callback(result);
			},
			error:function(xhr,status,err){
				error(err);
			}
		})
	}
	
  	//댓글 수정
	function update(reply,callback){
		$.ajax({
			type:"PUT",
			url:"/reply/"+reply.replynum,
			data:JSON.stringify(reply),
			contentType:"application/json;charset=utf-8",
			success:function(result){
				if(callback){
					callback(result);
				}
			},
			error:function(err){
				if(error){
					error(err);
				}
			}
		})
	}
	
  	//포매팅 타임(시간 포매팅)
	function fmtTime(reply){
		const regdate = reply.regdate;
		const updatedate = reply.updatedate;
		
		const now = new Date();
		
		const check = regdate == updatedate;
		
		const dateObj = new Date(check ? regdate : updatedate);
		//date객체.getTime() : date객체가 가지고 있는 시간 정보를 밀리초 단위로 추출
		let gap = now.getTime() - dateObj.getTime();
		
		let str = "";
		if(gap < 1000*60*60*24){
			let hh = dateObj.getHours();
			let mi = dateObj.getMinutes();
			let ss = dateObj.getSeconds();
			str = (hh>9?'':'0')+hh+":"+(mi>9?'':'0')+mi+":"+(ss>9?'':'0')+ss;
		}
		else{
			let yy = dateObj.getFullYear();
			let mm = dateObj.getMonth()+1;
			let dd = dateObj.getDate();
			
			str = (yy>9?'':'0')+yy+"/"+(mm>9?'':'0')+mm+":"+(dd>9?'':'0')+dd;
		}
		return (check?'':'(수정됨) ')+str;
	}
	
	
	return {add:insert, getList:selectAll, remove:drop, modify:update, displayTime:fmtTime}
})(); //함수 만듦과 동시에 호출

get.html에서 <script th:src="@{/board/js/reply.js}"></script>로 js 연결


☑️ 흐름 정리

1. replyService.add로 매개변수(객체, 함수) 넘기면서 add 호출

    replyService.add(
			<!-- 첫번째 매개변수 : 객체 -->
			{"boardnum":boardnum,"userid":loginUser, "replycontents":replycontents},
			<!-- 두번째 매개변수 : 함수 -->
			function(result){
				alert("등록!");
				
			}
		)

2. add에 연결되어 있는 insert 호출(reply:첫번째 매개변수/callback:두번째 매개변수)

const replyService = (function(){
  	//insert : 외부에서 ajax 통신만 함
	function insert(reply, callback){	//reply 객체와 callback 함수 받아옴(reply : ajax통신할 데이터 / callback함수 : 어떤 돔이 구현되어 있는 함수)
		$.ajax({	//어떤식으로 통신할지 정의해서 객체로 넘겨줌
			type:"POST",	//전송방식
			url:"/reply/regist",	//url
			data:JSON.stringify(reply),	//보낼 데이터
			contentType:"application/json;charset=utf-8",	//보내고 있는 내용의 형태
			success:function(result,status,xhr){		//성공시
				callback(result);
			}
		});
	}
  
  return {add:insert}

3. 넘겨온 매개변수들을 json화 시켜 ajax 통신

4. ajax 통신 성공시 success라는 이름으로 넘겨준 함수 호출 -> callback 함수 호출 -> dom 진행


ReplyController.java 생성

package com.kh.demo.controller;

@RequestMapping("/reply/*")
@RestController
public class ReplyController {
		@PostMapping(value = "regist", consumes = "application/json")	//consumes : 소비할 데이터
	public ResponseEntity<String> regist(){
    
    }
}

ReplyMapper.java 생성

package com.kh.demo.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.ReplyDTO;

@Mapper
public interface ReplyMapper {
	//insert
	int insertReply(ReplyDTO reply);
	
	//update
	int updateReply(ReplyDTO reply);
	
	//delete
	int deleteReply(Long replynum);
	int deleteByBoardnum(Long boardnum);
	
	//select
	Long getLastNum(String userid);
	int getTotal(Long boardnum);
	List<ReplyDTO> getList(Criteria cri, Long boardnum);
	int getRecentReply(Long boardnum);
}

ReplyMapper.xml 생성

<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kh.demo.mapper.ReplyMapper">
	<insert id="insertReply">
		insert into t_reply(replycontents, userid, boardnum)
		values(#{replycontents},#{userid},#{boardnum})
	</insert>
	
	<update id="updateReply">
		update t_reply set replycontents=#{replycontents}, updatedate=now()
		where replynum=#{replynum}
	</update>
	
	<delete id="deleteReply">
		delete from t_reply where replynum=#{replynum}
	</delete>
	<delete id="deleteByBoardnum">
		delete from t_reply where boardnum=#{boardnum}
	</delete>
	
	<select id="getTotal">
		select count(*) from t_reply where boardnum=#{boardnum}
	</select>
	<select id="getRecentReply">
		<![CDATA[
			select count(*) from t_reply where boardnum=#{boardnum} and timestampdiff(HOUR,regdate,now())<1
		]]>
	</select>
	<select id="getLastNum">
		select max(replynum) from t_reply where userid=#{userid}
	</select>
	<select id="getList">
		select * from t_reply where boardnum=#{boardnum}
		limit #{cri.startrow},#{cri.amount}
	</select>
</mapper>

ReplyService.java 인터페이스 생성

package com.kh.demo.service;

import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.ReplyDTO;
import com.kh.demo.domain.dto.ReplyPageDTO;

public interface ReplyService {
	boolean regist(ReplyDTO reply);
	
	boolean modify(ReplyDTO reply);
	
	boolean remove(Long replynum);
	
	ReplyPageDTO getList(Criteria cri, Long boardnum);
	
	Long getLastNum(String userid);
}

ReplyServiceImpl.java 클래스 생성

package com.kh.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.ReplyDTO;
import com.kh.demo.domain.dto.ReplyPageDTO;
import com.kh.demo.mapper.ReplyMapper;

@Service
public class ReplyServiceImpl implements ReplyService {
	@Autowired
	private ReplyMapper rmapper;

	@Override
	public boolean regist(ReplyDTO reply) {
		return rmapper.insertReply(reply) == 1;
	}

	@Override
	public boolean modify(ReplyDTO reply) {
		return rmapper.updateReply(reply) == 1;
	}

	@Override
	public boolean remove(Long replynum) {
		return rmapper.deleteReply(replynum) == 1;
	}

	@Override
	public ReplyPageDTO getList(Criteria cri, Long boardnum) {
		return new ReplyPageDTO(rmapper.getTotal(boardnum), rmapper.getList(cri, boardnum));
	}

	@Override
	public Long getLastNum(String userid) {
		return rmapper.getLastNum(userid);
	}
	
}

ReplyPageDTO.java 생성

package com.kh.demo.domain.dto;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ReplyPageDTO {
	int replyCnt;	//페이징처리때매 댓글 전체 갯수 필요
	List<ReplyDTO> list;

}

ReplyController.java 작성

package com.kh.demo.controller;

//import 생략

@RequestMapping("/reply/*")
@RestController
public class ReplyController {
	@Autowired
	private ReplyService service;
	
	//ResponseEntity : 서버의 상태코드, 응답 메세지, 응답 데이터 등을 담을 수 있는 타입
	//consumes : 이 메소드가 호출될 때 소비할 데이터의 타입(넘겨지는 RequestBody의 데이터 타입)
	//@RequestBody : 넘겨지는 body의 데이터 타입을 해석해서 해당 파라미터에 채워넣기
	@PostMapping(value = "regist", consumes = "application/json")	//consumes : 소비할 데이터
	public ResponseEntity<String> regist(@RequestBody ReplyDTO reply){
		boolean check = service.regist(reply);
		Long replynum = service.getLastNum(reply.getUserid());
		
		return check ? new ResponseEntity<String>(replynum+"",HttpStatus.OK) : 
			new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);	
            //성공하면 OK, 실패하면 error
	}
	
	// /reply/pages/100/1 ; 100번 게시글의 1 페이지 댓글 리스트
	@GetMapping(value = "/pages/{boardnum}/{pagenum}")
	public ResponseEntity<ReplyPageDTO> getList(
    		//@PathVariable : path에 포함된 변수	
			@PathVariable("boardnum") Long boardnum, //path에 있는 boardnum을 boardnum 변수에 담기  
			@PathVariable("pagenum") int pagenum
	){
		Criteria cri = new Criteria(pagenum, 5);	//amount=5 -> 한페이지에 댓글 5개씩
		return new ResponseEntity<ReplyPageDTO>(service.getList(cri, boardnum), HttpStatus.OK);
	}
	
	//@DeleteMapping : REST 방식의 설계 이용 시 삭제 요청을 받을 때 사용하는 매핑 방식
	//produces : 이 메소드가 호출된 결과로 생산해낼 데이터의 타입(돌려주는 ResponseBody의 데이터 타입)
	@DeleteMapping(value = "{replynum}", produces = MediaType.TEXT_PLAIN_VALUE)
	public ResponseEntity<String> remove(@PathVariable("replynum") Long replynum){
		return service.remove(replynum) ?
				new ResponseEntity<String>("success",HttpStatus.OK) : 
				new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
		
	}
	
    //수정 => PUT, PATCH
	//PUT
	// 모든 데이터들을 다 전달, 자원의 전체 수정, 자원 내의 모든 필드를 전달해야 함
	//PATCH
	// 자원의 일부 수정, 수정할 필드만 전송
	//@PatchMapping(value = "{replynum}", consumes = "application/json")
	@PutMapping(value = "{replynum}", consumes = "application/json")
	public ResponseEntity<String> modify(@RequestBody ReplyDTO reply){
		return service.modify(reply) ? 
				new ResponseEntity<String>("success", HttpStatus.OK) :
				new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
						
	}
	
}

돔(DOM) 구현

get.html

<!-- 댓글 기능 돔 구현 부분 javascript -->
<script th:inline="javascript">
	//전역변수 선언
	const replies = $(".replies")
	const page = $(".page")
	let nowpage = 0;
	
  	function showList(pagenum){
		replyService.getList(
			{boardnum:boardnum, pagenum:pagenum||1},  //pagenum이 비어있다면 1
			function(replyCnt, list){
				let str = "";
				if(list == null || list.length == 0){
					str+= '<li class="noreply" style="clear:both;">등록된 댓글이 없습니다.</li>';
					replies.html(str);
					return;
				}
              
				for(let i=0;i<list.length;i++){
					//<li style="clear:both;" class="li3">
					str += '<li style="clear:both;" class="li'+list[i].replynum+'">';
					str += '<div style="display:inline; float:left;9
                  width:80%;">';
					//<strong class="userid3">apple</strong>
					str += '<strong class="userid'+list[i].replynum+'">'+list[i].userid+'</strong>';
					//<p class="reply3">댓글내용</p>
					str += '<p class="reply'+list[i].replynum+'">'+list[i].replycontents+'</p>';
					str += '</div><div style="text-align:right;">'
					str += '<strong>'+replyService.displayTime(list[i])+'</strong>'
					if(list[i].userid == loginUser){
						//<a href="3" class="modify">수정</a>
						str += '<a href="'+list[i].replynum+'" class="modify">수정</a>';
						str += '<a href="'+list[i].replynum+'" class="mfinish" style="display:none;">수정 완료</a>';
						str += '<a href="'+list[i].replynum+'" class="remove">삭제</a>';
					}
					str += '</div></li>';
				}
				replies.html(str);
				
				$(".remove").on("click",deleteReply);
				$(".modify").on("click",modifyReply);
				$(".mfinish").on("click",modifyReplyOk);
				
				showReplyPage(replyCnt, pagenum);
			}
		)
	}
	
	function showReplyPage(replyCnt, pagenum){
		let endPage = Math.ceil(pagenum/5)*5;
		let startPage = endPage - 4;
		
		let prev = startPage != 1;
		endPage = (endPage-1)*5 >= replyCnt ? Math.ceil(replyCnt/5) : endPage;
		let next = endPage*5 < replyCnt ? true : false;
		
		let str = "";
		if(prev){
			//<a class="changePage" href="5"><code>&lt;</code></a>
			str += '<a class="changePage" href="'+(startPage-1)+'"><code>&lt;</code></a>';
		}
		for(let i=startPage;i<=endPage;i++){
			if(i == pagenum){
				//<code class="nowPage">7</code>
				str += '<code class="nowPage">'+i+'</code>';
			}
			else{
				//<a class="changePage" href="9"><code>9</code></a>
				str += '<a class="changePage" href="'+i+'"><code>'+i+'</code></a>';
			}
		}
		if(next){
			str += '<a class="changePage" href="'+(endPage+1)+'"><code>&gt;</code></a>';
		}
		
		page.html(str);
		
		$(".changePage").on("click",function(e){
			e.preventDefault();
			let target = $(this).attr("href");
			nowpage = parseInt(target);
			showList(nowpage);
			window.setTimeout(function(){
				window.scrollTo(0,document.body.scrollHeight)
			},10)
		})
	}
	
	function deleteReply(e){
		e.preventDefault(); //페이지 이동 막아주기
		let replynum = $(this).attr("href");
		replyService.remove(
			replynum,
			function(result){
				if(result == "success"){
					alert(replynum+"번 댓글 삭제 완료!");
					showList(nowpage);
				}
			}
		)
	}

	let replyFlag = false;
	function modifyReply(e){
		e.preventDefault();
		if(!replyFlag){		//여러개 한번에 수정 안되게 하기 위한 flag
			replyFlag = true;
			let replynum = $(this).attr("href");
			const replyTag = $(".reply"+replynum);
			//<textarea class="3 mdf">댓글내용</textarea> 
			replyTag.html('<textarea class="'+replynum+' mdf">'+replyTag.text()+'</textarea>')
			$(this).hide();
			$(this).next().show();
		}
		else{
			alert("수정중인 댓글이 있습니다.");
		}
	}
	function modifyReplyOk(e){
		e.preventDefault();
		replyFlag = false;
		
		let replynum = $(this).attr("href");
		let replycontents = $("."+replynum).val();
		
		if(replycontents == ""){
			alert("수정할 댓글 내용을 입력하세요.");
			return;
		}
		replyService.modify(
			{replynum:replynum, replycontents:replycontents, boardnum:boardnum, userid:loginUser},
			function(result){
				if(result == "success"){
					alert(replynum+"번 댓글 수정 완료!");
					showList(nowpage);
				}
			}
		)
	}
	
	
	function modify(){
		const boardForm = document.boardForm;
		boardForm.setAttribute("action",/*[[@{/board/modify}]]*/'');
		boardForm.setAttribute("method","get");
		boardForm.submit();
	}
	
	$(".cancel").on("click",function(e){
		e.preventDefault();
		$(".replyForm").hide();
		$(".regist").show();
		$("[name='replycontents']").val("");
	})
</script>
	

0개의 댓글