SpringBoot with JPA 프로젝트(M:N) 5.영화등록,등록된글 리스트처리
📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/mreview2022
1.영화/리뷰 프로젝트 적용하기
- 영화(Movie)의 등록과 수정에는 파일 업로드 기능을 활용해 영화 포스터 등을 등록할 수 있도록 구성
- 회원(Member)은 기존 회원들이 존재한다고 가정하고 DB에 존재하는 회원들 이용
- 회원(Member)은 특정한 영화 조회 페이지에서 평점과 자신의 감상을 리뷰(Review)로 기록할 수 있다
- 조회 화면에서 회원(Member)은 자신이 기록한 리뷰(Review)의 내용을 수정/삭제할 수 있다
2.영화(Movie)등록 처리
2-1.DTO 클래스 생성
MovieDTO 는 Movie 클래스를 기준으로작성한다, MovieDTO는 화면에 영화 이미지들도 같이 수집해 전달해야 하므로 내부적으로 리스트를 이용해 수집한다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
private Long mno;
private String title;
@Builder.Default
private List<MovieImageDTO> imageDTOList = new ArrayList<>();
}
2-2.MovieImageDTO 클래스 작성
MovieDTO 클래스 내부에는 업로드된 파일들의 정보를 포함해야 하므로 MovieImageDTO 클래스도 같이 추가한다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MovieImageDTO {
private String uuid;
private String imgName;
private String path;
public String getImageURL(){
try {
return URLEncoder.encode(path + "/" + uuid + "_" + imgName,"UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
public String getThumbnailURL(){
try {
return URLEncoder.encode(path+"/s_" + uuid + "_" +imgName ,"UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
}
2-3.Service interface 작성
Movie를 JPA로 처리하기 위해 MovieDTO를 Movie 객체로 변환해 주어야 하므로 MovieService에 dtoToEntity()를 추가한다
- 주의점
-> Movie 객체뿐 아니라 MovieImage 객체들도 같이 처리된다는 점! 한번에 두가지 종류의 객체를 반환해야 하므로 Map 타입을 이용해 반환한다
=> 추가된 dtoToEntity()는 Map 타입으로 Movie 객체와 MovieImage 객체의 리스트를 처리한다
package com.example.mreview2022.service;
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.MovieImageDTO;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.entity.MovieImage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public interface MovieService {
Long register(MovieDTO movieDTO);
default Map<String, Object> dtoToEntity(MovieDTO movieDTO) { //Map 타입으로 변환
Map<String,Object> entityMap = new HashMap<>();
Movie movie = Movie.builder()
.mno(movieDTO.getMno())
.title(movieDTO.getTitle())
.build();
entityMap.put("movie",movie);
List<MovieImageDTO> imageDTOList = movieDTO.getImageDTOList();
//MovieImageDTO 처리
if(imageDTOList != null && imageDTOList.size() > 0){
List<MovieImage> movieImageList = imageDTOList.stream().map(movieImageDTO -> {
MovieImage movieImage = MovieImage.builder()
.path(movieImageDTO.getPath())
.imgName(movieImageDTO.getImgName())
.uuid(movieImageDTO.getUuid())
.movie(movie)
.build();
return movieImage;
}).collect(Collectors.toList());
entityMap.put("imgList",movieImageList);
}
return entityMap;
}
}
2-4.ServiceImpl 클래스 작성
dtoToEntity()에서 반환한 객체들을 이용해 save()를 처리한다
package com.example.mreview2022.service;
import com.example.mreview2022.dto.MovieDTO;
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.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService {
private final MovieRepository movieRepository; // final
private final MovieImageRepository imageRepository; // final
@Transactional
@Override
public Long register(MovieDTO movieDTO) {
Map<String,Object> entityMap = dtoToEntity(movieDTO);
Movie movie = (Movie) entityMap.get("movie");
List<MovieImage> movieImageList = (List<MovieImage>) entityMap.get("imgList");
movieRepository.save(movie);
movieImageList.forEach(movieImage -> {
imageRepository.save(movieImage);
});
return movie.getMno();
}
}
2-5.Controller 작성
MovieController 에서는 POST 방식으로 전달된 파라미터들을 MovieDTO로 수집해 MovieService 타입 객체의 register()를 호출하도록 작성한다
* 목록페이지는 추후에 작성한다
package com.example.mreview2022.controller;
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.service.MovieService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
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 {
private final MovieService movieService; //final
@GetMapping("/register")
public void register(){
}
@PostMapping("/register")
public String register(MovieDTO movieDTO, RedirectAttributes redirectAttributes){
Long mno = movieService.register(movieDTO);
redirectAttributes.addFlashAttribute("msg",mno);
return "redirect:/movie/list";
}
}
2-6.Html 작성
화면에서 Sumbit 버튼을 클릭했을 때 작업처리 순서
1) 각 이미지 li 태그의 data- 속성들을 읽는다
2) 읽어 들인 속성값을 이용해 form 태그 내에 input type ='hidden' 태그를 생성한다
3) input type ='hidden'의 이름에는 imageDTOList[0]과 같이 인덱스 번호를 붙혀 처리한다
<!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 Register Page</h1>
<form th:action="@{/movie/register}" th:method="post" >
<div class="form-group">
<label >Title</label>
<input type="text" class="form-control" name="title" placeholder="Enter Title">
</div>
<div class="form-group fileForm">
<label >Image Files</label>
<div class="custom-file">
<input type="file" class="custom-file-input files" id="fileInput" multiple>
<label class="custom-file-label" data-browse="Browse"></label>
</div>
</div>
<div class="box">
</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>
</ul>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<script>
$(document).ready(function(e) {
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz|tiff)$");
var maxSize = 10485760; //10MB
function checkExtension(fileName, fileSize){
if(fileSize >= maxSize){
alert("파일 사이즈 초과");
return false;
}
if(regex.test(fileName)){
alert("해당 종류의 파일은 업로드할 수 없습니다.");
return false;
}
return true;
}
$(".custom-file-input").on("change", function() {
var fileName = $(this).val().split("\\").pop();
$(this).siblings(".custom-file-label").addClass("selected").html(fileName);
var formData = new FormData();
var inputFile = $(this);
var files = inputFile[0].files;
var appended = false;
for (var i = 0; i < files.length; i++) {
if(!checkExtension(files[i].name, files[i].size) ){
return false;
}
console.log(files[i]);
formData.append("uploadFiles", files[i]);
appended = true;
}
//upload를 하지 않는다.
if (!appended) {return;}
for (var value of formData.values()) {
console.log(value);
}
//실제 업로드 부분
//upload ajax
$.ajax({
url: '/uploadAjax',
processData: false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result){
console.log(result);
showResult(result);
},
error: function(jqXHR, textStatus, errorThrown){
console.log(textStatus);
}
}); //$.ajax
}); //end change event
function showResult(uploadResultArr){
var uploadUL = $(".uploadResult ul");
var str ="";
$(uploadResultArr).each(function(i, obj) {
str += "<li data-name='" + obj.fileName + "' data-path='"+obj.folderPath+"' data-uuid='"+obj.uuid+"'>";
str + " <div>";
str += "<button type='button' data-file=\'" + obj.imageURL + "\' "
str += "class='btn-warning btn-sm'>X</button><br>";
str += "<img src='/display?fileName=" + obj.thumbnailURL + "'>";
str += "</div>";
str + "</li>";
});
uploadUL.append(str);
}
$(".uploadResult ").on("click", "li button", function(e){
console.log("delete file");
var targetFile = $(this).data("file");
var targetLi = $(this).closest("li");
$.ajax({
url: '/removeFile',
data: {fileName: targetFile},
dataType:'text',
type: 'POST',
success: function(result){
alert(result);
targetLi.remove();
}
}); //$.ajax
});
//prevent submit
$(".btn-primary").on("click", function(e) {
e.preventDefault();
var str = "";
$(".uploadResult li").each(function(i,obj){
var target = $(obj);
str += "<input type='hidden' name='imageDTOList["+i+"].imgName' value='"+target.data('name') +"'>";
str += "<input type='hidden' name='imageDTOList["+i+"].path' value='"+target.data('path')+"'>";
str += "<input type='hidden' name='imageDTOList["+i+"].uuid' value='"+target.data('uuid')+"'>";
});
//태그들이 추가된 것을 확인한 후에 comment를 제거
$(".box").html(str);
$("form").submit();
});
}); //document ready
</script>
</th:block>
</th:block>
- 실행화면
현재 목록 리스트 화면이 구현되어있지않아 Submit 버튼 클릭시 에러화면이 뜨지만 DB를 조회해보면 저장된 것을 알 수 있다
- 전체과정
1) 파일업로드가 되면 li 태그가 구성된다
2) Submit 버튼 클릭시 form 태그 내의 태그들 생성
3) MovieController에서 POST 방식으로 전달된 데이터는 MovieImageDTO 로 수집된다
4) MovieService에서 MovieImageDTO들은 Movie 엔티티 객체 내에 MovieImage로 처리된다
5) JPA에 의해 save()처리 후 DB에 저장된다
- 영화(Movie)의 등록과 수정에는 파일 업로드 기능을 활용해 영화 포스터 등을 등록할 수 있도록 구성
- 회원(Member)은 기존 회원들이 존재한다고 가정하고 DB에 존재하는 회원들 이용
- 회원(Member)은 특정한 영화 조회 페이지에서 평점과 자신의 감상을 리뷰(Review)로 기록할 수 있다
- 조회 화면에서 회원(Member)은 자신이 기록한 리뷰(Review)의 내용을 수정/삭제할 수 있다
2-1.DTO 클래스 생성
MovieDTO 는 Movie 클래스를 기준으로작성한다, MovieDTO는 화면에 영화 이미지들도 같이 수집해 전달해야 하므로 내부적으로 리스트를 이용해 수집한다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
private Long mno;
private String title;
@Builder.Default
private List<MovieImageDTO> imageDTOList = new ArrayList<>();
}
2-2.MovieImageDTO 클래스 작성
MovieDTO 클래스 내부에는 업로드된 파일들의 정보를 포함해야 하므로 MovieImageDTO 클래스도 같이 추가한다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MovieImageDTO {
private String uuid;
private String imgName;
private String path;
public String getImageURL(){
try {
return URLEncoder.encode(path + "/" + uuid + "_" + imgName,"UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
public String getThumbnailURL(){
try {
return URLEncoder.encode(path+"/s_" + uuid + "_" +imgName ,"UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
}
2-3.Service interface 작성
Movie를 JPA로 처리하기 위해 MovieDTO를 Movie 객체로 변환해 주어야 하므로 MovieService에 dtoToEntity()를 추가한다
- 주의점
-> Movie 객체뿐 아니라 MovieImage 객체들도 같이 처리된다는 점! 한번에 두가지 종류의 객체를 반환해야 하므로 Map 타입을 이용해 반환한다
=> 추가된 dtoToEntity()는 Map 타입으로 Movie 객체와 MovieImage 객체의 리스트를 처리한다
package com.example.mreview2022.service;
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.MovieImageDTO;
import com.example.mreview2022.entity.Movie;
import com.example.mreview2022.entity.MovieImage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public interface MovieService {
Long register(MovieDTO movieDTO);
default Map<String, Object> dtoToEntity(MovieDTO movieDTO) { //Map 타입으로 변환
Map<String,Object> entityMap = new HashMap<>();
Movie movie = Movie.builder()
.mno(movieDTO.getMno())
.title(movieDTO.getTitle())
.build();
entityMap.put("movie",movie);
List<MovieImageDTO> imageDTOList = movieDTO.getImageDTOList();
//MovieImageDTO 처리
if(imageDTOList != null && imageDTOList.size() > 0){
List<MovieImage> movieImageList = imageDTOList.stream().map(movieImageDTO -> {
MovieImage movieImage = MovieImage.builder()
.path(movieImageDTO.getPath())
.imgName(movieImageDTO.getImgName())
.uuid(movieImageDTO.getUuid())
.movie(movie)
.build();
return movieImage;
}).collect(Collectors.toList());
entityMap.put("imgList",movieImageList);
}
return entityMap;
}
}
2-4.ServiceImpl 클래스 작성
dtoToEntity()에서 반환한 객체들을 이용해 save()를 처리한다
package com.example.mreview2022.service;
import com.example.mreview2022.dto.MovieDTO;
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.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService {
private final MovieRepository movieRepository; // final
private final MovieImageRepository imageRepository; // final
@Transactional
@Override
public Long register(MovieDTO movieDTO) {
Map<String,Object> entityMap = dtoToEntity(movieDTO);
Movie movie = (Movie) entityMap.get("movie");
List<MovieImage> movieImageList = (List<MovieImage>) entityMap.get("imgList");
movieRepository.save(movie);
movieImageList.forEach(movieImage -> {
imageRepository.save(movieImage);
});
return movie.getMno();
}
}
2-5.Controller 작성
MovieController 에서는 POST 방식으로 전달된 파라미터들을 MovieDTO로 수집해 MovieService 타입 객체의 register()를 호출하도록 작성한다
* 목록페이지는 추후에 작성한다
package com.example.mreview2022.controller;
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.service.MovieService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
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 {
private final MovieService movieService; //final
@GetMapping("/register")
public void register(){
}
@PostMapping("/register")
public String register(MovieDTO movieDTO, RedirectAttributes redirectAttributes){
Long mno = movieService.register(movieDTO);
redirectAttributes.addFlashAttribute("msg",mno);
return "redirect:/movie/list";
}
}
2-6.Html 작성
화면에서 Sumbit 버튼을 클릭했을 때 작업처리 순서
1) 각 이미지 li 태그의 data- 속성들을 읽는다
2) 읽어 들인 속성값을 이용해 form 태그 내에 input type ='hidden' 태그를 생성한다
3) input type ='hidden'의 이름에는 imageDTOList[0]과 같이 인덱스 번호를 붙혀 처리한다
<!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 Register Page</h1>
<form th:action="@{/movie/register}" th:method="post" >
<div class="form-group">
<label >Title</label>
<input type="text" class="form-control" name="title" placeholder="Enter Title">
</div>
<div class="form-group fileForm">
<label >Image Files</label>
<div class="custom-file">
<input type="file" class="custom-file-input files" id="fileInput" multiple>
<label class="custom-file-label" data-browse="Browse"></label>
</div>
</div>
<div class="box">
</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>
</ul>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<script>
$(document).ready(function(e) {
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz|tiff)$");
var maxSize = 10485760; //10MB
function checkExtension(fileName, fileSize){
if(fileSize >= maxSize){
alert("파일 사이즈 초과");
return false;
}
if(regex.test(fileName)){
alert("해당 종류의 파일은 업로드할 수 없습니다.");
return false;
}
return true;
}
$(".custom-file-input").on("change", function() {
var fileName = $(this).val().split("\\").pop();
$(this).siblings(".custom-file-label").addClass("selected").html(fileName);
var formData = new FormData();
var inputFile = $(this);
var files = inputFile[0].files;
var appended = false;
for (var i = 0; i < files.length; i++) {
if(!checkExtension(files[i].name, files[i].size) ){
return false;
}
console.log(files[i]);
formData.append("uploadFiles", files[i]);
appended = true;
}
//upload를 하지 않는다.
if (!appended) {return;}
for (var value of formData.values()) {
console.log(value);
}
//실제 업로드 부분
//upload ajax
$.ajax({
url: '/uploadAjax',
processData: false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result){
console.log(result);
showResult(result);
},
error: function(jqXHR, textStatus, errorThrown){
console.log(textStatus);
}
}); //$.ajax
}); //end change event
function showResult(uploadResultArr){
var uploadUL = $(".uploadResult ul");
var str ="";
$(uploadResultArr).each(function(i, obj) {
str += "<li data-name='" + obj.fileName + "' data-path='"+obj.folderPath+"' data-uuid='"+obj.uuid+"'>";
str + " <div>";
str += "<button type='button' data-file=\'" + obj.imageURL + "\' "
str += "class='btn-warning btn-sm'>X</button><br>";
str += "<img src='/display?fileName=" + obj.thumbnailURL + "'>";
str += "</div>";
str + "</li>";
});
uploadUL.append(str);
}
$(".uploadResult ").on("click", "li button", function(e){
console.log("delete file");
var targetFile = $(this).data("file");
var targetLi = $(this).closest("li");
$.ajax({
url: '/removeFile',
data: {fileName: targetFile},
dataType:'text',
type: 'POST',
success: function(result){
alert(result);
targetLi.remove();
}
}); //$.ajax
});
//prevent submit
$(".btn-primary").on("click", function(e) {
e.preventDefault();
var str = "";
$(".uploadResult li").each(function(i,obj){
var target = $(obj);
str += "<input type='hidden' name='imageDTOList["+i+"].imgName' value='"+target.data('name') +"'>";
str += "<input type='hidden' name='imageDTOList["+i+"].path' value='"+target.data('path')+"'>";
str += "<input type='hidden' name='imageDTOList["+i+"].uuid' value='"+target.data('uuid')+"'>";
});
//태그들이 추가된 것을 확인한 후에 comment를 제거
$(".box").html(str);
$("form").submit();
});
}); //document ready
</script>
</th:block>
</th:block>
- 실행화면
현재 목록 리스트 화면이 구현되어있지않아 Submit 버튼 클릭시 에러화면이 뜨지만 DB를 조회해보면 저장된 것을 알 수 있다
- 전체과정
1) 파일업로드가 되면 li 태그가 구성된다
2) Submit 버튼 클릭시 form 태그 내의 태그들 생성
3) MovieController에서 POST 방식으로 전달된 데이터는 MovieImageDTO 로 수집된다
4) MovieService에서 MovieImageDTO들은 Movie 엔티티 객체 내에 MovieImage로 처리된다
5) JPA에 의해 save()처리 후 DB에 저장된다
3.목록 처리와 평균 평점
3-1.페이지 처리
N:1 프로젝트에서 사용했던 PageRequestDTO 와 PageResultDTO 를 추가해준다
3-2.MovieDTO 수정
MovieService 의 getList() 는 Movie,MovieImage,Double,Long을 Object[] 배열을 리스트에 담은 형태이다
각 Object[]을 MovieDTO라는 하나의 객체로 처리해야 한다 MovieDTO에는 Double 타입의 평점 평균과 리뷰이 개수를 처리하는 파라미터, 날짜 관련된 부분도 추가해준다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
private Long mno;
private String title;
@Builder.Default
private List<MovieImageDTO> imageDTOList = new ArrayList<>();
//영화의 평균 평점
private double avg;
//리뷰 수 jpa의 count()
private int reviewCnt;
private LocalDateTime regDate;
private LocalDateTime modDate;
}
3-3.Service 수정
JPA를 통해 나오는 엔티티 객체들과 Double,Long 등의 값을 MovieDTO로 변환하는 entitiesToDto()를 추가하고 컨트롤러가 호출할 때 사용할 getList()를 추가한다
- entitiesToDto() 가 받는 파라미터들
-> Movie 엔티티, List<MovieImage">엔티티 , Double 타입의 평점 평균,Long 타입의 리뷰 개수
- List<MovieImage">엔티티 리스트로 받은 이유 : 조회화면에서 여러 개의 MovieImage를 처리하기 위해
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.MovieImageDTO;
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 java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public interface MovieService {
Long register(MovieDTO movieDTO);
PageResultDTO<MovieDTO,Object[]> getList(PageRequestDTO requestDTO); // 목록처리
default MovieDTO entitiesToDTO(Movie movie,List<MovieImage> movieImages,Double avg,Long reviewCnt){
MovieDTO movieDTO = MovieDTO.builder()
.mno(movie.getMno())
.title(movie.getTitle())
.regDate(movie.getRegDate())
.modDate(movie.getModDate())
.build();
List<MovieImageDTO> movieImageDTOList = movieImages.stream().map(movieImage -> {
return MovieImageDTO.builder().imgName(movieImage.getImgName())
.path(movieImage.getPath())
.uuid(movieImage.getUuid())
.build();
}).collect(Collectors.toList());
movieDTO.setImageDTOList(movieImageDTOList);
movieDTO.setAvg(avg);
movieDTO.setReviewCnt(reviewCnt.intValue());
return movieDTO;
}
}
3-4.ServiceImpl 수정
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.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 PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO requestDTO) {
Pageable pageable = requestDTO.getPageable(Sort.by("mno").descending());
Page<Object[]> result = movieRepository.getListPage(pageable);
Function<Object[], MovieDTO> fn = (arr -> entitiesToDTO(
(Movie) arr[0],
(List<MovieImage>) (Arrays.asList((MovieImage) arr[1])),
(Double) arr[2],
(Long) arr[3])
);
return new PageResultDTO<>(result,fn);
}
}
3-6.Controller 수정
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.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("/list")
public void list(PageRequestDTO pageRequestDTO, @NotNull Model model){
model.addAttribute("result",movieService.getList(pageRequestDTO));
}
}
3-6.html 수정
목록에 출력하는 데이터는 Model에 담겨있으므로 Model result를 이용해 출력한다
<!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 List Page
<span>
<a th:href="@{/movie/register}">
<button type="button" class="btn btn-outline-primary">REGISTER
</button>
</a>
</span>
</h1>
<form action="/movie/list" method="get" id="searchForm">
<input type="hidden" name="page" value="1">
</form>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Picture</th>
<th scope="col">Review Count</th>
<th scope="col">AVG Rating</th>
<th scope="col">Regdate</th>
</tr>
</thead>
<tbody>
<tr th:each="dto : ${result.dtoList}" >
<th scope="row">
<a th:href="@{/movie/read(mno = ${dto.mno}, page= ${result.page})}">
[[${dto.mno}]]
</a>
</th>
<td><img th:if="${dto.imageDTOList.size() > 0 && dto.imageDTOList[0].path != null }"
th:src="|/display?fileName=${dto.imageDTOList[0].getThumbnailURL()}|" >[[${dto.title}]]</td>
<td><b>[[${dto.reviewCnt}]]</b></td>
<td><b>[[${dto.avg}]]</b></td>
<td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>
<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item " th:if="${result.prev}">
<a class="page-link" th:href="@{/movie/list(page= ${result.start -1})}" tabindex="-1">Previous</a>
</li>
<li th:class=" 'page-item ' + ${result.page == page?'active':''} " th:each="page: ${result.pageList}">
<a class="page-link" th:href="@{/movie/list(page = ${page})}">
[[${page}]]
</a>
</li>
<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/movie/list(page= ${result.end + 1} )}">Next</a>
</li>
</ul>
<script th:inline="javascript">
</script>
</th:block>
</th:block>
- 실행결과
Author And Source
이 문제에 관하여(SpringBoot with JPA 프로젝트(M:N) 5.영화등록,등록된글 리스트처리), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@alswl689/SpringBoot-with-JPA-프로젝트MN-5
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
N:1 프로젝트에서 사용했던 PageRequestDTO 와 PageResultDTO 를 추가해준다
MovieService 의 getList() 는 Movie,MovieImage,Double,Long을 Object[] 배열을 리스트에 담은 형태이다
각 Object[]을 MovieDTO라는 하나의 객체로 처리해야 한다 MovieDTO에는 Double 타입의 평점 평균과 리뷰이 개수를 처리하는 파라미터, 날짜 관련된 부분도 추가해준다
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
private Long mno;
private String title;
@Builder.Default
private List<MovieImageDTO> imageDTOList = new ArrayList<>();
//영화의 평균 평점
private double avg;
//리뷰 수 jpa의 count()
private int reviewCnt;
private LocalDateTime regDate;
private LocalDateTime modDate;
}
JPA를 통해 나오는 엔티티 객체들과 Double,Long 등의 값을 MovieDTO로 변환하는 entitiesToDto()를 추가하고 컨트롤러가 호출할 때 사용할 getList()를 추가한다
- entitiesToDto() 가 받는 파라미터들
-> Movie 엔티티, List<MovieImage">엔티티 , Double 타입의 평점 평균,Long 타입의 리뷰 개수 - List<MovieImage">엔티티 리스트로 받은 이유 : 조회화면에서 여러 개의 MovieImage를 처리하기 위해
import com.example.mreview2022.dto.MovieDTO;
import com.example.mreview2022.dto.MovieImageDTO;
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 java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public interface MovieService {
Long register(MovieDTO movieDTO);
PageResultDTO<MovieDTO,Object[]> getList(PageRequestDTO requestDTO); // 목록처리
default MovieDTO entitiesToDTO(Movie movie,List<MovieImage> movieImages,Double avg,Long reviewCnt){
MovieDTO movieDTO = MovieDTO.builder()
.mno(movie.getMno())
.title(movie.getTitle())
.regDate(movie.getRegDate())
.modDate(movie.getModDate())
.build();
List<MovieImageDTO> movieImageDTOList = movieImages.stream().map(movieImage -> {
return MovieImageDTO.builder().imgName(movieImage.getImgName())
.path(movieImage.getPath())
.uuid(movieImage.getUuid())
.build();
}).collect(Collectors.toList());
movieDTO.setImageDTOList(movieImageDTOList);
movieDTO.setAvg(avg);
movieDTO.setReviewCnt(reviewCnt.intValue());
return movieDTO;
}
}
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.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 PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO requestDTO) {
Pageable pageable = requestDTO.getPageable(Sort.by("mno").descending());
Page<Object[]> result = movieRepository.getListPage(pageable);
Function<Object[], MovieDTO> fn = (arr -> entitiesToDTO(
(Movie) arr[0],
(List<MovieImage>) (Arrays.asList((MovieImage) arr[1])),
(Double) arr[2],
(Long) arr[3])
);
return new PageResultDTO<>(result,fn);
}
}
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.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("/list")
public void list(PageRequestDTO pageRequestDTO, @NotNull Model model){
model.addAttribute("result",movieService.getList(pageRequestDTO));
}
}
목록에 출력하는 데이터는 Model에 담겨있으므로 Model result를 이용해 출력한다
<!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 List Page
<span>
<a th:href="@{/movie/register}">
<button type="button" class="btn btn-outline-primary">REGISTER
</button>
</a>
</span>
</h1>
<form action="/movie/list" method="get" id="searchForm">
<input type="hidden" name="page" value="1">
</form>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Picture</th>
<th scope="col">Review Count</th>
<th scope="col">AVG Rating</th>
<th scope="col">Regdate</th>
</tr>
</thead>
<tbody>
<tr th:each="dto : ${result.dtoList}" >
<th scope="row">
<a th:href="@{/movie/read(mno = ${dto.mno}, page= ${result.page})}">
[[${dto.mno}]]
</a>
</th>
<td><img th:if="${dto.imageDTOList.size() > 0 && dto.imageDTOList[0].path != null }"
th:src="|/display?fileName=${dto.imageDTOList[0].getThumbnailURL()}|" >[[${dto.title}]]</td>
<td><b>[[${dto.reviewCnt}]]</b></td>
<td><b>[[${dto.avg}]]</b></td>
<td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>
<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item " th:if="${result.prev}">
<a class="page-link" th:href="@{/movie/list(page= ${result.start -1})}" tabindex="-1">Previous</a>
</li>
<li th:class=" 'page-item ' + ${result.page == page?'active':''} " th:each="page: ${result.pageList}">
<a class="page-link" th:href="@{/movie/list(page = ${page})}">
[[${page}]]
</a>
</li>
<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/movie/list(page= ${result.end + 1} )}">Next</a>
</li>
</ul>
<script th:inline="javascript">
</script>
</th:block>
</th:block>
Author And Source
이 문제에 관하여(SpringBoot with JPA 프로젝트(M:N) 5.영화등록,등록된글 리스트처리), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@alswl689/SpringBoot-with-JPA-프로젝트MN-5저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)