SpringBoot with JPA 프로젝트(M:N) 6.영화조회,리뷰등록,삭제
📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/mreview2022
1.조회 페이지
조회 페이지는 목록 페이지에서 영화의 번호를 클릭하면 "/movie/read" URL처리를 해야한다
1-1.Service 수정
import com.example.mreview2022.entity.Movie;
public interface MovieService {
//... 생략
MovieDTO getMovie(Long mno);
}
1-2.ServiceImpl 수정
MovieDTO 를 만들어 내기 위해 MovieRepository에서 가져오는 Movie, MovieImage 리스트, 평점 평균, 리뷰 개수의 리스트를 가공한다
import antlr.PreservingFileWriter;
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.PageRequestDTO;
import com.example.mreview2022.dto.PageResultDTO;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.entity.MovieImage;
import com.example.mreview2022.repository.MovieImageRepository;
import com.example.mreview2022.repository.MovieRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService {
@Autowired
private final MovieRepository movieRepository; // final
@Autowired
private final MovieImageRepository imageRepository; // final
//... 생략
@Override
public MovieDTO getMovie(Long mno) {
List<Object[]> result = movieRepository.getMovieWithAll(mno);
Movie movie = (Movie) result.get(0)[0] ; // Movie 엔티티는 가장 앞에 존재 - 모든 Row가 동일한 값이다
List<MovieImage> movieImageList = new ArrayList<>(); // 영화의 이미지개수만큼 MovieImage 객체 필요
result.forEach(arr ->{
MovieImage movieImage = (MovieImage) arr[1];
movieImageList.add(movieImage);
});
Double avg = (Double) result.get(0)[2]; //평균 평점 - 모든 Row가 동일한 값
Long reviewCnt = (Long) result.get(0)[3]; //리뷰 개수 - 모든 Row가 동일한 값
return entitiesToDTO(movie,movieImageList,avg,reviewCnt);
}
}
1-3.Controller 수정
GET 방식으로 '/movie/read?mno=xxx'와 같은 URL을 처리한다(수정 작업에도 동일한 코드가 사용된다)
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.PageRequestDTO;
import com.example.mreview2022.service.MovieService;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/movie")
@RequiredArgsConstructor
public class MovieController {
@Autowired
private final MovieService movieService; //final
//... 생략
@GetMapping({"/read", "/modify"})
public void read(long mno, @ModelAttribute("requestDTO") PageRequestDTO requestDTO,Model model){
MovieDTO movieDTO = movieService.getMovie(mno);
model.addAttribute("dto",movieDTO);
}
}
1-4.html 추가
<!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>
<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>
<script>
$(document).ready(function(e) {
});
</script>
</th:block>
</th:block>
2.Ajax로 리뷰처리
2-1.ReviewDTO 생성
Review가 Movie 와 Member를 참조하는 구성으로 되어있으므로 ReviewDTO는 엔티티 클래스와 달리 단순 문자열 이나 영화 번호를 참초하는 형태로 변경된다
- ReviewDTO는 화면에 필요한 모든 내용을 가지고 있어야 하기 때문에 회원이 아이디,닉네임,이메일도 같이 처리할 수 있도록 설계한다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReviewDTO {
//review num
private Long reviewnum;
//Movie mno
private Long mno;
//Member id
private Long mid;
//Member nickname
private String nickname;
//Member email
private String email;
private int grade;
private String text;
private LocalDateTime regDate, modDate;
}
2-2.Review 엔티티 클래스 수정
리뷰평점과 리뷰 내용 수정할 수 있는 기능 추가
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"movie","member"})
public class Review extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long reviewnum;
@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
private int grade;
private String text;
// 추가
public void changeGrade(int grade){
this.grade = grade;
}
// 추가
public void changeText(String text){
this.text = text;
}
}
2-3.ReviewService interface 생성
기능정의
1) 특정한 영화의 모든 리뷰 가져오는기능
2) 새로운 리뷰를 등록하는 기능
3) 특정 영화 리뷰 수정 기능
4) 특정 리뷰 삭제 기능
import com.example.mreview2022.dto.ReviewDTO;
import com.example.mreview2022.entity.Member;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.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;
}
}
2-4.ReviewServiceImpl 생성
import com.example.mreview2022.dto.ReviewDTO;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.entity.Review;
import com.example.mreview2022.repository.ReviewRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ReviewServiceImpl implements ReviewService{
@Autowired
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);
}
}
2-5.ReviewController 생성
ReviewController는 Ajax로 동작하기 때문에 @RestController로 설계하고 RevieDTO는 JSON 형태로 변환되어 처리한다, 새로운 영화 리뷰 등록 역시 JSON 포맷으로 전송 처리한다
import com.example.mreview2022.dto.ReviewDTO;
import com.example.mreview2022.service.ReviewService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/reviews")
@RequiredArgsConstructor
public class ReviewController {
@Autowired
private final ReviewService reviewService;
@GetMapping("/{mno}/all") // 결과데이터 : ReviewDTO 리스트, 해당영화의 모든 리뷰 반환
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}/{reviewnum}") // 리뷰 삭제
public ResponseEntity<Long> removieReview(@PathVariable Long reviewnum){
reviewService.remove(reviewnum);
return new ResponseEntity<>(reviewnum,HttpStatus.OK);
}
}
2-6.html 수정
2개의 모달창 추가한다 -> 리뷰등록 모달창, 영화 이미지 클릭했을 때의 모달창
- reviewModal : 실제 영화 리뷰에 대한 처리를 하기 때문에 회원 아이디(mid), 별점,리뷰 내용을 입력할 수 있는 태그를 가진다
- imageModal : 단순히 이미지(원본이미지)를 화면에 보여주는 용도로 작성
- 별점처리 라이브러리 : http://dobtco.github.io/starrr/라이브러리 사용
=> starrr 라이브러리는 jQuery의 플러그인의 형태로 동작하므로 starrr()를 이용해 별점의 값이 변하는 이벤트를 처리할 수 있다, grade라는 변수로 별점을 처리한다
- 리뷰 등록 : reviewSaveBtn을 클릭하면 회원의 아이디,점수,내용을 JSON데이터로 만들어 전송하고 데이터 처리가 성공하면 self.location.reload()를 이용해 URL을 다시호출하여 영화 리뷰가 등록된 후 변화하는 평균평점과 리뷰의 개수를 갱신하게 된다
- 리뷰 리스트 보여주기 : getMovieReviews()를 호출하여 페이지가 열리면 jQuery의 getJSON()을 이용해 MovieReviewController 를 호출하고 reviewList라는 클래스 속성으로 지정된 div 태그에 내용물을 채운다
- 특정 리뷰 선택 : 리뷰를 선택하면 해당 리뷰의 정보를 가져와 reviewModal로 셋팅하고 모달창을 보여준다
- 리뷰의 수정, 삭제 : reviewModal 창에 나오는 리뷰의 수정과 삭제작업 모두 Ajax를 통해 PUT OR DELETE 방식으로 동작한다. 수정과 삭제 작업이 모두 처리된 후에는 현재 페이지를 다시 호출해 서버로부터 변경된 데이터를 받도록 처리한다
<!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>
2-7.이미지의 원본이미지 보기 - UploadController 수정
String size 파라미터를 추가해 원본파일인지 섬네일인지 구분할 수 있도록 구성한다
-> 만약 size 변수의 값이 1인 경우 원본 파일을 전송한다
import com.example.mreview2022.dto.UploadResultDTO;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
public class UploadController {
@Value("${com.example.upload.path}") // application.properties의 변수
private String uploadPath;
//...생략
@GetMapping("/display")
public ResponseEntity<byte[]> getFile(String fileName, String size){
ResponseEntity<byte[]> result = null;
try {
String srcFileName = URLDecoder.decode(fileName,"UTF-8");
File file = new File(uploadPath + File.separator + srcFileName);
if(size != null && size.equals("1")){
file = new File(file.getParent(),file.getName().substring(2));
}
HttpHeaders header = new HttpHeaders();
//MIME타입 처리
header.add("Content-Type", Files.probeContentType(file.toPath()));
//파일 데이터 처리
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
}catch (Exception e){
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return result;
}
//...생략
}
- 실행결과
Author And Source
이 문제에 관하여(SpringBoot with JPA 프로젝트(M:N) 6.영화조회,리뷰등록,삭제), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@alswl689/SpringBoot-with-JPA-프로젝트MN-6.영화조회리뷰등록삭제
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
조회 페이지는 목록 페이지에서 영화의 번호를 클릭하면 "/movie/read" URL처리를 해야한다
import com.example.mreview2022.entity.Movie;
public interface MovieService {
//... 생략
MovieDTO getMovie(Long mno);
}
MovieDTO 를 만들어 내기 위해 MovieRepository에서 가져오는 Movie, MovieImage 리스트, 평점 평균, 리뷰 개수의 리스트를 가공한다
import antlr.PreservingFileWriter;
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.PageRequestDTO;
import com.example.mreview2022.dto.PageResultDTO;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.entity.MovieImage;
import com.example.mreview2022.repository.MovieImageRepository;
import com.example.mreview2022.repository.MovieRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService {
@Autowired
private final MovieRepository movieRepository; // final
@Autowired
private final MovieImageRepository imageRepository; // final
//... 생략
@Override
public MovieDTO getMovie(Long mno) {
List<Object[]> result = movieRepository.getMovieWithAll(mno);
Movie movie = (Movie) result.get(0)[0] ; // Movie 엔티티는 가장 앞에 존재 - 모든 Row가 동일한 값이다
List<MovieImage> movieImageList = new ArrayList<>(); // 영화의 이미지개수만큼 MovieImage 객체 필요
result.forEach(arr ->{
MovieImage movieImage = (MovieImage) arr[1];
movieImageList.add(movieImage);
});
Double avg = (Double) result.get(0)[2]; //평균 평점 - 모든 Row가 동일한 값
Long reviewCnt = (Long) result.get(0)[3]; //리뷰 개수 - 모든 Row가 동일한 값
return entitiesToDTO(movie,movieImageList,avg,reviewCnt);
}
}
GET 방식으로 '/movie/read?mno=xxx'와 같은 URL을 처리한다(수정 작업에도 동일한 코드가 사용된다)
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.PageRequestDTO;
import com.example.mreview2022.service.MovieService;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/movie")
@RequiredArgsConstructor
public class MovieController {
@Autowired
private final MovieService movieService; //final
//... 생략
@GetMapping({"/read", "/modify"})
public void read(long mno, @ModelAttribute("requestDTO") PageRequestDTO requestDTO,Model model){
MovieDTO movieDTO = movieService.getMovie(mno);
model.addAttribute("dto",movieDTO);
}
}
<!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>
<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>
<script>
$(document).ready(function(e) {
});
</script>
</th:block>
</th:block>
2-1.ReviewDTO 생성
Review가 Movie 와 Member를 참조하는 구성으로 되어있으므로 ReviewDTO는 엔티티 클래스와 달리 단순 문자열 이나 영화 번호를 참초하는 형태로 변경된다
- ReviewDTO는 화면에 필요한 모든 내용을 가지고 있어야 하기 때문에 회원이 아이디,닉네임,이메일도 같이 처리할 수 있도록 설계한다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReviewDTO {
//review num
private Long reviewnum;
//Movie mno
private Long mno;
//Member id
private Long mid;
//Member nickname
private String nickname;
//Member email
private String email;
private int grade;
private String text;
private LocalDateTime regDate, modDate;
}
2-2.Review 엔티티 클래스 수정
리뷰평점과 리뷰 내용 수정할 수 있는 기능 추가
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"movie","member"})
public class Review extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long reviewnum;
@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
private int grade;
private String text;
// 추가
public void changeGrade(int grade){
this.grade = grade;
}
// 추가
public void changeText(String text){
this.text = text;
}
}
2-3.ReviewService interface 생성
기능정의
1) 특정한 영화의 모든 리뷰 가져오는기능
2) 새로운 리뷰를 등록하는 기능
3) 특정 영화 리뷰 수정 기능
4) 특정 리뷰 삭제 기능
import com.example.mreview2022.dto.ReviewDTO;
import com.example.mreview2022.entity.Member;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.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;
}
}
2-4.ReviewServiceImpl 생성
import com.example.mreview2022.dto.ReviewDTO;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.entity.Review;
import com.example.mreview2022.repository.ReviewRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ReviewServiceImpl implements ReviewService{
@Autowired
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);
}
}
2-5.ReviewController 생성
ReviewController는 Ajax로 동작하기 때문에 @RestController로 설계하고 RevieDTO는 JSON 형태로 변환되어 처리한다, 새로운 영화 리뷰 등록 역시 JSON 포맷으로 전송 처리한다
import com.example.mreview2022.dto.ReviewDTO;
import com.example.mreview2022.service.ReviewService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/reviews")
@RequiredArgsConstructor
public class ReviewController {
@Autowired
private final ReviewService reviewService;
@GetMapping("/{mno}/all") // 결과데이터 : ReviewDTO 리스트, 해당영화의 모든 리뷰 반환
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}/{reviewnum}") // 리뷰 삭제
public ResponseEntity<Long> removieReview(@PathVariable Long reviewnum){
reviewService.remove(reviewnum);
return new ResponseEntity<>(reviewnum,HttpStatus.OK);
}
}
2-6.html 수정
2개의 모달창 추가한다 -> 리뷰등록 모달창, 영화 이미지 클릭했을 때의 모달창
- reviewModal : 실제 영화 리뷰에 대한 처리를 하기 때문에 회원 아이디(mid), 별점,리뷰 내용을 입력할 수 있는 태그를 가진다
- imageModal : 단순히 이미지(원본이미지)를 화면에 보여주는 용도로 작성
- 별점처리 라이브러리 : http://dobtco.github.io/starrr/라이브러리 사용
=> starrr 라이브러리는 jQuery의 플러그인의 형태로 동작하므로 starrr()를 이용해 별점의 값이 변하는 이벤트를 처리할 수 있다, grade라는 변수로 별점을 처리한다- 리뷰 등록 : reviewSaveBtn을 클릭하면 회원의 아이디,점수,내용을 JSON데이터로 만들어 전송하고 데이터 처리가 성공하면 self.location.reload()를 이용해 URL을 다시호출하여 영화 리뷰가 등록된 후 변화하는 평균평점과 리뷰의 개수를 갱신하게 된다
- 리뷰 리스트 보여주기 : getMovieReviews()를 호출하여 페이지가 열리면 jQuery의 getJSON()을 이용해 MovieReviewController 를 호출하고 reviewList라는 클래스 속성으로 지정된 div 태그에 내용물을 채운다
- 특정 리뷰 선택 : 리뷰를 선택하면 해당 리뷰의 정보를 가져와 reviewModal로 셋팅하고 모달창을 보여준다
- 리뷰의 수정, 삭제 : reviewModal 창에 나오는 리뷰의 수정과 삭제작업 모두 Ajax를 통해 PUT OR DELETE 방식으로 동작한다. 수정과 삭제 작업이 모두 처리된 후에는 현재 페이지를 다시 호출해 서버로부터 변경된 데이터를 받도록 처리한다
<!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>
2-7.이미지의 원본이미지 보기 - UploadController 수정
String size 파라미터를 추가해 원본파일인지 섬네일인지 구분할 수 있도록 구성한다
-> 만약 size 변수의 값이 1인 경우 원본 파일을 전송한다
import com.example.mreview2022.dto.UploadResultDTO;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
public class UploadController {
@Value("${com.example.upload.path}") // application.properties의 변수
private String uploadPath;
//...생략
@GetMapping("/display")
public ResponseEntity<byte[]> getFile(String fileName, String size){
ResponseEntity<byte[]> result = null;
try {
String srcFileName = URLDecoder.decode(fileName,"UTF-8");
File file = new File(uploadPath + File.separator + srcFileName);
if(size != null && size.equals("1")){
file = new File(file.getParent(),file.getName().substring(2));
}
HttpHeaders header = new HttpHeaders();
//MIME타입 처리
header.add("Content-Type", Files.probeContentType(file.toPath()));
//파일 데이터 처리
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
}catch (Exception e){
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return result;
}
//...생략
}
- 실행결과
Author And Source
이 문제에 관하여(SpringBoot with JPA 프로젝트(M:N) 6.영화조회,리뷰등록,삭제), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@alswl689/SpringBoot-with-JPA-프로젝트MN-6.영화조회리뷰등록삭제저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)