A Developing Developer

[스파르타코딩클럽] 웹개발의 봄, Spring - 4주차 본문

웹개발의 봄, Spring 81기

[스파르타코딩클럽] 웹개발의 봄, Spring - 4주차

H-JJOO 2022. 10. 3. 23:08

[수업 목표]

1. Controller - Service - Repository 에 대해 자신감을 가진다!

2. 자바를 이용해 API를 이용하는 방법을 익힌다.

3. 스프링 스케줄러를 이용하여, 서버에게 원하는 작업을 원하는 시간에 시키는 방법을 익힌다.

 

01. [4주차] 이번 주 배울 것

 

- 3계층 숙달, API handling

 

기본기란 익히면 익힐수록 흘러가는 것이 아니라 누적되고, 다른 기술을 익히기 쉽도록 도와주는 것입니다. 그런 의미에서 3계층(Controller, Service, Repository) 숙달과 API handling은 무엇보다 중요한 기본기라고 할 수 있는데요.

 

1. 3계층 숙달

 

- 스프링, 아니 개발의 핵심 중 하나가 분업과 느슨한 결합입니다. 각자가 맡은 바 책임을 다하면 기능이 온전히 작동하고, 느슨 히 결합함으로써 유연성과 확장성을 가지기 때문이지요.

 

- 3계층은 분업과 느슨한 결합의 대표적인 예시이자, 스프링이 돌아가게 만드는 척추입니다.

 

- 깊게 파자면 끝없는 이론이 있지만, 지금은 그것보다 우선 손에 익게 하는데 집중하세요. 손에 익어서, "아 나도 웬만한 사이 트는 노력하면 만들 수 있지!"라는 자신감을 가지는게 정말 중요해요.

 

2. API handling

 

- 혼자 하는 개발은 없습니다. 그리고 세상에는 다양한 API가 있습니다. API를 잘 다루면 재미있는 나만의 서비스를 더 많이, 더 빠르게 만들 수 있고 그럼으로써 개발자로 한 단계 성장할 수 있습니다.

 

- 또한, 앞에서 말씀드린 느슨한 결합의 대표적인 예시가 바로 API 입니다. 우리가 이용할 네이버 검색 API의 내부 로직이 어떻 게 되든 상관없이, 우리는 정해진 약속대로 요구하면 정해진 결과를 받을 수 있지요.

 

- 한 마디로 API가 아무리 많아도 쓸 줄 모르면 무용지물이죠. 그런데 JSON으로 주고받는 데이터를 어떻게 자바로 요청하고, 그 결과를 다룰 수 있는지 모르는 분들이 생각보다 많습니다. 이번에 그 기초를 확실하게 배워볼게요!

 

- 프로젝트 모습과 기능

 

[코드스니펫] <나만의 셀렉샵> 소개

http://spring.spartacodingclub.kr/

 

00만의 셀렉샵

관심상품을 선택하고, 최저가 알림을 확인해보세요!

spring.spartacodingclub.kr

 

- 요구 기능

 

- 상품명에 따른 검색

- 관심 상품 등록 & 조회

- 관심 상품에 대한 최저가 등록하기

 

- 자바에서 API를 요청하고 그 결과를 어떻게 다룰 수 있을까요?

- 모든 코드를 이해하기보다, 요점을 정확히 파악하고 데이터를 주고받을 수만 있으면 됩니다.

- ARC를 이용해서 네이버에 검색을 요청하고, 응답을 자바 코드로 변환한 뒤 DB에 저장해보겠습니다.

- 더해서, 자동으로 정보를 수집하고 업데이트하는 스케줄러까지 만들어볼거에요!

 

- Controller - Service - Repository 3계층 레이어, 손에 붙어야 합니다!

- 다시 한 번 리마인드 해볼까요?

 

- Controller 는 제일 바깥 쪽에서 요청을 받고, 응답을 되돌려주는 역할을 합니다.

- Service 는 중간에서 구체적인 작업 순서를 결정하고요.

- Repository 는 DB와 직접 소통함으로써 자료를 생성하고, 조회하고, 변경하고, 삭제합니다.

- 그리고 각 레이어 간에는 절대 Entity를 직접 사용하지 않고, DTO 라는 택배상자를 만들어 사용합니다.

- 각 계층 만드는 과정을 한층 심화해서 연습해보겠습니다.

 

- HTML, CSS, jQuery 도 최소한은 할 줄 알아야죠?

 

- 뼈대만 완성된 HTML, CSS, jQuery 소스를 드립니다.

- 함께 해볼 부분은, 여기에 데이터를 끼워서 직접 화면에 띄워보는 것이에요.

 

02. [4주차] 네이버 쇼핑 API 이용 신청하기

 

- 네이버 API란?

네이버 서비스를 코드로 이용할 수 있는 서비스입니다.

 

[코드스니펫] 네이버 API 목록 살펴보기

https://developers.naver.com/products/intro/plan/

 

https://developers.naver.com/products/intro/plan/

 

developers.naver.com

제공하는 API

 

 

- 한 눈에 보기에도 많은 서비스를 제공하는데요. 우리는 이 중에서 "검색 API"를 이용해보겠습니다.

 

- 사용할 API 살펴보기

 

검색 - SERVICE-API (naver.com)

 

검색 - SERVICE-API

검색 NAVER Developers - 검색 API 소개 웹, 뉴스, 블로그 등 분야별 네이버 검색 결과를 웹 서비스 또는 모바일 앱에서 바로 보여 줄 수 있습니다. 또한 ’OO역맛집’과 같은 지역 검색을 할 수도 있으

developers.naver.com

 

- API 이용 신청하기

 

1. "오픈 API 이용 신청" 버튼을 클릭합니다.

 

2. 네이버 로그인을 진행합니다.

 

3. 다음과 같이 입력한 뒤 "등록하기"를 클릭합니다.

- 애플리케이션 이름 - springboot

- 비로그인 오픈 API 서비스 환경 - WEB 선택 후 http://localhost 입력

 

 4. Client ID, Client Secret 이 생성된 것을 확인합니다.

 

5. 이용 신청이 완료되었습니다.

 

03. [4주차] 네이버 쇼핑 API 바로 사용해보기

 

 

- ARC로 검색해보기

 

1. ARC를 실행해주세요.

 

2. [코드스니펫] 네이버 쇼핑 API 설명 문서

검색 > 쇼핑 - Search API (naver.com)

 

검색 > 쇼핑 - Search API

검색 > 쇼핑 쇼핑 검색 개요 개요 검색 API와 쇼핑 검색 개요 검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 영화, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수 있는 API

developers.naver.com

 

3. 에서 출력포맷이 JSON 인 녀석의 요청 URL을 복사해서 ARC의 URL에 붙여 넣어주세요.

 

4. Headers 에서 "Add Header"를 두 번 클릭하고, 첫 번째 header의 name에 "X-Naver-Client-Id", value에 지난 시간에 만든 Client ID를 복사해서 넣어주세요. 두 번째 header에 "X-Naver-Client-Secret" 와 Client Secret을 각각 넣어주세요.

 

5. 주소 제일 마지막에 "?query=adidas"라고 적은 뒤 SEND 버튼을 누르세요.

GET 요청에서 필요한 정보를 같이 보내는 방법! ? 과 & 를 사용합니다.

 

6. 와우! 아디다스 검색에 성공하였습니다.

 

- 검색 결과 살펴보기

- 문서 설명

- 실제 결과

특히 중요한 정보는 title, link, image, lprice 가 되겠군요.

 

- 검색 기준 바꿔보기

GET 요청에서 필요한 정보를 같이 보내는 방법! ? 과 & 를 사용합니다.

 

04. [4주차] 자바로 네이버 쇼핑 API 이용하기

 

- 자바 코드 확인하기

- ARC를 이용해서 코딩 없이 바로 자바 코드를 얻을 수 있습니다!

- 앞으로 어떤 API를 사용하건 정말 잘 되는지, 결과는 어떻게 되는지 확인한 뒤 바로 코드를 얻을 수 있으니 적극적으로 사용해보 세요.

 

- 새로운 프로젝트 생성

 

1. 인텔리제이를 실행합니다.

 

2. New Project를 클릭합니다.

 

3. 왼쪽 메뉴에서 "Spring Initializr"를 클릭하고 "Next"를 클릭합니다.

 

4. [중요] 꼭 다음 사항을 확인해주세요.

- Group: com.sparta

- Artifact: week04

- Type: Gradle

- Language: Java Java Version: 8

특히 Type, Language, Java version 중 하나라도 다르면 정상적으로 실행이 되지 않습니다. 꼭 정상입력 확인해주세요!

 

5. 검색창을 클릭하고, 다음을 차례대로 검색한 뒤 엔터를 눌러주세요. 검색이 끝난 뒤 아래 캡처의 우측과 같이 5개의 요소가 포함 되어있으면 완료된 것입니다.

- Lombok, Spring Web, Spring Data JPA, H2 Database, MySQL Driver

 

6. Finish를 클릭합니다.

 

7. 잠시 기다리면, 아래 화면이 완성될 것입니다.

 

- 검색 실행하고 결과 확인하기

 

1. src > main > java > com.sparta.week04 에 utils 패키지를 만듭니다.

2. NaverShopSearch.java 파일을 생성합니다.

3. [코드스니펫] NaverShopSearch 클래스

 

public class NaverShopSearch {

    public String search() {

}

 

    public static void main(String[] args) {

        NaverShopSearch naverShopSearch = new NaverShopSearch();

        naverShopSearch.search();

    }

}

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

 

5. 정상 작동을 확인합니다.

 

6. [코드스니펫] 만약 안되면 이 코드를 써보세요!

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

05. [4주차] 프로젝트 설계하기

 

- 필요한 기능 확인하기

 

1. 키워드로 상품 검색하고 그 결과를 목록으로 보여주기

2. 관심 상품 등록하기

3. 관심 상품 조회하기

4. 관심 상품에 원하는 가격 등록하고, 그 가격보다 낮은 경우 표시하기 (숙제!)

 

- 나만의 셀렉샵 API

 

 

- 3계층 설계하기

 

1. Controller

- ProductRestController: 관심 상품 관련 컨트롤러

- SearchRequestController: 검색 관련 컨트롤러

 

2. Service

- ProductService: 관심 상품 가격 변경

 

3. Repository, 여기서 DB에 저장되는 녀석은 Product 뿐이라는 점!

- Product: 관심 상품 테이블

- ProductRepository: 관심 상품 조회, 저장

- ProductRequestDto: 관심 상품 등록하기

- ProductMypriceRequestDto: 관심 가격 변경하기

- IemDto: 검색 결과 주고받기

 

 

06. [4주차] 관심 상품 조회하기

 

- 요구조건 살펴보기

 

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

- Timestamped 클래스 만들기

[코드스니펫] Timestamped

package com.sparata.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 createAt;

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

 [코드스니펫] Week04Application

package com.sparata.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);
    }
}

- Product 클래스 만들기

- 요구조건 살펴보기

- title, image, link, lprice, myprice 정보가 필요함을 알 수 있습니다.

 

[코드스니펫] Product 클래스

package com.sparata.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 만들기

[코드스니펫] ProductRepository

package com.sparata.week04.models;

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

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

- ProductRestController 만들기

[코드스니펫] ProductRestController

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

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

- 확인해보기

ARC로 정상 작동함을 확인합니다.

 

07. [4주차] 관심 상품 등록하기

 

- 요구 조건 살펴보기

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

 검색 결과에서 제목, 이미지, 링크, 최저가를 가져오면 됩니다.

 

- Dto 클래스 만들기

[코드스니펫] ProductRequestDto

package com.sparata.week04.models;

import lombok.Getter;

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

[코드스니펫] ProductMypriceRequestDto

package com.sparata.week04.models;

import lombok.Getter;

@Getter
public class ProductMypriceRequestDto {
    private int myprice;
}

 

- Product 클래스 개선하기

[코드스니펫] Product - 관심 상품 등록하기

package com.sparata.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 updateByItemDto(ItemDto itemDto) {
        this.lprice = itemDto.getLprice();
    }
}

 

- ProductService 만들기

[코드스니펫] ProductService 생성

@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 개선하기

[코드스니펫] ProductRestController - 관심 상품 등록하기

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

    private final ProductRepository productRepository;
    private final ProductService productService;

// 등록된 전체 상품 목록 조회
    @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);
    }
}

 

08. [4주차] 키워드로 상품 검색하기 - NaverShopSearch 발전시키기

 

- 요구 조건 살펴보기

이전에 만들어 둔 NaverShopSearch 클래스를, 웹서비스에 이용할 수 있도록 발전시켜 보겠습니다.

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

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

 

[코드스니펫] 이전에 만들어 둔 NaverShopSearch

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

 

- 검색어 바꾸기 

검색어를 "아이맥"으로 바꿔보겠습니다.

 

[코드스니펫] search 메소드 바꾸기

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

}

- 검색 결과를 문자열에서 DTO로 바꾸기

 

- org.json 패키지 설치하기

JSON을 자바에서 다루기 위해, JSONObject, JSONArray 클래스가 필요합니다.

우리가 만드는 대신, 다른 사람이 만든 훌륭한 라이브러리를 바로 가져와서 사용하겠습니다.

이번 시간에는 "다른 사람이 만든 라이브러리 가져오는 방법(임포트)"에 대해 배워보겠습니다.

 

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 생성하기

package com.sparata.week04.models;

import lombok.Getter;
import org.json.JSONObject;

@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.link = itemJson.getString("link");
        this.image = itemJson.getString("image");
        this.lprice = itemJson.getInt("lprice");
    }
}

 

[코드스니펫] fromJSONtoItems 메소드 만들기

package com.sparata.week04.utils;

import com.sparata.week04.models.ItemDto;
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;

@Component // @RequiredArgsConstructor 와 함께 사용할 경우 스프링이 자동으로 생성합니다.
public class NaverShopSearch {
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "ZDmMn1uEbTnCuScQ2VQI");
        headers.add("X-Naver-Client-Secret", "srmCsX4PG9");
        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);//result == 문자열
        JSONArray items = rjson.getJSONArray("items");

        List<ItemDto> itemDtoList = new ArrayList<>();

        for (int i = 0; i < items.length(); i++) {
            JSONObject itemJson = items.getJSONObject(i);
            ItemDto itemDto = new ItemDto(itemJson);
            itemDtoList.add(itemDto);
        }
        return itemDtoList;
    }
}


 

09. [4주차] 키워드로 상품 검색하기 - 네이버 API와 서비스 연결하기

 

- 요구 조건 살펴보기

1. 사용자가 검색어를 입력하면, 컨트롤러가 그것을 전달받습니다.

2. 전달받은 검색어로 네이버 API에 요청하고, 그 결과를 사용자에게 응답합니다.

 

- NaverShopSearch 컴포넌트 등록하기

 

더 이상 검색을 main 메소드에서 진행하는게 아니라, Controller 에서 가져다 써야 합니다.

스프링이 자동으로 필요한 클래스를 필요한 곳에 생성하려면,

"아, 사용자가 요구하면 자동으로 생성할 클래스 목록이 이것이구나"

라고 확인할 수 있어야 하겠죠?

그 목록에 등록하는 간단한 방법이 바로 컴포넌트 등록입니다.

 

[코드스니펫] NaverShopSearch 컴포넌트 등록

@Component // @RequiredArgsConstructor 와 함께 사용할 경우 스프링이 자동으로 생성합니다.
public class NaverShopSearch {
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "ZDmMn1uEbTnCuScQ2VQI");
        headers.add("X-Naver-Client-Secret", "srmCsX4PG9");
        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);//result == 문자열
        JSONArray items = rjson.getJSONArray("items");

        List<ItemDto> itemDtoList = new ArrayList<>();

        for (int i = 0; i < items.length(); i++) {
            JSONObject itemJson = items.getJSONObject(i);
            ItemDto itemDto = new ItemDto(itemJson);
            itemDtoList.add(itemDto);
        }
        return itemDtoList;
    }
}

- SearchRequestController 만들기

[코드스니펫] SearchRequestController 만들기

package com.sparata.week04.controller;

import com.sparata.week04.models.ItemDto;
import com.sparata.week04.utils.NaverShopSearch;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

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

    private final NaverShopSearch naverShopSearch;

    @GetMapping("/api/search")
    public List<ItemDto> getItems(@RequestParam String query) {//변수로 받기위해서 @RequestParam 어노테이션 사용
        String resultString = naverShopSearch.search(query);
        return naverShopSearch.fromJSONtoItems(resultString);
    }
}

- ARC로 기능 확인하기

원하는 검색어로 자유롭게 테스트해보세요!

 

 

10. [4주차] HTML, 이미지 파일 준비하기

 

- 파일 분리

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

 

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

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

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

 

- 파일 준비

미리 준비한 HTML, CSS, Javascript 파일과 이미지 아이콘을 다운로드 받겠습니다.

 

1. 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. 검색창의 입력값을 가져온다.
// 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
// 3. GET /api/search?query=${query} 요청
// 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!
}
function addHTML(itemDto) {
    /**
     * class="search-itemDto" 인 녀석에서
     * image, title, lprice, addProduct 활용하기
     * 참고) onclick='addProduct(${JSON.stringify(itemDto)})'
     */
    return ``
}
function addProduct(itemDto) {
    /**
     * modal 뜨게 하는 법: $('#container').addClass('active');
     * data를 ajax로 전달할 때는 두 가지가 매우 중요
     * 1. contentType: "application/json",
     * 2. data: JSON.stringify(itemDto),
     */
// 1. POST /api/products 에 관심 상품 생성 요청
// 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 reponse.id 로 설정 (숙제로 myprice 설정하기 위함)
}
function showProduct() {
    /**
     * 관심상품 목록: #product-container
     * 검색결과 목록: #search-result-box
     * 관심상품 HTML 만드는 함수: addProductItem
     */
// 1. GET /api/products 요청
// 2. 관심상품 목록, 검색결과 목록 비우기
// 3. for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 붙이기!
}
function addProductItem(product) {
// link, image, title, lprice, myprice 변수 활용하기
    return ``;
}
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();
     */
}

[코드스니펫] 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 > resourses > static 에 images 폴더를 만들고, 아래 파일들을 저장합니다.

 

[코드스니펫] 저장 아이콘 (icon-save.png)

https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/spring/week04/icon-save.png

 

[코드스니펫] 검색 아이콘 (icon-search.png)

https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/spring/week04/icon-search.png

 

- 제대로 저장하였는지 확인하기

 index.html 파일을 켜고, 우측 상단 크롬 아이콘을 클릭한 뒤 아래와 같은 화면을 확인합니다.

 

11. [4주차] 상품 검색 기능 만들기

 

- 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
     */

    $('#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) {
            for (let i = 0; i < response.length; i++) {
                let itemDto = response[i];
                let tempHtml = addHTML(itemDto);
                $('#search-result-box').append(tempHtml);
            }
        }
    })
// 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!
}

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

 

12. [4주차] 관심 상품 등록하기

 

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

 

13. [4주차] 관심 상품 보여주기

 

- $(document).ready 함수의 의미

페이지가 모두 로드된 직후 실행할 자바스크립트 코드를 넣는 곳입니다.

일단 접속하면 관심 상품을 보여주어야 하기 때문에 showProduct 함수를 호출하고 있습니다.

 

- showProduct, addProductItem 만들기

 

[코드스니펫] GET /api/products 요청

// 1. 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 변수 활용하기

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

 

- 완성

모든 기능이 완성되었습니다! 검색, 상품 등록 기능을 확인해보세요.

 

[코드스니펫] 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>
[스파르타코딩클럽] 웹개발의 봄, Spring - 4주차 45
</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();
     */
}

 

14. [4주차] 스케줄러 만들기

 

- 요구 기능

매일 새벽 1시에 관심 상품 목록 제목으로 검색해서, 최저가 정보를 업데이트하는 스케줄러를 만들어보겠습니다.

 

- Scheduler 만들기

src > main > java > com.sparta.week04 > utils 에 Scheduler.java 파일을 만들어주세요.

 

[코드스니펫] Scheduler 클래스

package com.sparata.week04.utils;

import com.sparata.week04.models.ItemDto;
import com.sparata.week04.models.Product;
import com.sparata.week04.models.ProductRepository;
import com.sparata.week04.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.TimeUnit;

@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 클래스

package com.sparata.week04.service;

import com.sparata.week04.models.ItemDto;
import com.sparata.week04.models.Product;
import com.sparata.week04.models.ProductMypriceRequestDto;
import com.sparata.week04.models.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@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 IllegalArgumentException("아이디가 존재하지 않습니다.")
        );
        product.updateByItemDto(itemDto);
        return id;
    }
}

[코드스니펫] Week04Application 클래스

package com.sparata.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;

@EnableScheduling // 스프링 부트에서 스케줄러가 작동하게 합니다.
@EnableJpaAuditing // 시간 자동 변경이 가능하도록 합니다.
@SpringBootApplication // 스프링 부트임을 선언합니다.
public class Week04Application {
    public static void main(String[] args) {
        SpringApplication.run(Week04Application.class, args);
    }
}

- 확인

임의의 시간을 넣어 작동함을 확인합니다.

 

15. [4주차] 끝 & 숙제설명

 

- 잠깐 복습

 

- 3계층(Controller-Service-Repository)에 대해 심화하여 익혀보았습니다.

 

- 자바로 API에 데이터를 요청하고, 응답을 JSONObject, JSONArray로 다루는 방법을 살펴보았습니다.

 

- 프론트엔드 구현에 필요한 최소한의 Javascript, jQuery를 다시 한 번 연습해보았습니다.

→ backtick, val, append, $.ajax,

 

 

=================================================================================

출처 : [스파르타코딩클럽] 웹개발의 봄, Spring(강의자료)

=================================================================================

 

4주차부터 뭔가 어렵게느껴진다.

 

개발일지도 뭔가 양이 방대해져서 시간이 많이 들었다.

 

기본 구조인 3계층이나 API 사용방법은 구성과 사용방법까지는 알겠는데, 아직 혼자 무언가를 만들 수준은 못되는것 같다. 

 

그래도 5주차 마무리를하고 10월 31일부터 시작할 '내일배움캠프 4기'  에 합류하기위해 사전학습을 하고 5개월간의 코딩의 항해를 잘 해보겠다.