[스파르타 코딩 클럽] Spring 4주차 개발일지 - 네이버 API 를 사용해 셀렉샵 구현

2021. 8. 19. 16:55·스파르타 코딩클럽/Spring

1. 네이버 API 신청

https://developers.naver.com/docs/search/shopping/ 에 접속 후, '오픈 API 이용 신청' 클릭한다.

 

애플리케이션 등록에서 아래와 같이 입력한다.

 

 

생성된 Client ID 와 Client Secret 확인

 

 

이용 신청 완료.

 

2. 네이버 API 사용

https://developers.naver.com/docs/search/shopping/ 접속 후, '2. API 기본 정보' 에서 출력포맷이 JSON 인 녀석의 요청 URL을 복사해서 ARC의 URL에 붙여 넣어준다.

 

 

'2. API 기본 정보' 에서 출력포맷이 JSON 인 녀석의 요청 URL을 복사해서 ARC의 URL에 붙여 넣어준다.

 

 

Headers 에서 "Add Header"를 두 번 클릭하고, 첫 번째 header의 name에 "X-Naver-Client-Id", value에 API 를 생성하며 받은 Client ID를, 두 번째 header에 "X-Naver-Client-Secret" 와 Client Secret을 각각 넣어주고, 주소 제일 마지막에 "?query=adidas" 라고 적은 뒤 SEND 버튼을 누른다.

 

 

SEND 버튼을 클릭하면 검색 결과가 출력된다.

 

 

3. JAVA 로 네이버 API 사용하기

 

ARC 에서 'CODE SNIPPETS' 클릭

 

 

JAVA 탭 클릭

 

 

JAVA 에서 SPRING 탭 클릭시 아래와 같이 코드가 출력된다.

 

 

4. 프로젝트 설계

 

인텔리제이에서 새로운 Spring 프로젝트를 생성한다.

 

 

src > main > java > com.sparta.week04 에 utils 패키지를 만든 후, NaverShopSearch.java 파일을 생성한다.

 

public class NaverShopSearch {
    public String search() {
        public static void main (String[]args){
            NaverShopSearch naverShopSearch = new NaverShopSearch();
            naverShopSearch.search();
        }
    }
}

 

search 함수 안에 ARC 에서 복사한 코드를 붙여넣는다.

 

public class NaverShopSearch {
    public String search() {
        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=adidas", Ht
                HttpStatus httpStatus = responseEntity.getStatusCode();
        int status = httpStatus.value();
        String response = responseEntity.getBody();
        System.out.println("Response status: " + status);
        System.out.println(response);
        return response;
    }
    
    public static void main(String[] args) {
        NaverShopSearch naverShopSearch = new NaverShopSearch();
        naverShopSearch.search();
    }
}

 

  • 필요 기능

 

  1. 관심 상품 조회
  2. 관심 상품 등록
  3. 키워드로 상품 검색하고 그 결과를 목록으로 출력
  4. 관심 상품에 원하는 가격 등록하고, 그 가격보다 낮은 경우 표시

 

  • API 타임라인
기능 Method URL Return
키워드로 상품 검색하고 그 결과를 목록으로 보여주기 GET /api/search?query=검색어 List<ItemDto>
관심 상품 등록 POST /api/products Product
관심 상품 조회 GET /api/products List<Product>
관심 상품에 원하는 가격 등록하고, 그 가격보다 낮은 경우 표시 PUT /api/products/{id} id

 

  • 3계층 설계

 

1. Controller

  • ProductRestController : 관심 상품 관련 컨트롤러
  • SearchRequestController : 검색 관련 컨트롤러

 

2. Service

  • ProductService : 관심 상품 가격 변경

 

3. Repository (여기서 DB에 저장되는 건 Product 만 저장)

  • Product : 관심 상품 테이블
  • ProductRepository : 관심 상품 조회, 저장
  • ProductRequestDto : 관심 상품 등록
  • ProductMypriceRequestDto : 관심 가격 변경
  • ItemDto : 검색 결과 주고받기

 

▼ 요구조건

 

1. "모아보기" 탭을 눌렀을 때 아래와 같이 등록된 관심 상품을 조회할 수 있다.

 

 

Timestamped.java 생성

 

package com.sparta.week04.models;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter // get 함수를 자동 생성합니다.
@MappedSuperclass // 멤버 변수가 컬럼이 되도록 합니다.
@EntityListeners(AuditingEntityListener.class) // 변경되었을 때 자동으로 기록합니다.
public abstract class Timestamped {
    @CreatedDate // 최초 생성 시점
    private LocalDateTime createdAt;

    @LastModifiedDate // 마지막 변경 시점
    private LocalDateTime modifiedAt;
}

 

Week04Application.java 수정

 

package com.sparta.week04;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableJpaAuditing // 시간 자동 변경이 가능하도록 합니다.
@SpringBootApplication // 스프링 부트임을 선언합니다.
public class Week04Application {

    public static void main(String[] args) {
        SpringApplication.run(Week04Application.class, args);
    }

}

 

 

이 화면을 보면 title, image, link, lprice, myprice 정보가 필요함을 알 수 있다.

 

 

Product.java 생성

 

package com.sparta.week04.models;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class Product extends Timestamped{

    // ID가 자동으로 생성 및 증가합니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    // 반드시 값을 가지도록 합니다.
    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String image;

    @Column(nullable = false)
    private String link;

    @Column(nullable = false)
    private int lprice;

    @Column(nullable = false)
    private int myprice;
}

 

ProductRepository.java 생성

 

package com.sparta.week04.models;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

 

ProductRestController.java 생성

 

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@RestController // JSON으로 데이터를 주고받음을 선언합니다.
public class ProductRestController {

    private final ProductService productService;
    private final ProductRepository productRepository;

    // 등록된 전체 상품 목록 조회
    @GetMapping("/api/products")
    public List<Product> getProducts() {
        return productRepository.findAll();
    }
}

 

2. 상품을 검색한 후, 등록 버튼을 눌렀을 때 관심 상품이 생성되어야 한다.

 

 

ProductRequestDto.java 생성

 

package com.sparta.week04.models;

import lombok.Getter;

@Getter
public class ProductRequestDto {
    private String title;
    private String link;
    private String image;
    private int lprice;
}

 

ProductMypriceRequestDto.java 생성

 

package com.sparta.week04.models;

import lombok.Getter;

@Getter
public class ProductMypriceRequestDto {
    private int myprice;
}

 

Product.java 관심 상품 등록이 가능하게 수정

 

package com.sparta.week04.models;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class Product extends Timestamped{

    // ID가 자동으로 생성 및 증가합니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    // 반드시 값을 가지도록 합니다.
    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String image;

    @Column(nullable = false)
    private String link;

    @Column(nullable = false)
    private int lprice;

    @Column(nullable = false)
    private int myprice;
    
    // 관심 상품 생성 시 이용합니다.
    public Product(ProductRequestDto requestDto){
        this.title = requestDto.getTitle();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.image = requestDto.getImage();
        this.myprice = 0;
    }
    
    // 관심 가격 변경 시 이용합니다.
    public void update(ProductMypriceRequestDto requestDto){
        this.myprice = requestDto.getMyprice();
    }
}

 

ProductService.java 생성

 

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@Service // 서비스임을 선언합니다.
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional // 메소드 동작이 SQL 쿼리문임을 선언합니다.
    public Long update(Long id, ProductMypriceRequestDto requestDto) {
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 아이디가 존재하지 않습니다.")
        );
        product.update(requestDto);
        return id;
    }
}

 

ProductRestController.java 관심 상품 등록이 가능하게 수정

 

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@RestController // JSON으로 데이터를 주고받음을 선언합니다.
public class ProductRestController {

    private final ProductService productService;
    private final ProductRepository productRepository;

    // 등록된 전체 상품 목록 조회
    @GetMapping("/api/products")
    public List<Product> getProducts() {
        return productRepository.findAll();
    }

    // 신규 상품 등록
    @PostMapping("/api/products")
    public Product createProduct(@RequestBody ProductRequestDto requestDto){
        Product product = new Product(requestDto);
        return productRepository.save(product);
    }
}

 

이전에 만들어 둔 NaverShopSearch.java 를 웹서비스에 이용할 수 있도록 수정

 

1. 검색어를 요구에 따라 바꿀 수 있어야 한다.

2. 검색 결과를 문자열에서 DTO 로 바꿔야 한다.

 

 

만약 검색어를 "아이맥" 으로 바꾼다면,

 

public class NaverShopSearch {
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "xj1Bssu2Fmq7QzIW1T4p");
        headers.add("X-Naver-Client-Secret", "nhkiw6X5xf");
        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;
    }

    public static void main(String [] args){
        NaverShopSearch naverShopSearch = new NaverShopSearch();
        String result = naverShopSearch.search("아이맥");
    }
}

 

검색 결과를 문자열에서 DTO 로 바꾸기 위해선, org.json 패키지가 필요하다.

▶ JSON을 자바에서 다루기 위해, JSONObject, JSONArray 클래스가 필요하다. 우리가 만드는 대신, 다른 사람이 만든 라이브러리를 import 해서 바로 가져와서 사용할 수 있다.

 

1. 구글에 maven central 검색 후 첫 번째 결과 클릭

2. 검색창에 json 입력 후 엔터

3. JSON In Java 클릭 

4. 숫자 가장 높은 버전 클릭 

5. Gradle 탭 클릭 

6. 내용 복사하여 build.gradle > dependencies 안에 붙여넣기 

7. dependencies 옆의 Run 버튼 클릭 

8. 임포트 완료

 

▼ JSONObject, JSONArray 연습하기

 

(1). 문자열 정보를 JSONObject 로 변경

JSONObject rjson = new JSONObject(result);

 

(2). JSONObject에서 items 배열 추출

JSONArray items = rjson.getJSONArray("items");

 

(3). JSONArray 로 for 문 사용

for (int i=0; i<items.length(); i++) {
JSONObject itemJson = (JSONObject) items.get(i);
System.out.println(itemJson);
}

 

(4). JSONObject 에서 String, int 데이터 추출

String title = itemJson.getString("title");
int lprice = itemJson.getInt("lprice");

 

ItemDto.java 생성

 

@Getter
public class ItemDto {
    private String title;
    private String link;
    private String image;
    private int lprice;

    public ItemDto(JSONObject itemJson){
        this.title = itemJson.getString("title");
        this.image = itemJson.getString("image");
        this.link = itemJson.getString("link");
        this.lprice = itemJson.getInt("lprice");
    }
}

 

fromJSONtoItems 메소드 생성

 

public class NaverShopSearch {
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "xj1Bssu2Fmq7QzIW1T4p");
        headers.add("X-Naver-Client-Secret", "nhkiw6X5xf");
        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;
    }

    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 = (JSONObject) items.get(i);
            ItemDto itemDto = new ItemDto(itemJson);
            ret.add(itemDto);
        }
        return ret;
    }

    public static void main(String [] args){
        NaverShopSearch naverShopSearch = new NaverShopSearch();
        String result = naverShopSearch.search("아이맥");
        naverShopSearch.fromJSONtoItems(ret);
    }
}

 

3. 사용자가 검색어를 입력하면, 컨트롤러가 그것을 전달받고, 전달받은 검색어로 네이버 API 에 요청하고, 그 결과를 사용자에게 응답한다.

 

▶ 더 이상 검색을 main 메소드에서 진행하는게 아니라, Controller 에서 가져다 써야 한다. 스프링이 자동으로 필요한 클래스를 필요한 곳에 생성하려면, "아, 사용자가 요구하면 자동으로 생성할 클래스 목록이 이것이구나" 라고 확인할 수 있어야 하는데, 그 목록에 등록하는 간단한 방법이 바로 컴포넌트 등록이다.

 

NaverShopSearch.java 에 컴포넌트 등록

 

@Component // 이제부터, @RequiredArgsConstructor 와 함께 사용할 경우 스프링이 자동으로 생성합니다.
public class NaverShopSearch {
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "xj1Bssu2Fmq7QzIW1T4p");
        headers.add("X-Naver-Client-Secret", "nhkiw6X5xf");
        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;
    }

    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 = (JSONObject) items.get(i);
            ItemDto itemDto = new ItemDto(itemJson);
            ret.add(itemDto);
        }
        return ret;
    }
}

 

SearchRequestController.java 생성

기능 Method URL
키워드로 상품 검색하고 그 결과를 목록으로 보여주기 GET /api/search?query=검색어

 

@RequiredArgsConstructor // final 로 선언된 클래스를 자동으로 생성합니다.
@RestController // JSON으로 응답함을 선언합니다.
public class SearchRequestController {

    private final NaverShopSearch naverShopSearch;

    @GetMapping("/api/search")
    public List<ItemDto> getItems(@RequestParam String query) {
        String resultString = naverShopSearch.search(query);
        return naverShopSearch.fromJSONtoItems(resultString);
    }
}

 

5. 프로젝트 프론트엔드 설계

 

▶ 파일분리는, HTML 파일이 CSS 와 Javascript 때문에 지나치게 길어지는 것을 방지하고 가독성을 높이기 위한 방법이다.

 

1. .css와 .js 로 끝나는 파일을 만들고,

2. link 와 script 태그로 각 파일을 불러온다.

 

그러면 index.html 파일에 모두 작성한 것과 동일하게 작동한다.

 

src > main > resources > static 에 index.html, basic.js, style.css 파일을 생성

 

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(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() {
    /**
     * 숙제! 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.
    let myprice = $('#myprice').val();

    // 2.
    if (myprice == '') {
        alert('검색어를 입력해주세요');
        $('#myprice').focus();
        return;
    }

    // 3.
    $.ajax({
        type: 'POST',
        url: `/api/products/${targetId}`,
        contentType: "application/json",
        data: JSON.stringify({myprice: myprice}),
        success: function (response){
            // 4.
            $('#container').removeClass('active');

            // 5.
            alert("최저가가 수정되었습니다.");

            // 6.
            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;
}

 

이미지까지 저장하면 정상적으로 화면이 출력되는 것을 확인할 수 있다.

 

 

6. 상품 검색 기능

 

▼ execSearch, addHTML 함수 생성

 

검색창 입력값 추출

 

let query = $('#query').val();

 

검색창 입력값을 검사하고, 아무것도 입력하지 않았을 경우 focus

 

if (query == '') {
alert('검색어를 입력해주세요');
$('#query').focus();
return;
}

 

GET /api/search?query=${query} 요청

 

$.ajax({
    type: 'GET',
    url: `/api/search?query=${query}`,
    success: function (response) {
        $('#search-result-box').empty();
    }
})

 

for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기

 

for (let i = 0; i < response.length; i++) {
    let itemDto = response[i];
    let tempHtml = addHTML(itemDto);
    $('#search-result-box').append(tempHtml);
}

 

addHTML 완성하기

 

function addHTML(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>`
}

 

execSearch, addHTML 완성

 

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>`
}

 

7. 관심 상품 등록

 

▼ 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',
        contentType: "application/json",
        data: JSON.stringify(itemDto),
        success: function (response) {
            
        }
    })
}

 

modal 뜨게 하기

 

$('#container').addClass('active');

 

targetId = response.id

 

targetId = response.id;

 

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',
        contentType: "application/json",
        data: JSON.stringify(itemDto),
        success: function (response) {
            // 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 reponse.id 로 설정 (숙제로 myprice 설정하기 위함)
            $('#container').addClass('active');
            targetId = response.id;
        }
    })
}

 

8. 관심 상품 화면에 출력

 

$(document).ready 함수는 페이지가 모두 로드된 직후 실행할 자바스크립트 코드를 넣는 곳이다. 일단 접속하면 관심 상품을 보여주어야 하기 때문에 showProduct 함수를 호출하고 있다.

 

▼ showProduct, addProductItem 생성

 

GET /api/products 요청

 

$.ajax({
    type: 'GET',
    url: '/api/products',
    success: function (response) {
     
    }
})

 

관심상품 목록, 검색결과 목록 초기화

 

// 2. 관심상품 목록, 검색결과 목록 비우기
$('#product-container').empty();
$('#search-result-box').empty();

 

for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 추가

 

// 3. for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 붙이기!
for (let i = 0; i < response.length; i++) {
    let product = response[i];
    let tempHtml = addProductItem(product);
    $('#product-container').append(tempHtml);
}

 

link, image, title, lprice, myprice 변수 활용

 

// 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>`;

 

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>`;
}

 

9. 스케줄러 생성

 

스케줄러의 기능은 매일 원하는 시간에 관심 상품 목록 제목으로 검색해서, 최저가 정보를 업데이트하는 기능을 가지고 있다.

 

src > main > java > com.sparta.week04 > utils 에 Scheduler.java 파일 생성

 

@RequiredArgsConstructor // final 멤버 변수를 자동으로 생성합니다.
@Component // 스프링이 필요 시 자동으로 생성하는 클래스 목록에 추가합니다.
public class Scheduler {

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

    // 초, 분, 시, 일, 월, 주 순서
    @Scheduled(cron = "0 0 1 * * *")
    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);
        }
    }
}

 

ProductService.java 추가 수정

 

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@Service // 서비스임을 선언합니다.
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional // 메소드 동작이 SQL 쿼리문임을 선언합니다.
    public Long update(Long id, ProductMypriceRequestDto requestDto) {
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 아이디가 존재하지 않습니다.")
        );
        product.update(requestDto);
        return id;
    }

    @Transactional // 메소드 동작이 SQL 쿼리문임을 선언합니다.
    public Long updateBySearch(Long id, ItemDto itemDto) {
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 아이디가 존재하지 않습니다.")
        );
        product.updateByItemDto(itemDto);
        return id;
    }
}

 

 

Week04Application.java 추가 수정

 

@EnableScheduling // 스프링 부트에서 스케줄러가 작동하게 합니다.
@EnableJpaAuditing // 시간 자동 변경이 가능하도록 합니다.
@SpringBootApplication // 스프링 부트임을 선언합니다.
public class Week04Application {

    public static void main(String[] args) {
        SpringApplication.run(Week04Application.class, args);
    }

}

 

 

이상 끝 ٩( ᐛ )و

'스파르타 코딩클럽 > Spring' 카테고리의 다른 글

[스파르타 코딩 클럽] Spring 3주차 개발일지 - Spring 을 사용해 Memo 웹페이지 구현  (0) 2021.08.12
[스파르타 코딩 클럽] Spring 2주차 개발일지  (0) 2021.08.05
'스파르타 코딩클럽/Spring' 카테고리의 다른 글
  • [스파르타 코딩 클럽] Spring 3주차 개발일지 - Spring 을 사용해 Memo 웹페이지 구현
  • [스파르타 코딩 클럽] Spring 2주차 개발일지
오도형석
오도형석
  • 오도형석
    형석이의 성장일기
    오도형석
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • MSA 모니터링 서비스
        • DB
      • 스파르타 코딩클럽
        • SQL
        • Spring
      • 백엔드
        • Internet
        • Java
        • DB
      • 캡스톤
        • Django
        • 자연어처리
      • Spring
        • JPA
        • MSA
      • ETC
        • ERROR
      • 개발 일기 N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 인기 글

  • 태그

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
오도형석
[스파르타 코딩 클럽] Spring 4주차 개발일지 - 네이버 API 를 사용해 셀렉샵 구현
상단으로

티스토리툴바