상품을 등록을 했으니 상품이 뜨도록 해보자.
index
페이지이기 때문에 ProductController통하지않고 rootController에서 상품 글어와서 index.html의 products섹션에 다 뿌려야한다.-> RootController
-> ProductService getProdeucts()
-> IProductMapper selectProdeucts
- ProductEntity타입을 돌려주는 selectProdeucts 생성.
-> ProductMapper.xml selectProdeucts
-> ProductService getProdeucts()
-> RootController
ProductEntity[] productEntities = this.productService.getProducts(); modelAndView.addObject("productEntities", productEntities); 추가
-> 테이블 수정 배송타입 추가 (delivery_value
)
products
테이블에delivery_value
추가product_deliveries
테이블 추가 : 배송 타입에 대한 테이블
- 일반배송, 로켓배송, 로켓프레쉬
productEntity
매개변수 추가 /ProductMapper.xml
쿼리 추가 수정!
-> index.html
<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/cider-product.png}">
<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>
< 위 index.html에 대한 풀이 >
-> index.html 의 products
라는 클래스 section 아래에서 a
태그 자체가 반복이 되야함.
<section class="products"> <a class="product" th:each="product : ${productEntities}" th:href="@{'/product/detail/' + ${product.getIndex()}}" th:with="dt = ${#dates.createNow()}">
-> title 설정
<span class="number" th:text="${#numbers.formatInteger(product.getPrice(), 3, 'COMMA')}">8,920</span>
- 가격정보에 콤마를 넣기위해서 추가(외우면 좋지만 검색을 이용하자.)
- 여기서 구글 검색 팁
내가 지금 하고 있는 것 부터 맨 앞에 적기 / 바꾼다. (format = 동사다) / 콤마
thymeleft format with comma
-> 배송타입에 따른 이미지 설정
<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 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>
${T(org.apache.commons.lang3.time.DateUtils).addDays(dt, 1)}"
DateUtils
: lang3에서 지원하는 어떤 날짜에 시간이나 분을 추가위해서 사용.DateUtils
타입을 가져와서addDays
메서드 사용.
addDays
는 static이기때문에 저렇게 사용이 가능하다. dt(현재일자)에 하루를 더하고"${#dates.format(tomorrow, 'M/d') + '(' + #dates.dayOfWeekName(dt).substring(0,1 )+ ') 도착 보장'}"
- dates의
format
을 M/d로 해서 7/5으로 표현할 수 있도록 한자리로 설정.tomorrow
: 내일 날짜dayOfWeekName
: 요일(월~일)을 한글로 받아오고 '월요일'이라는 3글자가 다 필요없기 때문에 0~1자리만 잘라서 가져온다.
- 상품 등록을 하면 Db에 값이 넘어가고 해당 옵션과 동일하게 페이지가 뜨게 된다.
- 현재 상세페이지 눌러서 들어갔을 때 현황
: 로켓프레쉬, 로켓배송, 일반배송 다 다르게 뜨는 것을 확인 할 수 있다.
-> ProductController getDetail
-> DetailVo 추가만 하고 DetailResult 추가
-> 다시 DetailVo
private DetailResult result
+gettersetter
- 여기서 하나 빠졌음. detailVo.setIndext 어쩌고 저쩌고
-> ProductService getProduct
-> IProductMapper selectProductByIndex
-> ProductMapper.xml selectProductByIndex
-> ProductService getProduct
추가
-> ProductController
- 서비스 호출
this.productService.getProduct(detailVo); modelAndView.addObject("detailVo", detailVo);
-> detail.html
<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>
<img class="image"
th:if="${detailVo.getThumbnailId() != null}"
th:src="@{'/product/detail/thumbnail?id=' + ${detailVo.getThumbnailId()}}">
<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="productIndex" type="hidden" th:value="${detailVo.getIndex()}">
<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>
-> detail.css
<style>
@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,143);
border-radius: 0.125rem;
color: rgb(225,225,225);
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;
}
</style>
< detail.html 설명 >
<title th:text="${'쿠팡! - ' + detailVo.getTitle()}">쿠팡! - 상품 추가</title>
NOT_FOUND
면alert
설정.
<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="productIndex" type="hidden" th:value="${detailVo.getIndex()}"> <input name="thumbnail" type="file" accept="image/png, image/jpeg"> <input type="submit" value="상품 이미지 등록"> </form>
- 첨부파일이나 이미지같은 것을 받을 때 controller에 맵핑된 메서드에서 파일타입을 MultiPartFile로 들어왔다.
- 여기서는 직접 설정을 해줘야한다.
enctype="multipart/form-data"
method="post"
th:action="@{/product/detail/thumbnail/add}
이 주소에서 post는 리뷰작성으로 사용될 확률이 높기 떄문에action
으로 뺸다.accept="image/png, image/jpeg">
: 파일 선택 눌렀을 때 png/ jpeg 만 뜬다.th:if="${userEntity != null && userEntity.isAdmin() == true}"
상품 썸네일 등록은 관리자만 보이게 한다. 로그아웃시 보이지 않는다.
<section class="body"> <h2 class="title">상품 상세</h2> <section class="content" th:utext="${detailVo.getContent()}"></section> </section>
utext
=unescape text
: 상품 상세 부분에서 WYSIWYG 에디터를 이용해서 html태그로 저장이 된다. 그냥 text로 하게 되면 태그자체가 다 떠버린다. 그래서 utext를 사용한다.
- text 사용.
- utext 사용.
-> ProductController postDetailThumbnailAdd
-> ProductService putThumbnail
-> 테이블 생성 product_thumbnails
- product는
thumbnailId
를 가지고 있는데 외래키는 걸지 않았다. 왜냐하면 null일 수도 있기 때문이다. 그래서product_thumbnails
테이블 생성을 한다.product
와product_thumbnails
은 데이터베이스 상 연관이 없다. 썸네일을 등록하려고 하는데 id는 랜덤 값으로 만들어 줄거고 해당 상품에 대한 썸네일을 올리면 해당 상품에 대한 imageId가 등록해야하는데 우리가 지금 서버로 넘겨주는 데이터는 그것을 포함하지 않는다. html로 넘어가서 설정하자.
-> detail.html
- 어느 상품의 섬네일을 등록할 것인가에 대한 정보를 주는 input추가
-> ProductController
RequestParam
추가
-> ThumbnailEntity
-> IProductMapper insertThumbnail
-> ProductMapper.xml insertThumbnail
-> ProductService
-> ProductMapper.xml updateProduct
-> ProductController getDetailThumbnail
headers.add("Content-Type", "image/png"); headers.add("Content-Disposition", String.format("attachment; filename=\"%s\"", id + ".png")); headers.add("Content-length", String.valueOf(data.length));
- 섬네일은 파일타입을 만들지 않아서
"image/png"
로 만든다.
-> ProductService
-> IProductMapper selectThumbnail
-> ProductMapper.xml selectThumbnail
-> detail.html
<img class="image" th:if="${detailVo.getThumbnailId() != null}" th:src="@{'/product/detail/thumbnail?id=' + ${detailVo.getThumbnailId()}}">
-> 결과