2024. 1. 19. 22:53ㆍProject/ShoppingMall
[Admin_상품등록] 다중 이미지 삽입 / 이미지-상품 테이블 조인 / enum 카테고리, 판매 상태(1)
🟢 Constant 열거형 : 고정된 값들의 집합 ▪️ CategoryTypeRole 제품 카테고리 설정 CategoryTypeRole이라는 열거형 정의 열거 상수(ALL, LIVING, BATHROOM, KITCHEN, MEMBERSALE)는 해당 카테고리의 설명(description)을
dalhyehye.tistory.com
🟢 상품 등록
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/adminlayout}">
<head>
<meta charset="utf-8">
<title>ADMIN - Product Insert</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
</head>
<body>
<div layout:fragment="content">
<div class="main-wrapper wrapper-2">
<div class="breadcrumb-area breadcrumb-padding-8">
<div class="container">
<div class="breadcrumb-content text-center">
<div class="breadcrumb-title">
<h2>product</h2>
</div>
</div>
</div>
</div>
<div class="notice-area bg-white pb-130">
<div class="container">
<div class="notice-info-wrap">
<form th:action="@{'/admin_product_insert'}" method="post" enctype="multipart/form-data" th:object="${productDTO}">
<table class="table notice-table">
<tbody>
<tr>
<th scope="row" class="notice-padding" id="productName">상품명</th>
<td class="notice-info">
<input type="text" name="productName" th:field="*{productName}">
<p class="text-danger" th:if="${#fields.hasErrors('productName')}" th:errors="*{productName}"></p>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productContent">상품설명</th>
<td class="notice-info">
<input type="text" name="productContent" th:field="*{productContent}">
<p class="text-danger" th:if="${#fields.hasErrors('productContent')}" th:errors="*{productContent}"></p>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="CategoryTypeRole">카테고리</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<select name="CategoryTypeRole" th:field="*{categoryTypeRole}">
<option th:each="state : ${categoryType}"
th:unless="${state.name() == 'ALL'}"
th:value="${state.name()}"
th:text="${state.getDescription()}"
th:selected="${state.name() eq data?.categoryTypeRole?.name()}"></option>
</option>
</select>
</div>
</div>
</td>
</tr>
<!-- 상품 등록 폼 -->
<tr>
<th scope="row" class="notice-padding">소비자가</th>
<td class="notice-info">
<input type="text" id="productCost" name="productCost" th:field="*{productCost}" oninput="calculateDiscount()">
<p class="text-danger" th:if="${#fields.hasErrors('productCost')}" th:errors="*{productCost}"></p>
</td>
</tr>
<!-- 판매가 -->
<tr>
<th scope="row" class="notice-padding" id="productPrice">판매가</th>
<td class="notice-info">
<input type="text" id="productPrice" name="productPrice" th:field="*{productPrice}" oninput="calculateDiscount()">
<p class="text-danger" th:if="${#fields.hasErrors('productPrice')}" th:errors="*{productPrice}"></p>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productCnt">재고수</th>
<td class="notice-info">
<input type="number" name="productCnt" th:field="*{productCnt}">
<p class="text-danger" th:if="${#fields.hasErrors('productCnt')}" th:errors="*{productCnt}"></p>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="SellStateRole">판매상태</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<select name="SellStateRole">
<option th:each="state:${sellsState}"
th:value="${state.name()}"
th:text="${state.getDescription()}"
th:selected="${state.name() eq data?.sellStateRole?.name()}"></option>
</option>
</select>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productDetail">상품정보</th>
<td class="notice-info">
<textarea rows="7" name="productDetail" th:field="*{productDetail}"></textarea>
</td>
</tr>
<!-- 대표 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[0]">대표이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[0].imageType" value="0">
<input type="file" name="images" onchange="previewImage(this, 'previewImg0')">
<img id="previewImg0" class="img-thumbnail" style="max-width: 100px; max-height: 100px;">
</td>
</tr>
<!-- 서브 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[1]">서브이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[1].imageType" value="1">
<input type="file" name="images" onchange="previewImage(this, 'previewImg1')">
<img id="previewImg1" class="img-thumbnail" style="max-width: 100px; max-height: 100px;">
</td>
</tr>
<!-- 상세 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[2]">상세이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[2].imageType" value="2">
<input type="file" name="images" onchange="previewImage(this, 'previewImg2')">
<img id="previewImg2" class="img-thumbnail" style="max-width: 100px; max-height: 100px;">
</td>
</tr>
<script>
// 파일 입력(change) 이벤트를 처리하는 함수
function previewImage(input, previewId) {
var preview = document.getElementById(previewId);
var file = input.files[0];
var reader = new FileReader();
reader.onload = function (e) {
preview.src = e.target.result;
};
if (file) {
reader.readAsDataURL(file);
}
}
// 파일 입력에 이벤트 리스너를 추가합니다.
$(document).ready(function () {
$('input[name^="images"]').change(function () {
var index = $(this).attr('id').replace('fileInput', '');
previewImage(this, 'previewImg' + index);
});
});
</script>
</tbody>
</table>
<div class="row">
<div class="col d-flex justify-content-center">
<div class="notice-button-container">
<div class="notice-content">
<div class="notice-btn-date-wrap list-btn">
<div class="notice-btn">
<button type="submit" class="btn btn-lg">등록</button>
<button type="reset" class="btn btn-lg">다시</button>
<button type="button" class="btn btn-lg" th:onclick="|location.href='@{/admin_productlist}'|">취소</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div><!-- content 끝-->
</body>
</html>
▪️ 사진 등록
enctype="multipart/form-data"
▪️ enum
scope : 해당 헤더 셀이 괸련되는 셀의 종류를 명시
(일반 웹 브라우저에서는 아무런 시각적 효과가 나타나지 않지만, 스크린 리더기와 같은 장치에서는 유용하게 사용 됨)
속성값 | 설명 |
col | 해당 셀이 열(column)을 위한 헤더 셀임을 명시 |
row | 해당 셀이 행(row)을 위한 헤더 셀임을 명시 |
colgroup | 해당 셀이 열의 그룹을 위한 헤더 셀임을 명시 |
rowgroup | 해당 셀이 행의 그룹을 위한 헤더 셀임을 명시 |
<tr>
<th scope="row" class="notice-padding" id="CategoryTypeRole">카테고리</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<select name="CategoryTypeRole" th:field="*{categoryTypeRole}">
<option th:each="state : ${categoryType}"
th:unless="${state.name() == 'ALL'}"
th:value="${state.name()}"
th:text="${state.getDescription()}"></option>
</option>
</select>
</div>
</div>
</td>
</tr>
th:field="*{categoryTypeRole}" → <select> 요소가 categoryTypeRole 필드와 바인딩
th:unless="${state.name() == 'ALL'}" → All은 제외( 'ALL'이 아닌 경우에만 해당 )
th:text="${state.getDescription()}" → 텍스트로 state의 설명을 사용
▪️ 이미지 삽입
> 이미지 관리 DTO → id
private List<ImageDTO> imageDTOs;
> 이미지파일 처리
private List<MultipartFile> images;
이미지 파일이름
private String imageFile;
이미지 종류 (대표이미지=0, 서브이미지=1, 상세이미지=2)
private Integer imageType;
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[0]">대표이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[0].imageType" value="0">
<input type="file" name="images" onchange="previewImage(this, 'previewImg0')">
<img id="previewImg0" class="img-thumbnail" style="max-width: 100px; max-height: 100px;">
</td>
</tr>
<script>
//매개변수 : 파일 입력란과 미리보기 이미지의 ID
function previewImage(input, previewId) {
var preview = document.getElementById(previewId);
//파일 입력란에서 선택된 파일 객체를 가져오기
var file = input.files[0];
//파일을 읽어오는데 사용되는 FileReader 객체를 생성
var reader = new FileReader();
//파일이 로드될 때 실행되는 이벤트 핸들러를 정의
reader.onload = function (e) {
preview.src = e.target.result;
};
//파일이 존재하는 경우에만 readAsDataURL 메서드를 사용하여
//파일을 읽어와 데이터 URL로 변환
if (file) {
reader.readAsDataURL(file);
}
}
//이벤트 핸들러를 사용하여 문서가 로드되면 아래의 코드 블록 실행
$(document).ready(function () {
//"images"로 시작하는 모든 파일 입력란에 대해 change 이벤트 핸들러를 추가
$('input[name^="images"]').change(function () {
//현재 파일 입력란의 ID에서 'fileInput'을 제거하여 인덱스를 추출
var index = $(this).attr('id').replace('fileInput', '');
//파일 입력란과 미리보기 이미지의 ID를 전달하여 previewImage 함수를 호출
previewImage(this, 'previewImg' + index);
});
});
</script>
🟢 상품 목록
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/adminlayout}">
<head>
<meta charset="utf-8">
<title>ADMIN - Product List</title>
</head>
<body>
<div layout:fragment="content">
<div class="main-wrapper wrapper-2">
<div class="breadcrumb-area breadcrumb-padding-8">
<div class="container">
<div class="breadcrumb-content text-center">
<div class="breadcrumb-title">
<h2>PRODUCT</h2>
</div>
<ul>
<li>
<a href="/admin_productlist">상품목록</a>
</li>
<li>
|
</li>
<li>
<a href="/admin_product_insert">상품등록</a>
</li>
</ul>
</div>
</div>
</div>
<div class="wishlist-area bg-white pb-130">
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-4"></div>
<div class="col-sm-8">
<!-- 검색 폼 -->
<form th:action="@{/admin_productlist}" method="get" id="searchForm">
<input type="hidden" name="page" value="1">
<div class="input-group mb-3 mt-5">
<!-- 대분류 셀렉트 리스트 -->
<select class="form-select" name="type" id="searchType">
<option value="" th:selected="${type == ''}">== 선택 ==</option>
<option value="n" th:selected="${type == 'n'}">상품명</option>
<option value="nc" th:selected="${type == 'nc'}">상품명+내용</option>
<option value="ca" th:selected="${type == 'ca'}">카테고리</option>
<option value="s" th:selected="${type == 's'}">판매상태</option>
</select>
<!-- 검색창 -->
<input type="text" class="form-control" name="keyword" id="keyword" th:value="${keyword}" style="display: none;"></input>
<!-- 중분류(판매상태) 셀렉트 리스트 -->
<select class="form-select" name="categoryType" id="categoryType" style="display: none;">
<option value="" th:selected="${categoryType == ''}">== 선택 ==</option>
<option value="ALL" th:selected="${categoryType == 'ALL'}">전체</option>
<option value="LIVING" th:selected="${categoryType == 'LIVING'}">생활용품</option>
<option value="BATHROOM" th:selected="${categoryType == 'BATHROOM'}">욕실용품</option>
<option value="KITCHEN" th:selected="${categoryType == 'KITCHEN'}">주방용품</option>
<option value="MEMBERSALE" th:selected="${categoryType == 'MEMBERSALE'}">회원특가</option>
</select>
<!-- 중분류(판매상태) 셀렉트 리스트 -->
<select class="form-select" name="sellState" id="sellStateOptions" style="display: none;">
<option value="" th:selected="${sellsState == ''}">== 선택 ==</option>
<option value="SELL" th:selected="${sellsState == 'SELL'}">판매중</option>
<option value="STOP" th:selected="${sellsState == 'STOP'}">판매중지</option>
<option value="LACK" th:selected="${sellsState == 'LACK'}">재고없음</option>
</select>
<!-- 검색 버튼 -->
<button type="submit" class="btn btn-primary admin-dart" name="searchButton">검색</button>
<!-- 리셋 버튼 -->
<button type="reset" class="btn btn-light admin-light " name="searchButton" onclick="resetSearchForm()">다시</button>
</div>
</form>
<!-- JavaScript 코드 -->
<script>
document.getElementById('searchType').addEventListener('change', function () {
resetSearchForm();
});
function resetSearchForm() {
var selectedSearchType = document.getElementById('searchType').value;
var selectedCategoryType = document.getElementById('categoryType').value;
var selectedSellState = document.getElementById('sellStateOptions').value;
applyStylesAfterSearch(selectedSearchType, selectedCategoryType, selectedSellState);
toggleElementDisplay('keyword', selectedSearchType === 'n' || selectedSearchType === 'nc');
}
function applyStylesAfterSearch(searchType, categoryType, sellState) {
toggleElementDisplay('categoryType', searchType === 'ca');
toggleElementDisplay('sellStateOptions', searchType === 's');
// 검색 타입이 상품명 또는 상품명+내용일 때 keyword 입력 필드를 보이게 설정
toggleElementDisplay('keyword', searchType === 'n' || searchType === 'nc');
if (searchType === 'ca') {
// 현재 선택된 값이 없을 때만 초기화
var categoryTypeElement = document.getElementById('categoryType');
if (categoryTypeElement.value === '') {
// localStorage에서 이전 선택 값을 가져옴
categoryTypeElement.value = localStorage.getItem('selectedCategoryType') || categoryType;
}
}
// 판매 상태에 대한 유사한 로직 추가
var sellStateOptionsElement = document.getElementById('sellStateOptions');
if (searchType === 's') {
// localStorage에서 이전 선택 값을 가져옴
sellStateOptionsElement.value = localStorage.getItem('selectedSellState') || sellState;
}
// ... (다른 엘리먼트에 대한 유사한 로직)
}
function toggleElementDisplay(elementId, condition) {
var element = document.getElementById(elementId);
element.style.display = condition ? 'block' : 'none';
}
// 페이지 로드 시 초기 스타일 적용
applyStylesAfterSearch(
document.getElementById('searchType').value,
document.getElementById('categoryType').value,
document.getElementById('sellStateOptions').value
);
// 초기 검색창 상태 설정
// 검색 이후에는 초기화하지 않도록 수정
// resetSearchForm();
// 페이지 로드 시 localStorage에서 이전 선택 값을 가져와 적용
window.onload = function () {
applyStylesAfterSearch(
document.getElementById('searchType').value,
document.getElementById('categoryType').value,
document.getElementById('sellStateOptions').value
);
}
// 페이지 언로드 시 localStorage에 현재 선택 값을 저장
window.onbeforeunload = function () {
localStorage.setItem('selectedCategoryType', document.getElementById('categoryType').value);
localStorage.setItem('selectedSellState', document.getElementById('sellStateOptions').value);
}
</script>
</div>
</div>
</div>
<div class="row mb-20"></div>
<div class="container">
<div class="row">
<div class="col-12">
<div class="wishlist-table-content">
<div class="table-content">
<table>
<thead>
<tr>
<th class="width-price">번호</th>
<th class="width-price"></th>
<th class="width-price">상품종류</th>
<th class="width-price">상품명</th>
<th class="width-price">소비자가</th>
<th class="width-price">판매자가</th>
<th class="width-price">재고수</th>
<th class="width-price">판매상태</th>
</tr>
</thead>
<tbody>
<tr th:each="data:${productDTOS}"> <!--반복 영역-->
<td class="product-name" th:text="${data.productId}">
<h5>상품번호</h5>
</td>
<td class="product-name">
<!-- 대표이미지 -->
<th:block th:if="${data.imageDTOs != null and data.imageDTOs.size() > 0 and data.imageDTOs[0].imageFile != null}">
<img th:src="|/images/item/@{${data.imageDTOs[0].imageFile}}|" width="100" height="100">
</th:block>
</td>
<!-- 상품종류(카테고리 유형) -->
<td class="product-name" th:if="${data != null}">
<div th:if="${data.categoryTypeRole != null}">
<div th:utext="${#strings.replace(data.categoryTypeRole.description, '\n', ' ')}"></div>
</div>
<div th:unless="${data.categoryTypeRole != null}"></div>
</td>
<!-- 상품명 -->
<td class="product-name">
<h5>
<a th:href="@{/admin_product_indetail(productId=${data.productId})}" th:text="${data.productName}">상품이름</a>
</h5>
</td>
<td class="product-name" th:text="${data.productCost}">
<h5>소비자가</h5>
</td>
<td class="product-name" th:text="${data.productPrice}">
<h5>판매가</h5>
</td>
<td class="product-name" th:text="${data.productCnt}">
<h5>상품재고수량</h5>
</td>
<!-- 판매상태 -->
<td class="product-name" th:if="${data != null}">
<div th:if="${data.SellStateRole != null}">
<div th:utext="${#strings.replace(data.SellStateRole.description, '\n', ' ')}"></div>
</div>
<div th:unless="${data.SellStateRole != null}"></div>
</td>
</tr>
</tbody>
</table>
<!-- 페이지 번호 추가 -->
<div class="pagination-style text-center mt-30">
<ul>
<li th:unless="${startPage == 1}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=1)}">처음</a>
</li>
<li th:unless="${currentPage == 1}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${prevPage})}"><</a>
</li>
<li th:each="page: ${#numbers.sequence(startPage, endPage)}">
<a th:if="${currentPage != page}" th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${page})}">[[${page}]]</a>
<a th:if="${currentPage == page}" href="#" class="active">[[${page}]]</a>
</li>
<li th:unless="${currentPage == lastPage}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${nextPage})}">></a>
</li>
<li th:unless="${endPage == lastPage}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${lastPage})}">끝</a>
</li>
</ul>
</div>
<!-- 페이지 번호 추가 끝 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main JS -->
<script src="assets/js/main.js"></script>
</div>
</body>
</html>
▪️ 검색영역
All은 제외 → th:unless
한글로 불러오기 → th:text
<!-- 검색 폼 -->
<form th:action="@{/admin_productlist}" method="get" id="searchForm">
<input type="hidden" name="page" value="1">
<div class="input-group mb-3 mt-5">
<!-- 대분류 셀렉트 리스트 -->
<select class="form-select" name="type" id="searchType">
<option value="" th:selected="${type == ''}">== 선택 ==</option>
<option value="n" th:selected="${type == 'n'}">상품명</option>
<option value="nc" th:selected="${type == 'nc'}">상품명+내용</option>
<option value="ca" th:selected="${type == 'ca'}">카테고리</option>
<option value="s" th:selected="${type == 's'}">판매상태</option>
</select>
<!-- 검색창 -->
<input type="text" class="form-control" name="keyword" id="keyword" th:value="${keyword}" style="display: none;"></input>
<!-- 중분류(판매상태) 셀렉트 리스트 -->
<select class="form-select" name="categoryType" id="categoryType" style="display: none;">
<option value="" th:selected="${categoryType == ''}">== 선택 ==</option>
<option value="ALL" th:selected="${categoryType == 'ALL'}">전체</option>
<option value="LIVING" th:selected="${categoryType == 'LIVING'}">생활용품</option>
<option value="BATHROOM" th:selected="${categoryType == 'BATHROOM'}">욕실용품</option>
<option value="KITCHEN" th:selected="${categoryType == 'KITCHEN'}">주방용품</option>
<option value="MEMBERSALE" th:selected="${categoryType == 'MEMBERSALE'}">회원특가</option>
</select>
<!-- 중분류(판매상태) 셀렉트 리스트 -->
<select class="form-select" name="sellState" id="sellStateOptions" style="display: none;">
<option value="" th:selected="${sellsState == ''}">== 선택 ==</option>
<option value="SELL" th:selected="${sellsState == 'SELL'}">판매중</option>
<option value="STOP" th:selected="${sellsState == 'STOP'}">판매중지</option>
<option value="LACK" th:selected="${sellsState == 'LACK'}">재고없음</option>
</select>
<!-- 검색 버튼 -->
<button type="submit" class="btn btn-primary admin-dart" name="searchButton">검색</button>
<!-- 리셋 버튼 -->
<button type="reset" class="btn btn-light admin-light " name="searchButton" onclick="resetSearchForm()">다시</button>
</div>
</form>
<!-- JavaScript 코드 -->
<script>
document.getElementById('searchType').addEventListener('change', function () {
resetSearchForm();
});
function resetSearchForm() {
var selectedSearchType = document.getElementById('searchType').value;
var selectedCategoryType = document.getElementById('categoryType').value;
var selectedSellState = document.getElementById('sellStateOptions').value;
applyStylesAfterSearch(selectedSearchType, selectedCategoryType, selectedSellState);
toggleElementDisplay('keyword', selectedSearchType === 'n' || selectedSearchType === 'nc');
}
function applyStylesAfterSearch(searchType, categoryType, sellState) {
toggleElementDisplay('categoryType', searchType === 'ca');
toggleElementDisplay('sellStateOptions', searchType === 's');
// 검색 타입이 상품명 또는 상품명+내용일 때 keyword 입력 필드를 보이게 설정
toggleElementDisplay('keyword', searchType === 'n' || searchType === 'nc');
if (searchType === 'ca') {
// 현재 선택된 값이 없을 때만 초기화
var categoryTypeElement = document.getElementById('categoryType');
if (categoryTypeElement.value === '') {
// localStorage에서 이전 선택 값을 가져옴
categoryTypeElement.value = localStorage.getItem('selectedCategoryType') || categoryType;
}
}
// 판매 상태에 대한 유사한 로직 추가
var sellStateOptionsElement = document.getElementById('sellStateOptions');
if (searchType === 's') {
// localStorage에서 이전 선택 값을 가져옴
sellStateOptionsElement.value = localStorage.getItem('selectedSellState') || sellState;
}
// ... (다른 엘리먼트에 대한 유사한 로직)
}
function toggleElementDisplay(elementId, condition) {
var element = document.getElementById(elementId);
element.style.display = condition ? 'block' : 'none';
}
// 페이지 로드 시 초기 스타일 적용
applyStylesAfterSearch(
document.getElementById('searchType').value,
document.getElementById('categoryType').value,
document.getElementById('sellStateOptions').value
);
// 초기 검색창 상태 설정
// 검색 이후에는 초기화하지 않도록 수정
// resetSearchForm();
// 페이지 로드 시 localStorage에서 이전 선택 값을 가져와 적용
window.onload = function () {
applyStylesAfterSearch(
document.getElementById('searchType').value,
document.getElementById('categoryType').value,
document.getElementById('sellStateOptions').value
);
}
// 페이지 언로드 시 localStorage에 현재 선택 값을 저장
window.onbeforeunload = function () {
localStorage.setItem('selectedCategoryType', document.getElementById('categoryType').value);
localStorage.setItem('selectedSellState', document.getElementById('sellStateOptions').value);
}
</script>
▪️ 이미지 불러오기
th:block → 조건이 참일 때만 내부의 코드 블록이 실행
이미지의 소스(src) 속성을 동적으로 설정
th:src= /이미지 폴더 위치 /@{${변수}}
<th:block th:if="${data.imageDTOs != null and data.imageDTOs.size() > 0 and data.imageDTOs[0].imageFile != null}">
<img th:src="|/images/item/@{${data.imageDTOs[0].imageFile}}|" width="100" height="100">
</th:block>
▪️ 카테고리 불러오기
<td class="product-name" th:if="${data != null}">
<div th:if="${data.categoryTypeRole != null}">
<div th:text="${data.categoryTypeRole.description}"></div>
</div>
<div th:unless="${data.categoryTypeRole != null}"></div>
</td>
▪️ 페이지네이션
startPage, currentPage, lastPage, endPage
#numbers.sequence( startPage, endPage)
<!-- 페이지 번호 추가 -->
<div class="pagination-style text-center mt-30">
<ul>
<li th:unless="${startPage == 1}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=1)}">처음</a>
</li>
<li th:unless="${currentPage == 1}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${prevPage})}"><</a>
</li>
<li th:each="page: ${#numbers.sequence(startPage, endPage)}">
<a th:if="${currentPage != page}" th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${page})}">[[${page}]]</a>
<a th:if="${currentPage == page}" href="#" class="active">[[${page}]]</a>
</li>
<li th:unless="${currentPage == lastPage}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${nextPage})}">></a>
</li>
<li th:unless="${endPage == lastPage}">
<a th:href="@{/admin_productlist(type=${type}, keyword=${keyword}, page=${lastPage})}">끝</a>
</li>
</ul>
</div>
<!-- 페이지 번호 추가 끝 -->
🟢 상품 상세
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/adminlayout}">
<head>
<meta charset="utf-8">
<title>ADMIN - Product Detail</title>
</head>
<body>
<div layout:fragment="content">
<div class="main-wrapper wrapper-2">
<div class="breadcrumb-area breadcrumb-padding-8">
<div class="container">
<div class="breadcrumb-content text-center">
<div class="breadcrumb-title">
<h2>product Detail</h2>
</div>
</div>
</div>
</div>
<div class="notice-area bg-white pb-130">
<div class="container">
<div class="notice-info-wrap">
<table class="table notice-table" th:object="${productDTO}">
<tbody>
<tr>
<th scope="row" class="notice-padding" id="productName">상품명</th>
<td class="notice-info">
<input type="text" name="productName" th:field="*{productName}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productContent">상품설명</th>
<td class="notice-info">
<input type="text" name="productContent" th:field="*{productContent}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="CategoryTypeRole">카테고리</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<!-- null 체크 추가 -->
<input type="text" th:if="${productDTO.categoryTypeRole != null}" th:value="${productDTO.categoryTypeRole.getDescription()}" readonly>
<input type="text" th:if="${productDTO.categoryTypeRole == null}" value="카테고리 없음" readonly>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productCost">소비자가</th>
<td class="notice-info">
<input type="text" name="productCost" th:field="*{productCost}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productPrice">판매가</th>
<td class="notice-info">
<input type="text" name="productPrice" th:field="*{productPrice}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productCnt">재고수</th>
<td class="notice-info">
<input type="number" name="productCnt" th:field="*{productCnt}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="SellStateRole">판매상태</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<!-- null 체크 추가 -->
<input type="text" th:if="${productDTO.sellStateRole != null}" th:value="${productDTO.sellStateRole.getDescription()}" readonly>
<input type="text" th:if="${productDTO.sellStateRole == null}" value="판매상태 없음" readonly>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding">등록일</th>
<td class="notice-info">
<input type="text" name="reDate" th:value="${#temporals.format(productDTO.reDate, 'yyyy-MM-dd')}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding">수정일</th>
<td class="notice-info">
<input type="text" name="modDate" th:value="${#temporals.format(productDTO.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productDetail">상품정보</th>
<td class="notice-info">
<textarea rows="7" name="productDetail" th:field="*{productDetail}" readonly></textarea>
</td>
</tr>
<!-- 대표 이미지 -->
<tr th:if="${#lists.size(productDTO.imageDTOs) > 0}">
<th scope="row" class="notice-padding" id="imageDTOs[0]">대표이미지</th>
<td class="notice-info">
<!-- 이미지가 있는 경우에만 표시 -->
<img th:if="${productDTO.imageDTOs[0] != null}"
th:src="|/images/item/@{${productDTO.imageDTOs[0].imageFile}}|"
onerror="this.onerror=null; this.src='../assets/images/product/null.png';" width="300" height="300">
</td>
</tr>
<!-- 서브 이미지 -->
<tr th:if="${#lists.size(productDTO.imageDTOs) > 1}">
<th scope="row" class="notice-padding" id="imageDTOs[1]">서브이미지</th>
<td class="notice-info">
<!-- 이미지가 있는 경우에만 표시 -->
<img th:if="${productDTO.imageDTOs[1] != null}"
th:src="|/images/item/@{${productDTO.imageDTOs[1].imageFile}}|"
onerror="this.onerror=null; this.src='../assets/images/product/null.png';" width="300" height="300">
</td>
</tr>
<!-- 상세 이미지 -->
<tr th:if="${#lists.size(productDTO.imageDTOs) > 2}">
<th scope="row" class="notice-padding" id="imageDTOs[2]">상세이미지</th>
<td class="notice-info">
<img th:if="${productDTO.imageDTOs[2] != null}"
th:src="|/images/item/@{${productDTO.imageDTOs[2].imageFile}}|"
onerror="this.onerror=null; this.src='../assets/images/product/null.png';" style="width:50%;">
</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col d-flex justify-content-center">
<div class="notice-button-container">
<div class="notice-content">
<div class="notice-btn-date-wrap list-btn">
<div class="notice-btn">
<button type="button" class="btn btn-lg" th:onclick="|location.href='@{/admin_productlist}'|">목록</button>
<button type="button" class="btn btn-lg" th:onclick="|location.href='@{/admin_product_update(productId=${productDTO.productId})}'|">수정</button>
<button type="button" class="btn btn-lg" th:onclick="|location.href='@{/admin_product_delete(productId=${productDTO.productId})}'|">삭제</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- content 끝-->
</body>
</html>
▪️ 카테고리 불러오기
null인 경우 : value 설정
null 아닌 경우 : getDescription 한글로 받아오기
<input type="text" th:if="${productDTO.categoryTypeRole != null}" th:value="${productDTO.categoryTypeRole.getDescription()}" readonly>
<input type="text" th:if="${productDTO.categoryTypeRole == null}" value="카테고리 없음" readonly>
▪️ 이미지 불러오기
<tr th:if="${#lists.size(productDTO.imageDTOs) > 0}">
<th scope="row" class="notice-padding" id="imageDTOs[0]">대표이미지</th>
<td class="notice-info">
<!-- 이미지가 있는 경우에만 표시 -->
<img th:if="${productDTO.imageDTOs[0] != null}"
th:src="|/images/item/@{${productDTO.imageDTOs[0].imageFile}}|"
onerror="this.onerror=null; this.src='../assets/images/product/null.png';" width="300" height="300">
</td>
</tr>
🟢 상품 수정
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/adminlayout}">
<head>
<meta charset="utf-8">
<title>ADMIN - Product Update</title>
</head>
<body>
<div layout:fragment="content">
<div class="main-wrapper wrapper-2">
<div class="breadcrumb-area breadcrumb-padding-8">
<div class="container">
<div class="breadcrumb-content text-center">
<div class="breadcrumb-title">
<h2>product Update</h2>
</div>
</div>
</div>
</div>
<div class="notice-area bg-white pb-130">
<div class="container">
<div class="notice-info-wrap">
<form th:action="@{'/admin_product_update'}" method="post" enctype="multipart/form-data" th:object="${productDTO}">
<input type="hidden" name="productId" th:field="*{productId}">
<input type="hidden" th:field="*{modDate}" name="modDate">
<table class="table notice-table">
<tbody>
<tr>
<th scope="row" class="notice-padding" id="productName">상품명</th>
<td class="notice-info">
<input type="text" name="productName" th:field="*{productName}">
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productContent">상품설명</th>
<td class="notice-info">
<input type="text" name="productContent" th:field="*{productContent}">
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="CategoryTypeRole">카테고리</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<select name="categoryTypeRole">
<option th:each="state : ${categoryType}"
th:value="${state.name()}"
th:text="${state.getDescription()}"
th:selected="${state.name() eq productDTO.categoryTypeRole?.name()}">
</option>
</select>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding">소비자가</th>
<td class="notice-info">
<input type="text" name="productCost" th:field="*{productCost}">
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productPrice">판매가</th>
<td class="notice-info">
<input type="text" name="productPrice" th:field="*{productPrice}">
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productCnt">재고수</th>
<td class="notice-info">
<input type="number" name="productCnt" th:field="*{productCnt}">
</td>
</tr>
<!-- 판매상태 -->
<tr>
<th scope="row" class="notice-padding" id="SellStateRole">판매상태</th>
<td class="notice-info">
<div class="sidebar-widget update">
<div class="sidebar-archive-wrap">
<!---수정-->
<select id="sellState" name="sellStateRole">
<!-- 열거형 값들을 반복하며 옵션을 생성 -->
<option th:each="state : ${sellState}"
th:value="${state.name()}"
th:text="${state.getDescription()}"
th:selected="${state.name() eq productDTO.sellStateRole?.name()}">
</option>
</select>
<!--수정끝-->
</div>
</div>
</td>
</tr>
<tr>
<th scope="row" class="notice-padding" id="productDetail">상품정보</th>
<td class="notice-info">
<textarea rows="7" name="productDetail" th:field="*{productDetail}"></textarea>
</td>
</tr>
<!-- 대표 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[0]">대표이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[0].imageId" th:value="*{imageDTOs[0].imageId}">
<input type="hidden" name="imageDTOs[0].imageType" value="0">
<input type="file" name="images" onchange="previewImage(this, 'previewImg0')">
<img id="previewImg0" class="img-thumbnail" style="max-width: 100px; max-height: 100px;" th:if="${imageDTOs[0].imageFile != null}" th:src="|/images/item/${imageDTOs[0].imageFile}|">
</td>
</tr>
<!-- 서브 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[0]">서브이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[1].imageId" th:value="*{imageDTOs[1].imageId}">
<input type="hidden" name="imageDTOs[1].imageType" value="1">
<input type="file" name="images" onchange="previewImage(this, 'previewImg1')">
<img id="previewImg1" class="img-thumbnail" style="max-width: 100px; max-height: 100px;" th:if="${imageDTOs[1].imageFile != null}" th:src="|/images/item/${imageDTOs[1].imageFile}|">
</td>
</tr>
<!-- 상세 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[2]">상세이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[2].imageId" th:value="*{imageDTOs[2].imageId}">
<input type="hidden" name="imageDTOs[2].imageType" value="2">
<input type="file" name="images" onchange="previewImage(this, 'previewImg2')">
<img id="previewImg2" class="img-thumbnail" style="max-width: 100px; max-height: 100px;" th:if="${imageDTOs[2].imageFile != null}" th:src="|/images/item/${imageDTOs[2].imageFile}|">
</td>
</tr>
<script>
// 미리보기 이미지 업데이트 함수
function previewImage(input, imgId) {
var preview = document.getElementById(imgId);
var file = input.files[0];
var reader = new FileReader();
reader.onloadend = function () {
preview.src = reader.result;
};
if (file) {
reader.readAsDataURL(file);
} else {
preview.src = "";
}
}
</script>
</tbody>
</table>
<div class="row">
<div class="col d-flex justify-content-center">
<div class="notice-button-container">
<div class="notice-content">
<div class="notice-btn-date-wrap list-btn">
<div class="notice-btn">
<button type="submit" class="btn btn-lg">수정</button>
<button type="reset" class="btn btn-lg">다시</button>
<button type="button" class="btn btn-lg" th:onclick="|location.href='@{/admin_productlist}'|">목록</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div><!-- content 끝-->
</body>
</html>
▪️ 카테고리 설정
<select name="categoryTypeRole">
<option th:each="state : ${categoryType}"
th:value="${state.name()}"
th:text="${state.getDescription()}"
th:selected="${state.name() eq productDTO.categoryTypeRole?.name()}">
</option>
</select>
▪️ 이미지 설정
<!-- 대표 이미지 -->
<tr>
<th scope="row" class="notice-padding" id="imageDTOs[0]">대표이미지</th>
<td class="notice-info">
<input type="hidden" name="imageDTOs[0].imageType" value="0">
<input type="file" name="images" onchange="previewImage(this, 'previewImg0')">
<img id="previewImg0" class="img-thumbnail" style="max-width: 100px; max-height: 100px;">
</td>
</tr>
<script>
function previewImage(input, previewId) {
var preview = document.getElementById(previewId);
var file = input.files[0];
var reader = new FileReader();
reader.onload = function (e) {
preview.src = e.target.result;
};
if (file) {
reader.readAsDataURL(file);
}
}
$(document).ready(function () {
$('input[name^="images"]').change(function () {
var index = $(this).attr('id').replace('fileInput', '');
previewImage(this, 'previewImg' + index);
});
});
</script>
'Project > ShoppingMall' 카테고리의 다른 글
[User_공지사항] 댓글 등록, 목록, 수정 / 댓글 좋아요 싫어요 (3) (0) | 2024.01.25 |
---|---|
[User_공지사항] 게시글 목록 / 상세보기 / 조회수 / 이전 다음 페이지 이동 (2) (0) | 2024.01.23 |
[Admin_공지사항] enum 공지사항 유형 / 조회수 증가 / 다중 검색 기능(1) (0) | 2024.01.20 |
[User_상품진열] enum 카테고리별로 진열 / 상품 상세페이지 / 할인율 계산 / 자동합산 / 연관상품 (0) | 2024.01.20 |
[Admin_상품등록] 다중 이미지 삽입 / 이미지-상품 테이블 조인 / enum 카테고리, 판매 상태(1) (0) | 2024.01.19 |