IT 프로젝트/쇼핑몰 만들기

[Spring Boot] 스프링 부트 프로젝트/쇼핑몰 만들기 - 상품 상세 조회

happygram 2020. 2. 9. 22:56

설명

상품 클릭 시 상품의 상세한 내용을 보여주는 기능을 구현하도록 하겠습니다.

Database

data.sql

테스트를 위해서 discount 값을 수정합니다.

...
-- 청바지
INSERT INTO product
(name, price, description, image_url, color, size, category_id, discount, create_timestamp, update_timestamp)
VALUES('Blue Jeans', 39800, '일자 청바지입니다.', '/images/pants-2.png', 'Blue,Black', '28,29,30,31,32,33,34', 103, 10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
...

Dependencies

build.gradle

DTO 와 Entity 변환을 수월하게 하기 위해서 modelMapper 를 추가합니다.

dependencies {
    ...
    compile group: 'org.modelmapper', name: 'modelmapper', version: '2.3.5'
    ...
}

Back-End

BeanConfiguration.java

Bean 정의를 모아놓을 클래스를 작성합니다.

package happygram.ecommerce.config;

import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfiguration {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }

}

ProductDto.java

Entity 변수와 대부분 동일하고, 서비스 레이어에서 특정 값을 계산 후 Front-End 에 전달하기 위해서 discountPrice 변수를 추가 하였습니다.

package happygram.ecommerce.dto;

import java.time.LocalDateTime;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Setter
@Getter
public class ProductDto {

    private String name;

    private Long price;

    private String description;

    private String imageUrl;

    private String color;

    private String size;

    private Long discount;

    private Long categoryId;

    private LocalDateTime createTimestamp;

    private LocalDateTime updateTimestamp;

    private Long discountPrice;

}

ProductService.java

modelMapper 오브젝트를 이용하여 데이터베이스에서 가져온 값을 그대로 DTO 클래스에 매핑합니다.
discountPrice 값은 계산하여 적용합니다.

@Service
public class ProductService {

    @Autowired
    private ModelMapper modelMapper;

    @Autowired
    private ProductRepository productRepository;

    /**
     * 상품 상세 조회
     * @param categoryId
     * @return
     */
    public ProductDto getProduct(Long categoryId){
        ProductDto productDto = modelMapper.map(productRepository.findById(categoryId).get(), ProductDto.class);
        productDto.setDiscountPrice(productDto.getPrice() * (100 - productDto.getDiscount())/100);
        return productDto;
    }
    ...
}

ProductController.java

@Controller
@RequestMapping("/product")
public class ProductController {

   @Autowired
   private ProductService productService;

   /**
    * 상품 상세 조회
    */
   @RequestMapping(value = "/view/detail/{id}")
   public String viewProductDetail(@PathVariable Long id, Model model) {

      // data
      ProductDto productDto = productService.getProduct(id);
      model.addAttribute("product", productDto);

      // view
      model.addAttribute("template", "fragments/content/product/detail");
      return "index";
   }
   ...
}

Front-End

list.html

th:href 부분을 다음처럼 product.id 값을 받을 수 있게 수정합니다.

...
<!-- image -->
<div class="text-center">
    <a th:href="@{'/product/view/detail/' + ${product.id}}">
        <img class="img-fluid" th:src="@{'/static/' + ${product.imageUrl}}" alt="Product picture">
    </a>
</div>
...

detail.html

Back-End 에서 받은 product 값을 이용하여 값을 채웁니다.
(설명, 리뷰, 평가 등 부가적인 내용은 추후 완성 예정입니다)

<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
  <!-- Content Header (Page header) -->
  <section class="content-header">
    <div class="container-fluid">
      <div class="row mb-2">
        <div class="col-sm-6">
          <h1>티셔츠</h1>
        </div>
        <div class="col-sm-6">
          <ol class="breadcrumb float-sm-right">
            <li class="breadcrumb-item"><a href="#">Home</a></li>
            <li class="breadcrumb-item active">티셔츠</li>
          </ol>
        </div>
      </div>
    </div><!-- /.container-fluid -->
  </section>

  <!-- Main content -->
  <section class="content">

    <!-- Default box -->
    <div class="card card-solid">
      <div class="card-body">
        <div class="row">
          <div class="col-12 col-sm-6">
            <h3 class="d-inline-block d-sm-none" th:text="${product.name}"></h3>
            <div class="col-12">
              <img th:src="@{'/static/' + ${product.imageUrl}}" class="product-image" alt="Product Image">
            </div>
            <div class="col-12 product-image-thumbs">
              <div class="product-image-thumb active"><img th:src="@{'/static/' + ${product.imageUrl}}" alt="Product Image"></div>
              <div class="product-image-thumb" ><img th:src="@{'/static/' + ${product.imageUrl}}" alt="Product Image"></div>
              <div class="product-image-thumb" ><img th:src="@{'/static/' + ${product.imageUrl}}" alt="Product Image"></div>
              <div class="product-image-thumb" ><img th:src="@{'/static/' + ${product.imageUrl}}" alt="Product Image"></div>
              <div class="product-image-thumb" ><img th:src="@{'/static/' + ${product.imageUrl}}" alt="Product Image"></div>
            </div>
          </div>
          <div class="col-12 col-sm-6">
            <h3 class="my-3" th:text="${product.name}"></h3>
            <p th:text="${product.description}">
            </p>
            <hr>
            <h4>색상</h4>
            <div class="btn-group btn-group-toggle" data-toggle="buttons">
              <label class="btn btn-default text-center active" th:each="color,status : ${#strings.arraySplit(product.color, ',')}">
                <input type="radio" name="color_option" th:id="'color_option' + ${status.index}" autocomplete="off" checked="" th:text="${color}">
                <br>
                <i class="fas fa-circle fa-2x" th:classappend="'text-' + ${#strings.toLowerCase(color)}"></i>
              </label>
            </div>

            <h4 class="mt-3">사이즈</h4>
            <div class="btn-group btn-group-toggle" data-toggle="buttons">
              <label class="btn btn-default text-center" th:each="size,status : ${#strings.arraySplit(product.size, ',')}">
                <input type="radio" name="size_option" th:id="'size_option' + ${status.index}" autocomplete="off">
                <span class="text-xl" th:text="${size}"></span>
              </label>
            </div>

            <div class="bg-gray py-2 px-3 mt-4">
              <h4 class="mt-0">
                <small><del th:text="${product.price}"></del>원</small>
              </h4>
              <h2 class="mb-0" th:text="${product.discountPrice + '원'}">
              </h2>
            </div>

            <div class="mt-4">
              <div class="btn btn-primary btn-lg btn-flat">
                <i class="fas fa-cart-plus fa-lg mr-2"></i> 
                Add to Cart
              </div>

              <div class="btn btn-default btn-lg btn-flat">
                <i class="fas fa-heart fa-lg mr-2"></i> 
                Add to Wishlist
              </div>
            </div>

            <div class="mt-4 product-share">
              <a href="#" class="text-gray">
                <i class="fab fa-facebook-square fa-2x"></i>
              </a>
              <a href="#" class="text-gray">
                <i class="fab fa-twitter-square fa-2x"></i>
              </a>
              <a href="#" class="text-gray">
                <i class="fas fa-envelope-square fa-2x"></i>
              </a>
              <a href="#" class="text-gray">
                <i class="fas fa-rss-square fa-2x"></i>
              </a>
            </div>

          </div>
        </div>
        <div class="row mt-4">
          <nav class="w-100">
            <div class="nav nav-tabs" id="product-tab" role="tablist">
              <a class="nav-item nav-link active" id="product-desc-tab" data-toggle="tab" href="#product-desc" role="tab" aria-controls="product-desc" aria-selected="true">설명</a>
              <a class="nav-item nav-link" id="product-comments-tab" data-toggle="tab" href="#product-comments" role="tab" aria-controls="product-comments" aria-selected="false">리뷰</a>
              <a class="nav-item nav-link" id="product-rating-tab" data-toggle="tab" href="#product-rating" role="tab" aria-controls="product-rating" aria-selected="false">평가</a>
            </div>
          </nav>
          <div class="tab-content p-3" id="nav-tabContent">
            <div class="tab-pane fade show active" id="product-desc" role="tabpanel" aria-labelledby="product-desc-tab">상세 설명</div>
            <div class="tab-pane fade" id="product-comments" role="tabpanel" aria-labelledby="product-comments-tab">리뷰</div>
            <div class="tab-pane fade" id="product-rating" role="tabpanel" aria-labelledby="product-rating-tab">평가</div>
          </div>
        </div>
      </div>
      <!-- /.card-body -->
    </div>
    <!-- /.card -->

  </section>
  <!-- /.content -->
</div>
<!-- /.content-wrapper -->