hyeonga_code

reProject_37_사용자 상품 상세 페이지 기능 구현 옵션 선택/삭제/수량 변경 본문

Project_WEATHERWEAR

reProject_37_사용자 상품 상세 페이지 기능 구현 옵션 선택/삭제/수량 변경

hyeonga 2024. 1. 29. 06:59
반응형

 

reProject_36_사용자 장바구니 기능 구현(수량 변경, 선택 삭제, 전체 삭제)

2024.01.28 장바구니 페이지를 작업하려고 보니 ZenBlog에는 table 태그에 관련된 코드가 없어 기존에 작업한 화면을 적용하여 작업했다. 기능 부분도 기존의 작업을 적용하여 작업. 현재 회원/비회원

hyeonga493.tistory.com

 

2024.01.28

상품 상세 페이지 작업은 reProject 시 작업한 파일을 가져다가 수정해서 작업했다.

아래 파일은 관리자에서 사용한 코드를 동일하게 사용했다.

- ProductService.java

- ProductServiceImple.java

- ProductDAO.java

- product-mapping.xml

 

productInfo.jsp 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WeatherWear 사용자</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;500&amp;family=Inter:wght@400;500&amp;family=Playfair+Display:ital,wght@0,400;0,700;1,400;1,700&amp;display=swap" rel="stylesheet">
 
<!-- Vendor CSS Files -->
<link href="resources/client/ZenBlog/assets/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="resources/client/ZenBlog/assets/vendor/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
 
<!-- Template Main CSS Files -->
<link href="resources/client/ZenBlog/assets/css/main.css" rel="stylesheet">
<link href="resources/client/ZenBlog/assets/css/variables.css" rel="stylesheet">
 
<style>
.mb-2 { width: 210px; height: 50px; overflow: hidden}
.col-lg-3 { height: 440px; padding-left: 5%;}
.selectOption {color: #909090; font-size: 17px;}
.optionbox { display: flex; flex-direction: column;}
.optionbox_p { width: 50%; }
.total_div {display: flex; justify-content: space-around; font-size:20px; font-weight: 300;}
.selectValue{ width:100%; display: flex; flex-direction:row;}
</style>
</head>
<body class="hold-transition sidebar-collapse layout-top-nav">
    <div class="wrapper">
        <%@ include file="../header.jsp" %>
        <main id="main">
            <section>
                <div class="container">
                    <div class="row">
                        <div class="col-md-9 post-content aos-init aos-animate" data-aos="fade-up">
                            <!-- ======= Single Post Content ======= -->
                            <div class="single-post border-bottom">
                                <div class="post-meta">
                                    <input type="hidden" name="stockList" value="${ optionList }">
                                    <input type="hidden" name="productName" value="${ product.productName }">
                                    <input type="hidden" name="productId" value="${ product.productId }">
                                    <input type="hidden" name="optionInfo" value="${ optionInfo }">
                                    <input type="hidden" name="productPrice" value="${ product.productPrice }">
                                    <input type="hidden" name="odTotal" value="0">
                                    <span class="date"><a href="main.do">HOME</a></span> 
                                    <span class="mx-1"></span> 
                                    <span><a href="#">OUTER</a></span>
                                </div>
                                <h1 class="mb-5">${ product.productName }</h1>
                                <img src="${ product.mainImage }" alt="" class="img-fluid"><br><br>
                            </div>
                            <div class="single-post">
                                <c:forEach var="detail" items="${ detailImageList }">
                                    <img src="${ detail.detailImage }" alt="" class="img-fluid"><br><br>
                                </c:forEach>                                 
                              <p>${ product.productContent }</p>
                            </div>
                            <!-- End Single Post Content -->
                        </div>
                        <!-- Category -->
                        <div class="col-md-3">
                            <div class="aside-block">
                                <div class="tab-content" id="pills-tabContent">
                                    <!-- Popular -->
                                    <div class="tab-pane fade show active" id="pills-popular" role="tabpanel" aria-labelledby="pills-popular-tab">
                                        <div class="post-entry-1 border-bottom">
                                            <h2>${ product.productName }</h2>
                                            <h4><fmt:formatNumber value="${ product.productPrice }" pattern="###,###"/></h4>
                                        </div>
                                        <div class="post-entry-1 border-bottom">
                                            <!-- Color -->
                                            <div class="post-meta"><span class="date">Color</span></div>
                                            <c:forEach var="color" items="${ optionInfo.optionColorList }">
                                                <button type="button" id="color_${ color }" class="btn btn-outline-primary colorOption optionBtn" value="${ color }" onclick="select(this)">${ color }</button>&nbsp;
                                            </c:forEach>
                                            <input type="hidden" name="colorValue">
                                            <br><br>
                                            <!-- Size -->
                                            <div class="post-meta"><span class="date">Size</span></div>
                                            <c:forEach var="size" items="${ optionInfo.optionSizeList }">
                                                <button type="button" id="size_${ size }"  class="btn btn-outline-primary sizeOption optionBtn" value="${ size }" onclick="select(this)">${ size }</button>&nbsp;
                                            </c:forEach>
                                            <input type="hidden" name="sizeValue">
                                            <br><br>
                                        </div>
                                        <div class="post-entry-1 border-bottom optionbox" id="optionbox">
                                            
                                        </div>
                                        <div class="post-entry-1 border-bottom total_div">
                                            <span><b>Total</b></span>
                                            <span class="totalPrice"></span>
                                            <br><br>
                                        </div>
                                        <div class="post-entry-1 border-bottom">
                                            <button type="button" class="btn btn-outline-primary">WishList</button>
                                            <button type="button" class="btn btn-outline-primary">Add Cart</button>
                                            <button type="button" class="btn btn-primary">Buy Now</button>
                                            <br><br>
                                        </div>
                                    </div>
                                    <!-- End Popular -->
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
        </main>
        <%@ include file="../footer.jsp" %>
    </div>
 
<script src="resources/client/ZenBlog/assets/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
 
<!-- Template Main JS File -->
<script src="resources/client/ZenBlog/assets/js/main.js"></script>
 
<script src="resources/client/js/product.js"></script>
</body>
</html>

 

 

ClientController.java 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.w2.client.controller;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
import com.w2.product.service.ProductService;
import com.w2.util.SearchOrderby;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
@Controller
public class ClientController {
    
    @Autowired
    private ProductService productService;
    
    @RequestMapping("test.do")
    public String test() {
        return "test";
    }
 
    /**
     * 메인 화면 호출
     * @return
     */
    @RequestMapping("main.do")
    public String mainView(Model model, HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        model.addAttribute("bestItem", productService.getMainProductList("best"));
        model.addAttribute("newItem", productService.getMainProductList("new"));
        //model.addAttribute("userInfo", session.getAttribute("userInfo"));
        return "main";
    }
 
    /**
     * 상품 목록 화면 호출
     * @return
     */
    @RequestMapping("productList.do")
    public String productList(Model model, @RequestParam(required = false, defaultValue = "1"int page,
            @RequestParam(required = false, defaultValue = "1"int range, @RequestParam(required = false, defaultValue = "productName"String searchType,
            @RequestParam(required = falseString keyword, @RequestParam(required = false, defaultValue = "productRegDate"String orderby, @ModelAttribute("search") SearchOrderby search) {
        // 검색
        model.addAttribute("search", search);
        if(search.getListSize()==10)
            search.setListSize(20);
        search.setSearchType(searchType);
        search.setKeyword(keyword);
        search.setOrderby(orderby);
        
        // 전체 게시글 개수
        int listCnt = productService.getProductListCnt(search);
        
        // 검색 페이지 정보
        search.pageInfo(page, range, listCnt);
        // 페이징
        model.addAttribute("pagination", search);
        // 화면 출력
        model.addAttribute("productList", productService.getProductList(search));
        
        return "product/productList";
    }
 
    /**
     * 상품 상세 화면 호출
     * @return
     */
    @RequestMapping("productInfo.do")
    public String productInfo(@RequestParam(value = "productId", required = false)String productId, Model model, HttpServletRequest request) {
        model.addAttribute("product", productService.getProduct(productId, model));
        return "product/productInfo";
    }
}

 

product.js > client-product.js 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
 
// 옵션 정보 가져오기
function getOpList(){
    let optionList = $("input[name='optionInfo']").val();
    let opList = [];
    
    optionList = optionList.slice(1-1);
    let option = optionList.split("}, {");
    
    option.forEach(op => {
        let option = new Map();
        op = op.split(', ');
        
        for(let i=0; i < op.length; i++){
            let keyValue = op[i].split('=');
            option.set(keyValue[0], keyValue[1]);
        }
        opList.push(option);
    });
    return opList;
}
 
// 옵션 수량 변경
function count(num, name) {
    var cnt = parseInt($("div[name='" + name + "'] input[name='cnt" + name + "']").val());
    var stCnt = parseInt($("div[name='" + name + "'] input[name='pro_" + name + "_cnt']").val());
    var odTotal = parseInt($("input[name='odTotal']").val());
    let productPrice = parseInt($("input[name='productPrice']").val());
    
    if (num == -1) {
        odTotal += productPrice;
        cnt++;
    } else if (num == -2){
        odTotal -= productPrice;
        cnt--;
    } else {
        let totalPriceList = document.getElementsByClassName("optioncount");
        let productCnt = 0;
        Array.prototype.forEach.call(totalPriceList, function(proCnt){
            productCnt += parseInt(proCnt.value);
        });
        odTotal = productPrice*productCnt;
        cnt = num;
    }
    
    if (cnt < 1) {
        cnt = 0;
        $("div[name='" + name + "']").remove();
    } else if(cnt > stCnt){
        cnt = stCnt;
        alert("재고가 부족합니다.");
    }
    
    $("div[name='" + name + "'] input[name='cnt" + name + "']").val(cnt);
    $("input[name='odTotal']").val(odTotal);
    setOdTotal();
}
 
function setOdTotal(){
    var odTotal = parseInt($("input[name='odTotal']").val());
    var formatter = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW', minimumFractionDigits: 0, maximumFractionDigits: 0 });
    var formattedNum = formatter.format(odTotal);
    
    $(".totalPrice").html(formattedNum);
}
 
// 선택 버튼 활성화
function activeBtn(selectElement){
    if(selectElement == null){
        let optionBtns = document.getElementsByClassName("optionBtn");
        Array.prototype.forEach.call(optionBtns, function(option){
            option.classList.remove("active");
        });
        $("input[name='colorValue'").val("");
        $("input[name='sizeValue'").val("");
        return;
    }
 
    let className = (selectElement.id).split("_")[0]+"Option";
 
    let optionBtns = document.getElementsByClassName(className);
    
    Array.prototype.forEach.call(optionBtns, function(option){
        option.classList.remove("active");
    });
    
    $("#" + selectElement.id).addClass("active");
    $("input[name='"+(selectElement.id).split("_")[0]+"Value'").val(selectElement.value);
}
 
// 옵션 선택
function select(selectElement) {
    let option = selectElement.id;
 
    activeBtn(selectElement);
 
    let opList = getOpList();
    
    // 선택한 값
    let color = $("input[name='colorValue'").val();
    let size = $("input[name='sizeValue'").val();
    let cnt = 0;
    
    // 재고 확인
    let stockCnt = 0;
    
    // 색상 + 사이즈 + 수량
    let stockList = $("input[name='stockList']").val();
    let stList = [];
    
    stockList = stockList.slice(1-1);
    stockList = stockList.slice(1-1);
    let stock = stockList.split("}, {");
    
    let odTotal = parseInt($("input[name='odTotal']").val());
    let productName = $("input[name='productName']").val();
    let productId = $("input[name='productId']").val();
    let productPrice = parseInt($("input[name='productPrice']").val());
    if(color == null || color == "" || size == null || size == ""){
        return;
    }
    
    stock.forEach(st => {
        let stock = new Map();
        st = st.split(', ');
        
        for(let i=0; i < st.length; i++){
            let keyValue = st[i].split('=');
            stock.set(keyValue[0], keyValue[1]);
        }
        stList.push(stock);
    });
    
    stList.forEach(st => {
        if(st.get("optionColor"== color && st.get("optionSize"== size){
            stockCnt = st.get("stockCnt");
        }
    });
    
    if (stockCnt == '0'){
        cnt = 0;
        alert("재고가 부족합니다.");
        // 초기화
        activeBtn(null);
        return;
    }
    
    var name= color+size;
    
    if($("#"+name).length){
        alert("이미 선택한 옵션입니다.");
        count(-1name);
    } else {
        var select = "";
        select += "<div name='" + color + size + "' id='" + color + size + "' class='selectValue'>"
                + "<p class='optionbox_p'>" + productName + "<br><span class='selectOption'>&nbsp;/&nbsp;" + color + "&nbsp;/&nbsp;" + size + "&nbsp;&nbsp;&nbsp;</span></p><p class='optionbox_p'>"
                + "<span class='author mb-3 d-block'>" + productPrice + "</span><button id='id' name='" + name 
                + "'class='pro_btn' onclick='deleteSelected(this)' style='float:right; margin-left:10px;'>X</button><span class='quantity' style='width:65px;'><a href='#' class='pro_btn' onclick='count(-2,\"" + name +"\")'>-</a>&nbsp;"
                + "<input type='text' name='cnt" + name + "' class='quantity_opt eProductQuantityClass optioncount' id='cnt" + name + "'  name='cnt" + name 
                + "' style='width:35px; height:27px; margin-left:3px; margin-right:3px; text-align:center;' value=1 max-value=" + stockCnt + " onChange='count(this.value,\"" + name +"\")'>&nbsp;"
                + "<a href='#' class='pro_btn' onclick='count(-1,\"" + name +"\")'>+</a></span>"
                + "<input type='hidden' name='optionId' value='" + productId + name + "'><input type='hidden' name='pro_" + name + "_cnt' value='" + stockCnt + "'>"
                + "</p></div>";
        count(-1name);
        $("#optionbox").append(select);
    }            
    // 초기화
    activeBtn(null);
}
 
function moveToProductInfoMenu(){
    document.querySelector('.product_info_menu').scrollIntoView();
}
 
function deleteSelected(element){
    let cnt = $(element).parent().parent().find("input[name='cnt" + element.name + "']").val();
    console.log(">>> cnt" + cnt);
    for(let i=0; i<cnt; i++){
    console.log("delete");
        count(-2, element.name);
    }
    $(element).parent().parent().remove();
}
 
// 장바구니에 추가
function addCart(){
    // cartVO 객체를 가지는 리스트
    let productId = $("input[name='productId']").val();
    let list = addList(productId);
    
    $.ajax({
        url: "/w2/clientAddCart.do",
        type: "POST",
        async: true,
        data: JSON.stringify(list),
        contentType: "application/json",
        success: function(response){
            if(confirm("장바구니에 상품이 담겼습니다.\n장바구니로 이동하시겠습니까?")) {
                 location.href="clientCart.do";
             } else {
                 reload:"clientProductInfo.do?productId="+productId;
             }
        },
        error : function(error){
            alert("실패");
        }
    });
}
 
// 상품 리스트에 담기
function addList(productId){
    let list = [];
    let productCounts = document.querySelectorAll(".selectValue");
 
    productCounts.forEach(product => {
        let colorSize = product.id;
        let cart = {}; // 객체 생성
        
        cart.productId = productId;
        cart.optionId = product.querySelector("input[name='optionId']").value;
        cart.cartCnt = product.querySelector("input[name='cnt" + colorSize + "']").value;
        
        list.push(cart);
    });
    
    return list;
}
 

 

>> 실행

 

>>> 옵션을 button으로 작업하여 선택시 active 클래스를 추가하여 선택된 옵션을 확인할 수 있게 작업했다.

 

>>> 색상과 사이즈를 모두 선택하면 선택한 값을 초기화하고 아래 옵션 란에 선택한 옵션의 정보가 추가되며 Total 가격이 변경된다.

-- 동일한 옵션이 이미 존재하는 경우 수량이 변경되도록 작업

-- 수량이 1이하인경우 옵션란에서 삭제되게 작업

 

>>> 수량 변경 적용

-- 기존 파일에 문제를 발견

옵션을 여러 개 선택했을 때, 수량을 직접 입력하여 변경하는 경우 해당 태그의 값만 전체 가격에 추가되는 것을 발견했다. 직접 수량을 변경하는 경우 class에 optioncount를 가지고 있는 모든 태그를 훑어 가격을 조정하도록 변경했다. 

 

 

 >>> 여러 옵션 선택

 

 

>>> 옵션 삭제

 

 

reProject_38_sweetAlert2 적용하기, myBatis foreach문으로 반복 update, delete 적용

2024.01.30 기존에 작업했던 파일중 foreach문이 실행되지 않아 DAO.java에서 for문으로 작업한 기능들을 수정 alert()을 custom하기위해 sweetAlert을 적용했다. -- sweetAlert 적용 구글링을 하다가 참고한 사이

hyeonga493.tistory.com

 

반응형