Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

공유메모장

[Spring boot Project] 조회 페이지와 영화 리뷰 본문

프로젝트

[Spring boot Project] 조회 페이지와 영화 리뷰

댕칠이 2023. 9. 27. 16:39

영화 조회를 위해 필요한 정보들 : Movie 객체, MovieImage 객체리스트, 리뷰 평균 평점, 리뷰 개수

조회를 위한 JPQL 은 아래와 같다.

 

영화 세부 조회

MovieRepository interface

 // 리뷰와 조인한 후에 count, avg 등 함수를 이용하게 되는데 이때 영화 이미지 별로 group by를 실행해야만 한다.
    //그래야 영화 이미지들의 개수만큼 데이터를 만들어낼 수 있다.
    @Query("select m, mi, avg(coalesce(r.grade,0)), count(distinct(r))" +
            "from Movie m left outer join MovieImage mi on mi.movie = m "+
            "left outer join Review r on r.movie =m "+
            "where m.mno=:mno group by mi")
    List<Object[]> getMovieWithAll(Long mno);

하나의 영화는 하나의 리뷰 평균 평점, 리뷰 개수를 가질테지만, 영화이미지가 복수개일 수 있기 때문에 결과값을 List로 받는다.

select 순서대로 Object[0] 에는 m, Object[1]에는 mi, Object[2]에는 avg(영화리뷰평점) , Object[3]에는 count(영화리뷰) 가 들어간다. 

List에 들어있는 각 데이터 당 Object[0], Object[2], Object[3]은 같은 값이고, Object[1]만 다른 객체가 들어올테니 이를 처리해주어야 한다.

 

MovieServiceImpl

 @Override
    public MovieDTO getMovie(Long mno) {
        List<Object[]> result = movieRepository.getMovieWithAll(mno);
        Movie movie = (Movie) result.get(0)[0];
        Double avg= (Double) result.get(0)[2];
        Long reviewCnt = (Long) result.get(0)[3];
        List<MovieImage> movieImageList = new ArrayList<>();
        for(int i=0; i<result.size(); i++){
            movieImageList.add( (MovieImage)result.get(i)[1]);
        }
        return entitiesToDTO(movie,movieImageList, avg, reviewCnt);
    }

위의 jpql 실행으로부터 받아온 result를 하나씩 받아서 값을 처리한다. Object[] 타입이기 때문에 형변환은 필수이다. 

entitiesToDTO 메소드를 이용하여 entity들을 모두 dto로 변환해준다. 

 

MovieController.java

    @GetMapping({"/read","/modify"})
    public void read(long mno, @ModelAttribute("requestDTO") PageRequestDTO requestDTO , Model model){
        log.info(mno+": mno");
        MovieDTO movieDTO = movieService.getMovie(mno);
        model.addAttribute("dto", movieDTO);

    }

PageRequestDTO는 당장에 필요는 없지만, 검색처리에 사용되는 keyword 와 type을 저장하고 있는 객체이기 때문에 우선 파라미터로 받아둔다. 

url로 mno를 받아 movieService.getMovie(mno)를 수행하고 그 결과를 MovieDTO 객체에 저장한다.

이를 model에  dto라는 이름으로 붙여서 화면로 전달한다. 


리뷰 작성

리뷰 작성을 위한 repository 메소드는 쿼리메소드 save()를 사용했다.

 

ReviewServiceImple.java 

    @Override
    public Long register(ReviewDTO movieReviewDTO) {
        Review review = dtoToEntity(movieReviewDTO);

        reviewRepository.save(review);
        return review.getReviewnum();
    }

컨트롤러로부터 받아온 ReviewDTO를 dtoToEntity 메소드를 통해 Review 엔티티로 변환시킨다. 

쿼리메소드인 save를 이용해서 저장한 후, reviewnum을 리턴한다.

 

ReviewController.java (RestController)

    @PostMapping("/{mno}")
    public ResponseEntity<Long> addReview(@RequestBody ReviewDTO movieReviewDTO){
        System.out.println(movieReviewDTO);
        Long reviewnum =  reviewService.register(movieReviewDTO);
        return new ResponseEntity<>(reviewnum, HttpStatus.OK);
    }

addReview는 ajax로 ReviewDTO 객체를 받는다. 

 

ReviewDTO의 구성은 아래와 같다.

public class ReviewDTO {
    private Long reviewnum;
    private Long mno;
    private Long mid;
    private String nickname;
    private String email;
    private int grade;
    private String text;
    private LocalDateTime regDate,modDate;
}

화면에서는 mno, mid, grade, text, reviewnum 를 받아서 ReviewDTO에 싣는다. 

 

프로그램의 흐름은 Controller -> ServiceImpl -> Repository 이다. 

controller에서 가져온 ReviewDTO를 serviceImpl로 전송하고, serviceImple은 이를 Entity로 변환하여 Repository에 전달한다.


리뷰 수정 

리뷰 수정도 JpaRepository가 제공하는 쿼리메소드를 사용한다. 수정의 경우, Jpa에서 변경감지를 자동으로 해주기 때문에 변경하고자 하는 내용을 포함한 Entity를 다시 save() 하면 된다.

수정되는 것은 리뷰 평점, 리뷰 내용이기 때문에 Review 엔티티는 이에 대해서 setter을 열어주어야 한다. 

 

Review.java

 public void changeText(String text){
        this.text= text;
    }
    public void changeGrade(int grade){
        this.grade= grade;
    }

 

ReviewServiceImpl

    @Override
    public void modify(ReviewDTO movieReviewDTO) {
        Optional<Review> result = reviewRepository.findById(movieReviewDTO.getReviewnum());
        if(result.isPresent()){
            Review movieReview =  result.get();
            movieReview.changeText(movieReviewDTO.getText());
            movieReview.changeGrade(movieReview.getGrade());

            reviewRepository.save(movieReview);
        }
    }

 service는 controller로 부터 ReviewDTO를 전달받는다. 이때, reviewnum을 이용해서 데이터베이스로부터 review 엔티티를 받아온다. 이유는 변경감지를 하기 위해서이다. 변경감지는 Jpa에서 제공하는 기능이기 때문에 영속성 컨텍스트에 변경을 추적하고자 하는 엔티티가 존재하고 있어야 한다. 그렇기 때문에 findById로 먼저 해당 Review 엔티티를 영속성 컨텍스트에 올린다. Review 객체를 Optional로 감싼 이유는 가져온 엔티티가 없는 경우(null)를 처리하기 위해서이다. 

조회 해 온 review 엔티티에 getter로 변경할 필드들을 전달한 후 다시 save 해 주면 해당 review의 내용이 변경된다. 

 

ReviewController.java

    @PutMapping("/{mno}/{reviewnum}")
    public ResponseEntity<Long> modifyReview(@PathVariable Long reviewnum , @RequestBody ReviewDTO movieReviewDTO){
        //RequestBody에 있는 ReviewDTO를 가져오겠다는 뜻.  putMapping은 form 객체의 데이터를 가져올 테니까!
        reviewService.modify(movieReviewDTO);
        return new ResponseEntity<>(reviewnum, HttpStatus.OK);
    }

데이터의 수정이기 때문에 PutMapping을 한다.  

수정도 역시 ajax로 이루어진다. RequestBody로 부터 ReviewDTO를 받아서 modify 처리를 하고, 

ResponseEntity에 reviewnum과 httpstatus 메시지를 담아서 보낸다.


리뷰 삭제

리뷰 삭제도 역시 JpaRepository가 제공하는 deleteById() 라는 쿼리 메소드를 이용한다.

 

ReviewServiceImpl.java

    @Override
    public void remove(Long reviewnum) {
        reviewRepository.deleteById(reviewnum);

    }

 

리뷰 삭제는 별다른 entity나 dto 없이 리뷰 번호로만 처리하기 때문에 서비스에 추가적인 처리가 필요없다.

 

ReviewController.java

    @DeleteMapping("/{mno}/{reviewnum}")
    public ResponseEntity<Long> removeReview(@PathVariable Long reviewnum ){
        reviewService.remove(reviewnum);
        return new ResponseEntity<>(reviewnum, HttpStatus.OK);
    }

삭제 작업이기 때문에 DeleteMapping을 한다.

reviewService의 remove 메소드를 호출하고, ResponseEntity에 reviewnum과 HttpStatus를 함께 붙여서 보낸다. 


html 과 자바스크립트 코드

read.html

    <div class="form-group">
      <label >Title</label>
      <input type="text" class="form-control" name="title" th:value="${dto.title}" readonly>
    </div>

    <div class="form-group">
      <label >Review Count </label>
      <input type="text" class="form-control" name="title" th:value="${dto.reviewCnt}" readonly>
    </div>

    <div class="form-group">
      <label >Avg </label>
      <input type="text" class="form-control" name="title" th:value="${dto.avg}" readonly>
    </div>

<div class="uploadResult">
      <ul >
        <li th:each="movieImage: ${dto.imageDTOList}" th:data-file="${movieImage.getThumbnailURL()}">
          <img  th:if="${movieImage.path != null}" th:src="|/display?fileName=${movieImage.getThumbnailURL()}|">
        </li>
      </ul>
    </div>
        <button type="button" class="btn btn-primary">
      Review Count <span class="badge badge-light">[[${dto.reviewCnt}]]</span>
    </button>

dto로부터 정보를 하나씩 꺼내서 value로 설정한다. MovieImage는 List이기 때문에 th:each로 처리하고, 만약 movieImage가 없는 경우에는 display하지 않는다. 

    <button type="button" class="btn btn-info addReviewBtn">
      Review Register
    </button>

    <div class="list-group reviewList">

    </div>

위의 button을 클릭하면 리뷰 작성 모달이 나온다. 

아래 div는 작성된 리뷰들이 보여질 영역이다.

 

리뷰 모달창의 정의이다.

    <div class="reviewModal modal" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">Movie Review</h5>

            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <div class="form-group">
              <label >Reviewer ID</label>
              <input type="text" class="form-control" name="mid" >
            </div>
            <div class="form-group">
              <label >Grade <span class="grade"></span></label>
              <div class='starrr'></div>
            </div>
            <div class="form-group">
              <label >Review Text</label>
              <input type="text" class="form-control" name="text" placeholder="Good Movie!" >
            </div>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
            <button type="button" class="btn btn-primary reviewSaveBtn">Save changes</button>
            <button type="button" class="btn btn-warning modifyBtn">Modify </button>
            <button type="button" class="btn btn-danger removeBtn">Remove </button>
          </div>
        </div>
      </div>
    </div>

모달창에는 작성자 아이디, 별점, 리뷰내용이 들어갈 것이고, 모달을 닫는 버튼, 리뷰를 저장하는 버튼, 리뷰를 수정하는 버튼, 리뷰를 지우는 버튼이 존재하는데, addReivew 버튼을 눌렀을 때는 모달을 닫는 버튼과 리뷰를 저장하는 버튼만 보이게 할 것이고, 리뷰 리스트에서 작성되어 있는 리뷰를 클릭하는 경우에는 모달을 닫는버튼, 리뷰를 수정하는 버튼, 리뷰를 지우는 버튼만 보여줄 것이다. 

 

실제 이미지의 크기를 보여줄 모달창이다. 

<div class="imageModal modal " tabindex="-2" role="dialog">
      <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">Picture</h5>

            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">

          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
          </div>
        </div>
      </div>
    </div>

실제 이미지를 출력하는 모달은 아래의 처리로 화면에 출력된다. ( 해당 코드도 script 안에 정의되어 있지만 편의를 위해 앞으로 끄집어 냈다.)

        $(".uploadResult li").click(function() {

          var file = $(this).data('file');

          console.log(file);

          $('.imageModal .modal-body').html("<img style='width:100%' src='/display?fileName="+file+"&size=1' >")

          $(".imageModal").modal("show");

        });

 uploadResult 의 li 태그를 클릭했을 때 커스텀태그가 file인 정보를 받아서 변수에 저장한 후, 

이미지 src 에 full-filename을 준다.  display/ .. Request Mapping은 UploadController에 정의되어 있고, 파일을 디코딩하여 byte[] 로 modal-body에 붙인다.   

이때, display로 넘어가는 url에 size = 1 을 붙여서, size가 1로 넘어온 경우에만 원본파일을 출력한다. 이러한 처리를 UploadController에 추가한다.

 

UploadContoller 수정

        try{
            String srcFileName= URLDecoder.decode(fileName,"UTF-8");
            log.info("fileName: "+ srcFileName);
            File file = new File(uploadPath + File.separator+srcFileName);
            //uploadPath = c:/upload , srcFileName = 년/월/일/파일이름 <- UploadResultDTO 객체로부터 받음
           /* 추가된 부분 */
           if(size!=null && size.equals("1")){
                file = new File(file.getParent(), file.getName().substring(2));
            }
            /* 추가끝 */ 
            log.info("file: "+ file);
            HttpHeaders header = new HttpHeaders();
}

만약 size가 1로 넘어온 경우에는 file 이름에서 s_ 를 떼고 파일을 조회한다. (substring(2) 부분)

 

리뷰를 매기는 것은 starrr 라이브러리를 사용할 것이다. 깃허브의 strrr라이브러리를 찾고, resources -> static -> css 에 starrr.css , static에 starrr.js 파일을 집어넣으면 starrr 라이브러리를 사용할 수 있게 된다.

아래 코드를 이용해서 js, css 사용을 명시해주면 준비는 끝이다.

 <script th:src="@{/starrr.js}"></script>
    <link th:href="@{/css/starrr.css}" rel="stylesheet">
    <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.min.css">

 

script 의 시작이다.

 <script>
      $(document).ready(function(e) {
        var grade = 0;
        var mno = [[${dto.mno}]];

document가 준비상태일 때 리뷰 평점을 저장할 grade 변수와 리뷰를 저장, 수정, 삭제할 때 필요한 변수인 mno를 선언 및 초기화 해준다. 

 

리뷰 평점을 처리하는 코드는 다음과 같다.

        $('.starrr').starrr({
          rating: grade,
          change: function(e, value){
            if (value) {
              console.log(value);
              grade = value;
            }
          }
        });

사용자 화면에서 몇번째 별을 클릭하느냐에 따라서 grade 가 매겨지고, change는 해당 value가 바뀔 때마다 grade에 저장해준다.  

 var reviewModal = $(".reviewModal");
 var inputMid = $('input[name="mid"]');
 var inputText = $('input[name="text"]');

reviewModal 클래스의 태그 정보를 reviewModal에 저장한다. inputMid와 inputText 도 input 태그에서 가져와 저장한다.

 

        $(".addReviewBtn").click(function () {
          inputMid.val("");
          inputText.val("");

          $(".removeBtn ,  .modifyBtn").hide();
          $(".reviewSaveBtn").show();

          reviewModal.modal('show');
        });

만약 리뷰 추가 버튼을 클릭한다면 mid 태그의 내용과 리뷰내용은 공백으로 처리한다. 그리고 remove 버튼, modify버튼은 숨기고 save버튼만 보이도록 설정한다.

이후 모달창을 화면에 출력시킨다.

 

리뷰를 저장하는 javascript이다.

 $('.reviewSaveBtn').click(function() {

          var data = {mno:mno, grade:grade, text:inputText.val(), mid: inputMid.val(), reviewnum: 0};

          console.log(data);

          $.ajax({
            url:'/reviews/'+mno,
            type:"POST",
            data:JSON.stringify(data),
            contentType:"application/json; charset=utf-8",
            dataType:"text",
            success: function(result){

              console.log("result: " + result);

              self.location.reload();

            }
          })
          reviewModal.modal('hide');

        });

리뷰 저장 버튼을 클릭하면 우선 영화식별키, 평점, 리뷰내용, 작성자 아이디를 data로 json 데이터 형식으로 묶는다. 원래 reviewnum은 entity로 넘어가서 db에 저장될 때 자동으로 설정되어야 하는데, dto를 entity로 변환할 때 reviewnum이 null이면 entity로 변환이 되지 않는 문제가 있어서 reviewnum의 값도 함께 data에 실어서 간다. reviewnum은 데이터베이스에 저장될 때 올바른 값으로 수정된다.

ajax를 이용하여 reviews/mno 로 data를 보내는데, json 데이터로 전송될 수 있도록 직렬화 한다. 

ajax로 데이터가 잘 보내지고, reviews/mno 에서 수행이 성공적으로 끝나면 self.location.reload() 를 이용해서 페이지를 새로고침한다. 

리뷰등록처리가 끝났으므로 reviewModal.modal('hide')를 이용하여 모달창을 화면에서 숨겨준다. 

 

이제 리뷰 조회 방법을 알아보겠다.

우선 리뷰에는 날짜데이터가 들어가는데, java에서 사용하는 LocalDateTime 객체를 javascript 에서 Date 객체로 변환해주어야 한다. formatTime() 함수로 이를 처리한다.

  function formatTime(str){
            var date = new Date(str);

            return date.getFullYear() + '/' +
                    (date.getMonth() + 1) + '/' +
                    date.getDate() + ' ' +
                    date.getHours() + ':' +
                    date.getMinutes();
          }

 

getMovieReviews 함수가 모든 리뷰를 ajax로 가져오는 함수이다. 

 function getMovieReviews() {

          function formatTime(str){
            var date = new Date(str);

            return date.getFullYear() + '/' +
                    (date.getMonth() + 1) + '/' +
                    date.getDate() + ' ' +
                    date.getHours() + ':' +
                    date.getMinutes();
          }

          $.getJSON("/reviews/"+ mno +"/all", function(arr){
            var str ="";

            $.each(arr, function(idx, review){

              console.log(review);

              str += '    <div class="card-body" data-reviewnum='+review.reviewnum+' data-mid='+review.mid+'>';
              str += '    <h5 class="card-title">'+review.text+' <span>'+ review.grade+'</span></h5>';
              str += '    <h6 class="card-subtitle mb-2 text-muted">'+review.nickname+'</h6>';
              str += '    <p class="card-text">'+ formatTime(review.regDate) +'</p>';
              str += '    </div>';
            });

            $(".reviewList").html(str);
          });
        }

getJSON을 이용해서 review/mno/all 로 매핑되어 있는 controller 메소드를 수행한다. 결과는 arr라는 변수에 저장된다.

$.each 를 이용해서 arr에 있는 내용들을 각각의 review 로 처리한 후, str 변수에 달아서 페이지로 가져간다.

str에 추가되는 string은 html 인데, reviewnum, 멤버의 id, grade, text, 멤버의 nickname, 등록날짜 를 화면에 표시하기 위한 html이다. 

또한 화면에서 보여지는 리뷰들은 언제든 클릭을 통해 수정 작업이 가능하다. 수정할 때 해당 태그에서 데이터들을 가져올 수 있어야 한다. 그래서 커스텀태그인 data-reviewnum, data-mid 로 리뷰의 식별키, 멤버의 식별키를 참조할 수 있도록 한다. 

마지막으로 $(".reviewList").html(str) 을 통해 str 을 reviewList 클래스의 내용물로 집어넣는다.

 

위는 어디까지나 이러한 동작을 하는 함수를 정의한 것이다. 실제 사용하기 위해서는 아래와 같은 코드를 추가해 실행해주어야 한다.

getMovieReviews();

 

리뷰 목록에서 특정한 리뷰를 선택하면 수정이나 삭제가 가능하도록 reviewModal창을 보이도록 한다. 

 

특정 리뷰 선택

       //modify reveiw

        var reviewnum;

        $(".reviewList").on("click", ".card-body", function() {

          $(".reviewSaveBtn").hide();
          $(".removeBtn , .modifyBtn").show();


          var targetReview = $(this);

          reviewnum = targetReview.data("reviewnum");
          console.log("reviewnum: "+ reviewnum);
          inputMid.val(targetReview.data("mid"));
          inputText.val(targetReview.find('.card-title').clone().children().remove().end().text());

          var grade = targetReview.find('.card-title span').html();
          $(".starrr a:nth-child("+grade+")").trigger('click');

          $('.reviewModal').modal('show');
        });

reviewnum은 수정, 삭제 작업에 계속 사용될 것이기 때문에 외부에 선언한다. 

reviewList 안에 있는 card-body 를 클릭하게 되면 모달창의 save버튼은 숨기고, remove와 modify버튼을 활성화한다.

targetReview는 클릭된 card-body 그 자체가 되며, reviewnum은 targetReview의 커스텀태그가 reviewnum인 것이고, inputMid의 value는 targetReview의 커스텀태그가 mid인 것이다.

 

아래는 text를 갖고 있는 card-title 이다.

 <h5 class="card-title">'+review.text+' <span>'+ review.grade+'</span></h5>

card-title 요소를 우선 clone()으로 복제하고, 이 card-title의 자식 요소로 들어가기 위해 chileren()으로 접근하고, remove()를 통해 자식 요소인 <span> ... </span> 내용을 전부 삭제한다. .end()를 통해 다시 card-title 요소로 돌아온 후, card-title 요소에 남은 텍스트  만을 value에 저장한다.

리뷰 평점의 경우에는  grade는 card-title의 span 내용을 그대로 가져온다. 

그리고 일부러 grade 값 만큼의 별을 클릭하도록 강제 이벤트를 발생시킨다. 

마지막으로 reviewModal 창을 화면에 보이도록 한다. 

 

리뷰 수정 ajax로 처리하기

$(".modifyBtn").on("click", function(){

          var data = {reviewnum: reviewnum, mno:mno, grade:grade, text:inputText.val(), mid: inputMid.val() };

          console.log(data);

          $.ajax({
            url:'/reviews/'+mno +"/"+ reviewnum ,
            type:"PUT",
            data:JSON.stringify(data),
            contentType:"application/json; charset=utf-8",
            dataType:"text",
            success: function(result){

              console.log("result: " + result);

              self.location.reload();

            }
          })
          reviewModal.modal('hide');
        });

 여기도 마찬가지로 리뷰와 영화에 관련된 데이터를 data 변수에 집어넣고, 리뷰 수정을 담당하는 controller 메소드로 넘어가는데, 수정이 일어나는 것이기 때문에 type은 PUT이다.

data는 json으로 직렬화해주고, ajax 통신이 성공적이라면 self.location.reload() 로 페이지를 새로고침한다. 

 

리뷰 삭제 js이다.

  $(".removeBtn").on("click", function(){

          var data = {reviewnum: reviewnum};

          console.log(data);

          $.ajax({
            url:'/reviews/'+mno +"/"+ reviewnum ,
            type:"DELETE",
            contentType:"application/json; charset=utf-8",
            dataType:"text",
            success: function(result){

              console.log("result: " + result);

              self.location.reload();

            }
          })
          reviewModal.modal('hide');
        });

리뷰를 삭제하기 위해서는 reviewnum만 필요하기 때문에 이에 대한 정보만 data에 붙여 전송한다.

삭제 작업이기 때문에 type은 delete이다. 

성공적으로 ajax가 실행되었다면 페이지를 reload한다.