Coding History

국비 지원 IT(웹앱개발) 취업반 강의 55일차 (Spring)

BlackBirdIT 2024. 8. 23. 17:47

어제 만든 doReaction을 가만 생각해보니까 ResultData를 만들어놨는데 왜 이걸 사용안했나 싶어서 수정을 했다.
졸면서 해서 정신이 없었나..

    @RequestMapping("/article/doReaction")
    @ResponseBody
    public ResultData doReaction(HttpServletRequest req ,@RequestParam int id, @RequestParam String relTypeCode, @RequestParam int relId) {
        Rq rq = (Rq) req.getAttribute("rq");

        int resultPoint = reactionPointService.toggleReactionPoint(rq.getLoginedMemberId(), relTypeCode, relId);

        String status = resultPoint > 0 ? "liked" : "unliked";
        ResultData<String> rd = ResultData.from("S-1", "좋아요기능 실행완료.", "status", status);
        rd.setData2("reactedArticleId", id);

        return rd;
    }

이렇게 수정을 해봤고 JSP로 넘어가려고 내가 새로 짰던 코드를 찬찬히 살펴봤다.

가만생각해보니까 처음 좋아요를 누를 땐, Insert가 맞지만, 이후에 다시 실어요를 누르거나 좋아요를 다시 눌러서 취소하는 경우를 상정을 안했던 것 같다. 만약 이렇게 되면 insert만 두번 들어가거나 하는 문제가 생길 것 같아서 Update 쿼리도 추가해야될 필요성을 느꼈다.

    @Update("UPDATE reactionPoint SET point = #{point}, updateDate = NOW() WHERE memberId = #{memberId} AND relTypeCode = #{relTypeCode} AND relId = #{relId}")
    void updateReactionPoint(int memberId, String relTypeCode, int relId, int point);

우선 이렇게 쿼리를 먼저 추가해보고

    public int toggleReactionPoint(int memberId, String relTypeCode, int relId) {
        ReactionPoint existingReaction = reactionPointRepository.getReactionPointByMemberIdAndRelId(memberId,
                relTypeCode, relId);

        if (existingReaction == null) {
            // 현재 반응이 없을 경우 좋아요를 추가
            reactionPointRepository.insertReactionPoint(memberId, relTypeCode, relId, 1);
            return 1;
        } else if (existingReaction.getPoint() == 1) {
            // 현재 좋아요인 경우 -> 싫어요로 변경
            reactionPointRepository.updateReactionPoint(memberId, relTypeCode, relId, -1);
            return -1;
        } else {
            // 현재 싫어요인 경우 -> 좋아요로 변경
            reactionPointRepository.updateReactionPoint(memberId, relTypeCode, relId, 1);
            return 1;
        }
    }

기존에 만들어두었던 토글 리액션 포인트 메서드도 수정을 거쳤다.

지금 쓰면서 깨달았는데 좋아요를 취소한 경우나 싫어요를 취소한 경우는 Delete로 처리하는게 맞는 것 같아서 if문 다시 손봐야될 것 같다..

@Service
public class ReactionPointService {
    @Autowired
    private ReactionPointRepository reactionPointRepository;

    public int toggleReactionPoint(int memberId, String relTypeCode, int relId, int newPoint) {

        ReactionPoint existingReaction = reactionPointRepository.getReactionPointByMemberIdAndRelId(memberId, relTypeCode, relId);

        if (existingReaction == null) {
            // 현재 반응이 없을 경우 -> 새로운 반응을 추가
            reactionPointRepository.insertReactionPoint(memberId, relTypeCode, relId, newPoint);
            return newPoint;
        } 

        if (existingReaction.getPoint() == 1 && newPoint == 0) {
            // 현재 좋아요인 경우 -> 좋아요를 취소
            reactionPointRepository.deleteReactionPoint(memberId, relTypeCode, relId);
            return 0;
        } 

        if (existingReaction.getPoint() == -1 && newPoint == 0) {
            // 현재 싫어요인 경우 -> 싫어요를 취소
            reactionPointRepository.deleteReactionPoint(memberId, relTypeCode, relId);
            return 0;
        }

        if (existingReaction.getPoint() == 1 && newPoint == -1) {
            // 현재 좋아요인 경우 -> 싫어요로 변경
            reactionPointRepository.updateReactionPoint(memberId, relTypeCode, relId, -1);
            return -1;
        }

        if (existingReaction.getPoint() == -1 && newPoint == 1) {
            // 현재 싫어요인 경우 -> 좋아요로 변경
            reactionPointRepository.updateReactionPoint(memberId, relTypeCode, relId, 1);
            return 1;
        }

        // 동일한 반응을 눌렀을 경우 취소
        reactionPointRepository.deleteReactionPoint(memberId, relTypeCode, relId);
        return 0;
    }

newPoint라는 변수를 생각해 내는데 시간이 좀 걸렸다. newPoint를 만들어서 JSP에서 사용자의 반응에 따라, 즉 좋아요를 누를 시에는 1을 전송하고 싫어요를 누를 땐 -1 을 전송하게끔 해주면 그 값은 변수니까 저렇게 따로 설정해서 받아와야지 쓸 수 있다는 것.

그걸 참고해서 일단 JSP에 JS를 만들어봤다.

<script>
    function reaction(point) {
        const articleId = ${article.id};
        const relTypeCode = 'article';
        const relId = articleId;

        $.post('/article/doReaction', {
            id : articleId,
            relTypeCode : relTypeCode,
            relId : relId,
            newPoint : point
        // 좋아요는 1, 싫어요는 -1로 설정
        }, function(response) {
            if (response.status === 'liked') {
                alert('게시물 좋아요.');
            } else if (response.status === 'unliked') {
                alert('게시물 좋아요 취소.');
            } else if (response.status === 'disliked') {
                alert('게시물 싫어요.');
            } else if (response.status === 'undisliked') {
                alert('게시물 싫어요 취소.');
            }
        });

    // 좋아요 버튼 클릭 시
    $('#likeBtn').on('click', function() {
        reaction(1); // 좋아요는 1로 설정
    });

    // 싫어요 버튼 클릭 시
    $('#dislikeBtn').on('click', function() {
        reaction(-1); // 싫어요는 -1로 설정
    });
</script>

일단은 이렇게 만들어서 해봤는데 작동이 되는건지 안되는건지 확인을 못하겠다. 그래서 이래저래 해보는 와중에 이제 풀이를 시작했다.

강사님은 JSP부터 만지셨다. 리액션 합산 값, 좋아요, 싫어요를 우선 가져올 준비를 하고 이걸 화면에 가져오려면 어떻게 해야되냐를 물으셨다.

detail화면은 article테이블과 member 테이블을 inner join해서 가져왔다.(닉네임 표기를 위해서) 조회수 까지 가져오려면 reaction 테이블까지 조인해야하는 상황이다.

그래서 강사님은 일단 IFNULL을 찾아보라고 하셨다,

IFNULL

보니까 가져오는 데이터가 'NULL'일 경우 값을 설정하는 함수였는데, 유저가 아무런 반응을 하지 않았더라면 당연히 리액션 테이블에는 아무런 데이터도 없을테니 join할 때 문제가 생긴다. 아마 이걸 방지하기 위한 빌드업이지 않을까 싶고,
기본 사용법은

SELECT IFNULL(Column명, "Null일 경우 대체 값") FROM 테이블명; 

이런 식이다. 아마 reaction이 칼럼명이 될테고 null일 경우에는 '0'으로 반환하면 문제 없이 조인한 결과를 화면에 나타낼 수 있겠다는 결론에 도달했다.

SELECT a.* , m.nickname AS extra__writer, ifnull(rp.`point`, 0)
FROM article AS a
INNER JOIN `member` AS m
ON a.memberId = m.id
LEFT JOIN reactionPoint AS rp
ON a.id = rp.relId AND rp.relTypeCode = 'article'
WHERE a.id = 1;

그렇게 내가 수정해본 쿼리는 이렇다.

강사님이 수정한 쿼리는

SELECT A.*, M.nickname AS extra__writer,
IFNULL(SUM(RP.point),0) AS extra__sumReactionPoint,
IFNULL(SUM(IF(RP.point > 0,RP.point,0)),0) AS extra__goodReactionPoint,
IFNULL(SUM(IF(RP.point < 0,RP.point,0)),0) AS extra__badReactionPoint
FROM article AS A
INNER JOIN `member` AS M
ON A.memberId = M.id
LEFT JOIN reactionPoint AS RP
ON A.id = RP.relId AND RP.relTypeCode = 'article'
WHERE A.id = 1;

이렇다. 애초에 3개를 보여줘야되는데 그걸 간과한 점이 문제고, 합산값을 나타내야된다는 것, 저 코드는 얼핏보기엔 어려워 보였는데 한 1분정도 들여다 보고 있으니 무슨 말을 하는지 이해할 수 있었다.

그리고는 list에서도 해당 정보를 볼 수 있게 쿼리를 수정해보라고 하셨다.

    @Select("""
            <script>
                SELECT A.*, M.nickname AS extra__writer,
                IFNULL(SUM(RP.point),0) AS extra__sumReactionPoint,
                IFNULL(SUM(IF(RP.point &gt; 0,RP.point,0)),0) AS extra__goodReactionPoint,
                IFNULL(SUM(IF(RP.point &lt; 0,RP.point,0)),0) AS extra__badReactionPoint
                FROM article AS A
                INNER JOIN `member` AS M
                ON A.memberId = M.id
                LEFT JOIN reactionPoint AS RP
                ON A.id = RP.relId AND RP.relTypeCode = 'article'
                WHERE 1
                <if test="boardId != 0">
                    AND boardId = #{boardId}
                </if>
                <if test="searchKeyword != ''">
                    <choose>
                        <when test="searchKeywordTypeCode == 'title'">
                            AND A.title LIKE CONCAT('%', #{searchKeyword}, '%')
                        </when>
                        <when test="searchKeywordTypeCode == 'body'">
                            AND A.`body` LIKE CONCAT('%', #{searchKeyword}, '%')
                        </when>
                        <when test="searchKeywordTypeCode == 'writer'">
                            AND M.nickname LIKE CONCAT('%', #{searchKeyword}, '%')
                        </when>
                        <otherwise>
                            AND A.title LIKE CONCAT('%', #{searchKeyword}, '%')
                            OR A.`body` LIKE CONCAT('%', #{searchKeyword}, '%')
                        </otherwise>
                    </choose>
                </if>
                GROUP BY A.id
                ORDER BY A.id DESC
                <if test="limitFrom >= 0">
                    LIMIT #{limitFrom}, #{limitTake}
                </if>
                </script>
            """)

쿼리 전문은 이렇다. 근데 이게 너무 기니까 DB에서 Update Join 개념을 적용시켜보기로 햇다.

UPDATE JOIN

기존의 다른 테이블의 데이터를 가져오는 것은 기본적으로 같은데 innerjoin이 아닌, 타 칼럼 각 테이블에 직접 데이터를 넣어주는 방식이다. 어떻게 적용했는가를 보는 것이 더 직관적으로 보일 것이다.

# article 테이블에 reactionPoint(좋아요) 관련 컬럼 추가
alter table article add column goodReactionPoint int(10) unsigned not null default 0;
ALTER TABLE article ADD COLUMN badReactionPoint INT(10) UNSIGNED NOT NULL DEFAULT 0;

# update join -> 기존 게시글의 good bad RP 값을 RP 테이블에서 추출해서 article table에 채운다
update article as A
inner join (
    select RP.relTypeCode, Rp.relId,
    SUM(IF(RP.point > 0,RP.point,0)) AS goodReactionPoint,
    SUM(IF(RP.point < 0,RP.point * -1,0)) AS badReactionPoint
    from reactionPoint As RP
    group by RP.relTypeCode,Rp.relId
) as RP_SUM
on A.id = RP_SUM.relId
set A.goodReactionPoint = RP_SUM.goodReactionPoint,
A.badReactionPoint = RP_SUM.badReactionPoint;

이렇게 DB를 좀 더 효율적이고 쿼리를 쓰기 쉽게 업데이트 했다.

그리고 진짜 핵심적으로 통신이 되게끔 만들었는데 이제부터 전에 내가 만들었던 Reaction 메서드들이 활용되기 시작했다.

이전에 만들어 두었던 것들을 약간씩 손을 보다가 로그인 하지 않았을 때 기능을 만드는 것에서 오래 고민하고 이것저것 해봤다. JS로 막아도 보고 JAVA로도 막아도 보고 하다가

깨달았다. 우리는 인터셉터를 만든 적이 있다는 것을.

그래서 인터셉터에서 처리하게끔 한문장만 넣으니 로그인 하지 않았을 때 좋아요나 싫어요를 누르면 로그인하라는 알림창이 뜨니 우리가 만들 메서드에 굳이 넣어줄 필요가 없게 됐다.
그러니까 MVC에 등록만 하면 됐던 것이다.

그래서 여하튼 좋아요나 싫어요 버튼을 클릭하면,

<script>
$(document).ready(function() {

    const articleId = ${article.id};
    const relTypeCode = 'article';
    const relId = articleId;

    function reaction(point) {
        $.post('/article/doReaction', {
            id: articleId,
            relTypeCode: relTypeCode,
            relId: relId,
            newPoint: point
        }, function(response) {
            if (response.resultCode.startsWith("S-")) {
                alert(response.msg);
            } else {
                alert(response.msg);
            }
            location.reload(); // 새로고침하여 결과 반영
        });
    }

    // 좋아요 버튼 클릭 시
    $('#likeBtn').on('click', function() {
        reaction(1); // 좋아요는 1로 설정
    });

    // 싫어요 버튼 클릭 시
    $('#disLikeBtn').on('click', function() {
        reaction(-1); // 싫어요는 -1로 설정
    });

});
</script>

이렇게 자바로 데이터를 전송,

article doReaction이 데이터를 받는다.


    @RequestMapping("/article/doReaction")
    @ResponseBody
    public ResultData doReaction(HttpServletRequest req , Model model, int id, String relTypeCode, int relId, int newPoint) {
        Rq rq = (Rq) req.getAttribute("rq");
        int loginedMemberId = rq.getLoginedMemberId();
        model.addAttribute("loginedMemberId", loginedMemberId);
        relTypeCode = "article";

        int resultPoint = reactionPointService.toggleReactionPoint(rq.getLoginedMemberId(), relTypeCode, relId, newPoint);

        System.err.println(resultPoint);

        String status = resultPoint > 0 ? "liked" : "unliked";
        ResultData rd = ResultData.from("S-1", "리액션기능 실행완료.", "status", status);
        rd.setData2("reactedArticleId", id);

        return rd;
    }

여기서 relTypeCode는 어차피 article에서만 작동하니, article로 고정해주고, 나머지 데이터를 서비스로 전송,

@Service
public class ReactionPointService {
    @Autowired
    private ReactionPointRepository reactionPointRepository;

    public int toggleReactionPoint(int loginedMemberId, String relTypeCode, int relId, int newPoint) {

        ReactionPoint existingReaction = reactionPointRepository.getReactionPointByMemberIdAndRelId(loginedMemberId, relTypeCode, relId);

        System.err.println(existingReaction);

        if (existingReaction == null) {
            // 현재 반응이 없을 경우 -> 새로운 반응을 추가
            reactionPointRepository.insertReactionPoint(loginedMemberId, relTypeCode, relId, newPoint);
            return newPoint;
        } 

        if (existingReaction.getPoint() == 1 && newPoint == 0) {
            // 현재 좋아요인 경우 -> 좋아요를 취소
            reactionPointRepository.deleteReactionPoint(loginedMemberId, relTypeCode, relId);
            return 0;
        } 

        if (existingReaction.getPoint() == -1 && newPoint == 0) {
            // 현재 싫어요인 경우 -> 싫어요를 취소
            reactionPointRepository.deleteReactionPoint(loginedMemberId, relTypeCode, relId);
            return 0;
        }

        if (existingReaction.getPoint() == 1 && newPoint == -1) {
            // 현재 좋아요인 경우 -> 싫어요로 변경
            reactionPointRepository.updateReactionPoint(loginedMemberId, relTypeCode, relId, -1);
            return -1;
        }

        if (existingReaction.getPoint() == -1 && newPoint == 1) {
            // 현재 싫어요인 경우 -> 좋아요로 변경
            reactionPointRepository.updateReactionPoint(loginedMemberId, relTypeCode, relId, 1);
            return 1;
        }

        // 동일한 반응을 눌렀을 경우 취소
        reactionPointRepository.deleteReactionPoint(loginedMemberId, relTypeCode, relId);
        return 0;
    }

    public int getTotalReactionPoints(String relTypeCode, int relId) {
        Integer totalPoints = reactionPointRepository.getTotalReactionPoints(relTypeCode, relId);
        return totalPoints != null ? totalPoints : 0;
    }
}

그리고 if문을 통과하면 해당 상황에 맞게 쿼리를 쏜다.

@Mapper
public interface ReactionPointRepository {

    @Select("SELECT * FROM reactionPoint WHERE memberId = #{memberId} AND relTypeCode = #{relTypeCode} AND relId = #{relId}")
    ReactionPoint getReactionPointByMemberIdAndRelId(int memberId, String relTypeCode, int relId);

    @Insert("INSERT INTO reactionPoint (regDate, updateDate, memberId, relTypeCode, relId, point) VALUES (NOW(), NOW(), #{memberId}, #{relTypeCode}, #{relId}, #{point})")
    void insertReactionPoint(int memberId, String relTypeCode, int relId, int point);

    @Delete("DELETE FROM reactionPoint WHERE memberId = #{memberId} AND relTypeCode = #{relTypeCode} AND relId = #{relId}")
    void deleteReactionPoint(int memberId, String relTypeCode, int relId);

    @Update("UPDATE reactionPoint SET point = #{point}, updateDate = NOW() WHERE memberId = #{memberId} AND relTypeCode = #{relTypeCode} AND relId = #{relId}")
    void updateReactionPoint(int memberId, String relTypeCode, int relId, int point);

    @Select("SELECT SUM(point) FROM reactionPoint WHERE relTypeCode = #{relTypeCode} AND relId = #{relId}")
    Integer getTotalReactionPoints(String relTypeCode, int relId);
}

이게 좋아요, 싫어요를 눌렀을 때 뒤에서 실행되는 로직이다. 근데 화면이 안바뀌어서 저기 sout 을 찍어두고 계속 확인하다가 DB자체를 까보니까 데이터가 들어갔다, 나왔다, 수정됐다, 가 잘 작동했다.

4번으로 회원가입하고 좋아요 취소를 했을 때, 변한 데이터를 찍어놨다.

근데 이게 화면이 왜 안바뀔까 를 생각해보니 아까 update join을 JAVA코드 내에서 실행되게끔 한 적이 없을 뿐더러 AJAX로도 처리하지 않았다. DB에만 존재하는 코드. 그래서 내가 직접 어떤 행동을 취하고 난 뒤, 저 쿼리를 실행시켜야 좋아요나 싫어요의 숫자가 업데이트 됐다. 이제 이걸 java내에 어디에 어떻게 넣는가와 비동기처리를 어떻게 하느냐가 관건인 것 같다. 아무튼 막 끝내니까 강사님이 하시는 것을 보여주셨다.

여튼 좋아요, 싫어요 버튼을 클릭할 때 테이블 여러개에 접근할 수 있는 것 까지하면 완성이다. 이건 다음 시간에 올려보도록 하겠따.