메인 화면과 상품 상세 페이지

진크·2022년 3월 5일
0
post-thumbnail

메인 화면

메인 페이지 구현도 상품 관리 메뉴 구현과 비슷합니다.

MainItemDto 생성

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 객체로 바로 받아올 수 있습니다.

ItemRepositoryCustom 수정

메인에서 사용할 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);

    }
}

ItemService 수정

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);
    }
}

MainController 수정

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";
    }
}

main.html 수정

별점은.....아직 미적용입니다.

<!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>

상품 상세 페이지

메인 페이지에서 상품 이미지나 상품 정보를 클릭 시 상품의 상세 정보를 보여주는 페이지를 구현해보겠습니다. 상품 상세 페이지에서는 주문 및 장바구니 추가 기능을 제공합니다.

ItemController 수정

먼저 상품 상세 페이지로 이동할 수 있도록 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";
    }
}

itemDetail.html 생성

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() : 현재 주문할 수량과 상품 한 개당 가격을 곱하여 결제 금액을 구해주는 함수입니다.

얼씨구...왜 하필 이미지가..

profile
철학있는 개발자 - 내가 무지하다는 것을 인정할 때 비로소 배움이 시작된다.

0개의 댓글