Portfolio, Project/Project(Programming)

Project (5-3) 메인 화면, 상품 상세 페이지 (1) (추가 내용 작성 필요할 듯...)

잇(IT) 2023. 8. 11. 16:17
- 메인 페이지

- 메인 페이지의 경우 상품 관리 메뉴와 비슷하며, Querydsl을 사용하여 페이징 처리 및 네비게이션바에 있는 Search 버튼을 이용하여 상품명으로 검색이 가능하도록 구현하겠다.

- 메인페이지의 경우 @QueryProjection을 이용하여 상품 조회 시 DTO 객체로 결과 값을 받는 방법을 사용한다.

- @QueryProjection을 이용하면 Item 객체로 값을 받은 후 DTO 클래스로 변환하는 과정 없이 바로 DTO 객체를 뽑아낼 수 있다.

 

- MainItemDto.java

@Getter @Setter
public class MainItemDto {

    private Long id;

    private String itemNm;

    private String itemDetail;

    private String imgUrl;

    private Integer price;

    @QueryProjection
    // 생성자에 @QueryProjection 어노테이션을 선언하여 Querydsl로 결과 조회 시
    // MainItemDto 객체로 바로 받아 오도록 활용한다.
    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 객체로 바로 받아 오도록 활용할 수 있다.

- @QueryProjection을 사용하면 Querydsl을 사용하는 것이기 때문에 엔티티와 마찬가지로 Q가 붙은 클래스를 생성해줘야 한다.

 

- ItemRepositoryCustom 클래스에 메인 페이지에 보여줄 상품 리스트를 가져오는 메소드를 생성한다.

- ItemRepositoryCustom.java

public interface ItemRepositoryCustom {

	...
    
    Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
}

 

- getMainItemPage() 메소드를 Impl에 구현합니다.

- ItemRepositoryCustomImpl.java

private BooleanExpression itemNmLike(String searchQuery) {
        // 검색어가 null이 아니면 상품명에 해당 검색어가 포함되는 상품을 조회하는 조건을 반환한다.
        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;

        QueryResults<MainItemDto> results = queryFactory
                .select(
                        new QMainItemDto(
                                //QmainItemDto의 생성자에 반환할 값들을 넣어준다.
                                //@QueryProjection을 사용하면 DTO로 바로 조회가 가능하다.
                                //엔티티 조회 후 DTO로 변환하는 과정을 줄일 수 있다.
                                item.id,
                                item.itemNm,
                                item.itemDetail,
                                itemImg.imgUrl,
                                item.price)
                )
                .from(itemImg)
                .join(itemImg.item, item)
                //itemImg와 item을 내부 조인한다.
                .where(itemImg.repImgYn.eq("Y"))
                //상품 이미지의 경우 대표 상품 이미지만 불러온다.
                .where(itemNmLike(itemSearchDto.getSearchQuery()))
                .orderBy(item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<MainItemDto> content = results.getResults();
        long total = results.getTotal();
        return new PageImpl<>(content, pageable, total);
    }
}

- itemNmLike 메서드를 통해 검색 조건 메서드를 생성한다.

 

 QueryResults<MainItemDto> results = queryFactory
                .select(
                        new QMainItemDto(
                                //QmainItemDto의 생성자에 반환할 값들을 넣어준다.
                                //@QueryProjection을 사용하면 DTO로 바로 조회가 가능하다.
                                //엔티티 조회 후 DTO로 변환하는 과정을 줄일 수 있다.
                                item.id,
                                item.itemNm,
                                item.itemDetail,
                                itemImg.imgUrl,
                                item.price)
                )

- @QueryProjection을 설정해준 MainItemDto로 바로 조회가 가능하다. 엔티티로 조회 후 DTO로 변환하는 과정을 줄일 수 있다.

- 즉, DB에 있는 item, itemImg 엔티티를 조회해서 바로 MainItemDto에 값을 복사할 수 있다.

 

.from(itemImg)
                .join(itemImg.item, item)
                //itemImg와 item을 내부 조인한다.
                .where(itemImg.repImgYn.eq("Y"))
                //상품 이미지의 경우 대표 상품 이미지만 불러온다.
                .where(itemNmLike(itemSearchDto.getSearchQuery()))
                .orderBy(item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<MainItemDto> content = results.getResults();
        long total = results.getTotal();
        return new PageImpl<>(content, pageable, total);
    }
}

- 쿼리 조건을 생성하여 조건에 맞게 DB에서 데이터를 가져온다.

- 쿼리 결과로 나온 엔티티를 List<MainItemDto> 리스트에 담는다.

- getTotal() 메서드를 통해 총 결과수를 가져오고, 해당 결과 값들을 파라미터로 넘겨 Page 클래스의 구현체인 PageImpl<> 객체를 생성한다.

 

- 메인 페이지에 보여줄 상품 데이터를 조회하는 메소드를 ItemService 클래스에 추가한다.

- ItemService.java

...

@Transactional(readOnly = true)
    public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
        return itemRepository.getMainItemPage(itemSearchDto, pageable);
    }
}

 

- 메인 페이지에 상품 데이터를 보여주기 위해서 기존에 작성했던 MainController 클래스를 수정한다.

- MainController.java

@Controller
@RequiredArgsConstructor
public class MainController {

    private final ItemService itemService;

    @GetMapping(value = "/")
    public String main(ItemSearchDto itemSearchDto, Optional<Integer> page, Model model) {
        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 6);
        Page<MainItemDto> items =
                itemService.getMainItemPage(itemSearchDto, pageable);
        model.addAttribute("items", items);
        model.addAttribute("itemSearchDto", itemSearchDto);
        model.addAttribute("maxPage", 5);
        return "main";
    }
}
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 6);

- 페이마다 총 6개의 엔티티를 출력한다.

Page<MainItemDto> items =
                itemService.getMainItemPage(itemSearchDto, pageable);
        model.addAttribute("items", items);
        model.addAttribute("itemSearchDto", itemSearchDto);
        model.addAttribute("maxPage", 5);
        return "main";
    }
}

- 위 코드는 상품 관리 코드와 유사한 것을 확인 할 수 있다.

- itemService 클래스에서 작성한 getMainItemPage 메서드를 이용해서 item의 id를 받아오면 해당 아이템에 있는 이미지들을 가져오고, item에 대한 상세 정보들을 Dto 객체에 복사해서 itemFormDto 객체를 반환한다.

- itemFormDto 객체에는 itemImgDtoList 필드가 있기 때문에 item id 조회를 통해 가져온 이미지들을 해당 List에 담아서 DTO로 전달 할 수 있다.

- main.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .carousel-inner > .item {
            height: 350px;
        }
        .margin{
            margin-bottom:30px;
        }
        .banner{
            height: 300px;
            position: absolute; top:0; left: 0;
            width: 100%;
            height: 100%;
        }
        .card-text{
            text-overflow: ellipsis;
            white-space: nowrap;
            overflow: hidden;
        }
        a:hover{
            text-decoration:none;
        }
        .center{
            text-align:center;
        }
    </style>
</th:block>

<div layout:fragment="content">

    <div id="carouselControls" class="carousel slide margin" data-ride="carousel">
        <!--1. 부트스트랩의 슬라이드를 보여주는 Carousel 컴포넌트를 이용하여 배너를 만들었다.
        쇼핑몰의 경우 보통 현재 행사 중인 상품을 광고하는 데 사용한다.-->
        <div class="carousel-inner">
            <div class="carousel-item active item">
                <img class="d-block w-100 banner"
                     src="https://img.freepik.com/free-vector/online-shopping-banner-with-3d-shopping-cart-clouds-and-social-icons-vector-illustration_548887-100.jpg" alt="First slide">
                <!--2. 이미지 태그의 src 속성에는 웹상에 존재하는 이미지 경로를 넣어주면 해당 이미지를 보여준다.-->
            </div>
        </div>
    </div>

    <input type="hidden" name="searchQuery" th:value="${itemSearchDto.searchQuery}">
    <!--3. 쇼핑몰 오른쪽 상단의 Search 기능을 이용해서 상품을 검색할 때 페이징 처리 시
    해당 검색어를 유지하기 위해서 hidden 값으로 검색어를 유지한다.-->
    <div th:if="${not #strings.isEmpty(itemSearchDto.searchQuery)}" class="center">
        <p class="h3 font-weight-bold" th:text="${itemSearchDto.searchQuery} + '검색 결과'"></p>
        <!--4. 상품을 검색했을 때 어떤 검색어로 조회된 결과인지를 보여준다.-->
    </div>

    <div class="row">
        <th:block th:each="item, status: ${items.getContent()}">
            <!--5. 조회한 메인 상품 데이터를 보여준다. 부트스트랩의 Card 컴포넌트를 이용했고,
            사용자가 카드 형태로 상품의 이름, 내용, 가격을 볼 수 있다.-->
            <!--items의 참조형은 Page이기 때문에 .getContent()로 가져오게 되면 해당-->
            <div class="col-md-4 margin">
                <div class="card">
                    <a th:href="'/item/' +${item.id}" class="text-dark">
                        <img th:src="${item.imgUrl}" class="card-img-top" th:alt="${item.itemNm}" height="400">
                        <div class="card-body">
                            <h4 class="card-title">[[${item.itemNm}]]</h4>
                            <p class="card-text">[[${item.itemDetail}]]</p>
                            <h3 class="card-title text-danger">[[${item.price}]]원</h3>
                        </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.number eq 0}?'disabled':''">
                <a th:href="@{'/' + '?searchQuery=' + ${itemSearchDto.searchQuery} + '&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:href="@{'/' +'?searchQuery=' + ${itemSearchDto.searchQuery} + '&page=' + ${page-1}}" th:inline="text" class="page-link">[[${page}]]</a>
            </li>

            <li class="page-item" th:classappend="${items.number+1 ge items.totalPages}?'disabled':''">
                <a th:href="@{'/' +'?searchQuery=' + ${itemSearchDto.searchQuery} + '&page=' + ${items.number+1}}" aria-label='Next' class="page-link">
                    <span aria-hidden='true'>Next</span>
                </a>
            </li>

        </ul>
    </div>

</div>

</html>


- 상품 상세 페이지

- 메인 페이지에서 상품 이미지나 상품 정보를 클릭 시 상품의 상세 정보를 보여주는 페이지를 구현해야 한다.

 

- ItemController.java

...

@GetMapping(value = "/item/{itemId}")
    public String itemDtl(Model model, @PathVariable("itemId") Long itemId) {
        ItemFormDto itemFormDto = itemService.getItemDtl(itemId);
        model.addAttribute("item", itemFormDto);
        return "item/itemDtl";
    }
}

 

- 시각적으로 보여 줄 상품 상세 페이지를 작성해준다.

- itemDtl.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<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(){

            calculateToalPrice();

            $("#count").change( function(){
                calculateToalPrice();
            });
        });

        function calculateToalPrice(){
            //1. 현재 주문할 수량과 상품 한 개당 가격을 곱해서 결제 금액을 구해주는 함수다.
            var count = $("#count").val();
            var price = $("#price").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" style="margin-left:25%;margin-right:25%">

    <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="wd50">
            <span th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="badge badge-primary mgb-15">
                판매중
            </span>
            <span th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).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 == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right">
                <button type="button" class="btn btn-light border border-primary btn-lg" onclick="addCart()">장바구니 담기</button>
                <button type="button" class="btn btn-primary btn-lg" onclick="order()">주문하기</button>
            </div>
            <div th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right">
                <button type="button" class="btn btn-danger btn-lg">품절</button>
            </div>
        </div>
    </div>

    <div class="jumbotron jumbotron-fluid mgt-30">
        <div class="container">
            <h4 class="display-5">상품 상세 설명</h4>
            <hr class="my-4">
            <p class="lead" th:text="${item.itemDetail}"></p>
        </div>
    </div>

    <div th:each="itemImg : ${item.itemImgDtoList}" class="text-center">
        <!--2. 등록된 상품 이미지를 반복 구문을 통해 보여주고 있다. 보통 실제 쇼핑몰에서는 상품에 대한 정보를
        좀 더 구체적이고 시각적으로 보기 좋게 만든다.-->
        <img th:if="${not #strings.isEmpty(itemImg.imgUrl)}" th:src="${itemImg.imgUrl}" class="rounded mgb-15" width="800">
    </div>

</div>

</html>
function calculateToalPrice(){
            //1. 현재 주문할 수량과 상품 한 개당 가격을 곱해서 결제 금액을 구해주는 함수다.
            var count = $("#count").val();
            var price = $("#price").val();
            var totalPrice = price*count;
            $("#totalPrice").html(totalPrice + '원');
        }

- 현재 주문할 수량과 상품 한 개당 가격을 곱해서 결제 금액을 구해주는 함수다.

 

- 위와 상품을 클릭하면 상세 페이가 보이는 것을 확인 할 수 있다.

 

728x90