Back-end/Spring

[SpringBoot] 4주차 스터디 ② (나만의 셀렉샵 만들기, 네이버 쇼핑 API)

poppy 2021. 9. 9. 02:29
반응형

이전 포스팅에 이어서 계속해서 나만의 셀렉샵을 만들어 보겠다.

https://soohyun6879.tistory.com/200

 

[SpringBoot] 4주차 스터디 ① (나만의 셀렉샵 만들기, 네이버 쇼핑 API)

네이버 쇼핑 API를 사용하여 나만의 셀렉샵을 만들어 보았다. 프로젝트 구조는 다음과 같다. 1. 네이버 쇼핑 API 신청하기 네이버 쇼핑 API 를 사용하기 위해서는 API 사용 신청을 해야 한다. 다음 링

soohyun6879.tistory.com

 

1. 네이버 쇼핑 API 사용하기

네이버 쇼핑 API 를 사용하여 검색된 상품 목록을 가져온다. 검색된 상품 목록은 String 형식으로 가져와지기 때문에 화면에 보여줄 수 있도록 하려면 DTO로 변환하는 과정이 필요하다.

@Component // 스프링이 필요 시 자동으로 생성하는 클래스 목록에 추가
public class NaverShopSearch {
    // API를 통해 검색된 상품 목록 조회
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "자신의 Client ID");
        headers.add("X-Naver-Client-Secret", "자신의 Client Secret");
        String body = "";

        HttpEntity<String> requestEntity = new HttpEntity<String>(body, headers);
        ResponseEntity<String> responseEntity = rest.exchange("https://openapi.naver.com/v1/search/shop.json?query=" + query, HttpMethod.GET, requestEntity, String.class);
        HttpStatus httpStatus = responseEntity.getStatusCode();
        int status = httpStatus.value();
        String response = responseEntity.getBody();
        System.out.println("Response status: " + status);
        System.out.println(response);

        return response;
    }
 
    // 검색된 상품 목록 데이터를 DTO로 변환
    public List<ItemDto> fromJSONtoItems(String result) {
        JSONObject rjson = new JSONObject(result);
        JSONArray items  = rjson.getJSONArray("items");
        List<ItemDto> ret = new ArrayList<>();
        for (int i=0; i<items.length(); i++) {
            JSONObject itemJson = items.getJSONObject(i);
            System.out.println(itemJson);
            ItemDto itemDto = new ItemDto(itemJson);
            ret.add(itemDto);
        }
        return ret;
    }
}

 

2. 스케줄러

매일 정해진 시간에 가격 업데이트가 실행되도록 스케줄러를 만든다.

@RequiredArgsConstructor // 꼭 필요한 요소(final) 자동 생성
@Component // 스프링이 필요 시 자동으로 생성하는 클래스 목록에 추가
public class Scheduler {

    private final ProductRepository productRepository;
    private final ProductService productService;
    private final NaverShopSearch naverShopSearch;

    // 예약된 시간에 메서드 실행 - 매일 새벽 1시 0분 0초 일 때 실행
    @Scheduled(cron = "0 0 1 * * *") // cron - 초, 분, 시, 일, 월, 주 순서
    public void updatePrice() throws InterruptedException {
        System.out.println("가격 업데이트 실행");
        // 저장된 모든 관심상품을 조회
        List<Product> productList = productRepository.findAll();
        for (int i=0; i<productList.size(); i++) {
            // 1초에 한 상품 씩 조회 (Naver 제한)
            TimeUnit.SECONDS.sleep(1);
            // i 번째 관심 상품을 꺼낸다
            Product p = productList.get(i);
            // i 번째 관심 상품의 제목으로 검색을 실행
            String title = p.getTitle();
            String resultString = naverShopSearch.search(title);
            // i 번째 관심 상품의 검색 결과 목록 중에서 첫 번째 결과를 꺼낸다
            List<ItemDto> itemDtoList = naverShopSearch.fromJSONtoItems(resultString);
            ItemDto itemDto = itemDtoList.get(0);
            // i 번째 관심 상품 정보를 업데이트
            Long id = p.getId();
            productService.updateBySearch(id, itemDto);
        }
    }
}

 

3. Front

마지막으로 상품을 검색하고 관심 상품을 등록할 수 있도록 화면을 만든다.

 

먼저 index.html 과 style.css 를 생성한다. index.html 은 보이는 부분만을 정의한 것이고, style.css 는 화면이 예쁘게 보일 수 있도록 스타일을 정의한 것이다.

<!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>
</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>
    <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>
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;
}

 

basic.js 파일을 생성하여 화면에서 여러 가지 기능을 수행할 수 있도록 서버와 연결해준다.

let targetId;

$(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();
})


function numberWithCommas(x) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

function execSearch() {
    /*
     * 검색어 input id: query
     * 검색결과 목록: #search-result-box
     * 검색결과 HTML 만드는 함수: addHTML
     */
    // 1. 검색창의 입력값을 가져온다.
    let query = $('#query').val();

    // 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
    if (query == '') {
        alert('검색어를 입력해주세요');
        $('#query').focus();
        return;
    }
    // 3. GET /api/search?query=${query} 요청
    $.ajax({
        type: 'GET',
        url: `/api/search?query=${query}`,
        success: function (response) {
            $('#search-result-box').empty();
            // 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',
        contentType: "application/json",
        data: JSON.stringify(itemDto),
        success: function (response) {
            // 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 reponse.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() {
    // 1. id가 myprice 인 input 태그에서 값을 가져온다.
    let myprice = $('#myprice').val();
    // 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
    if (myprice == '') {
        alert('최저가를 입력해주세요');
        return;
    }
    // 3. PUT /api/product/${targetId} 에 data를 전달한다.
    $.ajax({
        type: 'PUT',
        url: `/api/products/${targetId}`,
        contentType: "application/json",
        data: JSON.stringify({myprice: myprice}),
        success: function (response) {
            // 4. 모달을 종료한다.
            $('#container').removeClass('active');
            // 5. 성공적으로 등록되었음을 알리는 alert를 띄운다.
            alert('성공적으로 등록되었습니다');
            // 6. 창을 새로고침한다.
            window.location.reload();
        }
    })
}

 

여기까지하면 나만의 셀렉샵 프로젝트가 완성된다. 서버를 실행하여 웹페이지에 접속하면 다음과 같은 화면을 볼 수 있고, 상품을 검색하고 관심 상품을 등록할 수 있다.

 

탐색하기
모아보기

 

반응형