<html layout:decorate="~{layout}">
<div layout:fragment="content">
<style>
.aside_items {
display:none;
}
</style>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom" style="width: 100%;">
<div class="title_book d-flex" style="padding-top:5px;">
제목:
<div th:text="${book.subject}"></div>
/ 출판사:
<div th:text="${book.publisher}"></div>
</div>
</div>
<div class="bt_section">
<div class="book_top-section">
<div class="book_left-side">
<div style="width:400px; height:600px;">
<img th:src="@{|/file/${book.thumbnailImg}|}" alt="사진" style="width:
400px; height: 600px;">
</div>
</div>
<div class="center-detail">
<div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%; padding-left:10px;">
<h2>
<div class="card-text" style="white-space: pre-line;" th:text="${book.subject}"></div>
</h2>
</div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%; padding-left:10px;">
<h6>정가</h6>
<del th:text="${book.price + ' 원'}"></del>
<br>
<h6>할인가</h6>
<div class="d-flex" style="color:red;">
<div style="margin-right:10px;" th:text="${book.discountPrice} + ' 원'"></div>
<div th:text=" ' ( ' + ${book.discount} +' % 할인' + ' ) ' "></div>
</div>
</div>
</div>
<div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%; padding-left:10px;">
<h5>쿠폰 받기</h5>
<div class="card my-3">
<div class="card-body">
쿠폰 내용
<div class="card-text" style="white-space: pre-line;">
홈페이지 오픈 기념 이벤트 중 입니다
</div>
</div>
<div>
<p>신간 책 이벤트 중입니다</p>
</div>
</div>
</div>
</div>
</div>
<div class="book_right-side bg-light">
<div class="book_right-nav">
<div style="margin-bottom:5px">
<a class="btn btn-sm btn-primary" style="width: 150px">바로구매</a>
</div>
<div style="margin-bottom:5px">
<a class="addCartItem btn btn-sm btn-primary" style="width: 150px"
th:id="'addCartItem_' + ${book.id}" onclick="addCartItem(${book.id}); return false;">장바구니
담기</a>
</div>
<div>
<a class="addWish btn btn-sm btn-primary" th:id="'addWish_' + ${book.id}"
onclick="addWish(${book.id}); return false;" style="width:150px;">찜하기</a>
</div>
</div>
</div>
</div>
<div class="book_middle-section">
<div id="header" class="book_navbar header-scrolled text-black bg-light">
<div class="book_nav-container">
<a href="#ebook_info" class="card-body btn btn-light" onclick="activateSection('ebook_info')">
도서정보
</a>
<a href="#review_info" class="card-body btn btn-light" onclick="activateSection('review_info')">
리뷰
</a>
<div class="card-body" style="font-size:20px; color: red;">
<div class="card-text" style="white-space: pre-line;" th:text="${book.discountPrice + ' 원'}"></div>
</div>
<div style="margin-bottom:5px" class="justify-content-end">
<a class="btn btn-sm btn-primary" style="width: 100px; margin-right:10px;">바로구매</a>
<a class="addCartItem btn btn-sm btn-primary" style="width: 120px"
th:id="'addCartItem_' + ${book.id}" onclick="addCartItem(${book.id}); return false;">장바구니
담기</a>
</div>
</div>
</div>
<div id="ebook_info" class="book_info">
<div class="title_book" style="padding-top:5px;">
<h5>판매자</h5>
<br>
<div th:text="${book.publisher}"></div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;"></div>
<h5>판매 소개</h5>
<br>
<div th:text="${book.content}">></div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;"></div>
</div>
<div class="title_book" style="padding-top:5px;">
<h5>책 제목</h5>
<br>
<div th:text="${book.subject}"></div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;"></div>
<h5>책소개</h5>
<br>
<div th:text="${book.bookIntroduce}"></div>
<div class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;"></div>
</div>
<div class="title_book" style="padding-top:5px;">
<h5>출판사</h5>
<br>
<div th:text="${book.publisher}"></div>
<br>
</div>
</div>
<div class="book_bottom-section">
<div id="review_info" class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;"></div>
<h2>회원 리뷰</h2>
<div id="review_info" class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;"></div>
<div class="justify-content-end" style="margin-bottom:5px">
<div class="review_form mb-3">
<label for="content" class="form-label"></label>
<form th:action="@{|/review/create/${book.id}|}" method="post">
<textarea name="content" id="content" class="form-control" rows="3"
placeholder="책에 대한 스포일러 및 심한 욕설은 제외됩니다"></textarea>
<div class="rating">
<input type="hidden" name="rating" id="rating-value" value="0">
<span class="star" data-value="1">★</span>
<span class="star" data-value="2">★</span>
<span class="star" data-value="3">★</span>
<span class="star" data-value="4">★</span>
<span class="star" data-value="5">★</span>
</div>
<button type="submit" class="btn btn-sm btn-primary" style="width: 100px; margin-top: 5px;">
리뷰등록
</button>
</form>
</div>
<div class="a_comment_top">
<ul th:each="review : ${reviewList}">
<li>
<div id="review_info"
class="pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom"
style="width: 100%;">
<div class="review-box mb-2">
<span>평점: <span th:utext="|★| + ${review.rating}" class="star"></span></span>
<div style="display:flex;" th:text="${review.author.username}"></div>
<div style="display:flex; justify-content:end; padding-right:10px;"
th:text="${#temporals.format(review.createDate, 'yyyy-MM-dd ')}"></div>
<div style="display:flex; justify-content:end; padding-right:10px;"
th:text="${#temporals.format(review.modifiedDate, 'yyyy-MM-dd ')}"></div>
<div class="review-content">
<span th:text="${review.content}"></span>
</div>
<button sec:authorize="isAuthenticated()" class="btn btn-sm btn-primary btn-edit" style="width: 100px; margin-right:10px;">수정</button>
<a th:href="@{|/review/delete/${review.id}|}" sec:authorize="isAuthenticated()" class="btn btn-sm btn-primary"
style="width: 100px; margin-right:10px; margin-top:20px;">삭제</a>
<div class="review-edit-form">
<h5 class="my-3 border-bottom pb-2">리뷰수정</h5>
<form th:action="@{|/review/modify/${review.id}|}" th:object="${reviewForm}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea name="content" id="content" class="form-control" rows="10" th:text="${review.content}"></textarea>
</div>
<div class="mb-3">
<label for="modify-rating" class="form-label">평점</label>
<div class="rating" id="modify-rating">
<input type="hidden" name="rating" id="modify-rating-value" value="0">
<span class="star" data-value="1">★</span>
<span class="star" data-value="2">★</span>
<span class="star" data-value="3">★</span>
<span class="star" data-value="4">★</span>
<span class="star" data-value="5">★</span>
</div>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</div>
</div>
</li>
</ul>
</div>
<script>
window.onload = function () {
var reviewForms = document.querySelectorAll('.review-edit-form');
reviewForms.forEach(function (form) {
form.style.display = 'none';
});
var editButtons = document.querySelectorAll('.btn-edit');
editButtons.forEach(function (button) {
button.addEventListener('click', function () {
var parent = button.closest('.a_comment_top');
var editForm = parent.querySelector('.review-edit-form');
if (editForm.style.display === 'none') {
editForm.style.display = 'block';
} else {
editForm.style.display = 'none';
}
});
});
};
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const reviewStars = document.querySelectorAll('.review_form .star');
const modifyStars = document.querySelectorAll('.a_comment_top .star');
reviewStars.forEach(star => {
star.addEventListener('mouseover', () => {
const value = parseInt(star.getAttribute('data-value'));
reviewStars.forEach((s, index) => {
if (index < value) {
s.classList.add('active');
} else {
s.classList.remove('active');
}
});
});
star.addEventListener('click', () => {
const value = parseInt(star.getAttribute('data-value'));
const ratingInput = document.getElementById('rating-value');
if (ratingInput) {
ratingInput.value = value;
}
reviewStars.forEach((s, index) => {
if (index < value) {
s.classList.add('selected');
} else {
s.classList.remove('selected');
}
});
});
});
modifyStars.forEach(star => {
star.addEventListener('mouseover', () => {
const value = parseInt(star.getAttribute('data-value'));
modifyStars.forEach((s, index) => {
if (index < value) {
s.classList.add('active');
} else {
s.classList.remove('active');
}
});
});
star.addEventListener('click', () => {
const value = parseInt(star.getAttribute('data-value'));
const ratingInput = document.getElementById('modify-rating-value');
if (ratingInput) {
ratingInput.value = value;
}
modifyStars.forEach((s, index) => {
if (index < value) {
s.classList.add('selected');
} else {
s.classList.remove('selected');
}
});
});
});
const ratingContainer = document.querySelector('.rating');
ratingContainer.addEventListener('mouseleave', () => {
const ratingInput = ratingContainer.querySelector('input[type="hidden"]');
const currentValue = parseInt(ratingInput.value);
modifyStars.forEach((star, index) => {
if (index < currentValue) {
star.classList.add('selected');
} else {
star.classList.remove('selected');
}
});
});
});
</script>
<style>
.rating {
display: inline-block;
}
.rating .star {
font-size: 20px;
cursor: pointer;
color: #aaa;
transition: color 0.2s;
}
.rating .star.active,
.rating .star.selected {
color: #fdd835;
}
.star {
color: #fdd835;
}
</style>
<script>
$(document).ready(function () {
$(".addWish").click(function () {
var bookId = this.id.split('_')[1];
Swal.fire({
title: '찜 목록에',
text: '해당 상품을 담으시겠습니까?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '담기',
cancelButtonText: '취소',
preConfirm: () => {
return new Promise((resolve) => {
$.ajax({
url: '/wish/add/' + bookId,
type: 'GET',
success: function (data) {
resolve(true);
},
error: function () {
resolve(false);
}
});
});
}
}).then((result) => {
if (result.isConfirmed) {
Swal.fire(
'찜 목록에 상품을 담았습니다.',
'',
'success'
).then(() => {
window.location.href = '/book/detail/' + bookId;
});
}
});
});
});
</script>
<script>
$(document).ready(function () {
$(".addCartItem").click(function () {
var bookId = this.id.split('_')[1];
Swal.fire({
title: '장바구니에',
text: '해당 상품을 담으시겠습니까?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '담기',
cancelButtonText: '취소',
preConfirm: () => {
return new Promise((resolve) => {
$.ajax({
url: '/cartitem/add/' + bookId,
type: 'GET',
success: function (data) {
resolve(true);
},
error: function () {
resolve(false);
}
});
});
}
}).then((result) => {
if (result.isConfirmed) {
Swal.fire(
'장바구니에 상품을 담았습니다.',
'',
'success'
).then(() => {
window.location.href = '/book/detail/' + bookId;
});
}
});
});
});
</script>
</div>
</div>
</html>
package eBook.EatBook.domain.review.controller;
import eBook.EatBook.domain.book.entity.Book;
import eBook.EatBook.domain.book.service.BookService;
import eBook.EatBook.domain.member.entity.Member;
import eBook.EatBook.domain.member.service.MemberService;
import eBook.EatBook.domain.review.DTO.ReviewForm;
import eBook.EatBook.domain.review.entity.Review;
import eBook.EatBook.domain.review.service.ReviewService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import static org.springframework.data.jpa.domain.AbstractPersistable_.id;
@Controller
@RequiredArgsConstructor
@RequestMapping("/review")
public class ReviewController {
private final ReviewService reviewService;
private final MemberService memberService;
private final BookService bookService;
@PreAuthorize("isAuthenticated()")
@PostMapping("/create/{id}")
public String reviewCreate(@PathVariable("id") Integer id, @Valid ReviewForm reviewForm,
BindingResult bindingResult, Principal principal, org.springframework.ui.Model model){
Book books = this.bookService.getBookById(id);
if (books == null) {
// 책이 존재하지 않는 경우에 대한 처리
return "redirect:/books/list"; // 예시로 책 목록 페이지로 리다이렉트
}
float rating = reviewForm.getRating();
if (bindingResult.hasErrors()) {
model.addAttribute("books",books);
return "book_detail";
}
Member member = this.memberService.findByUsername(principal.getName());
if (member == null) {
// 작성자를 찾을 수 없는 경우에 대한 처리
return "redirect:/book/list"; // 예시로 홈 페이지로 리다이렉트
}
this.reviewService.create(books,reviewForm.getContent(), member,rating);
return String.format("redirect:/book/detail/%d", id);
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/modify/{id}")
public String reviewModify(@PathVariable("id") Integer id,@Valid ReviewForm reviewForm,
BindingResult bindingResult, Principal principal){
Review review = this.reviewService.getReview(id);
if (review == null) {
// 책이 존재하지 않는 경우에 대한 처리
return "redirect:/book/list"; // 예시로 책 목록 페이지로 리다이렉트
}
float rating = reviewForm.getRating();
if (bindingResult.hasErrors()) {
return "book_detail";
}
Member author = review.getAuthor();
// 현재 로그인한 사용자가 해당 리뷰의 작성자인지 확인
if (!principal.getName().equals(author.getUsername())) {
// 작성자와 다른 사용자가 수정을 시도한 경우에 대한 처리
return "redirect:/books/list"; // 예시로 책 목록 페이지로 리다이렉트
}
review.setContent(reviewForm.getContent());
this.reviewService.modify(review, reviewForm.getContent(),rating);
return String.format("redirect:/book/detail/%s",review.getBook().getId());
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/delete/{id}")
public String reviewDelete(Principal principal, @PathVariable("id") Integer id) {
Review review = this.reviewService.getReview(id);
Member member = this.memberService.findByUsername(principal.getName());
if (member == null) {
// 작성자를 찾을 수 없는 경우에 대한 처리
return "redirect:/book/list"; // 예시로 홈 페이지로 리다이렉트
}
this.reviewService.delete(review);
return String.format("redirect:/book/detail/%s",review.getBook().getId());
}
}
@Getter
@Setter
public class ReviewForm {
@NotEmpty(message = "내용을 입력해주세요")
private String content;
private float rating;
private Integer bookId;
private String nickname;
}
package eBook.EatBook.domain.review.entity;
import eBook.EatBook.domain.book.entity.Book;
import eBook.EatBook.domain.member.entity.Member;
import eBook.EatBook.global.baseEntity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.annotation.CreatedDate;
import java.time.LocalDateTime;
import java.util.Set;
@Entity
@Getter
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class Review extends BaseEntity {
@ManyToOne
private Book book;
@ManyToOne
private Member author;
@Column
private String content;
@Column
private float rating; // 별점
@ManyToMany
private Set<Member> voter;
@CreatedDate
private LocalDateTime createDate;
public void setContent(String content) {
this.content = content;
}
public void setModifiedDate(LocalDateTime now) {
}
public void setRating(float rating) {
this.rating = rating;
}
}
@Repository
public interface ReviewRepository extends JpaRepository<Review, Integer> {
Optional<Review> findById(Integer id);
List<Review> findAllByBook(Book book);
}
@Service
@RequiredArgsConstructor
public class ReviewService {
private final ReviewRepository reviewRepository;
private final BookRepository bookRepository;
public Review getReview(Integer id) {
Optional<Review> review = this.reviewRepository.findById(id);
if (review.isEmpty()) {
return null;
}
return review.get();
}
public void create(Book books, String content, Member author,float rating) {
Review review = Review.builder()
.book(books)
.content(content)
.rating(rating)
.createDate(LocalDateTime.now())
.modifiedDate(LocalDateTime.now())
.author(author)
.build();
reviewRepository.save(review);
}
public void modify(Review review, String content,float rating){
// 기존의 리뷰 객체의 내용을 업데이트
review.setContent(content);
review.setRating(rating);
review.setModifiedDate(LocalDateTime.now()); // 수정일 업데이트
reviewRepository.save(review); // 업데이트된 리뷰 저장
}
public void delete(Review review) {
reviewRepository.delete(review);
}
public List<Review> findAllByBook(Book book) {
return this.reviewRepository.findAllByBook(book);
}