Spring

[Spring] 나만의 셀렉샵 프로젝트 - 클라이언트(HTML, Javascript, jQuery, CSS)

jelliclesu 2024. 7. 30. 19:08

프로젝트 설계하기

필요 기능

  • 키워드로 상품 검색하고 그 결과를 목록으로 보여주기
  • 관심 상품 등록하기
  • 관심 상품 조회하기
  • 관심 상품에 원하는 가격 등록하고, 그 가격보다 낮은 경우 표시하기

 

HTML, 이미지 파일 준비

  • 파일 분리
    ✔️ HTML 파일이 CSS 와 Javascpript 때문에 지나치게 길어지는 것을 방지하고 가독성을 높이기 위함
    ✔️ .css 와 .js 로 끝나는 파일 생성 후 link 와 script 태그로 각 파일을 불러오면 index.html 파일에 모두 작성한 것과 동일하게 작동
    1. src > main > resources > static 에 파일 생성
      • index.html
        <!doctype html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport"
                  content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <link rel="stylesheet" href="style.css">
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
            <script src="basic.js"></script>
            <title>나만의 셀렉샵</title>
        </head>
        <body>
        <div class="header">
            Select Shop
        </div>
        <div class="nav">
            <div class="nav-see active">
                모아보기
            </div>
            <div class="nav-search">
                탐색하기
            </div>
        </div>
        <div id="see-area">
            <div id="product-container">
                <div class="product-card" onclick="window.location.href='https://spartacodingclub.kr'">
                    <div class="card-header">
                        <img src="https://shopping-phinf.pstatic.net/main_2085830/20858302247.20200602150427.jpg?type=f300"
                             alt="">
                    </div>
                    <div class="card-body">
                        <div class="title">
                            Apple 아이폰 11 128GB [자급제]
                        </div>
                        <div class="lprice">
                            <span>919,990</span>원
                        </div>
                        <div class="isgood">
                            최저가
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div id="search-area">
            <div>
                <input type="text" id="query">
                <!--    <img src="images/icon-search.png" alt="">-->
            </div>
            <div id="search-result-box">
                <div class="search-itemDto">
                    <div class="search-itemDto-left">
                        <img src="https://shopping-phinf.pstatic.net/main_2399616/23996167522.20200922132620.jpg?type=f300" alt="">
                    </div>
                    <div class="search-itemDto-center">
                        <div>Apple 아이맥 27형 2020년형 (MXWT2KH/A)</div>
                        <div class="price">
                            2,289,780
                            <span class="unit">원</span>
                        </div>
                    </div>
                    <div class="search-itemDto-right">
                        <img src="images/icon-save.png" alt="" onclick='addProduct()'>
                    </div>
                </div>
            </div>
            <div id="container" class="popup-container">
                <div class="popup">
                    <button id="close" class="close">
                        X
                    </button>
                    <h1>⏰최저가 설정하기</h1>
                    <p>최저가를 설정해두면 선택하신 상품의 최저가가 떴을 때<br/> 표시해드려요!</p>
                    <div>
                        <input type="text" id="myprice" placeholder="200,000">원
                    </div>
                    <button class="cta" onclick="setMyprice()">설정하기</button>
                </div>
            </div>
        </div>
        </body>
        </html>
      • basic.js
        let targetId;
        
        // $(document).ready: 페이지가 모두 로드된 직후 실행할 자바스크립트 코드를 넣는 곳
        $(document).ready(function () {
            // id 가 query 인 녀석 위에서 엔터를 누르면 execSearch() 함수를 실행하라는 뜻입니다.
            $('#query').on('keypress', function (e) {
                if (e.key == 'Enter') {
                    execSearch();
                }
            });
            $('#close').on('click', function () {
                $('#container').removeClass('active');
            })
        
            $('.nav div.nav-see').on('click', function () {
                $('div.nav-see').addClass('active');
                $('div.nav-search').removeClass('active');
        
                $('#see-area').show();
                $('#search-area').hide();
            })
            $('.nav div.nav-search').on('click', function () {
                $('div.nav-see').removeClass('active');
                $('div.nav-search').addClass('active');
        
                $('#see-area').hide();
                $('#search-area').show();
            })
        
            $('#see-area').show();
            $('#search-area').hide();
        
            showProduct();  // 일단 접속하면 관심 상품을 보여주어야 하기 때문에 showProduct() 함수 호출
        })
        
        
        function numberWithCommas(x) {
            return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        }
        
        //////////////////////////////////////////////////////////////////////////////////////////
        ///// 여기 아래에서부터 코드를 작성합니다. ////////////////////////////////////////////////////////
        //////////////////////////////////////////////////////////////////////////////////////////
        
        function execSearch() {
            /**
             * 검색어 input id: query
             * 검색결과 목록: #search-result-box
             * 검색결과 HTML 만드는 함수: addHTML
             */
        
            // 검색 결과 비우기
            $('#search-result-box').empty();
        
            // 1. 검색창의 입력값을 가져온다.
            let query = $('#query').val();
        
            // 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
            if (query == '') {
                alert('검색어를 입력해주세요');
                $('#query').focus();
            }
        
            // 3. GET /api/search?query=${query} 요청
            $.ajax({
                type: 'GET',
                url: `/api/search?query=${query}`,
                success: function (response) {
                    // 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!
                    for (let i = 0; i < response.length; i++) {
                        let itemDto = response[i];
                        let tempHtml = addHTML(itemDto);
                        $('#search-result-box').append(tempHtml);
                    }
                }
            })
        
        }
        
        function addHTML(itemDto) {
            /**
             * class="search-itemDto" 인 녀석에서
             * image, title, lprice, addProduct 활용하기
             * 참고) onclick='addProduct(${JSON.stringify(itemDto)})'
             */
            return `<div class="search-itemDto">
                <div class="search-itemDto-left">
                    <img src="${itemDto.image}" alt="">
                </div>
                <div class="search-itemDto-center">
                    <div>${itemDto.title}</div>
                    <div class="price">
                        ${numberWithCommas(itemDto.lprice)}
                        <span class="unit">원</span>
                    </div>
                </div>
                <div class="search-itemDto-right">
                    <img src="images/icon-save.png" alt="" onclick='addProduct(${JSON.stringify(itemDto)})'>
                </div>
            </div>`
        }
        
        function addProduct(itemDto) {
            /**
             * modal 뜨게 하는 법: $('#container').addClass('active');
             * data를 ajax로 전달할 때는 두 가지가 매우 중요
             * 1. contentType: "application/json",
             * 2. data: JSON.stringify(itemDto),
             */
            // 1. POST /api/products 에 관심 상품 생성 요청
            $.ajax({
                type: 'POST',
                url: '/api/products',
                data: JSON.stringify(itemDto),
                contentType: 'application/json',
                success: function (response) {
                    // 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 response.id 로 설정 (숙제로 myprice 설정하기 위함)
                    $('#container').addClass('active');
                    targetId = response.id;
                }
            })
        }
        
        function showProduct() {
            /**
             * 관심상품 목록: #product-container
             * 검색결과 목록: #search-result-box
             * 관심상품 HTML 만드는 함수: addProductItem
             */
            // 1. GET /api/products 요청
            $.ajax({
                type: 'GET',
                url: '/api/products',
                success: function (response) {
                    // 2. 관심상품 목록, 검색결과 목록 비우기
                    $('#product-container').empty();
                    $('#search-result-box').empty();
                    // 3. for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 붙이기!
                    for (let i = 0; i < response.length; i++) {
                        let product = response[i];
                        let tempHtml = addProductItem(product);
                        $('#product-container').append(tempHtml);
                    }
                }
            })
        }
        
        function addProductItem(product) {
            // link, image, title, lprice, myprice 변수 활용하기
            return `<div class="product-card" onclick="window.location.href='${product.link}'">
                    <div class="card-header">
                        <img src="${product.image}"
                             alt="">
                    </div>
                    <div class="card-body">
                        <div class="title">
                            ${product.title}
                        </div>
                        <div class="lprice">
                            <span>${numberWithCommas(product.lprice)}</span>원
                        </div>
                        <div class="isgood ${product.lprice > product.myprice ? 'none' : ''}">
                            최저가
                        </div>
                    </div>
                </div>`;
        }
        
        function setMyprice() {
            /**
             * 숙제! myprice 값 설정하기.
             * 1. id가 myprice 인 input 태그에서 값을 가져온다.
             * 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
             * 3. PUT /api/product/${targetId} 에 data를 전달한다.
             *    주의) contentType: "application/json",
             *         data: JSON.stringify({myprice: myprice}),
             *         빠뜨리지 말 것!
             * 4. 모달을 종료한다. $('#container').removeClass('active');
             * 5, 성공적으로 등록되었음을 알리는 alert를 띄운다.
             * 6. 창을 새로고침한다. window.location.reload();
             */
        
            // 1. id가 myprice 인 input 태그에서 값을 가져온다.
            let myprice = $('#myprice').val();
        
            // 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
            if (myprice == '') {
                alert("올바른 가격을 입력해주세요");
                return;
            }
            // 3. PUT /api/products/${targetId} 에 data를 전달한다.
            $.ajax({
                type: "PUT",
                url: `/api/products/${targetId}`,
                contentType: "application/json",
                data: JSON.stringify({myprice: myprice}),
                success: function (response) {
                    // 4. 모달을 종료한다. $('#container').removeClass('active');
                    $('#container').removeClass('active');
                    // 5, 성공적으로 등록되었음을 알리는 alert를 띄운다.
                    alert("성공적으로 등록되었습니다.");
                    // 6. 창을 새로고침한다. window.location.reload();
                    window.location.reload();
                }
            })
        
        
        }
      • style.css
        body {
            margin: 0px;
        }
        
        #search-result-box {
            margin-top: 15px;
        }
        
        .search-itemDto {
            width: 530px;
            display: flex;
            flex-direction: row;
            align-content: center;
            justify-content: space-around;
        }
        
        .search-itemDto-left img {
            width: 159px;
            height: 159px;
        }
        
        .search-itemDto-center {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: space-evenly;
        }
        
        .search-itemDto-center div {
            width: 280px;
            height: 23px;
            font-size: 18px;
            font-weight: normal;
            font-stretch: normal;
            font-style: normal;
            line-height: 1.3;
            letter-spacing: -0.9px;
            text-align: left;
            color: #343a40;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }
        
        .search-itemDto-center div.price {
            height: 27px;
            font-size: 27px;
            font-weight: 600;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.54px;
            text-align: left;
            color: #E8344E;
        }
        
        .search-itemDto-center span.unit {
            width: 17px;
            height: 18px;
            font-size: 18px;
            font-weight: 500;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.9px;
            text-align: center;
            color: #000000;
        }
        
        .search-itemDto-right {
            display: inline-block;
            height: 100%;
            vertical-align: middle
        }
        
        .search-itemDto-right img {
            height: 25px;
            width: 25px;
            vertical-align: middle;
            margin-top: 60px;
            cursor: pointer;
        }
        
        input#query {
            padding: 15px;
            width: 526px;
            border-radius: 2px;
            background-color: #e9ecef;
            border: none;
        
            background-image: url('images/icon-search.png');
            background-repeat: no-repeat;
            background-position: right 10px center;
            background-size: 20px 20px;
        }
        
        input#query::placeholder {
            padding: 15px;
        }
        
        button {
            color: white;
            border-radius: 4px;
            border-radius: none;
        }
        
        .popup-container {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            bottom: 0;
            right: 0;
            background-color: rgba(0, 0, 0, 0.5);
            align-items: center;
            justify-content: center;
        }
        
        .popup-container.active {
            display: flex;
        }
        
        .popup {
            padding: 20px;
            box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
            position: relative;
            width: 370px;
            height: 190px;
            border-radius: 11px;
            background-color: #ffffff;
        }
        
        .popup h1 {
            margin: 0px;
            font-size: 22px;
            font-weight: 500;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -1.1px;
            color: #000000;
        }
        
        .popup input {
            width: 330px;
            height: 39px;
            border-radius: 2px;
            border: solid 1.1px #dee2e6;
            margin-right: 9px;
            margin-bottom: 10px;
            padding-left: 10px;
        }
        
        .popup button.close {
            position: absolute;
            top: 15px;
            right: 15px;
            color: #adb5bd;
            background-color: #fff;
            font-size: 19px;
            border: none;
        }
        
        .popup button.cta {
            width: 369.1px;
            height: 43.9px;
            border-radius: 2px;
            background-color: #15aabf;
            border: none;
        }
        
        #search-area, #see-area {
            width: 530px;
            margin: auto;
        }
        
        .nav {
            width: 530px;
            margin: 30px auto;
            display: flex;
            align-items: center;
            justify-content: space-around;
        }
        
        .nav div {
            cursor: pointer;
        }
        
        .nav div.active {
            font-weight: 700;
        }
        
        .header {
            background-color: #15aabf;
            color: white;
            text-align: center;
            padding: 50px;
            font-size: 45px;
            font-weight: bold;
        }
        
        #product-container {
            grid-template-columns: 100px 50px 100px;
            grid-template-rows: 80px auto 80px;
            column-gap: 10px;
            row-gap: 15px;
        }
        
        .product-card {
            width: 300px;
            margin: auto;
            cursor: pointer;
        }
        
        .product-card .card-header {
            width: 300px;
        }
        
        .product-card .card-header img {
            width: 300px;
        }
        
        .product-card .card-body {
            margin-top: 15px;
        }
        
        .product-card .card-body .title {
            font-size: 15px;
            font-weight: normal;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.75px;
            text-align: left;
            color: #343a40;
            margin-bottom: 10px;
        }
        
        .product-card .card-body .lprice {
            font-size: 15.8px;
            font-weight: normal;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.79px;
            color: #000000;
            margin-bottom: 10px;
        }
        
        .product-card .card-body .lprice span {
            font-size: 21.4px;
            font-weight: 600;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.43px;
            text-align: left;
            color: #E8344E;
        }
        
        .product-card .card-body .isgood {
            margin-top: 10px;
            padding: 10px 20px;
            color: white;
            border-radius: 2.6px;
            background-color: #ff8787;
            width: 42px;
        }
        
        .none {
            display: none;
        }
    2. src > main > resources > static 에 images 폴더 생성 후 이미지 저장

 

 

함수 작성하기

basic.js

  • 상품 검색 기능 - execSearch, addHTML 함수
    function execSearch() {
        /**
         * 검색어 input id: query
         * 검색결과 목록: #search-result-box
         * 검색결과 HTML 만드는 함수: addHTML
         */
    
        // 검색 결과 비우기
        $('#search-result-box').empty();
    
        // 1. 검색창의 입력값을 가져온다.
        let query = $('#query').val();
    
        // 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
        if (query == '') {
            alert('검색어를 입력해주세요');
            $('#query').focus();
        }
    
        // 3. GET /api/search?query=${query} 요청
        $.ajax({
            type: 'GET',
            url: `/api/search?query=${query}`,
            success: function (response) {
                // 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!
                for (let i = 0; i < response.length; i++) {
                    let itemDto = response[i];
                    let tempHtml = addHTML(itemDto);
                    $('#search-result-box').append(tempHtml);
                }
            }
        })
    
    }
    
    function addHTML(itemDto) {
        /**
         * class="search-itemDto" 인 녀석에서
         * image, title, lprice, addProduct 활용하기
         * 참고) onclick='addProduct(${JSON.stringify(itemDto)})'
         */
        return `<div class="search-itemDto">
            <div class="search-itemDto-left">
                <img src="${itemDto.image}" alt="">
            </div>
            <div class="search-itemDto-center">
                <div>${itemDto.title}</div>
                <div class="price">
                    ${numberWithCommas(itemDto.lprice)}
                    <span class="unit">원</span>
                </div>
            </div>
            <div class="search-itemDto-right">
                <img src="images/icon-save.png" alt="" onclick='addProduct(${JSON.stringify(itemDto)})'>
            </div>
        </div>`
    }
  • 관심 상품 등록 - addProduct 함수
    function addProduct(itemDto) {
        /**
         * modal 뜨게 하는 법: $('#container').addClass('active');
         * data를 ajax로 전달할 때는 두 가지가 매우 중요
         * 1. contentType: "application/json",
         * 2. data: JSON.stringify(itemDto),
         */
        // 1. POST /api/products 에 관심 상품 생성 요청
        $.ajax({
            type: 'POST',
            url: '/api/products',
            data: JSON.stringify(itemDto),
            contentType: 'application/json',
            success: function (response) {
                // 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 response.id 로 설정 (숙제로 myprice 설정하기 위함)
                $('#container').addClass('active');
                targetId = response.id;
            }
        })
    }
  • 관심 상품 조회 - showProduct, addProductItem 함수
    function showProduct() {
        /**
         * 관심상품 목록: #product-container
         * 검색결과 목록: #search-result-box
         * 관심상품 HTML 만드는 함수: addProductItem
         */
        // 1. GET /api/products 요청
        $.ajax({
            type: 'GET',
            url: '/api/products',
            success: function (response) {
                // 2. 관심상품 목록, 검색결과 목록 비우기
                $('#product-container').empty();
                $('#search-result-box').empty();
                // 3. for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 붙이기!
                for (let i = 0; i < response.length; i++) {
                    let product = response[i];
                    let tempHtml = addProductItem(product);
                    $('#product-container').append(tempHtml);
                }
            }
        })
    }
    
    function addProductItem(product) {
        // link, image, title, lprice, myprice 변수 활용하기
        return `<div class="product-card" onclick="window.location.href='${product.link}'">
                <div class="card-header">
                    <img src="${product.image}"
                         alt="">
                </div>
                <div class="card-body">
                    <div class="title">
                        ${product.title}
                    </div>
                    <div class="lprice">
                        <span>${numberWithCommas(product.lprice)}</span>원
                    </div>
                    <div class="isgood ${product.lprice > product.myprice ? 'none' : ''}">
                        최저가
                    </div>
                </div>
            </div>`;
    }
  • 관심 가격 설정 - setMyprice 함수
    function setMyprice() {
        /**
         * 숙제! myprice 값 설정하기.
         * 1. id가 myprice 인 input 태그에서 값을 가져온다.
         * 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
         * 3. PUT /api/product/${targetId} 에 data를 전달한다.
         *    주의) contentType: "application/json",
         *         data: JSON.stringify({myprice: myprice}),
         *         빠뜨리지 말 것!
         * 4. 모달을 종료한다. $('#container').removeClass('active');
         * 5, 성공적으로 등록되었음을 알리는 alert를 띄운다.
         * 6. 창을 새로고침한다. window.location.reload();
         */
    
        // 1. id가 myprice 인 input 태그에서 값을 가져온다.
        let myprice = $('#myprice').val();
    
        // 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
        if (myprice == '') {
            alert("올바른 가격을 입력해주세요");
            return;
        }
        // 3. PUT /api/products/${targetId} 에 data를 전달한다.
        $.ajax({
            type: "PUT",
            url: `/api/products/${targetId}`,
            contentType: "application/json",
            data: JSON.stringify({myprice: myprice}),
            success: function (response) {
                // 4. 모달을 종료한다. $('#container').removeClass('active');
                $('#container').removeClass('active');
                // 5, 성공적으로 등록되었음을 알리는 alert를 띄운다.
                alert("성공적으로 등록되었습니다.");
                // 6. 창을 새로고침한다. window.location.reload();
                window.location.reload();
            }
        })
    
    
    }