쇼핑몰 프로젝트 --페이지네이션, 유효성검사, 리뷰 정렬(화면구현)
itemList, itemCategoryList, categoryGet, itemReviewGet페이지네이션 추가
itemList페이지네이션 추가
itemRepository
// 아이템 전체 총 개수 조회 (페이징 하기 위해)
public int countAll(String keyword) {
String queryString = "select count(i) from Item i";
if (keyword != null && !keyword.isEmpty()) {
queryString += " where i.itemName like :keyword";
}
TypedQuery<Long> query = em.createQuery(queryString, Long.class);
if (keyword != null && !keyword.isEmpty()) {
query.setParameter("keyword", "%" + keyword + "%");
}
return query.getSingleResult().intValue();
}
itemService
//아이템 전체 페이징 개수 조회
public int countAll(String keyword) {
return itemRepository.countAll(keyword);
}
itemController
//아이템 전체 조회
@GetMapping("/ypjs/item/get")
public String getAllItem(
@RequestParam(value = "page",defaultValue = "0") int page,
@RequestParam(value = "size",defaultValue = "3") int size,
@RequestParam(value = "sortBy", defaultValue = "itemId") String sortBy,
@RequestParam(value = "keyword", required = false) String keyword,
Model model) {
Pageable pageable = PageRequest.of(page, size);
List<ItemListDto> items = itemService.findAllItem(keyword, pageable, sortBy);
//총 페이지 수 계산
int totalPages = Page.totalPages(itemService.countAll(keyword), size);
model.addAttribute("items", items);
model.addAttribute("sortBy", sortBy); // 정렬 옵션을 다시 모델에 추가
model.addAttribute("keyword", keyword); //검색조건 유지
model.addAttribute("page",page); //페이징
model.addAttribute("size",size); //페이징
model.addAttribute("totalPages", totalPages); //총 페이지 수
return "item/itemList";
}
오늘 서비스, 레파지토리는 페이징 부분만 추가, 나머지 서비스, 레파지토리는 전에 적어논 글에 있음
item/itemList 페이지네이션 부분만 추가 됨
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="frame/header :: head">
<title>Zay Shop - Product Listing Page</title>
</head>
<style>
.fixed-size-img {
width: 300px; /* 원하는 너비로 설정 */
height: 300px; /* 원하는 높이로 설정 */
object-fit: cover; /* 이미지를 크기에 맞게 조정 */
}
</style>
<body>
<!--header-->
<header th:replace="frame/header :: header"></header>
<script src="/js/item/itemRatingsGet.js"></script>
<!-- Start Content -->
<div class="container py-5">
<div class="row">
<div class="container-fluid mt-3">
<div class="row">
<div class="col-md-12">
<ul class="list-inline shop-top-menu pb-3 pt-1">
<li class="list-inline-item">
<a class="h3 text-dark text-decoration-none mr-3" th:href="@{/ypjs/item/post}">상품 등록</a>
</li>
<li class="list-inline-item">
<a class="h3 text-dark text-decoration-none mr-3" th:href="@{/ypjs/category/post}">카테고리 등록</a>
</li>
<li class="list-inline-item">
<a class="h3 text-dark text-decoration-none mr-3" th:href="@{/ypjs/category/get}">카테고리 보기</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-3">
<h1 class="h2 pb-4">Categories</h1>
<ul class="list-unstyled templatemo-accordion">
<li class="pb-3">
<a class="collapsed d-flex justify-content-between h3 text-decoration-none" href="#">
Women
<i class="fa fa-fw fa-chevron-circle-down mt-1"></i>
</a>
<ul class="collapse show list-unstyled pl-3">
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=3)}">Outer</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=4)}">Top</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=5)}">Bottom</a></li>
</ul>
</li>
<li class="pb-3">
<a class="collapsed d-flex justify-content-between h3 text-decoration-none" href="#">
Man
<i class="pull-right fa fa-fw fa-chevron-circle-down mt-1"></i>
</a>
<ul id="collapseTwo" class="collapse list-unstyled pl-3">
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=6)}">Outer</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=7)}">Top</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=8)}">Bottom</a></li>
</ul>
</li>
</ul>
</div>
<div class="col-lg-9">
<div class="row">
<div class="col-md-6">
<ul class="list-inline shop-top-menu pb-3 pt-1">
<li class="list-inline-item">
<a class="h3 text-dark text-decoration-none mr-3" th:href="@{/ypjs/item/get}">All Items</a>
</li>
</ul>
</div>
<div class="col-lg-6">
<!-- 검색 폼 및 정렬 폼 통합 -->
<form th:action="@{/ypjs/item/get}" method="get" class="input-group">
<input type="text" id="keyword" name="keyword" class="form-control"
placeholder="Search by item name" th:value="${keyword}">
<div class="input-group-append">
<button type="submit" class="btn btn-link text-dark">
<i class="fa fa-search"></i>
</button>
</div>
<div class="col-auto">
<select class="form-control" name="sortBy" id="sortBy" onchange="this.form.submit()">
<option value="itemId" th:selected="${sortBy == 'itemId'}">최신순</option>
<option value="itemRatings" th:selected="${sortBy == 'itemRatings'}">별점 높은 순</option>
<option value="likeCount" th:selected="${sortBy == 'likeCount'}">찜많은순</option>
</select>
</div>
</form>
</div>
<!-- 반별 추가-->
<!-- <div class="container-fluid mt-3">-->
<!-- <div class="row">-->
<!-- <!– items 리스트의 각 항목을 반복 –>-->
<!-- <div th:each="item : ${items}" class="col-md-4">-->
<!-- <div class="card mb-4 product-wap rounded-0">-->
<!-- <!– 제품 이미지 –>-->
<!-- <div class="card rounded-0">-->
<!-- <a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}">-->
<!-- <img class="card-img rounded-0 img-fluid fixed-size-img" th:src="${item.itemFilePath}" alt="product-item">-->
<!-- <!– 필요한 경우 오버레이나 추가 요소를 여기에 추가할 수 있습니다 –>-->
<!-- <div class="card-img-overlay rounded-0 product-overlay d-flex align-items-center justify-content-center">-->
<!-- <!– 추가적인 콘텐츠가 필요하다면 여기에 추가할 수 있습니다 –>-->
<!-- <ul class="list-unstyled">-->
<!-- <!– 리스트의 다른 요소들 –>-->
<!-- </ul>-->
<!-- </div>-->
<!-- </a>-->
<!-- </div>-->
<!-- <!– 제품 정보 –>-->
<!-- <div class="card-body">-->
<!-- <!– 제품 링크 –>-->
<!-- <a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}" id="itemName" class="font-weight-bold" th:text="${item.itemName}"></a>-->
<!-- <!– 별점 및 평점 표시 –>-->
<!-- <div class="d-flex justify-content-center mb-1 align-items-center">-->
<!-- <ul class="list-unstyled d-flex mb-0" id="starRating" th:attr="data-rating=${item.itemRatings}">-->
<!-- <!– 별 아이콘은 JavaScript로 생성됩니다 –>-->
<!-- </ul> -->
<!-- <span id="itemRatings" class="font-weight-bold ml-2" th:text="${item.itemRatings}"></span>-->
<!-- </div>-->
<!-- <!– 가격 표시 –>-->
<!-- <span id="itemPrice" class="font-weight-bold" th:text="${item.itemPrice}"></span>-->
<!-- <span class="font-weight-bold">원</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <script>-->
<!-- document.addEventListener("DOMContentLoaded", function() {-->
<!-- var starRatingContainers = document.querySelectorAll("#starRating");-->
<!-- starRatingContainers.forEach(function(starRatingContainer) {-->
<!-- var rating = parseFloat(starRatingContainer.getAttribute("data-rating"));-->
<!-- starRatingContainer.innerHTML = generateStarRating(rating);-->
<!-- });-->
<!-- });-->
<!-- function generateStarRating(rating) {-->
<!-- var starsHTML = '';-->
<!-- var fullStars = Math.floor(rating); // 정수 부분의 별 개수-->
<!-- var halfStar = (rating % 1 >= 0.1 && rating % 1 <= 0.9) ? 1 : 0; // 반 별 조건-->
<!-- // 정수 부분의 별 추가-->
<!-- for (var i = 0; i < fullStars; i++) {-->
<!-- starsHTML += '<i class="text-warning fa fa-star"></i>';-->
<!-- }-->
<!-- // 반 별 추가-->
<!-- if (halfStar) {-->
<!-- starsHTML += '<i class="text-warning fa fa-star-half-alt"></i>';-->
<!-- }-->
<!-- // 남은 빈 별 추가-->
<!-- var emptyStars = 5 - fullStars - halfStar;-->
<!-- for (var i = 0; i < emptyStars; i++) {-->
<!-- starsHTML += '<i class="text-muted fa fa-star"></i>';-->
<!-- }-->
<!-- return starsHTML;-->
<!-- }-->
<!-- </script>-->
<!-- 꽉찬 별만-->
<div class="container-fluid mt-3">
<div class="row">
<!-- items 리스트의 각 항목을 반복 -->
<div th:each="item : ${items}" class="col-md-4">
<div class="card mb-4 product-wap rounded-0">
<!-- 제품 이미지 -->
<div class="card rounded-0">
<a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}">
<img class="card-img rounded-0 img-fluid fixed-size-img" th:src="${item.itemFilePath}" alt="product-item">
<!-- 필요한 경우 오버레이나 추가 요소를 여기에 추가할 수 있습니다 -->
<div class="card-img-overlay rounded-0 product-overlay d-flex align-items-center justify-content-center">
<!-- 추가적인 콘텐츠가 필요하다면 여기에 추가할 수 있습니다 -->
<ul class="list-unstyled">
<!-- 리스트의 다른 요소들 -->
</ul>
</div>
</a>
</div>
<!-- 제품 정보 -->
<div class="card-body">
<!-- 제품 링크 -->
<a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}" id="itemName" class="font-weight-bold" th:text="${item.itemName}"></a>
<!-- 별점 및 평점 표시 -->
<div class="d-flex justify-content-center mb-1 align-items-center">
<ul class="list-unstyled d-flex mb-0" id="starRating" th:attr="data-rating=${item.itemRatings}">
<!-- 별 아이콘은 JavaScript로 생성됩니다 -->
</ul>
<span id="itemRatings" class="font-weight-bold ml-2" th:text="${item.itemRatings}"></span>
</div>
<!-- 가격 표시 -->
<span id="itemPrice" class="font-weight-bold" th:text="${item.itemPrice}"></span>
<span class="font-weight-bold">원</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 페이지네이션 시작-->
<nav th:unless="${#lists.isEmpty(items)}" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${page == 0} ? 'disabled'">
<a class="page-link" th:href="@{'/ypjs/item/get?page=' + ${page - 1} + '&size=' + ${size} + '&sortBy=' + ${sortBy} + '&keyword=' + ${#strings.defaultString(keyword, '')}}">이전</a>
</li>
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}" th:classappend="${page == i} ? 'active'">
<a class="page-link" th:href="@{'/ypjs/item/get?page=' + ${i} + '&size=' + ${size} + '&sortBy=' + ${sortBy} + '&keyword=' + ${#strings.defaultString(keyword, '')}}" th:text="${i + 1}">1</a>
</li>
<li class="page-item" th:classappend="${page == totalPages - 1} ? 'disabled'">
<a class="page-link" th:href="@{'/ypjs/item/get?page=' + ${page + 1} + '&size=' + ${size} + '&sortBy=' + ${sortBy} + '&keyword=' + ${#strings.defaultString(keyword, '')}}">다음</a>
</li>
</ul>
</nav>
<!--페이지네이션 끝-->
<!-- End Content -->
<!-- Start Brands -->
<section class="bg-light py-5">
<div class="container my-4">
<div class="row text-center py-3">
<div class="col-lg-6 m-auto">
<h1 class="h1">Our Brands</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
Lorem ipsum dolor sit amet.
</p>
</div>
<div class="col-lg-9 m-auto tempaltemo-carousel">
<div class="row d-flex flex-row">
<!--Controls-->
<div class="col-1 align-self-center">
<a class="h1" href="#multi-item-example" role="button" data-bs-slide="prev">
<i class="text-light fas fa-chevron-left"></i>
</a>
</div>
<!--End Controls-->
<!--Carousel Wrapper-->
<div class="col">
<div class="carousel slide carousel-multi-item pt-2 pt-md-0" id="multi-item-example" data-bs-ride="carousel">
<!--Slides-->
<div class="carousel-inner product-links-wap" role="listbox">
<!--First slide-->
<div class="carousel-item active">
<div class="row">
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_01.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_02.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_03.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_04.png" alt="Brand Logo"></a>
</div>
</div>
</div>
<!--End First slide-->
<!--Second slide-->
<div class="carousel-item">
<div class="row">
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_01.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_02.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_03.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_04.png" alt="Brand Logo"></a>
</div>
</div>
</div>
<!--End Second slide-->
<!--Third slide-->
<div class="carousel-item">
<div class="row">
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_01.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_02.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_03.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_04.png" alt="Brand Logo"></a>
</div>
</div>
</div>
<!--End Third slide-->
</div>
<!--End Slides-->
</div>
</div>
<!--End Carousel Wrapper-->
<!--Controls-->
<div class="col-1 align-self-center">
<a class="h1" href="#multi-item-example" role="button" data-bs-slide="next">
<i class="text-light fas fa-chevron-right"></i>
</a>
</div>
<!--End Controls-->
</div>
</div>
</div>
</div>
</section>
<!--End Brands-->
<footer th:replace="frame/footer :: footer"></footer>
</body>
</html>
itemCategoryList 페이지네이션
itemRepository
//카테고리 별 아이템 총 개수 조회(페이징)
public int countAllCategoryItem(String keyword, Long categoryId) {
// 기본 쿼리 설정
String queryString = "select count(distinct i) from Item i join i.category c where i.category.categoryId = :categoryId";
// 검색 조건 추가
if (keyword != null && !keyword.isEmpty()) {
queryString += " and i.itemName like :keyword";
}
// 쿼리 생성
TypedQuery<Long> query = em.createQuery(queryString, Long.class);
query.setParameter("categoryId", categoryId);
// 검색 키워드 파라미터 설정
if (keyword != null && !keyword.isEmpty()) {
query.setParameter("keyword", "%" + keyword + "%");
}
// 결과 반환
return query.getSingleResult().intValue();
}
itemService
//카테고리 별 아이템 페이징 개수 조회
public int countAllCategoryItem(String keyword, Long categoryId) {
return itemRepository.countAllCategoryItem(keyword,categoryId);
}
itemController
//카테고리당 아이템 조회(정렬,검색,페이징(페이징받아서 페이징))
@GetMapping("/ypjs/categoryItem/get/{categoryId}")
public String getAllCategoryItem(@PathVariable("categoryId") Long categoryId,
@RequestParam(value = "page",defaultValue = "0") int page,
@RequestParam(value = "size",defaultValue = "3") int size,
@RequestParam(value = "sortBy", defaultValue = "itemId") String sortBy,
@RequestParam(value = "keyword", required = false) String keyword,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Category category = categoryService.findOneCategory(categoryId);
List<ItemListDto> items = itemService.finaAllItemPagingSortBy(categoryId, keyword, pageable, sortBy);
//총 페이지 수 계산
int totalPages = Page.totalPages(itemService.countAllCategoryItem(keyword, categoryId), size);
model.addAttribute("items",items);
model.addAttribute("category", category);
model.addAttribute("sortBy", sortBy); // 정렬 옵션을 다시 모델에 추가
model.addAttribute("keyword", keyword); //검색조건 유지
model.addAttribute("page",page); //페이징
model.addAttribute("size",size); //페이징
model.addAttribute("totalPages", totalPages); //총 페이지 수
return "item/itemCategoryList";
}
item/itemCategoryList
페이지네이션 부분만 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="frame/header :: head">
<title>Zay Shop - Product Listing Page</title>
</head>
<style>
.fixed-size-img {
width: 300px; /* 원하는 너비로 설정 */
height: 300px; /* 원하는 높이로 설정 */
object-fit: cover; /* 이미지를 크기에 맞게 조정 */
}
</style>
<body>
<!--header-->
<header th:replace="frame/header :: header"></header>
<script src="/js/item/itemRatingsGet.js"></script>
<!-- Start Content -->
<div class="container py-5">
<div class="row">
<div class="col-lg-3">
<h1 class="h2 pb-4">Categories</h1>
<ul class="list-unstyled templatemo-accordion">
<li class="pb-3">
<a class="collapsed d-flex justify-content-between h3 text-decoration-none" href="#">
Women
<i class="fa fa-fw fa-chevron-circle-down mt-1"></i>
</a>
<ul class="collapse show list-unstyled pl-3">
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=3)}">Outer</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=4)}">Top</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=5)}">Bottom</a></li>
</ul>
</li>
<li class="pb-3">
<a class="collapsed d-flex justify-content-between h3 text-decoration-none" href="#">
Man
<i class="pull-right fa fa-fw fa-chevron-circle-down mt-1"></i>
</a>
<ul id="collapseTwo" class="collapse list-unstyled pl-3">
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=6)}">Outer</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=7)}">Top</a></li>
<li><a class="text-decoration-none" th:href="@{/ypjs/categoryItem/get/{categoryId}(categoryId=8)}">Bottom</a></li>
</ul>
</li>
</ul>
</div>
<div class="col-lg-9">
<div class="row">
<div class="col-md-6">
<ul class="list-inline shop-top-menu pb-3 pt-1">
<li class="list-inline-item">
<a class="h3 text-dark text-decoration-none mr-3" th:href="@{/ypjs/item/get}">All Items</a>
</li>
</ul>
</div>
<div class="col-lg-6">
<!-- 검색 폼 및 정렬 폼 통합 -->
<form th:action="@{/ypjs/categoryItem/get/{categoryId}(categoryId=${category.categoryId})}" method="get" class="input-group">
<input type="text" id="keyword" name="keyword" class="form-control"
placeholder="Search by item name" th:value="${keyword}">
<div class="input-group-append">
<button type="submit" class="btn btn-link text-dark">
<i class="fa fa-search"></i>
</button>
</div>
<div class="col-auto">
<select class="form-control" name="sortBy" id="sortBy" onchange="this.form.submit()">
<option value="itemId" th:selected="${sortBy == 'itemId'}">최신순</option>
<option value="itemRatings" th:selected="${sortBy == 'itemRatings'}">별점 높은 순</option>
<option value="likeCount" th:selected="${sortBy == 'likeCount'}">찜많은순</option>
</select>
</div>
</form>
</div>
<div class="container-fluid mt-3">
<div class="row">
<!-- items 리스트의 각 항목을 반복 -->
<div th:each="item : ${items}" class="col-md-4">
<div class="card mb-4 product-wap rounded-0">
<!-- 제품 이미지 -->
<div class="card rounded-0">
<a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}">
<img class="card-img rounded-0 img-fluid fixed-size-img" th:src="${item.itemFilePath}" alt="product-item">
<!-- 필요한 경우 오버레이나 추가 요소를 여기에 추가할 수 있습니다 -->
<div class="card-img-overlay rounded-0 product-overlay d-flex align-items-center justify-content-center">
<!-- 추가적인 콘텐츠가 필요하다면 여기에 추가할 수 있습니다 -->
<ul class="list-unstyled">
<!-- 리스트의 다른 요소들 -->
</ul>
</div>
</a>
</div>
<!-- 제품 정보 -->
<div class="card-body">
<!-- 제품 링크 -->
<a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}" id="itemName" class="font-weight-bold" th:text="${item.itemName}"></a>
<!-- 별점 및 평점 표시 -->
<div class="d-flex justify-content-center mb-1 align-items-center">
<ul class="list-unstyled d-flex mb-0" id="starRating" th:attr="data-rating=${item.itemRatings}">
<!-- 별 아이콘은 JavaScript로 생성됩니다 -->
</ul>
<span id="itemRatings" class="font-weight-bold ml-2" th:text="${item.itemRatings}"></span>
</div>
<!-- 가격 표시 -->
<span id="itemPrice" class="font-weight-bold" th:text="${item.itemPrice}"></span>
<span class="font-weight-bold">원</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 페이지네이션 시작-->
<nav th:unless="${#lists.isEmpty(items)}" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${page == 0} ? 'disabled'">
<a class="page-link" th:href="@{'/ypjs/categoryItem/get/' + ${category.categoryId} + '?page=' + (${page - 1}) + '&size=' + ${size} + '&sortBy=' + ${sortBy} + '&keyword=' + ${#strings.defaultString(keyword, '')}}">이전</a>
</li>
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}" th:classappend="${page == i} ? 'active'">
<a class="page-link" th:href="@{'/ypjs/categoryItem/get/' + ${category.categoryId} + '?page=' + ${i} + '&size=' + ${size} + '&sortBy=' + ${sortBy} + '&keyword=' + ${#strings.defaultString(keyword, '')}}" th:text="${i + 1}">1</a>
</li>
<li class="page-item" th:classappend="${page == totalPages - 1} ? 'disabled'">
<a class="page-link" th:href="@{'/ypjs/categoryItem/get/' + ${category.categoryId} + '?page=' + (${page + 1}) + '&size=' + ${size} + '&sortBy=' + ${sortBy} + '&keyword=' + ${#strings.defaultString(keyword, '')}}">다음</a>
</li>
</ul>
</nav>
<!-- 페이지네이션 끝-->
<!-- End Content -->
<!-- Start Brands -->
<section class="bg-light py-5">
<div class="container my-4">
<div class="row text-center py-3">
<div class="col-lg-6 m-auto">
<h1 class="h1">Our Brands</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
Lorem ipsum dolor sit amet.
</p>
</div>
<div class="col-lg-9 m-auto tempaltemo-carousel">
<div class="row d-flex flex-row">
<!--Controls-->
<div class="col-1 align-self-center">
<a class="h1" href="#multi-item-example" role="button" data-bs-slide="prev">
<i class="text-light fas fa-chevron-left"></i>
</a>
</div>
<!--End Controls-->
<!--Carousel Wrapper-->
<div class="col">
<div class="carousel slide carousel-multi-item pt-2 pt-md-0" id="multi-item-example" data-bs-ride="carousel">
<!--Slides-->
<div class="carousel-inner product-links-wap" role="listbox">
<!--First slide-->
<div class="carousel-item active">
<div class="row">
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_01.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_02.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_03.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_04.png" alt="Brand Logo"></a>
</div>
</div>
</div>
<!--End First slide-->
<!--Second slide-->
<div class="carousel-item">
<div class="row">
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_01.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_02.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_03.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_04.png" alt="Brand Logo"></a>
</div>
</div>
</div>
<!--End Second slide-->
<!--Third slide-->
<div class="carousel-item">
<div class="row">
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_01.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_02.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_03.png" alt="Brand Logo"></a>
</div>
<div class="col-3 p-md-5">
<a href="#"><img class="img-fluid brand-img" src="/img/brand_04.png" alt="Brand Logo"></a>
</div>
</div>
</div>
<!--End Third slide-->
</div>
<!--End Slides-->
</div>
</div>
<!--End Carousel Wrapper-->
<!--Controls-->
<div class="col-1 align-self-center">
<a class="h1" href="#multi-item-example" role="button" data-bs-slide="next">
<i class="text-light fas fa-chevron-right"></i>
</a>
</div>
<!--End Controls-->
</div>
</div>
</div>
</div>
</section>
<!--End Brands-->
<footer th:replace="frame/footer :: footer"></footer>
</body>
</html>
사진은 itemList와 동일
categoryList부분 검색, 페이징 없앰
categoryRepository
//category전체 조회
public List<Category> findAll() {
return em.createQuery(
"select distinct c from Category c", Category.class)
.getResultList();
}
categoryService
public List<CategoryListDto> findAllCategory() {
List<Category> categories = categoryRepository.findAll();
List<CategoryListDto> result = categories.stream()
.map(c -> new CategoryListDto(c))
.collect(Collectors.toList());
return result;
}
categoryController
//카테고리 리스트 보기
@GetMapping("/ypjs/category/get")
public String getCategoryList(Model model) {
List<CategoryListDto> categories = categoryService.findAllCategory();
model.addAttribute("categories", categories);
return "category/categoryList";
}
화면은 전에 올린 글에 있음
categoryGet에 검색, 페이지네이션 추가
itemRepository, service
위에 카테고리 별 아이템 조회 그대로 씀
categoryController
//category 1개 조회
@GetMapping("/ypjs/category/get/{categoryId}")
public String getOneCategory (@PathVariable("categoryId") Long categoryId,
Model model,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "3") int size,
@RequestParam(value = "keyword", required = false) String keyword) {
Pageable pageable = PageRequest.of(page, size);
Category findCategory = categoryService.findOneCategory(categoryId);
List<ItemListDto> items = itemService.findAllCategoryItem(categoryId, pageable, keyword);
//총 페이지 수 계산
int totalPages = Page.totalPages(itemService.countAllCategoryItem(keyword, categoryId), size);
model.addAttribute("category", findCategory);
model.addAttribute("items",items);
model.addAttribute("keyword", keyword); //검색조건 유지
model.addAttribute("page",page); //페이징
model.addAttribute("size",size); //페이징
model.addAttribute("totalPages", totalPages); //총 페이지 수
return "category/categoryGet";
}
category/categoryGet 검색, 페이지네이션, 아이템 이름에 링크 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="frame/header :: head">
<title>Zay Shop - Product Listing Page</title>
</head>
<body>
<!--header-->
<header th:replace="frame/header :: header"></header>
<!-- Start Content -->
<div class="container py-5">
카테고리 번호 당 아이템 리스트
</div>
<!-- End Content -->
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- JS 파일 포함 -->
<script src="/js/jquery-1.11.0.min.js"></script>
<script src="/js/summernote-lite.js"></script>
<script src="/js/summernote-ko-KR.js"></script>
<script src="/js/item/category.js"></script>
<script src="/js/item/item.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.18/summernote.min.js"></script>
<!-- Start Contact -->
<div class="container py-5">
<div class="row py-5">
<!-- 폼 요소들 -->
<div class="row">
<style>
.category-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
max-width: 1000px; /* 최대 너비 설정 */
margin-left: auto;
margin-right: auto;
}
.category-label {
font-weight: bold;
}
.item-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
max-width: 1000px; /* 최대 너비 설정 */
margin-left: auto;
margin-right: auto;
}
.item-info {
/* Optional: Add additional styles for item information */
}
.category-label {
font-weight: bold;
}
.h3-indent {
margin-left: 100px; /* 왼쪽 여백을 줌으로써 텍스트를 오른쪽으로 이동 */
}
</style>
<div class="category-box">
<div>
<span class="category-label">카테고리 번호:</span>
<span>:</span>
<span th:text="${category.categoryId}"></span>
</div>
<div>
<span class="category-label">카테고리 부모 번호:</span>
<span>:</span>
<span th:text="${category.categoryParent.categoryId}"></span>
</div>
<div>
<span class="category-label">카테고리 이름:</span>
<span>:</span>
<span th:text="${category.categoryName}"></span>
</div>
<br>
<div class="container">
<div class="row justify-content-end">
<div class="col-auto">
<a th:href="@{/ypjs/category/update/{categoryId}(categoryId=${category.categoryId})}" class="btn btn-warning">수정하기</a>
</div>
<input type="hidden" id="categoryId" th:value="${category.categoryId}">
<div class="col-auto">
<button type="button" id="btn-categoryDelete" class="btn btn-danger">삭제하기</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-9">
<div class="row justify-content-start">
<div class="col-md-6 offset-md-9">
<br><br>
<!-- 검색 폼 -->
<form th:action="@{/ypjs/category/get/{categoryId}(categoryId=${category.categoryId})}" method="get" class="form-inline w-100">
<div class="input-group mb-3">
<input type="text" id="keyword" name="keyword" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2"
placeholder="Search by item name" th:value="${keyword}">
<button type="submit" class="btn btn-link text-dark mb-2 ml-2" id="button-addon2">
<i class="fa fa-search"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div th:if="${not #lists.isEmpty(items)}">
<br><br><br>
<h3 class="h3-indent">아이템 목록</h3>
<br>
<div th:each="item : ${items}" class="item-box">
<div class="item-info">
<div>
<span class="category-label">아이템 이름:</span>
<span>:</span>
<a th:href="@{/ypjs/item/get/{itemId}(itemId=${item.itemId})}" id="itemName" class="font-weight-bold" th:text="${item.itemName}"></a>
</div>
<div>
<span class="category-label">아이템 내용:</span>
<span>:</span>
<span th:utext="${item.itemContent}"></span>
</div>
<div>
<span class="category-label">아이템 가격:</span>
<span>:</span>
<span th:text="${item.itemPrice}"></span>
</div>
<div>
<span class="category-label">아이템 수량:</span>
<span>:</span>
<span th:text="${item.itemStock}"></span>
</div>
<!-- 필요한 다른 아이템 정보들을 추가 -->
<div class="container">
<div class="row justify-content-end">
<div class="col-auto">
<a th:href="@{/ypjs/item/update/{itemId}(itemId=${item.itemId})}"
class="btn btn-warning"
style="padding: 2px 10px; font-size: 12px; width: 90px; height: 40px; text-align: center; display: inline-block; line-height: 40px;">
수정하기
</a>
</div>
<button type="button"
class="btn btn-danger btn-sm btn-delete-item"
th:attr="data-itemId=${item.itemId}"
style="padding: 2px 10px; font-size: 16px; width: 90px; height: 40px;">
삭제하기
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 페이지네이션 시작-->
<nav th:unless="${#lists.isEmpty(items)}" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${page == 0} ? 'disabled'">
<a class="page-link" th:href="@{'/ypjs/category/get/' + ${category.categoryId} + '?page=' + (${page - 1}) + '&size=' + ${size} + '&keyword=' + ${#strings.defaultString(keyword, '')}}">이전</a>
</li>
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}" th:classappend="${page == i} ? 'active'">
<a class="page-link" th:href="@{'/ypjs/category/get/' + ${category.categoryId} + '?page=' + ${i} + '&size=' + ${size} + '&keyword=' + ${#strings.defaultString(keyword, '')}}" th:text="${i + 1}">1</a>
</li>
<li class="page-item" th:classappend="${page == totalPages - 1} ? 'disabled'">
<a class="page-link" th:href="@{'/ypjs/category/get/' + ${category.categoryId} + '?page=' + (${page + 1}) + '&size=' + ${size} + '&keyword=' + ${#strings.defaultString(keyword, '')}}">다음</a>
</li>
</ul>
</nav>
<!-- 페이지네이션 끝-->
<!-- End Contact -->
<footer th:replace="frame/footer :: footer"></footer>
</body>
</html>
검색
페이징
itemReview페이징
itemReviewRepository
//아이템리뷰 총 개수
public int countAllItemReview(Long itemId){
return em.createQuery("select count(distinct ir) from ItemReview ir join ir.item i where i.itemId = :itemId", Long.class)
.setParameter("itemId", itemId)
.getSingleResult()
.intValue();
}
itemReviewService
//아이템리뷰 페이징 개수 조회
public int countAllItemReview(Long itemId){
return itemReviewRepository.countAllItemReview(itemId);
}
itmReviewController
//아이템 당 리뷰조회
@GetMapping("/ypjs/itemReview/get/{itemId}")
public String getAllItemReview(@PathVariable(name = "itemId") Long itemId, Model model,
@RequestParam(value = "page",defaultValue = "0") int page,
@RequestParam(value = "size",defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
itemService.findOneItem(itemId);
List<ItemReviewListDto> itemReviews = itemReviewService.findAllItemReview(itemId, pageable);
//총 페이지 수 계산
int totalPages = Page.totalPages(itemReviewService.countAllItemReview(itemId), size);
model.addAttribute("itemReviews", itemReviews);
model.addAttribute("page",page); //페이징
model.addAttribute("size",size); //페이징
model.addAttribute("totalPages", totalPages); //총 페이지 수
return "itemreview/itemReviewGet";
}
itemreview/itemReviewGet 페이지네이션 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="frame/header :: head">
<title>Zay Shop - Product Listing Page</title>
</head>
<script src="/js/item/itemRatingsGet.js"></script>
<body>
<!--header-->
<header th:replace="frame/header :: header"></header>
<style>
.category-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.category-label {
font-weight: bold;
}
.item-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.item-info {
/* Optional: Add additional styles for item information */
}
.category-label {
font-weight: bold;
}
#reviewContent img {
display: block; /* 이미지가 한 줄에 나타나도록 설정 */
margin: auto; /* 가운데 정렬 */
max-width: 100%; /* 최대 너비 설정 */
height: auto; /* 높이 자동으로 조정 */
margin-bottom: 10px; /* 이미지 아래 여백 추가 */
}
</style>
<!-- Start Content -->
<div class="container py-5">
아이템 리뷰
</div>
<!-- End Content -->
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- JS 파일 포함 -->
<script src="/js/jquery-1.11.0.min.js"></script>
<script src="/js/summernote-lite.js"></script>
<script src="/js/summernote-ko-KR.js"></script>
<script src="/js/item/itemReview.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.18/summernote.min.js"></script>
<!-- Start Contact -->
<div class="container py-5">
<div class="row py-5">
<form class="col-md-9 m-auto" method="post">
<!-- 폼 요소들 -->
<div class="row">
<div th:if="${not #lists.isEmpty(itemReviews)}">
<br><br><br>
<h3>리뷰 목록</h3>
<br>
<div th:each="itemReview : ${itemReviews}" class="item-box">
<div class="item-info" style="text-align: right;">
<div>
<span style="font-weight: bold;">작성일 : </span>
<span id="itemReviewCreateDate"
th:text="${#temporals.format(itemReview.itemReviewCreateDate, 'yyyy-MM-dd')}"></span>
</div>
</div>
<div class="item-info">
<div>
<span class="category-label">제목 : </span>
<span th:text="${itemReview.itemReviewName}"></span>
</div>
<!-- 별점 및 평점 표시 -->
<div class="d-flex justify-content-left mb-1 align-items-left">
<span class="category-label">별점 : </span>
<ul class="list-unstyled d-flex mb-0" id="starRating"
th:attr="data-rating=${itemReview.itemScore}">
<!-- 별 아이콘은 JavaScript로 생성됩니다 -->
</ul>
</div>
<div>
<span class="category-label">리뷰 내용 : </span>
<div style="text-align: left;">
<!-- 썸머노트에서 입력한 리뷰 내용 -->
<div id="reviewContent" th:utext="${itemReview.itemReviewContent}">
<!-- 썸머노트에서 삽입된 이미지의 스타일 -->
</div>
</div>
</div>
<input type="hidden" id="itemId" th:value="${itemReview.itemId}">
<!-- 수정 및 삭제 버튼 -->
<div class="container">
<div class="row justify-content-end">
<div class="col-auto">
<a th:href="@{/ypjs/itemReview/update/{itemReviewId}(itemReviewId=${itemReview.itemReviewId})}"
class="btn btn-warning">수정하기</a>
</div>
<div class="col-auto">
<button type="button"
class="btn btn-danger btn-sm btn-delete-itemReview"
th:data-itemReviewId="${itemReview.itemReviewId}"
style="padding: 2px 10px; font-size: 16px; width: 90px; height: 40px;">
삭제하기
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 페이지네이션 시작 -->
<nav th:unless="${#lists.isEmpty(itemReviews)}" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${page == 0} ? 'disabled'">
<a class="page-link"
th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0].itemId}, page=${page - 1}, size=${size})}">이전</a>
</li>
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}"
th:classappend="${page == i} ? 'active'">
<a class="page-link"
th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0].itemId}, page=${i}, size=${size})}"
th:text="${i + 1}"></a>
</li>
<li class="page-item" th:classappend="${page == totalPages - 1} ? 'disabled'">
<a class="page-link"
th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0].itemId}, page=${page + 1}, size=${size})}">다음</a>
</li>
</ul>
</nav>
<!--페이지네이션 끝-->
<!-- End Contact -->
</div>
</form>
</div>
</div>
<footer th:replace="frame/footer :: footer"></footer>
</body>
</html>
itemReview js에 유효성 검사
itemReview.js 유효성 검사 부분 추가, summernote도 itemReviewPost.html에서 여기로 옮김
let itemBoardObject = {
init: function() {
let _this = this;
$("#btn-itemReviewPost").on("click", function() {
_this.insert();
}),
$("#btn-itemReviewUpdate").on("click", function() {
_this.update();
}),
$(document).on("click", ".btn-delete-itemReview", function() {
// 클릭된 버튼의 데이터 속성에서 itemReviewId를 가져옴
let itemReviewId = $(this).data("itemreviewid");
// 아이템 리뷰 삭제 함수 호출
_this.deleteItemReview(itemReviewId);
});
// Summernote 초기화
$("#itemReviewContent").summernote({
height: 300,
placeholder: '내용을 입력하세요...'
});
},
insert: function() {
let itemId = $("#itemId").val(); // URL에 사용할 itemId를 가져옴
let itemScore = $("#itemScore").val(); // 별점 가져오기
let itemReviewName = $("#itemReviewName").val(); // 제목 가져오기
let itemReviewContent = $("#itemReviewContent").summernote('code'); //썸머노트 내용 가져오기
// 별점을 선택하지 않은 경우 처리
if (itemScore === "0") {
alert("별점을 선택하세요.");
return; // 리뷰 등록 중단
}
// 제목이 비어 있는지 확인
if (!itemReviewName || itemReviewName.trim().length === 0) {
alert("제목을 입력하세요.");
return; // 리뷰 등록 중단
}
// 썸머노트에서 가져온 HTML 태그 제거 및 내용 검사
let plainTextContent = stripTags(itemReviewContent).trim();
if (!plainTextContent || plainTextContent.length === 0) {
alert("내용을 입력하세요.");
return;
}
let data = {
itemId: itemId, // itemId를 데이터에 포함
memberId: $("#memberId").val(), // 멤버 ID 추가
itemReviewName: $("#itemReviewName").val(),
itemScore: $("#itemScore").val(),
itemReviewContent: $("#itemReviewContent").val(),
};
$.ajax({
type: "POST",
url: "/ypjs/itemReview/post/" + itemId, // URL에 itemId를 추가
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
success: function(response) {
alert("리뷰가 등록되었습니다.");
window.location.href = "/ypjs/itemReview/get/" + itemId; // 성공 후 리디렉션
},
error: function(error) {
alert("에러 발생: " + JSON.stringify(error));
}
});
},
update: function() {
let itemReviewId = $("#itemReviewId").val();
let itemScore = $("#itemScore").val(); // 별점 가져오기
let itemReviewName = $("#itemReviewName").val(); // 제목 가져오기
let itemReviewContent = $("#itemReviewContent").summernote('code'); //썸머노트 내용 가져오기
// 별점을 선택하지 않은 경우 처리
if (itemScore === "0") {
alert("별점을 선택하세요.");
return; // 리뷰 등록 중단
}
// 제목이 비어 있는지 확인
if (!itemReviewName || itemReviewName.trim().length === 0) {
alert("제목을 입력하세요.");
return; // 리뷰 등록 중단
}
// 썸머노트에서 가져온 HTML 태그 제거 및 내용 검사
let plainTextContent = stripTags(itemReviewContent).trim();
if (!plainTextContent || plainTextContent.length === 0) {
alert("내용을 입력하세요.");
return;
}
let updateData = {
itemReviewName: $("#itemReviewName").val(),
itemScore: $("#itemScore").val(),
itemReviewContent: $("#itemReviewContent").val(),
};
$.ajax({
type: "PUT",
url: "/ypjs/itemReview/update/" + itemReviewId,
data: JSON.stringify(updateData),
contentType: "application/json; charset=utf-8",
success: function(response) {
alert("리뷰가 수정되었습니다.");
window.location.href = "/ypjs/itemReview/get/" + response ;
},
error: function(error) {
alert("에러 발생: " + JSON.stringify(error));
// 에러 발생 시 적절히 처리하도록 수정이 필요할 수 있습니다.
}
});
},
deleteItemReview: function(itemReviewId) {
let itemId = $("#itemId").val();
$.ajax({
type: "DELETE",
url: "/ypjs/itemReview/delete/" + itemReviewId,
success: function(response) {
alert("리뷰가 삭제되었습니다.");
window.location.href = "/ypjs/itemReview/get/" + itemId;
},
error: function(xhr, status, error) {
alert("에러 발생: " + error);
}
});
}
};
$(document).ready(function() {
itemBoardObject.init();
});
// HTML 태그 제거 함수
function stripTags(html) {
let tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
}
카테고리 유효성 검사
category.js에 유효성 검사 추가
let categoryBoardObject = {
init: function() {
let _this = this;
$("#btn-categoryPost").on("click", function() {
_this.insert();
}),
$("#btn-categoryUpdate").on("click", function() {
_this.update();
}),
$("#btn-categoryDelete").on("click", function() {
_this.delete(); // 수정된 부분: delete 함수 호출
});
},
insert: function() {
let categoryParent = $("#categoryParent").val();
let categoryName = $("#categoryName").val();
if (!categoryParent || categoryParent.trim().length === 0) {
alert("카테고리 부모 번호를 입력하세요");
return;
}
if(!categoryName || categoryName.trim().length === 0){
alert("카테고리 이름을 입력하세요");
return;
}
let data = {
categoryParent: $("#categoryParent").val(),
categoryName: $("#categoryName").val(),
};
$.ajax({
type: "POST",
url: "/ypjs/category/post",
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
success: function(response) {
alert("카테고리 저장되었습니다.");
window.location.href = "/ypjs/category/get";
},
error: function(error) {
window.location.href = "/ypjs/category/get";
}
});
},
update: function() {
let categoryId = $("#categoryId").val();
let categoryParent = $("#categoryParent").val();
let categoryName = $("#categoryName").val();
if (!categoryId || categoryId.trim().length === 0) {
alert("카테고리 번호를 입력하세요");
return;
}
if (!categoryParent || categoryParent.trim().length === 0) {
alert("카테고리 부모 번호를 입력하세요");
return;
}
if(!categoryName || categoryName.trim().length === 0){
alert("카테고리 이름을 입력하세요");
return;
}
let updateData = {
categoryId: categoryId, // 수정된 부분: categoryId 추가
categoryParent: $("#categoryParent").val(),
categoryName: $("#categoryName").val(),
};
$.ajax({
type: "PUT",
url: "/ypjs/category/update/" + categoryId,
data: JSON.stringify(updateData),
contentType: "application/json; charset=utf-8",
success: function(response) {
alert("카테고리가 수정되었습니다.");
window.location.href = "/ypjs/category/get";
},
error: function(error) {
alert("에러 발생: " + JSON.stringify(error));
}
});
},
delete: function() {
let categoryId = $("#categoryId").val(); // 삭제할 categoryId 가져오기
$.ajax({
type: "DELETE",
url: "/ypjs/category/delete/" + categoryId,
success: function(response) {
alert("카테고리가 삭제되었습니다.");
window.location.href = "/ypjs/category/get"; // 삭제 후 페이지 이동
},
error: function(xhr, status, error) {
alert("에러 발생: " + error);
}
});
}
};
$(document).ready(function() {
categoryBoardObject.init();
});
아이템 유효성 검사
itemPost.html, itemUpdate.html은 form으로 넘겨서 따로 itemValidation.js만들어서 유효성 검사,
썸머노트, 썸네일부분 script도 여기로 옮김
$(document).ready(function () {
// Summernote 초기화
$("#itemContent").summernote({
height: 500,
placeholder: '상품 내용을 입력하세요...',
callbacks: {
onChange: function (contents, $editable) {
// 내용이 변경될 때 추가 로직
}
}
});
// 파일명 표시 함수
function displayFileName(input) {
if (input.files.length > 0) {
var fileName = input.files[0].name;
document.getElementById('file-name-display').innerText = '선택된 파일: ' + fileName;
} else {
document.getElementById('file-name-display').innerText = '';
}
}
// 폼 제출 전 유효성 검사
$('#itemForm').submit(function (event) {
var categoryId = $("#categoryId").val();
var itemName = $("#itemName").val();
var itemPrice = $("#itemPrice").val();
var itemStock = $("#itemStock").val();
var itemContent = $("#itemContent").summernote('code');
if (!categoryId || categoryId.trim().length === 0) {
alert('카테고리 번호를 입력하세요.');
event.preventDefault(); // 폼 제출 방지
return;
}
if (!itemName || itemName.trim().length === 0) {
alert('상품명을 입력하세요.');
event.preventDefault(); // 폼 제출 방지
return;
}
if (!itemPrice || itemPrice.trim().length === 0) {
alert('상품가격을 입력하세요.');
event.preventDefault(); // 폼 제출 방지
return;
}
if (!itemStock || itemStock.trim().length === 0) {
alert('상품수량을 입력하세요.');
event.preventDefault(); // 폼 제출 방지
return;
}
// 썸머노트에서 가져온 HTML 태그 제거 및 내용 검사
let plainTextContent = stripTags(itemContent).trim();
if (!plainTextContent || plainTextContent.trim().length === 0) {
alert('상품내용을 입력하세요.');
event.preventDefault(); // 폼 제출 방지
return;
}
var fileInput = document.getElementById('file');
if (fileInput.files.length === 0) {
alert('썸네일 파일을 선택하세요.');
event.preventDefault(); // 폼 제출 방지
}
});
});
// HTML 태그 제거 함수
function stripTags(html) {
let tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
}
itemList.html, itemCategoryList.html, itemGet.html에 있던 별 script
itemRatingsGet.js따로 만듦
document.addEventListener("DOMContentLoaded", function() {
var starRatingContainers = document.querySelectorAll("#starRating");
starRatingContainers.forEach(function(starRatingContainer) {
var rating = parseFloat(starRatingContainer.getAttribute("data-rating"));
starRatingContainer.innerHTML = generateStarRating(rating);
});
// 별점을 생성하여 HTML 문자열 반환하는 함수 (순수 JavaScript)
function generateStarRating(rating) {
var starsHTML = '';
var numStars;
if (rating >= 0 && rating < 1) {
numStars = 0;
} else if (rating >= 1 && rating < 2) {
numStars = 1;
} else if (rating >= 2 && rating < 3) {
numStars = 2;
} else if (rating >= 3 && rating < 4) {
numStars = 3;
} else if (rating >= 4 && rating < 5) {
numStars = 4;
} else if (rating == 5) {
numStars = 5;
}
for (var i = 0; i < numStars; i++) {
starsHTML += '<i class="text-warning fa fa-star"></i>';
}
for (var i = numStars; i < 5; i++) {
starsHTML += '<i class="text-muted fa fa-star"></i>';
}
return starsHTML;
}
});
itemReviewPost.html, itemReviewUpdate.html에 있던 별 js파일 따로 만듦
itemRatingsPost.js
$(document).ready(function() {
var initialScore = $('#itemScore').val(); // 기존의 itemScore 값 가져오기
$('#starRating').html(generateStarRating(initialScore)); // 초기화: 기존의 별점으로 별 아이콘 표시
// 별 아이콘 클릭 이벤트 리스너 추가
$('#starRating').on('click', '.fa-star', function() {
var score = $(this).data('score');
$('#itemScore').val(score); // hidden 필드에 새로운 점수 설정
$('#starRating').html(generateStarRating(score)); // 별 아이콘 업데이트
});
// 별 아이콘을 생성하여 HTML 문자열 반환하는 함수
function generateStarRating(score) {
var starsHTML = '';
for (var i = 1; i <= 5; i++) {
if (i <= score) {
starsHTML += '<i class="fas fa-star rating-stars filled-star" data-score="' + i + '"></i>';
} else {
starsHTML += '<i class="fas fa-star rating-stars" data-score="' + i + '"></i>';
}
}
return starsHTML;
}
});
둘이 합치고 싶었지만 그러면 둘 중 하나만 적용 됨
리뷰 갯수 추가
//item1개 조회
@GetMapping("/ypjs/item/get/{itemId}")
public String getOneItem (@PathVariable("itemId") Long itemId,
Model model) {
Item findItem = itemService.findOneItem(itemId);
ItemOneDto item = new ItemOneDto(findItem);
model.addAttribute("item", item);
//조회수
itemService.increaseItemCnt(itemId);
//리뷰 갯수
int reviewCount = itemReviewService.countAllItemReview(itemId);
model.addAttribute("reviewCount", reviewCount);
return "item/itemGet";
}
서비스, 레파지토리는 위에 있음
item/itemGet
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="frame/header :: head">
<title>Zay Shop - Product Listing Page</title>
</head>
<script src="/js/item/itemRatings.js"></script>
<body>
<!--header-->
<header th:replace="frame/header :: header"></header>
<script src="/js/item/itemRatingsGet.js"></script>
<script src="/js/item/item.js"></script>
<style>
.bg-light {
background-color: transparent !important; /* 배경색을 투명하게 설정 */
}
/* 요소의 우선순위를 높여서 Bootstrap의 font-weight-bold 클래스가 적용될 수 있도록 함 */
h1.font-weight-bold, span.font-weight-bold {
font-weight: bold !important;
}
.custom-mt {
margin-top: -20px; /* 원하는 간격 값으로 설정 */
}
<!-- 가운데 정렬-->
.centered-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#itemContent {
text-align: center;
}
/* #itemContent를 flexbox로 설정하여 가운데 정렬 */
#itemContent {
display: flex;
flex-direction: column; /* 내부 요소들을 세로로 정렬하기 위해 */
justify-content: center; /* 수직 가운데 정렬 */
align-items: center; /* 수평 가운데 정렬 */
}
/* 썸머노트에서 삽입된 이미지의 스타일 */
#itemContent img {
max-width: 100%; /* 최대 너비 설정 */
height: auto; /* 높이 자동으로 조정 */
margin-bottom: 10px; /* 이미지 아래 여백 추가 */
}
/* 썸머노트에서 삽입된 텍스트의 스타일 */
#itemContent p {
text-align: center; /* 텍스트 가운데 정렬 */
margin-bottom: 10px; /* 문단 아래 여백 추가 */
}
</style>
<!-- Open Content -->
<div class="card-body pt-4 mt-3"> <!-- pt-4를 mt-3으로 변경 -->
<div class="row">
<div class="col-lg-7 offset-lg-1">
<h1 class="h2 mb-3 font-weight-bold" th:text="${item.itemName}"></h1>
<span id="itemPrice" class="font-weight-bold" th:text="${item.itemPrice}"></span>
<span class="font-weight-bold">원</span>
<p class="h3 py-2"></p>
</div>
</div>
<!-- 별점 및 평점 표시 -->
<div class="row mt-3">
<div class="col-lg-7 offset-lg-1">
<div class="d-flex justify-content-start align-items-center">
<ul class="list-unstyled d-flex mb-0 mr-3 text-left" id="starRating" th:attr="data-rating=${item.itemRatings}">
<!-- 별 아이콘은 JavaScript로 생성됩니다 -->
</ul>
<span id="itemRatings" th:text="${item.itemRatings}"></span>
<div>
<a th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${item.itemId})}">
Reviews (<span th:text="${reviewCount}"></span>)
</a>
</div>
</div>
</div>
</div>
<!-- 수정하기 및 삭제하기 버튼 -->
<div class="row justify-content-end mt-3">
<div class="col-auto">
<a th:href="@{/ypjs/item/update/{itemId}(itemId=${item.itemId})}" class="btn btn-warning">수정하기</a>
</div>
<div class="col-auto">
<input type="hidden" id="itemId" th:value="${item.itemId}">
<button type="button" id="btn-itemDelete" class="btn btn-danger">삭제하기</button>
</div>
</div>
<div class="row justify-content-end mt-3">
<div class="col-auto">
<span class="font-weight-bold"> 조회수 : </span>
<span id="itemCnt" class="font-weight-bold" th:text="${item.itemCnt}"></span>
<br>
<span class="font-weight-bold">작성일 : </span>
<span id="itemCreateDate" class="font-weight-bold" th:text="${#temporals.format(item.itemCreateDate, 'yyyy-MM-dd')}"></span>
</div>
<div class="col-auto">
</div>
</div>
</div>
</div>
</div>
<hr>
<section class="bg-light">
<div class="container pb-5">
<div class="row justify-content-center align-items-center">
<div class="col-lg-5 mt-5 text-center">
<div id="itemContent" th:utext="${item.itemContent}">
<!-- 썸머노트에서 입력된 내용 (글과 이미지) -->
</div>
</div>
</div>
</div>
</section>
<div class="row pb-3">
<div class="col-lg-6 offset-lg-5"> <!-- offset-lg-6 클래스 추가하여 오른쪽으로 옮김 -->
<ul class="list-inline pb-3 text-right"> <!-- text-right 클래스 추가하여 내용 오른쪽 정렬 -->
<li class="list-inline-item">
수량
<input type="hidden" name="product-quanity" id="product-quanity" value="1">
</li>
<li class="list-inline-item"><span class="btn btn-success" id="btn-minus">-</span></li>
<li class="list-inline-item"><span class="badge bg-secondary" id="var-value">1</span></li>
<li class="list-inline-item"><span class="btn btn-success" id="btn-plus">+</span></li>
</ul>
</div>
</div>
<div class="row pb-3">
<div class="col d-grid">
<button type="submit" class="btn btn-success btn-md" name="submit" value="buy">Buy</button>
</div>
<div class="col d-grid">
<button type="submit" class="btn btn-success btn-md" name="submit" value="addtocard">Add To Cart</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Close Content -->
<footer th:replace="frame/footer :: footer"></footer>
</body>
</html>
reviewCount부분 추가
리뷰 갯수 링크에 같이 연결돼서 나옴
리뷰 정렬
기본 최신순, 별점 높은 순, 별점 낮은 순 정렬
itemReviewRepository
//아이템 당 리뷰 조회
public List<ItemReview> findAllItemReview(Long itemId, Pageable pageable, String sortBy) {
// 기본 쿼리 문자열
String queryString = "select ir from ItemReview ir join fetch ir.item i where i.itemId = :itemId";
// 정렬 조건 추가
switch (sortBy) {
case "highScore":
queryString += " order by ir.itemScore desc, ir.itemReviewId desc"; // 'highScore'일 때 itemScore 기준 내림차순, 동일할 경우 itemReviewId 내림차순 정렬
break;
case "lowScore":
queryString += " order by ir.itemScore asc, ir.itemReviewId desc"; // 'lowScore'일 때 itemScore 기준 오름차순, 동일할 경우 itemReviewId 내림차순 정렬
break;
default:
queryString += " order by ir.itemReviewId desc"; // 기본 정렬: itemReviewId 기준 내림차순 정렬 (최신순)
break;
}
return em.createQuery(queryString, ItemReview.class)
.setParameter("itemId", itemId)
.setFirstResult((int) pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
}
itemReviewService
//아이템 당 리뷰조회
public List<ItemReviewListDto> findAllItemReview(Long itemId, Pageable pageable, String sortBy) {
List<ItemReview> reviews = itemReviewRepository.findAllItemReview(itemId, pageable, sortBy);
List<ItemReviewListDto> result = reviews.stream()
.map(ItemReviewListDto::new)
.collect(Collectors.toList());
return result;
}
itemReviewController
//아이템 당 리뷰조회
@GetMapping("/ypjs/itemReview/get/{itemId}")
public String getAllItemReview(@PathVariable(name = "itemId") Long itemId, Model model,
@RequestParam(value = "page",defaultValue = "0") int page,
@RequestParam(value = "sortBy", defaultValue = "itemReviewId") String sortBy,
@RequestParam(value = "size",defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
itemService.findOneItem(itemId);
List<ItemReviewListDto> itemReviews = itemReviewService.findAllItemReview(itemId, pageable, sortBy);
//총 페이지 수 계산
int totalPages = Page.totalPages(itemReviewService.countAllItemReview(itemId), size);
model.addAttribute("itemReviews", itemReviews);
model.addAttribute("sortBy", sortBy); // 정렬 옵션을 다시 모델에 추가
model.addAttribute("page",page); //페이징
model.addAttribute("size",size); //페이징
model.addAttribute("totalPages", totalPages); //총 페이지 수
return "itemreview/itemReviewGet";
}
itemreview/itemReviewGet
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="frame/header :: head">
<title>Zay Shop - Product Listing Page</title>
</head>
<script src="/js/item/itemRatingsGet.js"></script>
<body>
<!--header-->
<header th:replace="frame/header :: header"></header>
<style>
.category-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.category-label {
font-weight: bold;
}
.item-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
max-width: 1000px; /* 최대 너비 설정 */
margin-left: auto;
margin-right: auto;
}
.item-info {
/* Optional: Add additional styles for item information */
}
.category-label {
font-weight: bold;
}
.h3-indent {
margin-left: 100px; /* 왼쪽 여백을 줌으로써 텍스트를 오른쪽으로 이동 */
}
.custom-select-wrapper {
border: 1px solid #f0f8ff; /* 테두리 두께와 색상 설정 */
padding: 3px; /* 선택 영역의 여백 설정 */
background-color: #f0f8ff; /* 연한 하늘색 배경색 */
display: inline-block; /* 인라인 요소로 표시 */
border-radius: 10px; /* 테두리 둥글기 설정 */
}
.form-control {
border: 1px solid #f0f8ff; /* 내부 테두리 설정 */
border-radius: 8px; /* 안쪽 테두리 둥글기 설정 */
}
</style>
<!-- Start Content -->
<div class="container py-5">
아이템 리뷰
</div>
<!-- End Content -->
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="/js/item/itemReview.js"></script>
<!-- Start Contact -->
<!--정렬 추가-->
<form th:action="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0]?.itemId})}" method="get" class="row">
<div class="offset-sm-9 col-sm-1 mb-2 custom-select-wrapper" style="width: 8%;">
<select class="form-control" name="sortBy" id="sortBy" onchange="this.form.submit()">
<option th:value="itemReviewId" th:selected="${sortBy == 'itemReviewId'}">최신순</option>
<option th:value="highScore" th:selected="${sortBy == 'highScore'}">별점 높은 순</option>
<option th:value="lowScore" th:selected="${sortBy == 'lowScore'}">별점 낮은 순</option>
</select>
</div>
</form>
<div class="container py-2">
<div class="row py-5">
<!-- 폼 요소들 -->
<div class="row">
<div th:if="${not #lists.isEmpty(itemReviews)}">
<h3 class="h3-indent">리뷰 목록</h3>
<br>
<div th:each="itemReview : ${itemReviews}" class="item-box">
<div class="item-info" style="text-align: right;">
<div>
<span style="font-weight: bold;">작성일 : </span>
<span id="itemReviewCreateDate"
th:text="${#temporals.format(itemReview.itemReviewCreateDate, 'yyyy-MM-dd')}"></span>
</div>
</div>
<div class="item-info">
<div>
<span class="category-label">제목 : </span>
<span th:text="${itemReview.itemReviewName}"></span>
</div>
<!-- 별점 및 평점 표시 -->
<div class="d-flex justify-content-left mb-1 align-items-left">
<span class="category-label">별점 : </span>
<ul class="list-unstyled d-flex mb-0" id="starRating"
th:attr="data-rating=${itemReview.itemScore}">
<!-- 별 아이콘은 JavaScript로 생성됩니다 -->
</ul>
</div>
<div>
<span class="category-label">리뷰 내용 : </span>
<div style="text-align: left;">
<span th:utext="${itemReview.itemReviewContent}"></span>
</div>
</div>
<input type="hidden" id="itemId" th:value="${itemReview.itemId}">
<!-- 수정 및 삭제 버튼 -->
<div class="container">
<div class="row justify-content-end">
<div class="col-auto">
<a th:href="@{/ypjs/itemReview/update/{itemReviewId}(itemReviewId=${itemReview.itemReviewId})}"
class="btn btn-warning">수정하기</a>
</div>
<div class="col-auto">
<button type="button"
class="btn btn-danger btn-sm btn-delete-itemReview"
th:data-itemReviewId="${itemReview.itemReviewId}"
style="padding: 2px 10px; font-size: 16px; width: 90px; height: 40px;">
삭제하기
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 페이지네이션 시작 -->
<nav th:unless="${#lists.isEmpty(itemReviews)}" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${page == 0} ? 'disabled'">
<a class="page-link" th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0].itemId}, page=${(page > 0 ? page - 1 : 0)}, size=${size}, sortBy=${sortBy})}">이전</a>
</li>
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}" th:classappend="${page == i} ? 'active'">
<a class="page-link" th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0].itemId}, page=${i}, size=${size}, sortBy=${sortBy})}" th:text="${i + 1}"></a>
</li>
<li class="page-item" th:classappend="${page == totalPages - 1} ? 'disabled'">
<a class="page-link" th:href="@{/ypjs/itemReview/get/{itemId}(itemId=${itemReviews[0].itemId}, page=${(page < totalPages - 1 ? page + 1 : totalPages - 1)}, size=${size}, sortBy=${sortBy})}">다음</a>
</li>
</ul>
</nav>
<!--페이지네이션 끝-->
<!-- End Contact -->
</div>
</div>
</div>
<footer th:replace="frame/footer :: footer"></footer>
</body>
</html>
스타일, 정렬부분 추가