20211216 게시글 삭제

DUUUPPAAN·2021년 12월 16일
0

Spring_Framework

목록 보기
9/19

·게시글 삭제

-어제의 게시글 업로드 및 첨부파일 업로드에 이어서 게시글을 삭제하는 기능을 구현해봤다.
-우선 어제 했던 view.jsp 부분에 자바 스크립트로 각각의 버튼에 대한 처리를 해줬다. 물론, 오늘은 수정에 대한 부분을 하지 않았지만, 그래도 버튼이 눌렸을 때에 정보를 어디로 보내는지 정도는 구현해놨다.

<script>
	$(document).ready(function(){
	<c:choose>
		<c:when test="${empty hiBoard}">
		
			alert("조회하신 게시물이 존재하지 않습니다.");
			document.bbsForm.action = "/board/list";
			document.bbsForm.submit();
			
		</c:when>
		<c:otherwise>
		
			$("#btnList").on("click", function(){
				document.bbsForm.action = "/board/list";
				document.bbsForm.submit();
			});
			
			$("#btnReply").on("click", function(){
				document.bbsForm.action = "/board/replyForm";
				document.bbsForm.submit();
			});
			
			//수정 삭제 버튼은 boardMe가 Y일 때만 나타나야 함.
			<c:if test="${boardMe eq 'Y'}">
				$("#btnUpdate").on("click", function(){
					document.bbsForm.action = "/board/updateForm";
					document.bbsForm.submit();
				});
				
				$("#btnDelete").on("click", function(){
					if(confirm("정말 삭제 하시겠습니까?") == true)
					{
						//정말 삭제하겠다고 했을 때, ajax 통신
						$.ajax({
							type:"POST",
							url:"/board/delete",
							data:
							{
								hiBbsSeq: <c:out value="${hiBoard.hiBbsSeq}" />
							},
							datatype:"JSON",
							beforeSend:function(xhr){
								xhr.setRequestHeader("AJAX", "true");
							},
							success:function(response){
								if(response.code == 0)
								{
									alert("게시물이 삭제되었습니다.");
									location.href = "/board/list";
								}
								else if(response.code == 400)
								{
									alert("파라미터 값이 올바르지 않습니다.");
									//이동할 필요 없음
								}
								else if(response.code == 404)
								{
									alert("게시물을 찾을 수 없습니다.");
									location.href = "/board/list";
								}
								else if(response.code == 405)
								{
									alert("사용자의 게시물이 아닙니다.");
									location.href = "/board/list";
								}
								//메인글이 삭제되면 댓글은 어떻게 되냐를 설정해줘야 함.
								else if(response.code == 999)
								{
									//댓글 존재하는 경우
									alert("답변 게시물이 존재하여 삭제할 수 없습니다.");
									//구현을 하는 방식에 따라 다름
								}
								else
								{
									alert("게시물 삭제 중 오류가 발생했습니다.");
								}
							},
							complete:function(data){
								icia.common.log(data);
							},
							error:function(xhr, status, error)
							{
								icia.common.error(error);
							}
						});
						
					}
				});
			</c:if>
		</c:otherwise>
	</c:choose>	
	
		$("#btnUpdate").on("click",function(){
			document.bbsForm.action = "/board/update";
			document.bbsForm.submit();
		});
		
	});
</script>

-여기서 if문을 쓰지 않고 JSTL+EL 문법을 사용해서 각각의 상황에 맞게 기능하도록 구분해놨는데, 사실 저렇게 if문을 쓰는게 편하지 않았다. 우선 스크립트 태그 내의 영역이라 각각의 태그가 이클립스에서 가시성도 떨어지는데다가, 언어가 생각보다 편리하다고 보긴 어렵다는 생각을 했다. 차라리 전에 jsp처럼 <% %>이런식으로 사용하는 것이 훨씬 더 편해보였다. delete는 보여줄 페이지가 따로 없기 때문에, 비동기 통신으로 삭제가 되었는지 여부만 판단하면 된다. 그래서 ajax통신을 통해 요청을 보내고 응답을 받을 코드별 alert메세지를 정의해줬다. 그리고 구현의 방향에 따라 다르지만, 기본적으로 우리가 현재 배우고 있는 사이트에서는 게시글에 댓글이 달리면 게시글 자체도 삭제가 안되도록 하려고 한다. 그래서 게시글에 댓글이 존재하면 삭제가 안되도록 댓글 작성이 되면 그때 해당 부분을 처리해줘야 해서 주석을 남겼다.

-이제 전과 동일하게 "/board/delete"를 가지고 컨트롤러를 찾아가기 때문에 컨트롤러를 정의해줘야 한다. 물론, 사실상 컨트롤러보다 쿼리문을 먼저 진행했다. 쿼리문을 진행하기 전에, 앞서 언급했듯이 게시물에 댓글이 있으면 삭제가 안되도록 처리를 할 것이기 때문에 해당 게시물에 댓글이 있는지 여부부터 체크해야 한다. 그래서 해당 부분에 대한 쿼리문부터 작성했다.

--댓글에 대한 쿼리 작성할 것.
--댓글이 있는데 삭제가 안되게 해야 함. 그런식으로 처리할 것임.
SELECT COUNT(HIBBS_SEQ) AS CNT
  FROM TBL_HIBOARD
 WHERE HIBBS_PARENT = 7;

-HIBBS_PARENT의 값이 0이 아니라 1이라도 있다면, 해당 HIBBS_SEQ번호의 게시물, 혹은 댓글에 댓글 혹은 대댓글이 존재한다는 뜻이다. 그런 경우에는 삭제처리가 되지 않도록 해야 한다.

-이제 해당 쿼리문을 갖고 .xml을 정의한다.

<!-- 게시물 삭제 시 답변 글 수 체크 시작 -->
<select id="boardAnswersCount" parameterType="long" resultType="int">
SELECT COUNT(HIBBS_SEQ) AS CNT
  FROM TBL_HIBOARD
 WHERE HIBBS_PARENT = #{value}
</select>
<!-- 게시물 삭제 시 답변 글 수 체크 종료 -->

-SQLdeveloper에서는 하드코딩되어있는 7의 값은 당연히 쿼리가 정상작동하는지 여부를 판단하려고 넣은 값이다. 그래서 해당 값을 #{value}로 바꿔준다.

-select문이지만 결과가 count 하나이기 때문에 조회가 되는 값이 몇개냐를 리턴받는다. 그래서 결과의 타입은 int로 해주었다.

-이제 다시 게시글을 삭제하는 쿼리를 작성한다.

--게시물 삭제 쿼리 작성
DELETE FROM TBL_HIBOARD
WHERE
        HIBBS_SEQ = 7;

-역시 .xml에서 해당 쿼리를 정의해준다.

<!-- 게시물 삭제 시작 -->
<delete id="boardDelete" parameterType="long">
DELETE FROM TBL_HIBOARD
WHERE
        HIBBS_SEQ = #{value}
</delete>
<!-- 게시물 삭제 종료 -->

-게시물이 삭제되려면 첨부파일도 같이 삭제를 해야한다. 물론, 첨부파일이 없다면 상관없지만, 첨부파일이 있다면 같이 삭제해야 하기 때문에 쿼리문을 작성해서 .xml파일에 또 정의해준다.

--첨부파일도 같이 삭제해야됨
DELETE FROM TBL_HIBOARD_FILE
WHERE
        HIBBS_SEQ = 7
;

-여기서 약간의 의문이 생길 수 있는 부분이, 첨부파일은 HIBBS_SEQ와 FILE_SEQ를 복합키로 갖고 있는 테이블인데 왜 조건에 HIBBS_SEQ만 오는가에 대해 생각할 수 있다. 그런데 생각해보면 간단하다. FILE_SEQ는 기본적으로 같은 HIBBS_SEQ를 가진 것들(같은 게시물에 속해 있는 첨부파일들)을 구분하기 위해 또 그 안에서 순서를 주는 것인데, 기본적으로 HIBBS_SEQ가 같다는 것은 같은 게시물이라는 것이고, 해당 게시물이 삭제가된다면, FILE_SEQ와는 상관없이 HIBBS_SEQ가 같은 값들은 전부 삭제되는 것이 맞다. 따라서 조건에는 HIBBS_SEQ만 오면 된다.

<!-- 첨부 파일 삭제 시작 -->
<delete id="boardFileDelete" parameterType="long">
DELETE FROM TBL_HIBOARD_FILE
WHERE
        HIBBS_SEQ = #{value}
</delete>
<!-- 첨부 파일 삭제 종료 -->

-첨부파일을 삭제하는 쿼리까지 작성했으면, 서비스에서 사용할 수 있도록 추상메소드를 정의해준다.

	//게시물 삭제 시 답글 수 체크
	public int boardAnswersCount(long hiBbsSeq);
	
	//게시물 삭제
	public int boardDelete(long hiBbsSeq);
	
	//게시물 첨부파일 삭제
	public int boardFileDelete(long hiBbsSeq);

-어제 게시물 첨부파일 조회에 대한 추상메소드를 정의는 해놨는데, 따로 서비스에 정의해놓지는 않았다. boardView메소드 안에서 사용은 했는데, boardView가 아닌 첨부파일 조회에 대한 서비스를 따로 하나 더 만들었다. 삭제에 필요없는 로직이 많기 때문이다. 예를 들면 조회수를 증가시키는 쿼리문같은 것이다. 해당 첨부파일 조회가 왜 필요하냐면 나중에 첨부파일 다운로드를 위해 필요하다.

   //첨부파일 조회
   public HiBoardFile boardFileSelect(long hiBbsSeq) 
   {
	   HiBoardFile hiBoardFile = null;
	   
	   try 
	   {
		   hiBoardFile = hiBoardDao.boardFileSelect(hiBbsSeq);
	   }
	   catch(Exception e) 
	   {
		   logger.error("[HiBoardService] boardFileSelect Exception", e);
	   }
	   
	   return hiBoardFile;
   }

-그리고 게시물이 존재하는지 여부를 체크하는 서비스와, 게시물과 첨부파일을 트랜잭션으로 묶은 서비스를 하나씩 정의해줬다.

  //게시물 삭제 시 답변 글 수 체크
   public int boardAnswersCount(long hiBbsSeq) 
   {
	   int count = 0;
	   
	   try 
	   {
		   count = hiBoardDao.boardAnswersCount(hiBbsSeq);
	   }
	   catch(Exception e) 
	   {
		   logger.error("[HiBoardService] boardAnswersCount Exception", e);
	   }
	   
	    return count;
   }
   
   //게시물 삭제(첨부파일 있으면 함께 삭제)
   @Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
   public int boardDelete(long hiBbsSeq) throws Exception
   {
	   int count = 0;
	   
	   HiBoard hiBoard = hiBoardDao.boardSelect(hiBbsSeq);
	   //조회수 증가때문에 보드 뷰를 사용할 수 없음
	   
	   if(hiBoard != null) 
	   {
		   HiBoardFile hiBoardFile = hiBoardDao.boardFileSelect(hiBoard.getHiBbsSeq());
		   count = hiBoardDao.boardDelete(hiBoard.getHiBbsSeq());
		   
		   if(hiBoardFile !=null) 
		   {
			   //주소값 일치시키기
			   hiBoard.setHiBoardFile(hiBoardFile);
			   
			   if(hiBoardDao.boardFileDelete(hiBbsSeq)> 0) 
			   {
				   //FileUtil.getFileSeparator()얘는 역슬레시를 해주는 역할.
				   FileUtil.deleteFile(UPLOAD_SAVE_DIR + FileUtil.getFileSeparator()+hiBoardFile.getFileName());
			   }
		   }
	   }

	   return count;
   }

-여기서 중요한 점은 바로 트랜잭션으로 묶어준 부분인데, 첨부파일이 삭제가 되지 않았다면, 게시물만 삭제해서는 안되기 때문에 다 롤백시켜줘야 한다. 하나의 과정으로 묶어줘야 하기 때문에 이렇게 작성했다. 또한 FileUtil.getFileSeparator()이 부분이 이해가 조금 안갈 수 있는데, 공통모듈을 통해서 파일 경로에 /혹은 \를 넣어주는 기능을 한다. os마다 파일 경로에 대한 구분자가 다를 수 있는데, 해당 공통모듈을 통해 현재 os에 맞는 형식으로 구분자를 넣어주는 기능을 한다.

-이제 컨트롤러에서 삭제에 대한 서비스를 호출해서 ajax통신에 대한 응답을 처리해준다.

	@RequestMapping(value="/board/delete", method=RequestMethod.POST)
	@ResponseBody
	public Response<Object> delete(HttpServletRequest request, HttpServletResponse response)
	{
		Response<Object> ajaxResponse = new Response<Object>();
		
		String cookieUserId =  CookieUtil.getHexValue(request, AUTH_COOKIE_NAME);
		long hiBbsSeq =  HttpUtil.get(request, "hiBbsSeq", (long)0);
		
		if(hiBbsSeq >0 ) 
		{
			//0보다 클 때가 게시물 존재
			HiBoard hiBoard = hiBoardService.boardSelect(hiBbsSeq);
			
			if(hiBoard != null) 
			{
				 
				if(StringUtil.equals(hiBoard.getUserId(), cookieUserId)) 
				{
					//쿠키 아이디랑 게시물 작성자와 같음.
					try 
					{
						if(hiBoardService.boardAnswersCount(hiBoard.getHiBbsSeq()) > 0) 
						{
							//하이보드 객체에 있는 hiBbsSeq 값을 보냄. 근데 0보다 크면 게시물을 삭제하면 안됨. 댓글이 있다는 것.
							ajaxResponse.setResponse(999, "Answer exist and cannot be deleted");
						}
						else
						{
							//댓글이 없음, 삭제 가능
							if(hiBoardService.boardDelete(hiBoard.getHiBbsSeq()) >0) 
							{
								//정상 삭제
								ajaxResponse.setResponse(0, "Success");
							}
							else 
							{
								//삭제 실패
								ajaxResponse.setResponse(500, "Internal Server Error");
							}
						}
						
					}
					catch(Exception e) 
					{
						logger.error("[HiBoardController] delete Exception", e);
						ajaxResponse.setResponse(500, "Internal Server Error");
					}
				}
				else 
				{
					ajaxResponse.setResponse(405, "User Error");
				}
				 
			}
			else 
			{
				ajaxResponse.setResponse(404, "Not Exist");
			}
		}
		else 
		{
			ajaxResponse.setResponse(400, "Bad Request");
		}
		
		return ajaxResponse;
	}

-물론 전 페이지를 타고 왔다면, 쿠키아이디와 게시물 작성자의 아이디가 같다고 판단할 수 있지만, 애초에 경로를 그냥 치고 들어오는 사람을 막기 위해선 쿠키아이디와 게시글 작성자의 아이디가 같은지 확인한다. 또한, 중요한 부분이 바로 hiBoardService.boardAnswersCount(hiBoard.getHiBbsSeq())이 부분인데, 해당 부분이 게시물에 댓글이 있나 없나를 판단하는 쿼리다. 이 부분이 0보다 크다면 댓글이 있다는 뜻이므로 현재 우리가 진행하는 스프링 프로젝트에서는 게시글을 삭제할 수 없도록 처리해준다.

·첨부파일 다운로드

-첨부파일은 따로 버튼을 눌렀을 때 받아지는 것이 아니라, 첨부파일을 보여주는 a태그를 클릭했을 때 받아져야 한다. 그래서 view.jsp에 첨부파일 부분에 get방식으로 요청을 하고 응답을 받는 식으로 구현한다.

				<!-- 첨부파일은 있을 때만 보여주면 됨 -->
				<c:if test="${!empty hiBoard.hiBoardFile}">
                <!-- GET방식으로 넘어감 -->
                  &nbsp;&nbsp;&nbsp;<a href="/board/download?hiBbsSeq=${hiBoard.hiBoardFile.hiBbsSeq}" style="color:#000;">[첨부파일]${hiBoard.hiBoardFile.fileOrgName}</a>
                </c:if>

-이제 href="/board/download?hiBbsSeq=${hiBoard.hiBoardFile.hiBbsSeq}"이 부분을 참고해서 "/board/download"에 대한 컨트롤러를 만들어줘야 한다.

	//첨부파일 다운로드
	@RequestMapping("/board/download")
	public ModelAndView download(HttpServletRequest request, HttpServletResponse response) 
	{
		ModelAndView modelAndView = null;
		
		long hiBbsSeq = HttpUtil.get(request, "hiBbsSeq", (long)0);
		
		if(hiBbsSeq >0) 
		{
			//값이 넘어왔다는 뜻
			HiBoardFile hiBoardFile = hiBoardService.boardFileSelect(hiBbsSeq);
			
			if(hiBoardFile != null && hiBoardFile.getFileSize() > 0)
			{
				//파일이 존재하면
				//자바에서 제공하는 File객체라서 java.io임
				File file = new File(UPLOAD_SAVE_DIR + FileUtil.getFileSeparator()+hiBoardFile.getFileName());
				
				logger.debug("UPLOAD_SAVE_DIR : "+ UPLOAD_SAVE_DIR);
				//os버전에 따라서 슬래쉬 혹은 역슬래쉬 해주는 기능이 getFIleSeparator()임
				logger.debug("FileUtil.getFileSeparator() : "+ FileUtil.getFileSeparator());
				logger.debug("hiBoardFile.getFileName() : "+ hiBoardFile.getFileName());

				if(FileUtil.isFile(file)) 
				{
					//파일유틸에서 파일이 존재하는지 확인
					modelAndView = new ModelAndView();
					
					//어떤 클래스를 참조할 것이냐?
					modelAndView.setViewName("fileDownloadView"); //servlet-context.xml 에서 정의한 것(fileDownloadView)
					
					
					modelAndView.addObject("file", file);
					modelAndView.addObject("fileName", hiBoardFile.getFileOrgName());
					
					return modelAndView;
				}
			}
		}
		
		return modelAndView;
	}

-사실 이 부분을 아직 100퍼센트 이해했다고 할 수 없는 것 같다. 우선 ModelAndView 객체와, File객체 그리고 setViewName의 FileDownloadView.java 파일, 또 해당 자바파일이 상속받은 AbstractView, 그리고 FileDownloadView.java에 정의되어 있는 FileInputStream 객체와 OutputStream 객체 등등이 갑작스럽게 한번에 나와서 도무지 이해할 수 없었다.

-교수님께 따로 설명을 요청드렸으나, 시간도 촉박하고 기본적으로 제공하는 기능들이어서 직접 찾아보라는 답변만 들어서 직접 찾아봤다. 우선, ModelAndView는 기존의 사용했던 ModelMap과 흡사한데, ModelMap은 그냥 데이터만 갖고 있을 뿐이고 해당 데이터를 view에서 사용하기 위한 용도이다. 그러나 ModelAndView는 해당 데이터를 저장하고 해당 데이터를 어느 view로 보낼지까지 결정한다. 위에서는 최종적으로 FileDownloadView.java로 보내지는데, 환경변수를 설정하는 servlet-context.xml 부분에 다음과 같이 정의되어 있기 때문이다.

    <!-- 파일 다운로드 VIWE 시작 -->
    <bean id="fileDownloadView" class="com.icia.web.view.FileDownloadView"/>
	<!-- 파일 다운로드 VIWE 종료 -->

해당 경로로 들어가면 FileDownloadView.java를 찾을 수 있다.
-File객체는 해당 파일을 나타내는 것이 아니라 해당 파일에 대한 경로 혹은 참조 정보를 가진 추상적인 객체라고 보면 된다.

그리고 이제 해당 File 객체를 넘겨서 FileInputStream 객체와 OutputStream 객체를 통해서 파일을 바이트 단위의 출력으로 내보내게 된다.

FileInputStream : InputStream 클래스를 상속받은 자식 클래스, 하드 디스크 상에 있는 파일로부터 바이트 단위의 입력을 받는 클래스다. 출발 지점과 도착 지점을 연결하는 통로(스트림)을 생성한다.


FileOutputStream : OutputStream 클래스를 상속받은 자식 클래스, 파일로 바이트 단위의 출력을 내보내는 클래스.

출처: https://onlyfor-me-blog.tistory.com/193

-여기서 또 하나, FileDownloadView.java가 상속받은 AbstractView는 또 무엇인가?

-찾아보니 원래는 viewResolver가 컨트롤러에 응답에 대한 jsp파일을 찾아 응답하게 되는데, 그 사이의 지점을 잡아서 처리하는 것이 AbstractView라고 생각하면 된다.
-그래서 해당하는 .jsp를 찾아가는 것이 아니라 중간에 가로채서 해당하는 view에서 요청에 대한 처리를 하도록 하는 것 같다. 물론, 아직 제대로 100퍼센트 이해한 것이 아니라서 이렇게 말하는 것이 맞는 것인지는 잘 모르겠다.

·내일은 비대면 수업.

-그동안 너무 매일매일 학원에 나가고 복습 정리 복습 운동하고 1시 넘어서 잠에 들고, 또 학원에 조금 일찍 가서 공부를 하고 싶어서 6시 반에 일어나는 강행군을 했더니, 체력이 조금 떨어지는 것 같다. 그래서 원래도 내일은 우리 프로젝트 팀이 나가는 날이 아니기도 하고, 오늘 팀 프로젝트 회의 결과가 흡족하기도 하고, 또 체력적인 부분도 있기 때문에 내일은 비대면으로 수업을 진행하려고 한다. 팀원들도 있으니까 오류가 나더라도 같이 디버깅을 할 수 있어서 믿고 하루만 비대면을 하려한다.

-또한 프로젝트 회의와 방향, navigation bar에 대한 메뉴 및 서브메뉴 정리, 그리고 테이블에 대한 어느정도의 윤곽이 잡혀서 굉장히 프로젝트 출발이 산뜻한 것 같다. 그래도 가장 부담되는 것이 프로젝트였는데, 조금 덜 스트레스 받는 시작이 된 것 같아서 기쁘다. 정리는 끝났고 이제 다시 복습을 하고, 프로젝트 관련 정리를 하고 운동 후 잠에 들어야 할 것 같다. 이번주고 고생했고 내일만 버티도록 하자! 파이팅!...아 물론 그렇다고 주말에 놀면 안된다 내자신!

profile
비전공자란 이름으로 새로운 길을 가려 하는 신입

0개의 댓글