상품 목록(goodsManage.jsp)페이지에서 상품의 이름을 클릭하였을 때 이동하는 조회 페이지를 구현하고자 합니다. 조회 페이지에는 선택한 상품에 관한 등록된 정보가 출력되어야 합니다. 앞서 구현했던 작가 조회 페이지 구현과 동일합니다.
AdminMapper.java
조회 쿼리를 호출하는 메서드를 AdminMapper.java인터페이스에 추가합니다.
/* 상품 조회 페이지 */
public BookVO goodsGetDetail(int bookId);
AdminMapper.xml
위에서 작성한 메서드가 실행할 태그 및 쿼리문을 작성합니다. 조회 페이지에 사용자가 보기 편하도록 작가ID가 아닌 작가 이름이 나오도록 서브쿼리를 삽입하였습니다.
<!-- 상품 조회 페이지 -->
<select id="goodsGetDetail" resultType="com.test.model.BookVO">
select bookId, bookName, (select authorName from test_author where authorId = test_book.authorId) authorName,
authorId, publeYear, publisher, cateCode, bookPrice, bookStock, bookDiscount, bookIntro, bookContents, regDate, updateDate
from test_book where bookId = #{bookId}
</select>
Mapper와 Controller를 연결 시켜줄 Service메서드를 작성하겠습니다. AdminService.java인터페이스에 아래의 메서드 선언부를 추가합니다.
/* 상품 조회 페이지 */
public BookVO goodsGetDetail(int bookId);
AdminServiceImpl.java클래스에 앞서 추가한 메서드를 오버라이딩하여 구현합니다.
/* 상품 조회 페이지 */
@Override
public BookVO goodsGetDetail(int bookId) {
log.info("goodsGetDetail()..........." + bookId);
return adminMapper.goodsGetDetail(bookId);
}
AdminController.java클래스에 '상품 조회 페이지'이동을 위한 url매핑 메서드를 아래와 같이 추가해줍니다.
사용자가 선택한 상품에 대한 정보를 가져오는 mapper메서드를 실행시키기 위해 int타입의 bookId변수를 파라미터로 추가하였고, 상품 조회 페이지 이동 후 다시 목록 페이지로 이동할 때 필요로 한 데이터인 Criteria클래스, 상품 조회 페이지에 데이터를 전달해주기위해 Model클래스를 파라미터로 추가하였습니다.
/* 상품 조회 페이지 */
@GetMapping("/goodsDetail")
public void goodsGetInfoGet(int bookId, Criteria cri, Model model) {
logger.info("goodsGetInfo().........." + bookId);
/* 목록 페이지 조건 정보 */
model.addAttribute("cri", cri);
/* 조회 페이지 정보 */
model.addAttribute("goodsInfo", adminService.goodsGetDetail(bookId));
}
사용자가 '상품 목록(goodsManage.jsp) 페이지'에서 상품 이름을 클릭하였을 때 '상품 조회(goodsDetail.jsp)페이지'로 이동할 수 있도록 해주는 작업을 먼저 진행합니다.
goodsManage.jsp에서 상품 이름이 출력되는 코드를 아래의 태그와 같이 <a>태그로 감싸줍니다. <a>태그의 href속성은 선택되는 상품의 ID가 출력이 되도록 작성하였습니다.
<a class="move" href='<c:out value="${list.bookId}"/>'>
<c:out value="${list.bookName}"></c:out>
</a>
작성한 <a>태그가 동작하도록 <script>태그에 아래의 Javascript코드를 추가해줍니다.
/* 상품 조회 페이지 */
$(".move").on("click", function(e){
e.preventDefault();
moveForm.append("<input type='hidden' name='bookId' value='"+$(this).attr("href") + "'>");
moveForm.attr("action", "/admin/goodsDetail");
moveForm.submit();
});
"goodsDetail" jsp, css파일을 새로 생성해주고 jsp파일에 css파일을 연결해주는 <link>태그를 추가해줍니다. 그리고 jquery를 사용하기 위해 <script>코드와 jstl을 사용하기 위해 라이브러리코드를 추가해주었습니다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<link rel="stylesheet" href="../resources/css/admin/goodsDetail.css">
<script
src="https://code.jquery.com/jquery-3.4.1.js"
integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
crossorigin="anonymous"></script>
<body>태그 내부에 아래의 코드를 추가해줍니다.
등록 페이지의 태그 코드들을 그대로 가져와서 일부를 수정 및 추가하였습니다.
goodsDetail.jsp
<%@include file="../includes/admin/header.jsp" %>
<div class="admin_content_wrap">
<div class="admin_content_subject"><span>상품 상세</span></div>
<div class="admin_content_main">
<div class="form_section">
<div class="form_section_title">
<label>책 제목</label>
</div>
<div class="form_section_content">
<input name="bookName" value="<c:out value="${goodsInfo.bookName}"/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>등록 날짜</label>
</div>
<div class="form_section_content">
<input value="<fmt:formatDate value='${goodsInfo.regDate}' pattern='yyyy-MM-dd'/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>최근 수정 날짜</label>
</div>
<div class="form_section_content">
<input value="<fmt:formatDate value='${goodsInfo.updateDate}' pattern='yyyy-MM-dd'/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>작가</label>
</div>
<div class="form_section_content">
<input id="authorName_input" readonly="readonly" value="${goodsInfo.authorName }" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>출판일</label>
</div>
<div class="form_section_content">
<input name="publeYear" autocomplete="off" readonly="readonly" value="<c:out value="${goodsInfo.publeYear}"/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>출판사</label>
</div>
<div class="form_section_content">
<input name="publisher" value="<c:out value="${goodsInfo.publisher}"/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>책 카테고리</label>
</div>
<div class="form_section_content">
<div class="cate_wrap">
<span>대분류</span>
<select class="cate1" disabled>
<option value="none">선택</option>
</select>
</div>
<div class="cate_wrap">
<span>중분류</span>
<select class="cate2" disabled>
<option value="none">선택</option>
</select>
</div>
<div class="cate_wrap">
<span>소분류</span>
<select class="cate3" name="cateCode" disabled>
<option value="none">선택</option>
</select>
</div>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>상품 가격</label>
</div>
<div class="form_section_content">
<input name="bookPrice" value="<c:out value="${goodsInfo.bookPrice}"/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>상품 재고</label>
</div>
<div class="form_section_content">
<input name="bookStock" value="<c:out value="${goodsInfo.bookStock}"/>" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>상품 할인율</label>
</div>
<div class="form_section_content">
<input id="discount_interface" maxlength="2" disabled>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>책 소개</label>
</div>
<div class="form_section_content bit">
<textarea name="bookIntro" id="bookIntro_textarea" disabled>${goodsInfo.bookIntro}</textarea>
</div>
</div>
<div class="form_section">
<div class="form_section_title">
<label>책 목차</label>
</div>
<div class="form_section_content bct">
<textarea name="bookContents" id="bookContents_textarea" disabled>${goodsInfo.bookContents}</textarea>
</div>
</div>
<div class="btn_section">
<button id="cancelBtn" class="btn">상품 목록</button>
<button id="enrollBtn" class="btn enroll_btn">수정 </button>
</div>
</div>
<form id="moveForm" action="/admin/goodsManage" method="get" >
<input type="hidden" name="pageNum" value="${pageMaker.cri.pageNum}">
<input type="hidden" name="amount" value="${pageMaker.cri.amount}">
<input type="hidden" name="keyword" value="${pageMaker.cri.keyword}">
</form>
</div>
<%@include file="../includes/admin/footer.jsp" %>
페이지를 꾸며주기 위해서 'goodsDetail.css'파일에 아래의 코드를 추가해줍니다.
goodsDetail.css
@charset "UTF-8";
*{
margin: 0;
padding:0;
}
a{
text-decoration: none;
}
ul{
list-style: none;
}
/* 화면 전체 렙 */
.wrapper{
width: 100%;
}
/* content 랩 */
.wrap{
width : 1080px;
margin: auto;
}
/* 홈페이지 기능 네비 */
.top_gnb_area{
width: 100%;
height: 50px;
background-color: #f0f0f1;
position:relative;
}
.top_gnb_area .list{
position: absolute;
top: 0px;
right: 0;
}
.top_gnb_area .list li{
list-style: none;
float : left;
padding: 13px 15px 0 10px;
font-weight: 900;
cursor: pointer;
}
/* 관리제 페이지 상단 현페이지 정보 */
.admin_top_wrap{
height:110px;
line-height: 110px;
background-color: #5080bd;
margin-bottom: 15px;
}
.admin_top_wrap>span{
margin-left: 30px;
display:inline-block;
color: white;
font-size: 50px;
font-weight: bolder;
}
/* 관리자 wrap(네비+컨텐츠) */
.admin_wrap{
}
/* 관리자페이지 네비 영역 */
.admin_navi_wrap{
width: 20%;
height: 300px;
float:left;
height: 100%;
}
.admin_navi_wrap li{
display: block;
height: 80px;
line-height: 80px;
text-align: center;
}
.admin_navi_wrap li a{
display: block;
height: 100%;
width: 95%;
margin: 0 auto;
cursor: pointer;
font-size: 30px;
font-weight: bolder;
}
.admin_navi_wrap li a:link {color: black;}
.admin_navi_wrap li a:visited {color: black;}
.admin_navi_wrap li a:active {color: black;}
.admin_navi_wrap li a:hover {color: black;}
.admin_list_02{
background-color: #c8c8c8;
}
/* 관리자페이지 컨텐츠 영역 */
.admin_content_wrap{
width: 80%;
float:left;
min-height: 700px;
}
.admin_content_subject{ /* 관리자 컨텐츠 제목 영역 */
font-size: 40px;
font-weight: bolder;
padding-left: 15px;
background-color: #6AAFE6;
height: 80px;
line-height: 80px;
color: white;
}
/* 관리자 컨텐츠 메인 영역 */
.form_section{
width: 95%;
margin-left: 2%;
margin-top: 20px;
border: 1px solid #dbdde2;
background-color: #efefef;
}
.form_section_title{
padding: 20px 35px;
}
.form_section_title label{
display: block;
font-size: 20px;
font-weight: 800;
}
.form_section_content{
padding: 20px 35px;
border-top: 1px solid #dbdde2;
}
.form_section_content input{
width: 98%;
height: 25px;
font-size: 20px;
padding: 5px 1%;
}
.ui-datepicker-trigger { /* 캘린더 css 설정 */
margin-left: 25px;
width: 14%;
height: 38px;
font-weight: 600;
background-color: #dfe8f5;
font-size: 15px;
cursor:pointer;
}
.authorId_btn { /* 작가 선택 css 설정 */
margin-left: 20px;
width: 14%;
height: 38px;
font-weight: 600;
background-color: #dfe8f5;
font-size: 15px;
cursor:pointer;
}
.ck-content { /* ckeditor 높이 */
height: 170px;
}
/* 버튼 영역 */
.btn_section{
text-align: center;
margin: 80px 0;
}
.btn{
min-width: 180px;
padding: 4px 30px;
font-size: 25px;
font-weight: 600;
line-height: 40px;
}
.enroll_btn{
background-color: #dbdde2;
margin-left:15px;
}
#enrollBtn:hover {
background-color: #c9cbd0;
}
.form_section_content select { /* 카테고리 css 설정 */
width: 92%;
height: 35px;
font-size: 20px;
text-align-last: center;
margin-left: 5px;
}
.cate_wrap span {
font-weight: 600;
}
.cate_wrap:not(:first-child) {
margin-top: 20px;
}
/* footer navai 영역 */
.footer_nav{
width:100%;
height:50px;
}
.footer_nav_container{
width: 100%;
height: 100%;
background-color:#8EC0E4;
}
.footer_nav_container>ul{
font-weight : bold;
float:left;
list-style:none;
position:relative;
padding-top:10px;
line-height: 27px;
font-family: dotum;
margin-left: 10px;
}
.footer_nav_container>ul>li{
display:inline;
width: 45px;
height: 19px;
padding: 10px 9px 0 10px;
}
.footer_nav_container>ul>span{
margin: 0 4px;
}
/* footer 영역 */
.footer{
width:100%;
height:130px;
background-color:#D4DFE6;
padding-bottom : 50px;
}
.footer_container{
width: 100%;
height: 100%;
margin: auto;
}
.footer_left>img {
width: 150%;
height: 130px;
margin-left: -20px;
margin-top: -12px;
}
.footer_left{
float :left;
width: 170px;
margin-left: 20px;
margin-top : 30px;
}
.footer_right{
float :left;
width: 680px;
margin-left: 70px;
margin-top : 30px;
}
/* float 속성 해제 */
.clearfix{
clear: both;
}
현재의 할인율 값은 소수로 출력되는데 사용자가 보기 편하도록 자연수(1~99)로 출력되도록 할 것입니다. 이를 실현하기 위해서 서버로부터 받아온 할인율 값을 Javascript를 통해서 가공하여 할인율 항목에 출력되도록 할 것입니다.
goodsDetail.jsp의 <body>태그 제일 아래의 <script>태그를 추가해준 후 내부에 페이지가 렌더링 될 때 실행되는 메서드 (".read(function())")를 추가해줍니다.
$(document).ready(function(){
/* 할인율 값 삽입 */
let bookDiscount = '<c:out value="${goodsInfo.bookDiscount}"/>' * 100;
$("#discount_interface").attr("value", bookDiscount);
});
goodsEnroll.jsp때와 마찬가지로 ckeditor5를 적용해줄 것입니다. 먼저 ckeditor를 사용하기 위한 cdn코드를 <head>태그 내부에 추가해줍니다.
<script src="https://cdn.ckeditor.com/ckeditor5/26.0.0/classic/ckeditor.js"></script>
'책소개', '책목차' <textarea>태그에 ckeditor를 적용시켜주는 코드를 <script>코드 내부에 추가해주고 이 두 항목 또한 다른 항목과 마찬가지로 사용자가 수정을 할 수 없도록 변경해주어야 합니다. <textarea>단독으로 사용을 하였다면 'disable'속성만 추가해주면 됩니다. 하지만 ckeditor가 적용된 상태에서 <textarea>에 disabled속성을 추가해주더라도 사용자가 해당 페이지에 접속했을 때 '책 소개', '책 목차'를 클릭하거나 입력이 가능합니다.
입력을 막기 위해선 ckeditor 내부에서 입력 및 수정을 못하도록 설정을 해주어야합니다. 따라서 작성한 코드에 readonly설정을 추가합니다.
/* 책 소개 */
ClassicEditor
.create(document.querySelector('#bookIntro_textarea'))
.then(editor => {
console.log(editor);
editor.isReadOnly = true;
})
.catch(error=>{
console.error(error);
});
/* 책 목차 */
ClassicEditor
.create(document.querySelector('#bookContents_textarea'))
.then(editor => {
console.log(editor);
editor.isReadOnly = true;
})
.catch(error=>{
console.error(error);
});
'상품 등록(goodsEnroll.jsp)'페이지때는 카테고리 항목 사용자가 선택에 따라 대분류, 중분류, 소분류 순으로 출력이 되도록 구현을 했습니다. 하지만 '상품 조회(goodsDetail.jsp)'페이지의 경우 카테고리는 DB에 등록되어 있는 카테고리에 따라 선택(selected)된 상태로 출력이 되어야 합니다.
지금 현재의 카테고리 출력에 사용할 수 있는 데이터는 사용자가 최종적으로 선택하였던 DB에 등록되어 있는 소분류의 코드만 있습니다. 문제는 해당 데이터만으로는 대분류, 중분류 항목까지 출력시킬 수는 없습니다. 중분류, 대분류를 출력시키기 위해선 카테고리 항목이 무엇이 담겨있는지 알 수 있는 카테고리 항목 전체 데이터가 필요로 합니다. 따라서 '상품 등록(goodsEnroll.jsp)'에 사용하였던 카테고리 항목에 관한 전체 데이터를 '상품 조회(goodsDetail.jsp)'페이지에 가져와서 JSON데이터로 변환한 카테고리 분류에 따라 객체로 분류해둔 코드를 활용할 것입니다.
카테고리 출력의 전체적인 작업 순서는 소분류, 중분류, 대분류 순으로 진행할 것입니다.
본격적으로 작업을 진행하기 전 카테고리 리스트 출력에 필요한 카테고리 항목 데이터를 세팅하겠습니다. 상품 조회 url매핑 메서드에 아래의 코드를 추가해줍니다.
ObjectMapper mapper = new ObjectMapper();
/* 카레고리 리스트 데이터 */
model.addAttribute("cateList", mapper.writeValueAsString(adminService.cateList()));
'상품 조회(goodsDetail.jsp)'페이지로 돌아와서 "상품 등록"에 사용하였던 아래의 코드를 $(document).ready메서드 구현부에 작성합니다. 아래 코드는 서버로부터 전달받은 '카테고리 리스트'객체를 JSON으로 변환하고 소분류, 중분류, 대분류로 분류하여 각 데이터를 cate1Array, cate2Array, cate3Array변수에 저장하는 코드입니다.
/* 카테고리 */
let cateList = JSON.parse('${cateList}');
let cate1Array = new Array();
let cate2Array = new Array();
let cate3Array = new Array();
let cate1Obj = new Object();
let cate2Obj = new Object();
let cate3Obj = new Object();
let cateSelect1 = $(".cate1");
let cateSelect2 = $(".cate2");
let cateSelect3 = $(".cate3");
/* 카테고리 배열 초기화 메서드 */
function makeCateArray(obj,array,cateList, tier){
for(let i = 0; i < cateList.length; i++){
if(cateList[i].tier === tier){
obj = new Object();
obj.cateName = cateList[i].cateName;
obj.cateCode = cateList[i].cateCode;
obj.cateParent = cateList[i].cateParent;
array.push(obj);
}
}
}
/* 배열 초기화 */
makeCateArray(cate1Obj,cate1Array,cateList,1);
makeCateArray(cate2Obj,cate2Array,cateList,2);
makeCateArray(cate3Obj,cate3Array,cateList,3);
중분류, 소분류 카테고리에서 선택된(selected) 항목에 대한 데이터를 저장할 변수를 선언 후, 소분류 변수에는 DB에 저장된 사용자가 선택한 카테고리 코드로 초기화합니다.
let targetCate2 = '';
let targetCate3 = '${goodsInfo.cateCode}';
for(let i = 0; i < cate3Array.length; i++){
if(targetCate3 === cate3Array[i].cateCode){
targetCate3 = cate3Array[i];
}
}// for
for(let i = 0; i < cate3Array.length; i++){
if(targetCate3.cateParent === cate3Array[i].cateParent){
cateSelect3.append("<option value='"+cate3Array[i].cateCode+"'>" + cate3Array[i].cateName + "</option>");
}
}
$(".cate3 option").each(function(i,obj){
if(targetCate3.cateCode === obj.value){
$(obj).attr("selected", "selected");
}
});
중분류를 출력시키는 것은 소분류와 동일한 작업입니다. 아래의 코드를 작성하여 소분류와 마찬가지로 targetCate2 변수에 선택되어야 할 항목 객체로 초기화합니다. (cate2Array요소의 cateCode중 targetCate3의 cateParent값과 동일한 cate2Array요소를 찾는 작업입니다. 쉽게 말해 선택되어져야할 중분류를 찾는 작업입니다.)
for(let i = 0; i < cate2Array.length; i++){
if(targetCate3.cateParent === cate2Array[i].cateCode){
targetCate2 = cate2Array[i];
}
}// for
for(let i = 0; i < cate2Array.length; i++){
if(targetCate2.cateParent === cate2Array[i].cateParent){
cateSelect2.append("<option value='"+cate2Array[i].cateCode+"'>" + cate2Array[i].cateName + "</option>");
}
}
$(".cate2 option").each(function(i,obj){
if(targetCate2.cateCode === obj.value){
$(obj).attr("selected", "selected");
}
});
마지막 분류이기 때문에 따로 변수를 선언하여 진행하지 않았습니다. 왜냐하면 대분류에 있는 모든 항목들을 <option>태그로 추가해주면 되고, 선택(selected)되어야 할 값은 targetCate2.cateParent를 사용하면 되기 때문입니다.
for(let i = 0; i < cate1Array.length; i++){
cateSelect1.append("<option value='"+cate1Array[i].cateCode+"'>" + cate1Array[i].cateName + "</option>");
}
$(".cate1 option").each(function(i,obj){
if(targetCate2.cateParent === obj.value){
$(obj).attr("selected", "selected");
}
});