메인 페이지 구현도 상품 관리 메뉴 구현과 비슷합니다.
package me.jincrates.gobook.web.dto;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class MainItemDto {
private Long id;
private String itemNm;
private String itemDetail;
private String imgUrl;
private Integer price;
@QueryProjection
public MainItemDto(Long id, String itemNm, String itemDetail, String imgUrl, Integer price) {
this.id = id;
this.itemNm = itemNm;
this.itemDetail = itemDetail;
this.imgUrl = imgUrl;
this.price = price;
}
}
@QueryProjection
: 생성자에 해당 어노테이션을 선언하면 Querydsl로 결과 조회시 MainItemDto 객체로 바로 받아올 수 있습니다.메인에서 사용할 getMainItemPage()
메소드를 작성합니다.
package me.jincrates.gobook.domain.items;
//...
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {
//...
private BooleanExpression itemNmLike(String searchQuery) {
return StringUtils.isEmpty(searchQuery) ? null : QItem.item.itemNm.like("%" + searchQuery + "%");
}
@Override
public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
QItem item = QItem.item;
QItemImg itemImg = QItemImg.itemImg;
List<MainItemDto> content = queryFactory
.select(
new QMainItemDto(
item.id,
item.itemNm,
item.itemDetail,
itemImg.imgUrl,
item.price)
)
.from(itemImg)
.join(itemImg.item, item)
.where(itemImg.repimgYn.eq("Y"),
itemNmLike(itemSearchDto.getSearchQuery()))
.orderBy(item.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = queryFactory
.select(item.count())
.from(itemImg)
.join(itemImg.item, item)
.where(itemImg.repimgYn.eq("Y"),
itemNmLike(itemSearchDto.getSearchQuery()))
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
}
package me.jincrates.gobook.service;
///
@RequiredArgsConstructor
@Transactional
@Service
public class ItemService {
///
@Transactional(readOnly = true)
public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
return itemRepository.getMainItemPage(itemSearchDto, pageable);
}
}
package me.jincrates.gobook.web;
import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.service.ItemService;
import me.jincrates.gobook.web.dto.ItemSearchDto;
import me.jincrates.gobook.web.dto.MainItemDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.Optional;
@RequiredArgsConstructor
@Controller
public class MainController {
private final ItemService itemService;
@GetMapping(value = "/")
public String main(ItemSearchDto itemSearchDto, @PathVariable("page") Optional<Integer> page, Model model) {
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 8);
Page<MainItemDto> items = itemService.getMainItemPage(itemSearchDto, pageable);
model.addAttribute("items", items);
model.addAttribute("itemSearchDto", itemSearchDto);
model.addAttribute("maxPage", 5);
return "main";
}
}
별점은.....아직 미적용입니다.
<!DOCTYPE html>
<html xmlns:th="http//www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/default}">
<div layout:fragment="content">
<div class="mt-5">
<div class="row gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
<th:block th:each="item, status: ${items.getContent()}">
<div class="item col mb-5">
<div class="card h-100">
<a href="'/item/' + ${item.id}">
<!-- Product image-->
<!-- <img class="card-img-top" src="https://dummyimage.com/450x300/dee2e6/6c757d.jpg" alt="..." />-->
<img class="card-img-top" th:src="${item.imgUrl}" th:alt="${item.itemNm}" height="200px" />
<!-- Product details-->
<div class="card-body p-4">
<div class="text-center">
<!-- Product name-->
<h5 class="fw-bolder">[[${item.itemNm}]]</h5>
<!-- Product reviews-->
<div class="d-flex justify-content-center small text-warning mb-2">
<div class="bi-star-fill"></div>
<div class="bi-star-fill"></div>
<div class="bi-star-fill"></div>
<div class="bi-star-fill"></div>
<div class="bi-star-fill"></div>
</div>
<!-- Product price-->
[[${#numbers.formatCurrency(item.price)}]]
</div>
</div>
<!-- Product actions-->
<div class="card-footer p-4 pt-0 border-top-0 bg-transparent">
<div class="text-center"><a class="btn btn-outline-dark mt-auto" href="#">Add to cart</a></div>
</div>
</a>
</div>
</div>
</th:block>
</div>
<div th:with="start = ${(items.number / maxPage) * maxPage + 1}, end = (${(items.totalPages == 0) ? 1 : (start + (maxPage - 1) < items.totalPages ? start + (maxPage - 1) : items.totalPages)})" >
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${items.first}?'disabled'">
<a th:onclick="'javascript:page(' + ${items.number - 1} + ')'" aria-label='Previous' class="page-link">
<span aria-hidden='true'>Previous</span>
</a>
</li>
<li class="page-item" th:each="page: ${#numbers.sequence(start, end)}" th:classappend="${items.number eq page-1}?'active':''">
<a th:onclick="'javascript:page(' + ${page - 1} + ')'" th:inline="text" class="page-link">[[${page}]]</a>
</li>
<li class="page-item" th:classappend="${items.last}?'disabled'">
<a th:onclick="'javascript:page(' + ${items.number + 1} + ')'" aria-label='Next' class="page-link">
<span aria-hidden='true'>Next</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</html>
메인 페이지에서 상품 이미지나 상품 정보를 클릭 시 상품의 상세 정보를 보여주는 페이지를 구현해보겠습니다. 상품 상세 페이지에서는 주문 및 장바구니 추가 기능을 제공합니다.
먼저 상품 상세 페이지로 이동할 수 있도록 ItemContrller 클래스에 코드를 추가합니다.
package me.jincrates.gobook.web;
///
@RequiredArgsConstructor
@Controller
public class ItemController {
///
@GetMapping(value = "/item/{itemId}")
public String itemDetail(@PathVariable("itemId") Long itemId, Model model) {
ItemFormDto itemFormDto = itemService.getItemDetail(itemId);
model.addAttribute("item", itemFormDto);
return "item/itemDetail";
}
}
resouces/templates/item 폴더 하위에 itemDetail.html 파일을 만듭니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/default}">
<head>
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
</head>
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function(){
calculateTotalPrice();
$("#count").change(function() {
calculateTotalPrice();
});
});
function calculateTotalPrice() {
var price = $("#price").val();
var count = $("#count").val();
var totalPrice = price * count;
$("#totalPrice").html(totalPrice + '원');
}
</script>
</th:block>
<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
<style>
.mgb-15{
margin-bottom:15px;
}
.mgt-30{
margin-top:30px;
}
.mgt-50{
margin-top:50px;
}
.repImgDiv{
margin-right:15px;
height:auto;
width:50%;
}
.repImg{
width:100%;
height:400px;
}
.wd50{
height:auto;
width:50%;
}
</style>
</th:block>
<div layout:fragment="content">
<input type="hidden" id="itemId" th:value="${item.id}">
<div class="d-flex">
<div class="repImgDiv">
<img th:src="${item.itemImgDtoList[0].imgUrl}" class = "rounded repImg" th:alt="${item.itemNm}">
</div>
<div class="ms-3 w-50">
<span th:if="${item.itemSellStatus.toString().equals('SELL')}" class="badge btn-primary mgb-15">
판매중
</span>
<span th:unless="${item.itemSellStatus.toString().equals('SELL')}" class="badge btn-danger mgb-15" >
품절
</span>
<div class="h4" th:text="${item.itemNm}"></div>
<hr class="my-4">
<div class="text-right">
<div class="h4 text-danger text-left">
<input type="hidden" th:value="${item.price}" id="price" name="price">
<span th:text="${item.price}"></span>원
</div>
<div class="input-group w-50">
<div class="input-group-prepend">
<span class="input-group-text">수량</span>
</div>
<input type="number" name="count" id="count" class="form-control" value="1" min="1">
</div>
</div>
<hr class="my-4">
<div class="text-right mgt-50">
<h5>결제 금액</h5>
<h3 name="totalPrice" id="totalPrice" class="font-weight-bold"></h3>
</div>
<div th:if="${item.itemSellStatus.toString().equals('SELL')}" class="text-right">
<button type="button" class="btn btn-light border btn-outline-dark btn-lg" onclick="addCart()">장바구니 담기</button>
<button type="button" class="btn btn-dark btn-lg" onclick="order()">주문하기</button>
</div>
<div th:unless="${item.itemSellStatus.toString().equals('SELL')}" class="text-right">
<button type="button" class="btn btn-danger btn-lg">품절</button>
</div>
</div>
</div>
<div class="mgt-30">
<div class="container">
<h4 class="display-6">상품 상세 설명</h4>
<hr class="my-4">
<p class="lead" th:text="${item.itemDetail}"></p>
</div>
</div>
<div th:each="itemImg : ${item.itemImgDtoList}" class="text-center">
<img th:if="${not #strings.isEmpty(itemImg.imgUrl)}" th:src="${itemImg.imgUrl}" class="rounded mgb-15" width="800">
</div>
</div>
</html>
calculateTotalPrice()
: 현재 주문할 수량과 상품 한 개당 가격을 곱하여 결제 금액을 구해주는 함수입니다.얼씨구...왜 하필 이미지가..