ProductDto @Data
@AllArgsConstructor
@Builder
public static class Read {
private Integer pno;
private String vendor;
private String name;
private String info;
private String imagename;
private Integer price;
private Integer salesVolume;
private Double star;
private Integer countOfReview;
private String categoryCode;
private List<Review> reviews;
}Product public ProductDto.Read toRead(String path) {
Double star = countOfStar == 0 ? -1.0 : sumOfStar/countOfStar;
return ProductDto.Read.builder().pno(pno).name(name).info(info).imagename(path + imagename).price(price)
.salesVolume(salesVolume).countOfReview(countOfReview).categoryCode(categoryCode).star(star).build();
}ProductService public ProductDto.Read read(Integer pno) {
ProductDto.Read dto = productDao.findById(pno).toRead(imagePath);
dto.setReviews(reviewDao.findByPno(pno));
return dto;
}ProductController @GetMapping("/product/read")
public ModelAndView read(Integer pno) {
return new ModelAndView("product/read").addObject("product", service.read(pno));
}📝 리뷰 작성과 삭제는 로그인을 한 상태에서만 가능하기 때문에, 비로그인 상태에서는 관련 항목들이 아예 출력되지 않는다.
지난 포스팅에 이어 DTO를 보충하고, REST 컨트롤러 설정, HTML과 자바스크립트를 완성한다.
src/main/java - com.example.demo.dto - ReviewDto
@Data
@AllArgsConstructor
public static class Write {
private Integer pno;
private String content;
private Integer star;
public Review toEntity() {
return Review.builder().pno(pno).content(content).star(star).build();
}
}
src/main/java - com.example.demo.controller.rest - ReviewRestController
package com.example.demo.controller.rest;
import java.security.Principal;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import com.example.demo.dto.*;
import com.example.demo.service.*;
import lombok.*;
@RestController
@AllArgsConstructor
@Secured("ROLE_USER")
public class ReviewRestController {
private ReviewService service;
@PostMapping("/review/write")
public ResponseEntity<List<ReviewDto.RestRead>> write(ReviewDto.Write dto, BindingResult bindingresult, Principal principal) {
return ResponseEntity.ok(service.write(dto, principal.getName()));
}
@DeleteMapping("/review/delete")
public ResponseEntity<List<ReviewDto.RestRead>> delete(Integer rno, Integer pno, Principal principal) {
return ResponseEntity.ok(service.deleteByRno(rno, pno, principal.getName()));
}
}
src/main/resources - templates - product - read.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<title>Insert title here</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/main.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="/ckeditor/ckeditor.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="/css/read.css">
<th:block sec:authorize="isAnonymous()">
<script>
const isLogin = false;
</script>
</th:block>
<th:block sec:authorize="isAuthenticated()">
<script th:inline="javascript">
const isLogin = true;
const loginId = /*[[${session.SPRING_SECURITY_CONTEXT.authentication.principal.username}]]*/
</script>
</th:block>
<script th:inline="javascript">
// 제품 가격과 주문 수량, 리뷰는 변수에 저장한다.
const price = /*[[${product.price}]]*/
const reviews = /*[[${product.reviews}]]*/
let count = 1;
let star = 0;
function printReviews(reviews) {
$('#reviews').empty();
const $table = $('<table style="width:100%" th:each="{review: ${reviews}">').appendTo($('#reviews'));
$.each(reviews, function(idx, review) {
const $tr = $('<tr>').appendTo($table);
$('<td class="one">').text(review.rno).appendTo($tr);
const $td2 = $('<td class="two">').appendTo($tr);
for (let i = 1; i <= review.star; i++) {
if (i <= review.star)
$('<i class="fa fa-star">').appendTo($td2);
else
$('<i class="fa fa-star-o">').appendTo($td2);
}
$('<td class="three">').text(review.content).appendTo($tr);
$('<td class="four">').text(review.writer).appendTo($tr);
$('<td class="five">').text(review.writeday).appendTo($tr);
const $td6 = $('<td class="six">').appendTo($tr);
if (isLogin == true && loginId == review.writer) {
$('<button class="btn btn-info">삭제</button>').attr("data-rno", review.rno)
.attr("data-pno", review.pno).appendTo($td6);
}
})
}
$(document).ready(function() {
if (isLogin == false)
$('#review_div').remove();
$('#total_count').text(count);
$('#total_price').text(count * price);
printReviews(reviews);
// 리뷰 삭제
$('.delete_review').click(function() {
const params = {
rno : $(this).attr('data-rno'),
pno : $(this).attr('data-pno')
}
$.ajax({
url : "/review/delete",
method : "delete",
data : params
}).done(result => printReviews(result)).fail(response => console.log(response));
})
// 리뷰 작성
$('#review_write').click(function() {
if (star < 0) {
alert("리뷰를 남기시려면 별점을 선택해 주세요.");
return false;
}
const params = {
pno : $('#pno').text(),
content : $('#review_textarea').val(),
star : star
}
$.ajax({
url : "/review/write",
method : "post",
data : params
}).done(result => printReviews(result)).fail(response => console.log(response)).always(() => {
$('#review_textarea').val("");
$(".star").each(function(idx, s) {
$(s).removeClass('fa-star');
$(s).addClass('fa-star-o');
})
})
})
// 별점 주기
$('.star').click(function() {
star = $(this).attr("data-rating");
$(".star").each(function(idx, s) {
if (star >= $(s).attr("data-rating")) {
$(s).removeClass("fa-star-o");
$(s).addClass("fa-star")
} else {
$(s).removeClass("fa-star");
$(s).addClass("fa-star-o");
}
})
})
})
</script>
</head>
<body>
<div id="page">
<header th:replace="/fragments/header.html">
</header>
<nav th:replace="/fragments/nav.html">
</nav>
<div id="main">
<aside th:replace="/fragments/aside.html">
</aside>
<section>
<!-- <input type="hidden" id="_csrf" name="_csrf"> -->
<div id="upper">
<div id="left">
<img id="image" style="width:100%" th:src="${product.imagename}">
</div>
<div id="right">
<div id="pno" th:text="${product.pno}" style="display:none"></div>
<div id="vendor" th:text="${product.vendor}"></div>
<div id="name" th:text="${product.name}"></div>
<div id="price"><span th:text="${product.price}"></span> 원</div>
<hr>
<div id="product_div">
<div class='count' id='minus'>-</div>
<div class='count number' id="count_of_product">1</div>
<div class='count' id='plus'>+</div>
</div>
<hr>
<div id="price_div" style="overflow:hidden;">
<span style="font-weight:bold; font-size:1.25em;">총 금액</span>
<div style="float:right;">
<span style="color:#999">총 수량<span id="total_count"></span> 개</span>
<span><span id="total_price"></span> 원</span>
</div>
</div>
<div>
<button id="order">구매하기</button>
<button id="cart">장바구니</button>
</div>
</div>
</div>
<div id="lower">
<div id="content" th:utext="${product.info}" style="overflow:auto; height:600px; border: 1px solid #ccc; border-radius:5px;"></div>
</div>
<hr>
<div id="review_div">
<div>
<label>별점 주기</label>
<i class="fa fa-star-o star" data-rating="1" title='1점'></i>
<i class="fa fa-star-o star" data-rating="2" title='2점'></i>
<i class="fa fa-star-o star" data-rating="3" title='3점'></i>
<i class="fa fa-star-o star" data-rating="4" title='4점'></i>
<i class="fa fa-star-o star" data-rating="5" title='5점'></i>
</div>
<div class="form-group">
<label for="review_teaxarea">리뷰를 입력하세요.</label>
<textarea class="form-control" rows="5" id="review_textarea" placeholder="욕설이나 모욕적인 댓글은 삭제될 수 있습니다."></textarea>
</div>
<button type="button" class="btn btn-info" id="review_write">
<span class="glyphicon glyphicon-ok"></span>작성
</button>
</div>
<hr>
<div id="reviews">
</div>
</section>
</div>
<footer th:replace="/fragments/footer.html">
</footer>
</div>
</body>
</html>
따로 빼 놓은 CSS는 아래와 같다.
@charset "UTF-8";
#upper {
height: 400px;
}
#left {
float: left;
width: 48%;
}
#right {
float: right;
width: 48%;
}
#section {
overflow: hidden; /* float를 clear하는 방법 */
}
.count {
width: 35px;
height: 35px;
line-height: 35px;
display: inline-block;
text-align: center;
font-size: 1.25em;
}
.plus, .minus, #plus, #minus {
background-color: #ddd; /* color를 16진수로 지정. 16진수로 지정할 때 #555555 -> #555로 줄여쓸 수 있다 */
cursor: pointer;
}
#vendor, #name {
font-size: 1.5em;
font-weight: bold;
color: #222;
}
#price {
margin-top: 15px;
font-size: 24px;
color: rgb(107,144,220); /* color를 10진수로 지정. rgb, rgba 두 가지.*/
font-weight: 700;
text-align: right;
}
#total_price {
font-size: 1.75em; /* em은 퍼센트(175%). %는 문서 원래 크기 기준. em은 부모 기준.*/
font-weight: bold;
color:rgb(233,96,97);
}
#order, #cart {
width: 125px;
height: 50px;
line-height: 50px;
text-align: center;
color: white;
font-weight: bold;
/* 버튼을 커스터마이즈할 때 외곽선 */
border: 0;
outline: 0;
}
#order {
background-color: #3fc910;
}
#cart {
background-color: #565656;
}