상세 컨텐츠

본문 제목

스프링 부트 게시판 좋아요/싫어요 기능 구현하기(ajax)

강의&독학 학습 내용/JAVA

by 라타니 2021. 12. 6. 03:54

본문

동 기능을 수업 도중에 구현하던 와중, 강사님의 방식대로 진행하려다보니 오히려 내 머릿속도 뒤죽박죽 되어버렸고, 로직 또한 제대로 구상된 것 같지도 않아 내 맘에 안 들었다.

그래서 처음부터 아예 수업을 안 듣고 따로 만들어버렸다. 비대면 수업이라 눈치 안 보고 내 멋대로 만들어볼 수 있었다.

그런데 생판 처음 만들어보는 입장에선 너무나도 어려운 기능이었던 것 같다. 내가 부족해서 더 그랬겠지만.

리액션 기능 시연 영상
게시글 상세조회 접속부터 리액션 실행까지의 프로세스에 대한 다이어그램

어째....도식화하고 보니 더 복잡하게 느껴진다.

아마 다이어그램이 익숙하지 않아 중구난방으로 그려서일까 싶다.

또한 더욱 어렵게 느껴질 만한 요인은 유사한 변수와 메서드명 때문인 것이 분명하다.

정말...네이밍센스가 매우 중요하다는 것을 새삼 다시 한 번 느꼈다(아마 계속 하다보면 늘겠지..?).

 

우선, 가장 헷갈릴 만한 네이밍을 구별해보겠다.

Article Controller에서 실행하는 reactionPoint Service의 isAlreadyAddGood/BadRp() 메서드,

jsp 페이지 내에서 생성한 checkAddRpBefore() 자바스크립트 메서드,

그리고 jsp 페이지 내에서 생성한 isAlreadyAddGood/BadRp 자바스크립트 변수가 있다.

 

isAlreadyAddGood/BadRp() 메서드는 Article Controller에서 최초 1회 실행하여 리턴값을 detail.jsp에 넘겨주는 역할 뿐이다.

checkAddRpBefore() 메서드는 jsp가 실행되는 그 시점에서, 최초로 isAlreadyAddGood/BadRp() 메서드의 파라미터 값을 받아 수행하는 메서드이다. 따라서 checkAddRpBefore()의 역할은 '특정 게시물 상세조회 페이지 실행 시점을 기준으로, 동일한 회원이 이전에도 리액션을 한 적이 있는가?'를 판단하여, 그 결과를 버튼의 색상을 바꿔 표현해주는 것이다.

isAlreadyAddGood/BadRp 변수는 ajax 실행으로 인해, jsp 내에서 지속적으로 boolean값이 변해야하는 특성을 고려해 만든 것이다. 따라서 해당 변수는 생성되면서, isAlreadyAddGood/BadRp() 메서드의 파라미터 값에 의해 첫 boolean 값이 정해진다. 이후에는 제이쿼리의 리액션 클릭 이벤트를 통해 ajax가 실행될 때마다 boolean 값을 새로 부여받는다.

 

정리하자면, jsp단에서 계속해서 리액션 이벤트를 핸들링하는 건 결국 isAlreadyAddGood/BadRp 변수인 셈이다.

 

1. Article Controller

@RequestMapping("/usr/article/detail")
	public String showDetail(Model model, int id) {

		if (articleService.isArticleExists(id) == false) {
			return Util.jsHistoryBack(Util.f("%d번 게시물은 존재하지 않습니다.", id));
		}

		Article foundArticle = articleService.getForPrintArticle(id);

		model.addAttribute("foundArticle", foundArticle);
		model.addAttribute("isLogined", rq.isLogined());
		model.addAttribute("isAlreadyAddGoodRp", reactionPointService.isAlreadyAddGoodRp(id));
		model.addAttribute("isAlreadyAddBadRp", reactionPointService.isAlreadyAddBadRp(id));

		return "/usr/article/detail";
	}

detail.jsp에 해당 게시물 데이터와 현재 로그인 여부, 리액션 실행 여부 파라미터를 각각 넘겨준다.

 

2. detail.jsp

<!-- 변수 생성 -->
<script>
	const params = {};
	params.id = parseInt('${param.id}');

	var isAlreadyAddGoodRp = ${isAlreadyAddGoodRp};
	var isAlreadyAddBadRp = ${isAlreadyAddBadRp};
</script>

<!-- 메서드 생성 -->
<script>
	$(function() {
		ArticleDetail__increaseHitCount();
	})

	function checkAddRpBefore() {
    <!-- 변수값에 따라 각 id가 부여된 버튼에 클래스 추가(이미 눌려있다는 색상 표시) -->
		if (isAlreadyAddGoodRp == true) {
			$("#add-goodRp-btn").addClass("already-added");
		} else if (isAlreadyAddBadRp == true) {
			$("#add-badRp-btn").addClass("already-added");
		} else {
			return;
		}
		$(function() {
			checkAddRpBefore();
		});
	};
</script>

<!-- 리액션 실행 코드 -->
<script>
	$(document).ready(function() {
		<!-- 각 id가 부여된 버튼 클릭 시 로그인 요청 메시지 발송 -->
        $("#request-login-good").click(function() {
			alert('로그인 후 이용해주세요!');
			return;
		});
		$("#request-login-bad").click(function() {
			alert('로그인 후 이용해주세요!');
			return;
		});
        
        <!-- jsp 실행 이전의 리액션 여부 체크 및 버튼 색상 표현 -->
		$(function() {
			checkAddRpBefore();
		});
        
        <!-- 좋아요 버튼 클릭 이벤트 및 ajax 실행 -->
		$("#add-goodRp-btn").click(function() {
			
            <!-- 이미 싫어요가 눌려 있는 경우 반려 -->
            if (isAlreadyAddBadRp == true) {
				alert('이미 싫어요를 누르셨습니다.');
				return;
			}
            
            <!-- 좋아요가 눌려 있지 않은 경우 좋아요 1 추가 -->
			if (isAlreadyAddGoodRp == false) {
				$.ajax({
					url : "/usr/reactionPoint/increaseGoodRp",
					type : "POST",
					data : {
						id : params.id
					},
					success : function(goodReactionPoint) {
						$("#add-goodRp-btn").addClass("already-added");
						$(".add-goodRp").html(goodReactionPoint);
						isAlreadyAddGoodRp = true;
					},
					error : function() {
						alert('서버 에러, 다시 시도해주세요.');
					}
				});
                
              <!-- 이미 좋아요가 눌려 있는 경우 좋아요 1 감소 -->  
			} else if (isAlreadyAddGoodRp == true){
				$.ajax({
					url : "/usr/reactionPoint/decreaseGoodRp",
					type : "POST",
					data : {
						id : params.id
					},
					success : function(goodReactionPoint) {
						$("#add-goodRp-btn").removeClass("already-added");
						$(".add-goodRp").html(goodReactionPoint);
						isAlreadyAddGoodRp = false;
					},
					error : function() {
						alert('서버 에러, 다시 시도해주세요.');
					}
				});
			} else {
				return;
			}
		});
        
        <!-- 싫어요 버튼 클릭 이벤트 및 ajax 실행 -->
		$("#add-badRp-btn").click(function() {
			
            <!-- 이미 좋아요가 눌려 있는 경우 반려 -->
            if (isAlreadyAddGoodRp == true) {
				alert('이미 좋아요를 누르셨습니다.');
				return;
			}
            
            <!-- 싫어요가 눌려 있지 않은 경우 싫어요 1 추가 -->
			if (isAlreadyAddBadRp == false) {
				$.ajax({
					url : "/usr/reactionPoint/increaseBadRp",
					type : "POST",
					data : {
						id : params.id
					},
					success : function(badReactionPoint) {
						$("#add-badRp-btn").addClass("already-added");
						$(".add-badRp").html(badReactionPoint);
						isAlreadyAddBadRp = true;
					},
					error : function() {
						alert('서버 에러, 다시 시도해주세요.');
					}
				});
                
              <!-- 이미 싫어요가 눌려 있는 경우 싫어요 1 감소 --> 
			} else if (isAlreadyAddBadRp == true) {
				$.ajax({
					url : "/usr/reactionPoint/decreaseBadRp",
					type : "POST",
					data : {
						id : params.id
					},
					success : function(badReactionPoint) {
						$("#add-badRp-btn").removeClass("already-added");
						$(".add-badRp").html(badReactionPoint);
						isAlreadyAddBadRp = false;
					},
					error : function() {
						alert('서버 에러, 다시 시도해주세요.');
					}
				});
			} else {
				return;
			}
		});
	});
</script>

<!-- 눌려 있는 버튼 색상 표현 -->
<style type="text/css">
.already-added {
	background-color: #0D3EA3;
	color: white;
}
</style>

<!-- 리액션 관련 코드만 발췌 -->
              <c:if test="${isLogined }">
                <span id="add-goodRp-btn" class="btn btn-outline">
                  좋아요👍
                  <span class="add-goodRp ml-2">${foundArticle.goodReactionPoint}</span>
                </span>
                <span id="add-badRp-btn" class="ml-5 btn btn-outline">
                  싫어요👎
                  <span class="add-badRp ml-2">${foundArticle.badReactionPoint}</span>
                </span>
              </c:if>
              <c:if test="${!isLogined }">
                <span id="request-login-good" class="btn btn-outline">
                  좋아요👍
                  <span class="add-goodRp ml-2">${foundArticle.goodReactionPoint}</span>
                </span>
                <span id="request-login-bad" class="ml-5 btn btn-outline">
                  싫어요👎
                  <span class="add-badRp ml-2">${foundArticle.badReactionPoint}</span>
                </span>
              </c:if>

각 이벤트에 따라 ajax 실행 시, ReactionPoint Controller를 통해 새로운 값을 불러온다. 동시에 제이쿼리로 already-added 클래스를 추가하거나 제거하여 css 스타일의 변화를 준다(눌려있는 버튼/눌려있지 않은 버튼).

 

여기서 예상 외로 나를 힘들게 했던 건, 제이쿼리로 css 변화를 주려고 하는 클래스가 제대로 호출되지 않는 점이었다.

 

<!-- 변경 전 -->
<!-- .add-goodRp-btn가 호출되지 않는다.... -->
$(".add-goodRp-btn").addClass("already-added");
                
<span class="add-goodRp-btn btn btn-outline">
좋아요👍
<span class="add-goodRp ml-2">${foundArticle.goodReactionPoint}</span>
</span>

<!-- 변경 후 -->
<!-- span태그의 id로 따로 빼서 호출했더니 된다! -->
$("#add-goodRp-btn").addClass("already-added");
                
<span id="add-goodRp-btn" class="btn btn-outline">
좋아요👍
<span class="add-goodRp ml-2">${foundArticle.goodReactionPoint}</span>
</span>

아직도 원인을 정확히는 모르겠다. 다만, 수업에서는 데이지UI와 테일윈드를 적용하는데, 아마 이 때문에 문제가 발생하지 않았을까 싶다.

 

3. ReactionPoint Controller

@RequestMapping("/usr/reactionPoint/increaseGoodRp")
	@ResponseBody
	public int increaseGoodRp(int id) {
    	// article 테이블에서 해당 게시물의 좋아요 1 증가 
		reactionPointService.increaseGoodRp(id);
        // article 테이블에서 해당 게시물의 최신화된 좋아요 수 불러오기
		int goodRp = reactionPointService.getGoodRpCount(id);
		
        // reactionPoint 테이블에 리액션 정보(게시판 id, 게시물 id, 사용자 id)를 기록
		reactionPointService.addIncreasingGoodRpInfo(id, (int) rq.getLoginedMemberId());

		return goodRp;
	}
	
	@RequestMapping("/usr/reactionPoint/decreaseGoodRp")
	@ResponseBody
	public int decreaseGoodRp(int id) {
    	// article 테이블에서 해당 게시물의 좋아요 1 감소
		reactionPointService.decreaseGoodRp(id);
        // article 테이블에서 해당 게시물의 최신화된 좋아요 수 불러오기
		int goodRp = reactionPointService.getGoodRpCount(id);
		
        // reactionPoint 테이블에 리액션 정보(게시판 id, 게시물 id, 사용자 id) 기록을 삭제
		reactionPointService.deleteGoodRpInfo(id, (int) rq.getLoginedMemberId());

		return goodRp;
	}

	@RequestMapping("/usr/reactionPoint/increaseBadRp")
	@ResponseBody
	public int increaseBadRp(int id) {
		reactionPointService.increaseBadRp(id);
		int badRp = reactionPointService.getBadRpCount(id);

		reactionPointService.addIncreasingBadRpInfo(id, (int) rq.getLoginedMemberId());

		return badRp;
	}
	
	@RequestMapping("/usr/reactionPoint/decreaseBadRp")
	@ResponseBody
	public int decreaseBadRp(int id) {
		reactionPointService.decreaseBadRp(id);
		int badRp = reactionPointService.getBadRpCount(id);

		reactionPointService.deleteBadRpInfo(id, (int) rq.getLoginedMemberId());

		return badRp;
	}

 

 

4. ReactionPoint Service

	// detail.jsp의 ajax 관련 메서드(article 테이블에 진입해야 하므로 articleService로 넘김)
	public void increaseGoodRp(int id) {
		articleService.increaseGoodRp(id);
	}

	public void increaseBadRp(int id) {
		articleService.increaseBadRp(id);
	}

	public void decreaseGoodRp(int id) {
		articleService.decreaseGoodRp(id);
	}

	public void decreaseBadRp(int id) {
		articleService.decreaseBadRp(id);
	}

	public int getGoodRpCount(int id) {
		return articleService.getGoodRpCount(id);
	}

	public int getBadRpCount(int id) {
		return articleService.getBadRpCount(id);
	}

	// reactionPoint 테이블에 좋아요/싫어요 로그 기록 관련 메서드
	public void addIncreasingGoodRpInfo(int articleId, int memberId) {
		// 현재 게시물이 소속된 게시판 id를 가져옴
        int boardId = articleService.getBoardIdByArticle(articleId);
		reactionPointRepository.addIncreasingGoodRpInfo(boardId, articleId, memberId);
	}

	public void deleteGoodRpInfo(int articleId, int memberId) {
		int boardId = articleService.getBoardIdByArticle(articleId);
		reactionPointRepository.deleteGoodRpInfo(boardId, articleId, memberId);
	}

	public void addIncreasingBadRpInfo(int articleId, int memberId) {
		int boardId = articleService.getBoardIdByArticle(articleId);
		reactionPointRepository.addIncreasingBadRpInfo(boardId, articleId, memberId);
	}

	public void deleteBadRpInfo(int articleId, int memberId) {
		int boardId = articleService.getBoardIdByArticle(articleId);
		reactionPointRepository.deleteBadRpInfo(boardId, articleId, memberId);
	}

	public boolean isAlreadyAddGoodRp(int articleId) {
		// 좋아요 = 1, 싫어요 = 2, 취소 시 데이터 삭제
		// 현재 게시물에서, loginedMemberId의 pointTypeCode값이 1이면 좋아요 상태
		int getPointTypeCodeByMemberId = getRpInfoByMemberId(articleId, rq.getLoginedMemberId());

		if (getPointTypeCodeByMemberId == 1) {
			return true;
		}
		return false;
	}

	public boolean isAlreadyAddBadRp(int articleId) {
		// 좋아요 = 1, 싫어요 = 2, 취소 시 데이터 삭제
		// 현재 게시물에서, loginedMemberId의 pointTypeCode값이 2면 좋아요 상태
		int getPointTypeCodeByMemberId = getRpInfoByMemberId(articleId, rq.getLoginedMemberId());

		if (getPointTypeCodeByMemberId == 2) {
			return true;
		}
		return false;
	}
    
    private Integer getRpInfoByMemberId(int articleId, int memberId) {
		// 현재 사용자 id와 게시물 id로 좋아요/싫어요 기록을 가져옴
        Integer getPointTypeCodeByMemberId = reactionPointRepository.getRpInfoByMemberId(articleId, memberId);
		// 로그인 상태가 아닐 경우, null 에러를 방지하기 위해 임의로 99값을 부여
		if (getPointTypeCodeByMemberId == null) {
			getPointTypeCodeByMemberId = 99;
		}

		return (int) getPointTypeCodeByMemberId;
	}

가장 많은 시간을 들였던 메서드가 바로 getRpInfoByMemberId()이다.

개발자모드창과 번갈아보면서 온갖 시도는 다 해보았던 것 같다.

 

핵심 이슈의 원인은 바로 '로그인 상태가 아니어도 게시물 상세조회를 할 수 있다'는 조건이었다.

rq.loginedMemberId가 null인 상태(로그인이 아닌 상태)에서 detail.jsp에 진입했을 때, 리액션 관련 자바스크립트 메서드는 자동으로 실행이 된다. 이 때, int 타입에서 null 에러가 발생해버린다는 것이 문제였다.

 

바로 Integer 클래스를 활용하여 null값도 가질 수 있도록은 했지만, 결국 다른 메서드에서 해당 null값 파라미터를 받으면서 계속 문제가 되었다. 지금 와서 보면 너무도 간단하게 임의의 숫자를 부여함으로써 해결했지만, 당시에는 이 간단한 방법조차 생각하지 못 하고 애먼 곳에서 삽질만 해댔다. 더군다나 코드가 정립되지 않은 상태라 더욱 막막하다보니 그랬던 것 같다.

boolean 타입인 isAlreadyAddGood/BadRp()를 Object 타입으로 바꾸고 로그인 요청 스크립트 메시징+반려 처리까지도 해야할 지, 인터셉터에서 컷함으로써 아예 상세조회 진입 전에 로그인을 필수로 만들어버릴지, 일부 메서드를 rq로 옮겨서 rq단에서 초반에 처리를 해버려야 할지...

정말 현직자 분들께서 보시면 너무 허무맹랑해서 혀를 끌끌 차실 것만 같은 생각들을 했었다.

 

이 때, 그냥 노트북을 덮어버리고 다음날 하자고 마음 먹었던 것이 신의 한수였다.

엄청난 발열에 버벅거리던 내 뇌는 깔끔하게 재부팅되었고, 코드를 다시 쭉 읽어보니 보이지 않던 방법들이 보이기 시작했다. 전날 2~3시간을 헤매던 문제점들은 테스팅까지 깔끔하게 30분 컷으로 해결되었다.

혹시나 같은 상황에 처해 이 글을 보고 있는 학생(?)이라면, 심해에 빠지기 전에 얼른 휴식을 취하길 바란다.

 

5. SQL.xml(Article Repository와 ReactionPoint Repository를 합쳐 올립니다)

	<!-- Article Repository.xml -->
    <update id="increaseGoodRp">
		UPDATE article
		SET goodReactionPoint =
		goodReactionPoint + 1
		WHERE id =
		#{id}
	</update>

	<update id="increaseBadRp">
		UPDATE article
		SET badReactionPoint = badReactionPoint
		+ 1
		WHERE id =
		#{id}
	</update>

	<update id="decreaseGoodRp">
		UPDATE article
		SET goodReactionPoint =
		goodReactionPoint - 1
		WHERE id =
		#{id}
	</update>
	
	<update id="decreaseBadRp">
		UPDATE article
		SET badReactionPoint =
		badReactionPoint - 1
		WHERE id =
		#{id}
	</update>

	<select id="getGoodRpCount" resultType="int">
		SELECT goodReactionPoint
		FROM article
		WHERE id = #{id}
	</select>

	<select id="getBadRpCount" resultType="int">
		SELECT badReactionPoint
		FROM article
		WHERE id = #{id}
	</select>
	
	<select id="getBoardIdByArticle" resultType="int">
		SELECT A.boardId
		FROM article AS A
		WHERE id = #{id}
	</select>
    
    <!-- ReactionPoint Repository.xml -->
    <insert id="addIncreasingGoodRpInfo">
		INSERT INTO reactionPoint
		SET regDate = NOW(),
		updateDate =
		NOW(),
		boardId = #{boardId},
		articleId = #{articleId},
		memberId = #{memberId},
		pointTypeCode = 1
	</insert>
	
	<delete id="deleteGoodRpInfo">
		DELETE FROM reactionPoint
		WHERE boardId = #{boardId}
		AND articleId = #{articleId}
		AND memberId = #{memberId}
		AND pointTypeCode = 1
	</delete>

	<insert id="addIncreasingBadRpInfo">
		INSERT INTO reactionPoint
		SET regDate = NOW(),
		updateDate =
		NOW(),
		boardId = #{boardId},
		articleId = #{articleId},
		memberId = #{memberId},
		pointTypeCode = 2
	</insert>
	
	<delete id="deleteBadRpInfo">
		DELETE FROM reactionPoint
		WHERE boardId = #{boardId}
		AND articleId = #{articleId}
		AND memberId = #{memberId}
		AND pointTypeCode = 2
	</delete>

	<select id="getRpInfoByMemberId" resultType="Integer">
		SELECT pointTypeCode
		FROM reactionPoint 
		WHERE articleId = #{articleId} AND memberId = #{memberId};
	</select>

 

코드를 보시면 아시겠지만, 현재로써는 단순히 기능만 구현한 것일 뿐, 중복 코드가 많아 좀 더 개선이 필요한 상태이다.

코드 개선을 한 후에 포스팅을 할까 했지만, 이 글의 목적은 좀 더 깔끔한 코드를 남한테 보여주는 것이 아닌, 내 생생한 문제 해결 후기를 올리려는 것이기 때문에 날 것 그대로 올리게 되었다. 시간이 지나면 내가 무엇때문에 헤맸는지 기억을 못 할까봐...

아무튼 이번 해프닝을 계기로 좀 더 발전한 듯한 느낌이 들었다. 그동안 배웠던 것들이 조각조각이라 따로 노는 느낌이었다면, 그 조각들을 엮어서 하나의 천쪼가리라도 만들어본 것 같다. 

'강의&독학 학습 내용 > JAVA' 카테고리의 다른 글

자주 쓰이는 정규식 모음  (0) 2022.03.04
NullPointerException 해결 사례  (0) 2021.10.09
JAVA Exception 종류  (0) 2021.08.08

관련글 더보기