RootController
에 ProductService추가
ProductService에 메서드추가
RootController 모델 addObject 추가
DB에 products 테이블에 컬럼 delivery 추가
product_deliveries 테이블도 추가
IProductMapper 에 메서드 추가
ProductMapper.xml 에 ProductEntity에 delivery 컬럼 추가해주고 selectProducts 셀렉트문 추가
index.html에서 타임리프 추가.
로켓배송 이미지 rocket.png, 로켓프레시 이미지 rocket-fresh.png
productEntities를 product로 부른 후
getIndex, getTitle, getPrice 지정.
로켓배송, 로켓프레시 배송에 따라 이미지 다르게 지정 추가.
일반, 로켓배송, 로켓프레시 배송에 따라 도착 보장 텍스트를 달리 지정.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<!--/*@thymesVar id="userEntity" type="dev.dmchoi.coupang.entities.member.UserEntity"*/-->
<head>
<meta charset="UTF-8">
<title>쿠팡</title>
<link th:href="@{/resources/stylesheets/common.css}" rel="stylesheet">
<link th:href="@{/resources/stylesheets/index.css}" rel="stylesheet">
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard-dynamic-subset.css"/>
<script src="https://kit.fontawesome.com/0d51ab0f86.js" crossorigin="anonymous"></script>
</head>
<body>
<th:block th:replace="~{fragments/header.html :: content}"></th:block>
<main class="main">
<aside class="aside">
카테고리 자리임
</aside>
<section class="content">
<section class="controller">
<a class="link selected" href="#">이름순</a>
<a class="link" href="#">낮은가격순</a>
<a class="link" href="#">높은가격순</a>
<span class="spring"></span>
<a class="link" th:href="@{/product/add}" th:if="${userEntity != null && userEntity.isAdmin() == true}">상품
등록</a>
</section>
<section class="products">
<a class="product"
th:each="product : ${productEntities}"
th:href="@{'/product/detail/' + ${product.getIndex()}}"
th:with="dt = ${#dates.createNow()}">
<img alt="상품이미지" class="image" th:src="@{/resources/images/test-product-image.jpg}">
<span class="title" th:text="${product.getTitle()}">니베아 맨 센서티브 쉐이빙 폼</span>
<span class="price">
<span class="number" th:text="${#numbers.formatInteger(product.getPrice(), 3, 'COMMA')}">8,920</span>
<!--
#numbers.formatInteger(숫자, 자리수, 'COMMA' 혹은 'POINT' 혹은 'WHITESPACE')
숫자에 콤마 넣으려고 한거임.
Thousand 1.000
Million 1,000,000
Billion 1, 000,000,000
-->
<span class="won">원</span>
<img alt="로켓배송" class="rocket" th:src="@{resources/images/rocket.png}"
th:if="${product.getDelivery().equals('rocket')}">
<img alt="로켓프레시" class="rocket" th:src="@{resources/images/rocket-fresh.png}"
th:if="${product.getDelivery().equals('rocketFresh')}">
</span>
<span class="due" th:if="${product.getDelivery().equals('normal')}"
th:with="tomorrow = ${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 1)}"
th:text="${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 ) + ') 도착 보장'}"></span>
<span class="due" th:if="${product.getDelivery().equals('rocket')}"
th:with="tomorrow = ${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 1)}"
th:text="${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 ) + ') 도착 보장'}"></span>
<span class="due" th:if="${product.getDelivery().equals('rocketFresh')}"
th:with="tomorrow = ${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 1)}"
th:text="${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 ) + ') 새벽 도착 보장'}"></span>
<span class="stars three">
<i class="star one two three four five fa-solid fa-star"></i>
<i class="star two three four five fa-solid fa-star"></i>
<i class="star three four five fa-solid fa-star"></i>
<i class="star four five fa-solid fa-star"></i>
<i class="star five fa-solid fa-star"></i>
</span>
</a>
</section>
</section>
</main>
</body>
</html>
상품 등록을 일반, 로켓배송, 로켓프레시 배송으로 달리 해보았다.
이미지 제외 입력한대로 잘 불러와진다. 확인
등록된 상품을 들어가면 정보를 확인하기 위해
ProductController에 getDetail Get메서드 만든다.
DetailResult enum 추가
DetailVo 추가
ProductController에 DetailVo 추가
ProductService
IProductMapper에 인덱스로 해당 상품을 구분하기 위한 메서드 추가
xml 추가
ProductService 상품의 정보를 가진 productEntity 를 상속하는 DetailVo 타입. 상품의 인덱스를 가져왔을 때 상품이 없으면 NOT_FOUND , 있으면 그 정보를 detailVo에 담는다.
ProductController에서 상품 정보가 담긴 detailVo를 호출. modelAndView에 추가.
detail.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<!--/*@thymesVar id="userEntity" type="dev.dmchoi.coupang.entities.member.UserEntity"*/-->
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title th:text="'쿠팡 ! - ' + ${detailVo.getTitle()} "></title>
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard-dynamic-subset.css"/>
<link th:href="@{/resources/stylesheets/common.css}" rel="stylesheet">
<link th:href="@{/product/resources/stylesheets/detail.css}" rel="stylesheet">
<script th:if="${detailVo != null && detailVo.getResult().name().equals('NOT_FOUND')}">
alert('존재하지 않는 상품입니다.');
window.history.back();
</script>
<script src="https://kit.fontawesome.com/0d51ab0f86.js" crossorigin="anonymous"></script>
<script defer th:src="@{/resources/libraries/ckeditor/ckeditor.js}"></script>
<script defer th:src="@{/product/resources/scripts/add.js}"></script>
</head>
<body>
<th:block th:replace="~{fragments/header.html :: content}"></th:block>
<main class="main">
<section class="top">
<section class="left">
<div class="no-image" th:if="${detailVo.getThumbnailId() == null}">상품 이미지가 등록되지 않았습니다.</div>
<form class="thumbnail-form" th:action="@{/product/detail/thumbnail/add}" th:if="${userEntity != null && userEntity.isAdmin() == true}" enctype="multipart/form-data" method="post">
<input name="thumbnail" type="file" accept="image/png, image/jpeg">
<input type="submit" value="상품 이미지 등록">
</form>
</section>
<section class="right">
<span class="title" th:text="${detailVo.getTitle()}"></span>
<span class="stars five">
<i class="star one two three four five fa-solid fa-star"></i>
<i class="star two three four five fa-solid fa-star"></i>
<i class="star three four five fa-solid fa-star"></i>
<i class="star four five fa-solid fa-star"></i>
<i class="star five fa-solid fa-star"></i>
<a class="count" href="#review">12,345개 상품평</a>
</span>
<span class="price-container">
<span class="price" th:text="${#numbers.formatInteger(detailVo.getPrice(), 3, 'COMMA')}"></span>
<span class="won">원</span>
<img alt="로켓배송" class="rocket" th:src="@{/resources/images/rocket.png}"
th:if="${detailVo.getDelivery().equals('rocket')}">
<img alt="로켓프레시" class="rocket" th:src="@{/resources/images/rocket-fresh.png}"
th:if="${detailVo.getDelivery().equals('rocketFresh')}">
</span>
<span class="delivery-container" th:with="dt=${#dates.createNow()}">
<span class="fee" th:if="${detailVo.getDelivery().equals('normal')}">배송비 3,000원</span>
<span class="fee" th:if="${detailVo.getDelivery().equals('rocket') || detailVo.getDelivery().equals('rocketFresh')}">무료배송</span>
<span class="due" style="color: black;" th:if="${detailVo.getDelivery().equals('normal')}"
th:with="tomorrow = ${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 2)}"
th:text="${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 ) + ') 도착 예정'}"></span>
<span class="due" th:if="${detailVo.getDelivery().equals('rocket')}"
th:with="tomorrow = ${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 1)}"
th:text="${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 ) + ') 도착 보장'}"></span>
<span class="due" th:if="${detailVo.getDelivery().equals('rocketFresh')}"
th:with="tomorrow = ${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 1)}"
th:text="${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 ) + ') 새벽 도착 보장'}"></span>
</span>
<div class="order-container">
<label class="quantity-container">
<span hidden>개수</span>
<input class="quantity-input" max="99" min="1" name="quantity" type="number" value="1">
</label>
<a class="button cart" href="#">장바구니 넣기</a>
<a class="button order" href="#">바로 구매하기</a>
</div>
</section>
</section>
<section class="body">
<h2 class="title">상품 상세</h2>
<section class="content" th:utext="${detailVo.getContent()}"></section>
</section>
<section class="review">
<h2 class="title">상품 리뷰</h2>
</section>
</main>
</body>
</html>
detail.css
@charset "UTF-8";
body > .main {
width: var(--content-max-width);
align-items: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-top: 1rem;
padding-bottom: 3rem;
}
body > .main > .top {
align-items: flex-start;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
body > .main > .top > .left {
width: calc(var(--content-max-width) * 0.4);
align-items: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-right: 1.5rem;
}
body > .main > .top > .left > .no-image {
width: calc(var(--content-max-width) * 0.4);
height: calc(var(--content-max-width) * 0.4);
background-color: rgb(220, 220, 220);
align-items: center;
color: rgb(140, 140, 140);
display: flex;
flex-direction: column;
font-size: 1.125rem;
justify-content: center;
}
body > .main > .top > .left > .thumbnail-form {
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-top: 0.5rem;
}
body > .main > .top > .left > .thumbnail-form > input {
width: auto;
}
body > .main > .top > .left > .thumbnail-form > input[type=submit] {
background-color: rgb(66, 132, 243);
border-radius: 0.125rem;
color: rgb(255, 255, 255);
cursor: pointer;
padding: 0.5rem 1rem;
}
body > .main > .top > .right {
align-items: stretch;
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-start;
}
body > .main > .top > .right > .title {
font-size: 1.375rem;
font-weight: 500;
margin-bottom: 0.125rem;
text-align: justify;
}
body > .main > .top > .right > .stars {
border-bottom: 0.0625rem solid rgb(230, 230, 230);
color: rgb(205, 205, 205);
letter-spacing: -0.125rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
}
body > .main > .top > .right > .stars.one > .star.one {
color: rgb(255, 151, 0);
}
body > .main > .top > .right > .stars.two > .star.two {
color: rgb(255, 151, 0);
}
body > .main > .top > .right > .stars.three > .star.three {
color: rgb(255, 151, 0);
}
body > .main > .top > .right > .stars.four > .star.four {
color: rgb(255, 151, 0);
}
body > .main > .top > .right > .stars.five > .star.five {
color: rgb(255, 151, 0);
}
body > .main > .top > .right > .stars > .count {
color: rgb(66, 132, 243);
font-size: 0.8rem;
letter-spacing: normal;
margin-left: 0.25rem;
}
body > .main > .top > .right > .price-container {
align-items: center;
border-bottom: 0.0625rem solid rgb(230, 230, 230);
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
padding-bottom: 1rem;
}
body > .main > .top > .right > .price-container > .price {
color: rgb(174, 0, 0);
font-size: 1.75rem;
font-weight: 600;
}
body > .main > .top > .right > .price-container > .won {
color: rgb(174, 0, 0);
font-size: 1.75rem;
}
body > .main > .top > .right > .price-container > .rocket {
height: 1rem;
margin-left: 0.75rem;
}
body > .main > .top > .right > .delivery-container {
align-items: flex-start;
border-bottom: 0.0625rem solid rgb(230, 230, 230);
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-bottom: 1rem;
padding-bottom: 1rem;
}
body > .main > .top > .right > .delivery-container > .fee {
font-weight: 500;
margin-bottom: 0.25rem;
}
body > .main > .top > .right > .delivery-container > .due {
color: rgb(0, 137, 26);
font-size: 1rem;
font-weight: 500;
}
body > .main > .top > .right > .order-container {
align-items: stretch;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
body > .main > .top > .right > .order-container > * + * {
margin-left: 0.5rem;
}
body > .main > .top > .right > .order-container > .quantity-container {
align-items: stretch;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
body > .main > .top > .right > .order-container > .quantity-container > .quantity-input {
width: 5rem;
border: 0.0625rem solid rgb(200, 200, 200);
border-radius: 0.125rem;
font-size: 1.25rem;
font-weight: 500;
text-align: center;
}
body > .main > .top > .right > .order-container > .button {
border-radius: 0.125rem;
flex: 1;
padding: 0.75rem 1rem;
text-align: center;
}
body > .main > .top > .right > .order-container > .button.cart {
border: 0.0625rem solid rgb(200, 200, 200);
}
body > .main > .top > .right > .order-container > .button.order {
background-color: rgb(66, 132, 243);
color: rgb(255, 255, 255);
}
body > .main > .body,
body > .main > .review {
margin-top: 1.5rem;
}
body > .main > .body > .title,
body > .main > .review > .title {
background-color: rgb(240, 240, 240);
border-top: 0.125rem solid rgb(60, 60, 60);
border-bottom: 0.0625rem solid rgb(200, 200, 200);
font-size: 1.5rem;
font-weight: 500;
padding: 0.5rem 1rem;
}
상품을 클릭해 들어가보면 그 상품 인덱스로 구분하여 조회한다.
이제 썸네일을 받아와보자.
ProductController
ProductService
테이블 추가
ThumbnailEntity
IProductMapper
xml
ProductService 에서 해당 상품의 썸네일 id를 해싱
IProductMapper 에서 썸네일을 포함한 상품 정보로 업데이트해주자
xml
ProductService
ProductController
detail.html form태그 안 name = productIndex, name = thumbnail 연결되어있음.
IProductMapper
xml
ProductService
ProductController
업로드 해보면
product_thumbnail DB
products DB에도 썸네일 추가한 레코드에 값이 들어간다