package com.shop.repository;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface ItemRepositoryCustom {
Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
}
package com.shop.repository;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.dto.QMainItemDto;
import com.shop.entity.Item;
import com.shop.entity.QItem;
import com.shop.entity.QItemImg;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.thymeleaf.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{
private JPAQueryFactory queryFactory; // 동적쿼리 사용하기 위해 JPAQueryFactory 변수 선언
// 생성자
public ItemRepositoryCustomImpl(EntityManager em){
this.queryFactory = new JPAQueryFactory(em); // JPAQueryFactory 실질적인 객체가 만들어 집니다.
}
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus){
return searchSellStatus == null ?
null : QItem.item.itemSellStatus.eq(searchSellStatus);
//ItemSellStatus null이면 null 리턴 null 아니면 SELL, SOLD 둘중 하나 리턴
}
private BooleanExpression regDtsAfter(String searchDateType){ // all, 1d, 1w, 1m 6m
LocalDateTime dateTime = LocalDateTime.now(); // 현재시간을 추출해서 변수에 대입
if(StringUtils.equals("all",searchDateType) || searchDateType == null){
return null;
}else if(StringUtils.equals("1d",searchDateType)){
dateTime = dateTime.minusDays(1);
}else if(StringUtils.equals("1w",searchDateType)){
dateTime = dateTime.minusWeeks(1);
}else if(StringUtils.equals("1m",searchDateType)){
dateTime = dateTime.minusMonths(1);
}else if(StringUtils.equals("6m",searchDateType)){
dateTime = dateTime.minusMonths(6);
}
return QItem.item.regTime.after(dateTime);
//dateTime을 시간에 맞게 세팅 후 시간에 맞는 등록된 상품이 조회하도록 조건값 반환
}
private BooleanExpression searchByLike(String searchBy, String searchQuery){
if(StringUtils.equals("itemNm",searchBy)){ // 상품명
return QItem.item.itemNm.like("%"+searchQuery+"%");
}else if(StringUtils.equals("createdBy",searchBy)){ // 작성자
return QItem.item.createdBy.like("%"+searchQuery+"%");
}
return null;
}
// 너무 쉽죠!!
@Override
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){
QueryResults<Item> results = queryFactory.selectFrom(QItem.item).
where(regDtsAfter(itemSearchDto.getSearchDateType()),
searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
searchByLike(itemSearchDto.getSearchBy(),itemSearchDto.getSearchQuery()))
.orderBy(QItem.item.id.desc())
.offset(pageable.getOffset()).limit(pageable.getPageSize()).fetchResults();
List<Item> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content,pageable,total);
}
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;
//QMainItemDto @QueryProjection을 허용하면 DTO로 바로 조회 가능
QueryResults<MainItemDto> results = queryFactory.select(new QMainItemDto(item.id, item.itemNm,
item.itemDetail, itemImg.imgUrl, item.price))
// join 내부조인 .repImgYn.eq("Y") 대표이미지만 가져온다.
.from(itemImg).join(itemImg.item, 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);
}
}
package com.shop.controller;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.service.ItemService;
import lombok.RequiredArgsConstructor;
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 java.util.Optional;
@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, 5);
Page<MainItemDto> items = itemService.getMainItemPage(itemSearchDto, pageable);
System.out.println(items.getNumber() + "!!!!!!!!!!!!!");
System.out.println(items.getTotalPages() + "#############");
model.addAttribute("items", items);
model.addAttribute("itemSearchDto", itemSearchDto);
model.addAttribute("maxPage", 5);
return "main";
}
}
package com.shop.dto;
import com.shop.entity.ItemImg;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;
@Getter
@Setter
public class ItemImgDto {
private Long id;
private String imgName;
private String oriImgName;
private String imgUrl;
private String repImgYn;
private static ModelMapper modelMapper = new ModelMapper();
public static ItemImgDto of(ItemImg itemImg) {
return modelMapper.map(itemImg, ItemImgDto.class);
}
}
package com.shop.service;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.util.StringUtils;
@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {
@Value("${itemImgLocation}") // application.properties에 itemImgLocation
private String itemImgLocation;
private final ItemImgRepository itemImgRepository;
private final FileService fileService;
public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile) throws Exception {
String oriImgName = itemImgFile.getOriginalFilename(); // 오리지널 이미지 경로
String imgName = "";
String imgUrl = "";
System.out.println(oriImgName);
// 파일 업로드
if (!StringUtils.isEmpty(oriImgName)) { // oriImgName 문자열로 비어있지 않으면 실행
System.out.println("******");
imgName = fileService.uploadFile(itemImgLocation, oriImgName,
itemImgFile.getBytes());
System.out.println(imgName);
imgUrl = "/images/item/" + imgName;
}
System.out.println("1111");
// 상품 이미지 정보 저장
// oriImgName: 상품 이미지 파일의 원래 이름
// imgName: 실제로 로컬에 저장된 상품 이미지 파일의 이름
// imgUrl: 로컬에 저장된 상품 이미지 파일을 불러오는 경로
itemImg.updateItemImg(oriImgName, imgName, imgUrl);
System.out.println("(((((");
itemImgRepository.save(itemImg);
}
public void updateItemImg(Long itemImgId, MultipartFile itemImgFile) throws Exception {
if (!itemImgFile.isEmpty()) { // 상품의 이미지를 수정한 경우 상품 이미지 업데이트
ItemImg savedItemImg = itemImgRepository.findById(itemImgId)
.orElseThrow(EntityNotFoundException::new); // 기존 엔티티 조회
// 기존에 등록된 상품 이미지 파일이 있는 경우 삭제
if (!StringUtils.isEmpty(savedItemImg.getImgName())) {
fileService.deleteFile(itemImgLocation + "/" + savedItemImg.getImgName());
}
String oriImgName = itemImgFile.getOriginalFilename();
String imgName = fileService.uploadFile(itemImgLocation, oriImgName,
itemImgFile.getBytes()); // 파일 업로드
String imgUrl = "/images/item/" + imgName;
// 변경된 상품 이미지 정보를 세팅
// 상품 등록을 하는 경우에는 ItemImgRepository.save() 로직을 호출하지만
// 호출을 하지 않았다.
// savedItemImg 엔티티는 영속성 상태이다.
// 그래서 데이터를 변경하는 것만으로 변경을 감지하는 기능이 동작
// 트랜잭션이 끝날 때 update 쿼리가 실행된다.
// 영속성 상태여야만 사용 가능
savedItemImg.updateItemImg(oriImgName, imgName, imgUrl);
}
}
}
package com.shop.dto;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MainItemDto {
private Long id;
private String itemNm;
private String itemDetail;
private String imgUrl;
private Integer price;
@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;
}
}
package com.shop.entity;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity{
@Id
@Column(name = "item_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id; // 상품 코드
@Column(nullable = false, length = 50)
private String itemNm; // 상품명
@Column(name = "price", nullable = false)
private int price; // 가격
@Column(nullable = false)
private int stockNumber; // 수량
@Lob
@Column(nullable = false)
private String itemDetail; // 상품 상세 설명
@Enumerated
private ItemSellStatus itemSellStatus; // 상품 판매 상태
// private LocalDateTime regTime; // 등록 시간
//
// private LocalDateTime updateTime; // 수정 시간
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "member_item",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Member> member;
public void updateItem(ItemFormDto itemFormDto) {
this.itemNm = itemFormDto.getItemNm();
this.price = itemFormDto.getPrice();
this.stockNumber = itemFormDto.getStockNumber();
this.itemDetail = itemFormDto.getItemDetail();
this.itemSellStatus = itemFormDto.getItemSellStatus();
}
}
package com.shop.repository;
import com.shop.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item>,
ItemRepositoryCustom {
// select*from item where itemNm = itemNm;
List<Item> findByItemNm(String itemNm);
List<Item> findByItemNmOrItemDetail(String itemNm, String itemDetail);
List<Item> findByPriceLessThan(Integer price);
List<Item> findByPriceLessThanOrderByPriceDesc(Integer price);
@Query("select i from Item i where i.itemDetail like %:itemDetail% order by i.price desc")
List<Item> findByItemDetail(@Param("itemDetail")String itemDetail);
@Query(value = "select * from item i where i.item_detail like %:itemDetail% order by i.price desc",
nativeQuery = true)
List<Item> findByItemDetailNative(@Param("itemDetail") String itemDetail);
}
package com.shop.service;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemImgDto;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
private final ItemImgService itemImgService;
private final ItemImgRepository itemImgRepository;
public Long saveItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList)
throws Exception {
// 상품 등록
Item item = itemFormDto.createItem(); // 모델매퍼
itemRepository.save(item);
// 이미지 등록
for (int i = 0; i <itemImgFileList.size(); i++) {
ItemImg itemImg = new ItemImg();
itemImg.setItem(item);
if (i == 0) {
itemImg.setRepImgYn("Y");
} else {
itemImg.setRepImgYn("N");
}
itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
}
return item.getId();
}
@Transactional(readOnly = true)
public ItemFormDto getItemDtl(Long itemId) {
// Entity
List<ItemImg> itemImgList = itemImgRepository.findByItemIdOrderByIdAsc(itemId);
// DB에서 데이터를 가지고 온다.
// DTO
List<ItemImgDto> itemImgDtoList = new ArrayList<>();
for (ItemImg itemimg : itemImgList) {
// Entity -> Dto
ItemImgDto itemImgDto = ItemImgDto.of(itemimg);
itemImgDtoList.add(itemImgDto);
}
Item item = itemRepository.findById(itemId).orElseThrow(EntityNotFoundException::new);
// Item -> ItemFormDto modelMapper
ItemFormDto itemFormDto = ItemFormDto.of(item);
itemFormDto.setItemImgDtoList(itemImgDtoList);
return itemFormDto;
}
public Long updateItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList)
throws Exception {
// 상품 변경
Item item = itemRepository.findById(itemFormDto.getId())
.orElseThrow(EntityNotFoundException::new);
item.updateItem(itemFormDto);
// 상품 이미지 변경
List<Long> itemImgIds = itemFormDto.getItemImgIds();
for (int i = 0; i < itemImgFileList.size(); i++) {
itemImgService.updateItemImg(itemImgIds.get(i), itemImgFileList.get(i));
}
return item.getId();
}
@Transactional(readOnly = true)
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
return itemRepository.getAdminItemPage(itemSearchDto, pageable);
}
@Transactional(readOnly = true)
public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
return itemRepository.getMainItemPage(itemSearchDto, pageable);
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<th:block layout:fragment="css">
<style>
/* . item -> .item 띄어쓰기 수정 */
.carousel-inner > .item {
height : 350px;
}
.margin{
margin-bottom : 30px;
}
.banner{
height : 300px;
position : absolute; top:0; left:0;
width: 100%;
}
.card-text{
text-overflow : ellipsis;
white-space : nowrap;
overflow : hidden;
}
a:hover{
text-decoration:none;
}
/* text5-align -> text-align 오타 수정 */
.center{
text-align:center;
}
</style>
</th:block>
<div layout:fragment="content">
<div id="carouselControls" class="carousel slide margin" data-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active item">
<img src="https://user-images.githubusercontent.com/13268420/112147492-1ab76200-8c20-11eb-8649-3d2f282c3c02.png" class="d-block w-100 banner" alt="First slide">
</div>
</div>
</div>
<input type="hidden" name="searchQuery" th:value="${itemSearchDto.searchQuery}">
<div th:if="${not #strings.isEmpty(itemSearchDto.searchQuery)}" class="center">
<p class="h3 font-weight-bold" th:text="${itemSearchDto.searchQuery} + ' 검색결과'"></p>
</div>
<div class="container text-center">
<div class="row row-cols-5">
<th:block th:each="item, status: ${items.getContent()}">
<div class="col">
<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>
<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>
package com.shop.controller;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemSearchDto;
import com.shop.entity.Item;
import com.shop.service.ItemService;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping(value = "/admin/item/new")
public String itemForm(Model model) {
model.addAttribute("itemFormDto", new ItemFormDto());
return "/item/itemForm";
}
@PostMapping(value = "/admin/item/new")
public String itemNew(@Valid ItemFormDto itemFormDto, BindingResult bindingResult, Model model,
@RequestParam("itemImgFile")List<MultipartFile> itemImgFileList) {
if (bindingResult.hasErrors()) {
return "item/itemForm";
}
if (itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null) {
model.addAttribute("errorMessage",
"첫번째 상품 이미지는 필수 입력 값입니다.");
return "item/itemForm";
}
try {
itemService.saveItem(itemFormDto, itemImgFileList);
} catch (Exception e) {
model.addAttribute("errorMessage",
"상품 등록 중 에러가 발생하였습니다.");
return "item/itemForm";
}
return "redirect:/";
}
@GetMapping(value = "/admin/item/{itemId}")
public String itemDtl(@PathVariable("itemId") Long itemId, Model model) {
try {
ItemFormDto itemFormDto = itemService.getItemDtl(itemId);
model.addAttribute("itemFormDto", itemFormDto);
} catch (EntityNotFoundException e) {
model.addAttribute("errorMessage", "존재하지 않는 상품입니다.");
model.addAttribute("itemFormDto", new ItemFormDto());
// return "item/itemForm";
}
return "item/itemForm";
}
@PostMapping(value = "/admin/item/{itemId}")
public String itemUpdate(@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
@RequestParam("itemImgFile") List<MultipartFile> itemImgFileList,
Model model) {
if (bindingResult.hasErrors()) {
return "item/itemForm";
}
if (itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null) {
model.addAttribute("errorMessage", "첫번째 상품 이미지는 필수 입력 값입니다.");
return "item/itemForm";
}
try {
itemService.updateItem(itemFormDto, itemImgFileList);
} catch (Exception e) {
model.addAttribute("errorMessage", "상품 수정 중 에러가 발생하였습니다.");
return "item/itemForm";
}
return "redirect:/"; // 다시 실행
}
@GetMapping(value = {"/admin/item", "/admin/items/{page}"})
public String itemManage(ItemSearchDto itemSearchDto, @PathVariable("page")Optional<Integer> page,
Model model) {
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 5);
Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);
model.addAttribute("items", items);
model.addAttribute("itemSearchDto", itemSearchDto);
model.addAttribute("maxPage", 5);
return "item/itemMng";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function(){
$("#searchBtn").on("click",function(e){
e.preventDefault(); //검색버튼 클릭시 form 태그 전송을 막습니다.
page(0);
});
});
function page(page){
var searchDateType = $("#searchDateType").val();
var searchSellStatus = $("#searchSellStatus").val();
var searchBy = $("#searchBy").val();
var searchQuery = $("#searchQuery").val();
location.href="/admin/items/" + page + "?searchDateType=" + searchDateType
+ "&searchSellStatus=" + searchSellStatus + "&searchBy=" + searchBy
+ "&searchQuery=" + searchQuery;
}
</script>
</th:block>
<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
<style>
select{
margin-right : 10px
}
</style>
</th:block>
<div layout:fragment="content">
<form th:action="@{'/admin/items/'+${items.number}}" role="form" method="get" th:object="${items}">
<table class="table">
<thead>
<tr>
<td>상품아이디</td>
<td>상품명</td>
<td>상태</td>
<td>등록자</td>
<td>등록일</td>
</tr>
</thead>
<tbody>
<tr th:each="item, status : ${items.getContent()}">
<td th:text="${item.id}"></td>
<td>
<a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a>
</td>
<td th:text="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL} ? '판매중' : '품절'"></td>
<td th:text="${item.createdBy}"></td>
<td th:text="${item.regTime}"></td>
</tr>
</tbody>
</table>
<p th:text="${items.number}"></p>
<p th:text="${items.totalPages}"></p>
<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 class="row row-cols-lg-auto g-3 align-items-center" th:object="${itemSearchDto}">
<select th:field="*{searchDateType}" class="form-control" style="width:auto;">
<option value="all">전체기간</option>
<option value="1d">1일</option>
<option value="1w">1주</option>
<option value="1m">1개월</option>
<option value="6m">6개월</option>
</select>
<select th:field="*{searchSellStatus}" class="form-control" style="width:auto;">
<option value="">판매상태(전체)</option>
<option value="SELL">판매</option>
<option value="SOLD_OUT">품절</option>
</select>
<select th:field="*{searchBy}" class="form-control" style="width:auto;">
<option value="itemNm">상품명</option>
<option value="createdBy">등록자</option>
</select>
<input th:field="*{searchQuery}" type="text" class="form-control" style="width:auto; margin-right: 15px;" placeholder="검색어를 입력해주세요">
<button id="searchBtn" type="submit" class="form-control btn btn-primary" style="width:auto">검색</button>
</div>
</form>
</div>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<!--사용자 스크립트 추가-->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function() {
var errorMessage = [[${errorMessage}]];
if (errorMessage != null) {
alert(errorMessage);
}
bindDomEvent();
});
function bindDomEvent() {
$(".imgFile.form-control").on("change", function() {
// a.jpg
var fileName = $(this).val().split("\\").pop();
// . 기준으로 인덱스 +1 -> jpg or gif 등등
var fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
// 확장자 추출
fileExt = fileExt.toLowerCase(); // 소문자 소환
if (fileExt != "jpg" && fileExt != "jpeg" && fileExt != "gif"
&& fileExt != "png" && fileExt != "bmp") {
alert("이미지 파일만 등록이 가능합니다.");
$(this).val("");
return;
}
});
}
</script>
</th:block>
<!--사용자 CSS 추가-->
<th:block layout:fragment="css">
<style>
.input-group {
margin-bottom: 15px;
}
.img-div {
margin-bottom: 10px;
}
.fieldError {
color: #db2130;
}
</style>
</th:block>
<div layout:fragment="content">
<form role="form" class="container" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
<p class="h2">상품 등록</p>
<input type="hidden" th:field="*{id}">
<div class="row mb-4">
<select th:field="*{itemSellStatus}" class="form-select">
<option value="SELL">판매중</option>
<option value="SOLD_OUT">품절</option>
</select>
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">상품명</span>
</div>
<input type="text" th:field="*{itemNm}" class="form-control" placeholder="상품명을 입력해주세요.">
</div>
<p th:if="${#fields.hasErrors('itemNm')}" th:errors="*{itemNm}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">가격</span>
</div>
<input type="number" th:field="*{price}" class="form-control" placeholder="상품의 가격을 입력해주세요.">
</div>
<p th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">재고</span>
</div>
<input type="number" th:field="*{stockNumber}" class="form-control" placeholder="상품의 재고를 입력해주세요.">
</div>
<p th:if="${#fields.hasErrors('stockNumber')}" th:errors="*{stockNumber}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">상품 상세 내용</span>
</div>
<textarea class="form-control" aria-label="With textarea" th:field="*{itemDetail}"></textarea>
</div>
<p th:if="${#fields.hasErrors('itemDetail')}" th:errors="*{itemDetail}" class="fieldError">Incorrect data</p>
<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
<div class="row" th:each="num : ${#numbers.sequence(1,5)}">
<div class="input-group mb-3">
<label class="imgFile input-group-text" th:text="'상품이미지' + ${num}"></label>
<input type="file" class="imgFile form-control" name="itemImgFile">
</div>
</div>
</div>
<div th:if="${not #lists.isEmpty(itemFormDto.itemImgDtoList)}">
<div class="row" th:each="itemImgDto, status : ${itemFormDto.itemImgDtoList}">
<div class="input-group mb-3">
<label class="imgFile input-group-text"
th:text="${not #strings.isEmpty(itemImgDto.oriImgName) ? itemImgDto.oriImgName : '상품이미지' + (status.index + 1)}"></label>
<input type="file" class="imgFile form-control" name="itemImgFile">
<input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
</div>
</div>
</div>
<div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align : center">
<button th:formaction="@{/admin/item/new}" type="submit" class="btn btn-primary">저장</button>
</div>
<div th:if="${not #strings.isEmpty(itemFormDto.id)}" style="text-align : center">
<button th:formaction="@{'/admin/item/' + ${itemFormDto.id}}" type="submit" class="btn btn-primary">수정</button>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</div>
</html>
Querydsl을 활용해 상품 관리 페이지와 메인 상품 조회 기능을 구현하고, 동적 검색 조건과 페이징 처리를 학습했다.
사실 아직 어려워서 무슨 소리인지 반절은 이해 못한 거 같다.
또한 Thymeleaf에서 페이지 이동, 검색 조건 유지, 상품 목록 출력 구조를 구현했다.
이미지 조회 오류를 디버깅하며 DB → DTO → View → Resource 설정 흐름을 확인하였는데 생각보다 오타가 많아서 애를 많이 먹었다.
생소한 기호들을 많이 쓰다보니 한두개씩 빼먹었는데 그게 생각보다 찾기가 정말 힘들었다.
그래도 오류 찾고 제대로 실행되는 것들을 보니 뿌듯하고 재밌었다.