Project/ClassFlix

[ClassFlix] EP 10. view 페이지 제작과 컨트롤러 연결 - 5 : 리뷰 수정 구현

목차 (클릭시 해당 목차로 이동)


     

     

     

    프로젝트를 진행하면서 프론트부분은 최대한 템플릿을 이용해서 공부의 비중을 줄이려고 했습니다.

    리뷰수정 부분을 페이지 이동없이 만드려고 했는데, ajax없이는 만드는 것이 어렵다고 판단되어서 ajax를 공부한 뒤 도입했습니다.

     

     

    현재상황

    현재 구현된 리뷰 시스템은 아래와 같습니다.

    수정버튼, 삭제버튼 모두 비밀번호 검증을 합니다.

    처음 리뷰 작성시 입력했던 비밀번호를 알맞게 입력하면 해당 기능이 동작합니다.

     

     

    그런데, 삭제 후 비밀번호를 맞게 입력하면 댓글 하나를 삭제하면 되지만,

    수정을 누를 시 현재 화면이 바뀌지 않고 현재 리뷰 글이 수정모드로 바뀌게 구현하고 싶었습니다.

     

     

    구현계획 1

    제가 생각한 기능을 구현하기 위해서 두가지 방법이 있었습니다.

     

    1. 다른 페이지에 동일한 데이터를 다시뿌려주고 해당 리뷰글을 수정창으로 바꾸는 방법.
      - 장점 : 구현하기 편하다.
      - 단점 : 해당 페이지의 데이터를 다시 뿌려줘야한다.
    2. ajax를 이용해서 해당 수정창이 보이게 바꾸는 방법.
      - 장점 : 해당 페이지의 다른 내용을 건드리지 않고 수정창이 보이게 할 수 있다.
      - 단점 : ajax를 공부한뒤 기능을 넣어야 한다.

     

    1번의 방법은 구현하기 편했지만 해당 페이지에 데이터를 다시 뿌려주어야 했습니다.

    해당 페이지가 뿌려주는 데이터가 적었다면 그냥 1번의 방법을 선택하려 했었으나,

    해당 강의정보창은 똑같은 데이터를 다시 뿌려주기에는 너무 낭비가 심해 납득할 수 없어서 결국 ajax을 도입하기로 결정하였습니다.

     

     

    또한, 비밀번호기능을 제외합니다.

    어차피 스프링 MVC의 세션과 쿠키를 학습한뒤 실제 로그인 기능을 구현할 것이기 때문에 여기선 간단하게 비밀번호 검증을 하려고 했으나 이 기능도 결국엔 세션과 쿠키 없이 구현하는 것은 의미없기 때문에 제외합니다.

     

     

    정리

    • 수정, 삭제버튼은 비밀번호없이 누르면 바로 동작한다.
    • 수정버튼을 누를 시, ajax로 수정창을 바로 불러온다.

     

     

     

     

    구현계획2

     

    리뷰수정

    1. 각 리뷰들 아래에 리뷰수정 html코드를 넣을 공간을 만들어 둡니다.
      리뷰수정을 클릭시 fetch api가 작동해 리뷰수정코드를 불러옵니다.
    2. 리뷰수정코드는 form태그이고 method=post 입니다.
    3. 리뷰수정코드는 form 제출시 "lectures/{lectureId}?reviewId={reviewId}"로 이동합니다.
      (수정할 리뷰의 id를 파라미터로 넘겨줍니다.)
    4. 컨트롤러에서 lectureId와 reviewId를 받아서 업데이트를 해주고 redirect로 해당 강의로 다시 이동시켜줍니다.

     

     

    리뷰삭제

    1. 리뷰삭제버튼 클릭시 리뷰삭제 url로 이동하고 redirect로 해당 강의로 다시 이동시켜줍니다.

     

     

     

     

    구현계획3

    ajax 통신도 생각보다 어려워서 모달을 이용해서 구현해보려고 합니다.

     

     

    구현계획

     

    1. 수정하고싶은 리뷰의 수정버튼을 누르면 모달로 리뷰 수정창이 나옵니다.
      후기란에는 기존에 작성해놨던 데이터가 나오도록 합니다.
    2. 별점과 후기를 넣고 등록을 누르면 컨트롤러에서 해당 데이터로 업데이트 로직을 호출합니다.

     

     

    문제점

    수정할 리뷰의 ID를 넘겨주어야 합니다.

    그러나,  타임리프로 th:each 에서 반복해서 나온 리뷰이기때문에

    모달에서 해당 리뷰의 ID를 가져오는 방법을 찾아야합니다.

     

    그래서 저는 해당 리뷰의 아이디를 타임리프에서 자바스크립트로 넘기고 자바스크립트로 form태그를 수정하는 방법을 채택했습니다.

     

     

    실제 구현

     

     

    1. 수정버튼을 누르면 해당 리뷰의 아이디가 자바스크립트의 전역변수의 값으로 초기화됩니다.

    <a class="btn btn-primary" role="button"
    th:onclick="|SaveReviewEditId(${reviewDto.getReviewId()})|"
    data-target="#modal" data-toggle="modal">수정</a>
        var updateReviewId;
    
        function SaveReviewEditId(id) {
            updateReviewId = id;
        }
    

     

     

    2. 새로운 별점과 컨텐츠를 입력하고 수정완료를 누르면 form태그의 action, method를 지정하고 submit하는 자바스크립트 코드를 호출합니다.

    <button onclick="reviewEditForm()" type="submit" class="btn-primary" role="button">수정완료</button>
        var updateReviewId;
        var lectureId = [[${lectureId}]];
    
        function SaveReviewEditId(id) {
            updateReviewId = id;
        }
        function reviewEditForm() {
            const theForm = document.reviewEdit;
            theForm.method = "post";
            theForm.action = lectureId+"/updateReview/"+updateReviewId;
            theForm.submit();
        }

     

    자바스크립트에서 타임리프 변수를 바로 가져올 수 있는 방법이 있습니다.

    <script th:inline="javascript" language="JavaScript">

    th:inline="javascript" 를 지정하고,

    var lectureId = [[${lectureId}]];

    [[ ]] 안에 타임리프 문법으로 변수를 넣으면 됩니다.

     

     

     

    3. 컨트롤러에서 해당 url를 post로 받고 reviewService에서 update 로직을 실행합니다. 그 후 해당 강의로 redirect합니다.

        @PostMapping("/lectures/{lectureId}/updateReview/{reviewId}")
        private String updateReview(@PathVariable("lectureId") Long lectureId, @PathVariable("reviewId") Long reviewId,
                                           ReviewForm form, RedirectAttributes redirectAttributes) {
    
            reviewService.update(reviewId, form.getContent(), form.getRating());
            redirectAttributes.addAttribute("lectureId", lectureId);
            return "redirect:/lectures/{lectureId}";
        }

     

     

     

     

    정리

    • reviewId는 반복자를 통해 나온 값이기 때문에 해당 태그를 벗어나면 reviewId를 직접 접근할 수 가 없었습니다. 그래서 반복자를 통해 나온 revireId를 전역변수에 저장해놓고 수정버튼을 누를 때 사용할 수 있도록 했습니다.
    • lectureId는 어느 곳에서든 접근할 수 있는 변수였기 때문에 자바스크립트 코드에서 바로 [[]]로 불러와 사용했습니다.

     

     

     

    구동 확인

     

    하나의 리뷰가 존재하고 이것을 수정해보겠습니다.

     

    수정 버튼을 눌러서 모달로 된 수정창이 떴습니다.

    여기서 별점과 내용을 수정해보겠습니다.

     

    해당강의로 정상적으로 redirect가 되고 리뷰창이 수정된 것을 볼 수 있습니다.

     

     

    문제점 발생

    여기서도 문제점이 보입니다.

     

    1.

    리뷰는 수정되었지만 해당 강의의 평균리뷰는 수정되지 않았습니다.

    리뷰가 수정되면 해당 강의의 별점들도 refresh 하도록 리뷰서비스로직을 수정해야 합니다.

     

    2.

    리뷰 수정을 누르면 원래 리뷰의 내용이 보여야 합니다.

     

     

    현재 리뷰수정로직

     

    1. 아이디를 통해 해당 리뷰 객체 찾기
    2. review domain의 change 로직 수행
    3. reviewId반환
        // 리뷰 수정
        public Long update(Long reviewId, String content, Integer rating) {
            Review findReview = reviewRepository.findById(reviewId);
            findReview.changeContentAndRating(content, rating);
            return reviewId;
        }

     

     

     

    문제점 해결 (1)

     

    변경계획

    1. 아이디를 통해 해당 리뷰 객체 찾기
    2. review domain의 change 로직 수행
    3. lecture service 의 rating refresh 로직 수행
    4. reviewId반환

     

     

    rating refresh 로직을 두가지로 구현할 수 있습니다.

    1. 강의에 등록된 모든 리뷰를 다시 더한 뒤 리뷰 개수로 나눠주기
    2. 변경되는 값만 반영하기

    처음엔 1번으로 구현하려 했으나 만약 리뷰가 100개 1000개정도 달린다고 생각하면 등록, 수정, 삭제할때마다 1000개의 리뷰를 다시 더해야 하므로 성능이 매우 떨어질 것 같았습니다.

     

    그래서 2번으로 구현하려고 합니다.

     

    2번으로 구현하기 위해서는 수정된 리뷰의 원래 rating과 바뀌는 rating이 필요합니다.

    원래의 rating은 전달받는 reviewId를 통해 구할 수 있습니다. 

     

     

    리뷰수정시 avarage rating 로직 진행 순서

    LectureController -> (ReviewService -> LectureService) -> LectureDomain의 updateAverageRating 수행

     

    updateAverageRating은

    rating 한개를 넘길 시 : rating을 추가하며 update

    rating 2개를 넘길 시 : rating값을 변경하며 update

     

     

    LectureController

    reviewService.update(reviewId, lectureId, form.getContent(), form.getRating());

     

    ReviewService

    lectureService.refreshAverageRating(lectureId, findReview.getRating(), rating);

     

    LectureService

    findLecture.updateAverageRating(oldRating, newRating);

     

    Lecture

        public void updateAverageRating(int oldRating, int newRating) {
            this.averageRating = ((averageRating * reviewNum) - (oldRating-newRating)) / reviewNum;
        }

     

     

     

    구동확인

     

    3점, 5점짜리 리뷰가 있어서 평균 리뷰는 4점으로 나옵니다.


    여기서 5점짜리 리뷰를 1점으로 수정합니다.

     

     

    1점과 3점의 평균인 2점이 잘 나오는 것을 볼 수 있습니다.

     

     

    service 로직을 수정했으니 수정로직에 대한 테스트를 추가하고 테스트를 다시한번 돌렸습니다.

     

    3점, 1점 리뷰를 등록하고 3점 리뷰를 5점으로 수정했습니다.

    수정된 후 해당 강의의 평균 별점은 5, 1의 평균인 3이 나와야합니다.

        // 리뷰 수정
        @Test
        public void 리뷰수정() throws Exception {
            //given
            Member member = new Member("dongho", 25, Gender.MALE);
            em.persist(member);
    
            Lecture lecture = new Lecture("jpa", "김영한", "jpa강의", LocalDateTime.now());
            em.persist(lecture);
    
            //when
            Review review1 = new Review(member,"good", 3, lecture, LocalDateTime.now());
            Long reviewId1 = reviewService.create(review1);
    
            Review review2 = new Review(member,"good", 1, lecture, LocalDateTime.now());
            Long reviewId2 = reviewService.create(review2);
    
            reviewService.update(reviewId1, lecture.getId(), "very good", 5);
    
            //then
            assertThat(review1.getContent()).isEqualTo("very good");
            assertThat(review1.getRating()).isEqualTo(5);
            assertThat(lecture.getAverageRating()).isEqualTo(3);
    
        }

     

     

     

     

     

    문제점 해결 (2)

     

    수정창에서 원래 리뷰의 내용이 보여야합니다.

     

     

    구현계획

     

    1. 수정 버튼을 누를때 해당 리뷰의 content를 자바스크립트의 전역변수에 저장합니다.
    2. 수정폼의 content 입력 부분의 value를 위에서 저장한 content로 지정하면 됩니다.

     

     

    onclick시 두개의 자바스크립트 함수를 호출합니다.

    하나는 id를 전역변수에 저장, 하나는 content를 수정폼의 후기창의 value값으로 지정

    <a class="btn btn-primary" role="button" th:onclick="|saveReviewEditId(${reviewDto.getReviewId()}); setReviewEditContent(${reviewDto.getContent()});|"
        var updateReviewId;
        var lectureId = [[${lectureId}]];
    
        function saveReviewEditId(id) {
            updateReviewId = id;
        }
    
        function setReviewEditContent(content) {
            document.getElementById('reviewEdit').value = content;
        }

     

    이렇게 수정 했더니 onclick 부분에서 타임리프 에러가 났습니다.

     

     

    한번에 두개의 인자를 넘기는 법을 검색해 수정해보았습니다.

    <a class="btn btn-primary" role="button" th:onclick="|setReviewEdit('${reviewDto.getReviewId()}', '${reviewDto.getContent()}')|"
    data-target="#modal" data-toggle="modal">수정</a>

    똑같은 에러가 발생했습니다. 저 onclick 부분이 실행이 되지 않았습니다.

     

     

     

    다른 방법 두가지를 찾았습니다. (해결됨)

     

     

    1.

    [[]] 사이에 타임리프 변수 넣기

    <a class="btn btn-primary" role="button"
    th:onclick="setReviewEdit([[${reviewDto.getReviewId()}]], [[${reviewDto.getContent()}]]);"
    data-target="#modal" data-toggle="modal">수정</a>

     

     

    2.

    th:data-param1, th:data-param2 에 각 값을 넣고

    this.getAttribute('data-param1') 형식으로 데이터 가져오기.

    <a class="btn btn-primary" role="button" th:data-param1="${reviewDto.getReviewId()}" th:data-param2="${reviewDto.getContent()}"
    th:onclick="setReviewEdit(this.getAttribute('data-param1'), this.getAttribute('data-param2'));"
    data-target="#modal" data-toggle="modal">수정</a>

     

     

     

    구현확인

    수정버튼을 누르니 원래 댓글의 값이 이미 써져있는 것을 볼 수 있습니다.