MovieReview 프로젝트 (5) (목록 페이지 처리, 평균 평점)
MovieReview 프로젝트 (5) (목록 페이지 처리, 평균 평점)
MovieReview 프로젝트 (4) (영화 등록 Register dto / service / controller) MovieReview 프로젝트 (4) (영화 등록 Register dto / service / controller) MovieReview 프로젝트 (3) (파일 업로드) MovieReview 프로젝트 (3) (파일 업로드
soohykeee.tistory.com
조회를 위해 MovieService 인터페이스에 getMovie() 메소드를 추가해준다.
MovieDTO getMovie(Long mno);
앞서 MovieRepository에서 만들었던 getMovieWithAll 메소드를 이용하여 조회를 할것이다.
@Override
public MovieDTO getMovie(Long mno) {
List<Object[]> result = movieRepository.getMovieWithAll(mno);
Movie movie = (Movie) result.get(0)[0];
List<MovieImage> movieImageList = new ArrayList<>();
result.forEach(arr -> {
MovieImage movieImage = (MovieImage) arr[1];
movieImageList.add(movieImage);
});
Double avg = (Double) result.get(0)[2];
Long reviewCnt = (Long) result.get(0)[3];
return entitiesToDTO(movie, movieImageList, avg, reviewCnt);
}
MovieController에는 아래와 같이 추가해준다.
@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);
}
화면 구성을 할 read.html을 movie 하위 디렉토리에 생성후, 아래와 같이 작성해준다. 해당 조회페이지에서 review에 관해 작성 및 수정이 가능해야하므로 버튼을 클릭 시 modal 창이 생성되는 것도 추후에 추가를 해줄것이다. 우선적으로 이렇게만 작성 후 정상적으로 조회가 되는지 확인을 해볼것이다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">
<th:block th:fragment="content">
<h1 class="mt-4">Movie Read Page</h1>
<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>
<style>
.uploadResult {
width: 100%;
background-color: gray;
margin-top: 10px;
}
.uploadResult ul {
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
vertical-align: top;
overflow: auto;
}
.uploadResult ul li {
list-style: none;
padding: 10px;
margin-left: 2em;
}
.uploadResult ul li img {
width: 100px;
}
</style>
<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>
</th:block>
</th:block>
목록페이지에서 mno 클릭 시, 아래와 같이 조회페이지로 넘어간 후 정상적으로 정보가 나오는것을 확인할 수 있다.
Ajax로 영화 리뷰 처리
영화 리뷰가 등록되면 영화 자체의 리뷰 개수와 리뷰 평균이 변경되었기 때문에 현재 URL을 다시 호출해서 갱신하는 방식으로 처리해야한다.
이미 Review 엔티티의 경우는 만들어놓았고, 이에 맞는 ReviewDTO를 생성해줘야 한다.
ReviewDTO에는 화면에 필요한 모든 내용이 존재해야한다. 회원의 아이디, 닉네임, 이메일도 처리할 수 있도록 DTO에 추가해준다.
package com.example.moviereview.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
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;
}
ReviewService에는 앞서 다른 Service와 동일하게 dtoToEntity, entityToDTO를 default로 작성해주고 다음과 같은 메소드 기능들을 정의해줘야한다.
- 특정한 영화의 모든 리뷰를 가져오는 기능
- 새로운 영화 리뷰를 등록하는 기능
- 특정 영화 리뷰를 수정하는 기능
- 특정 영화 리뷰를 삭제하는 기능
수정을 위해서는 Review 엔티티에 change해주는 메소드를 public으로 작성해줘야한다.
public void changeGrade(int grade) {
this.grade = grade;
}
public void changeText(String text) {
this.text = text;
}
package com.example.moviereview.service;
import com.example.moviereview.dto.ReviewDTO;
import com.example.moviereview.entity.Member;
import com.example.moviereview.entity.Movie;
import com.example.moviereview.entity.Review;
import java.util.List;
public interface ReviewService {
// 영화의 모든 리뷰 가져오기
List<ReviewDTO> getListOfMovie(Long mno);
// 리뷰 추가
Long register(ReviewDTO movieReviewDTO);
// 리뷰 수정
void modify(ReviewDTO movieReviewDTO);
// 리뷰 삭제
void remove(Long reviewnum);
default Review dtoToEntity(ReviewDTO movieReviewDTO) {
Review movieReview = Review.builder()
.reviewnum(movieReviewDTO.getReviewnum())
.movie(Movie.builder().mno(movieReviewDTO.getMno()).build())
.member(Member.builder().mid(movieReviewDTO.getMid()).build())
.grade(movieReviewDTO.getGrade())
.text(movieReviewDTO.getText())
.build();
return movieReview;
}
default ReviewDTO entityToDto(Review movieReview) {
ReviewDTO movieReviewDTO = ReviewDTO.builder()
.reviewnum(movieReview.getReviewnum())
.mno(movieReview.getMovie().getMno())
.mid(movieReview.getMember().getMid())
.nickname(movieReview.getMember().getNickname())
.email(movieReview.getMember().getEmail())
.grade(movieReview.getGrade())
.text(movieReview.getText())
.regDate(movieReview.getRegDate())
.modDate(movieReview.getModDate())
.build();
return movieReviewDTO;
}
}
package com.example.moviereview.service;
import com.example.moviereview.dto.ReviewDTO;
import com.example.moviereview.entity.Movie;
import com.example.moviereview.entity.Review;
import com.example.moviereview.repository.ReviewRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Log4j2
@RequiredArgsConstructor
public class ReviewServiceImpl implements ReviewService {
private final ReviewRepository reviewRepository;
@Override
public List<ReviewDTO> getListOfMovie(Long mno) {
Movie movie = Movie.builder().mno(mno).build();
List<Review> result = reviewRepository.findByMovie(movie);
return result.stream().map(movieReview -> entityToDto(movieReview)).collect(Collectors.toList());
}
@Override
public Long register(ReviewDTO movieReviewDTO) {
Review movieReview = dtoToEntity(movieReviewDTO);
reviewRepository.save(movieReview);
return movieReview.getReviewnum();
}
@Override
public void modify(ReviewDTO movieReviewDTO) {
Optional<Review> result = reviewRepository.findById(movieReviewDTO.getReviewnum());
if (result.isPresent()) {
Review movieReview = result.get();
movieReview.changeGrade(movieReviewDTO.getGrade());
movieReview.changeText(movieReviewDTO.getText());
reviewRepository.save(movieReview);
}
}
@Override
public void remove(Long reviewnum) {
reviewRepository.deleteById(reviewnum);
}
}
ReviewController
ReviewController는 Ajax로 동작하기 때문에 @RestController로 설계하고, ReviewDTO는 JSON형태로 반환되어서 처리된다. 새로운 영화 리뷰 등록 역시 JSON 포맷으로 전송하여 처리한다. ReviewController는 '/reviews/'로 시작하는 경로를 처리하고, 다음과 같은 URL을 처리한다.
방식 | URL | 결과 데이터 | 설명 |
GET | /reviews/영화번호/all | ReviewDTO 리스트 | 해당 영화의 모든 리뷰 반환 |
POST | /reviews/영화번호 | 생성된 리뷰 번호 | 새로운 리뷰 등록 |
PUT | /reviews/영화번호/영화리뷰번호 | 리뷰의 수정 성공 여부 | 리뷰 수정 |
DELETE | /reviews/영화번호/영화리뷰번호 | 리뷰 삭제 | 리뷰 삭제 |
package com.example.moviereview.controller;
import com.example.moviereview.dto.ReviewDTO;
import com.example.moviereview.service.ReviewService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/reviews")
@Log4j2
@RequiredArgsConstructor
public class ReviewController {
private final ReviewService reviewService;
@GetMapping("/{mno}/all")
public ResponseEntity<List<ReviewDTO>> getList(@PathVariable("mno") Long mno) {
List<ReviewDTO> reviewDTOList = reviewService.getListOfMovie(mno);
return new ResponseEntity<>(reviewDTOList, HttpStatus.OK);
}
@PostMapping("/{mno}")
public ResponseEntity<Long> addReview(@RequestBody ReviewDTO movieReviewDTO) {
Long reviewnum = reviewService.register(movieReviewDTO);
return new ResponseEntity<>(reviewnum, HttpStatus.OK);
}
@PutMapping("/{mno}/{reviewnum}")
public ResponseEntity<Long> modifyReview(@PathVariable Long reviewnum, @RequestBody ReviewDTO movieReviewDTO) {
reviewService.modify(movieReviewDTO);
return new ResponseEntity<>(reviewnum, HttpStatus.OK);
}
@DeleteMapping("/{mno}/{reivewnum}")
public ResponseEntity<Long> removeReview(@PathVariable Long reivewnum) {
reviewService.remove(reivewnum);
return new ResponseEntity<>(reivewnum, HttpStatus.OK);
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">
<th:block th:fragment="content">
<h1 class="mt-4">Movie Read Page</h1>
<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>
<style>
.uploadResult {
width: 100%;
background-color: gray;
margin-top: 10px;
}
.uploadResult ul {
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
vertical-align: top;
overflow: auto;
}
.uploadResult ul li {
list-style: none;
padding: 10px;
margin-left: 2em;
}
.uploadResult ul li img {
width: 100px;
}
</style>
<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>
<button type="button" class="btn btn-info addReviewBtn">
Review Register
</button>
<div class="list-group reviewList">
</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">×</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>
<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">×</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 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>
$(document).ready(function(e) {
var grade = 0;
var mno = [[${dto.mno}]];
$('.starrr').starrr({
rating: grade,
change: function(e, value){
if (value) {
console.log(value);
grade = value;
}
}
});
//$(".reviewModal").modal("show"); 미리 보기용
var reviewModal = $(".reviewModal");
var inputMid = $('input[name="mid"]');
var inputText = $('input[name="text"]');
$(".addReviewBtn").click(function () {
inputMid.val("");
inputText.val("");
$(".removeBtn , .modifyBtn").hide();
$(".reviewSaveBtn").show();
reviewModal.modal('show');
});
$('.reviewSaveBtn').click(function() {
var data = {mno:mno, grade:grade, text:inputText.val(), mid: inputMid.val() };
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');
});
//페이지가 열리면 바로 리뷰 데이터들을 가져와서 사용한다.
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);
});
}
getMovieReviews();
//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');
});
$(".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');
});
$(".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');
});
$(".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");
});
});
</script>
</th:block>
</th:block>