Sparta/What I Learned

22.12.28

코딩하는 또롱이 2022. 12. 28. 20:20

 

강의

 

 

Project MySelectShop - Refactoring

[수업 목표]

teamsparta.notion.site

뭐지 지금 굉장히 뒤죽박죽으로 정리가 되어있는데,,,, 이거 뭦,,,,,?

 

🫘 Bean 직접 생성

더보기

[BeanConfigurator]

package com.sparta.myselectshop.config;

import com.sparta.myselectshop.repository.ProductRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfiguration {
    @Bean
    public ProductRepository productRepository() {
        String dbUrl = "jdbc:h2:mem:db";
        String username = "sa";
        String password = "";
        return new ProductRepository(dbUrl, username, password);
    }
}

 

[ProductController]

package com.sparta.myselectshop.controller;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLException;
import java.util.List;

@RestController
@RequestMapping("/api")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService =  productService;
    }

    // 관심 상품 등록하기
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto) throws SQLException {
        // 응답 보내기
        return productService.createProduct(requestDto);
    }

    // 관심 상품 조회하기
    @GetMapping("/products")
    public List<ProductResponseDto> getProducts() throws SQLException {
        // 응답 보내기
        return productService.getProducts();
    }

    // 관심 상품 최저가 등록하기
    @PutMapping("/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) throws SQLException {
        // 응답 보내기 (업데이트된 상품 id)
        return productService.updateProduct(id, requestDto);
    }

}

 

[ProductService]

package com.sparta.myselectshop.service;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.SQLException;
import java.util.List;

@Component
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public ProductResponseDto createProduct(ProductRequestDto requestDto) throws SQLException {
        // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = new Product(requestDto);

        return  productRepository.createProduct(product);
    }

    public List<ProductResponseDto> getProducts() throws SQLException {

        return productRepository.getProducts();
    }

    public Long updateProduct(Long id, ProductMypriceRequestDto requestDto) throws SQLException {
        Product product = productRepository.getProduct(id);

        if(product == null) {
            throw new NullPointerException("해당 상품은 존재하지 않습니다.");
        }

        return productRepository.updateProduct(product.getId(), requestDto);
    }

}

 

[ProductRepository]

package com.sparta.myselectshop.repository;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class ProductRepository{

    private final String dbUrl;
    private final String username;
    private final String password;

    public ProductRepository(String dbUrl, String username, String password) {
        this.dbUrl = dbUrl;
        this.username = username;
        this.password = password;
    }

    public ProductResponseDto createProduct(Product product) throws SQLException {

        // DB 연결
        Connection connection = getConnection();

        // DB Query 작성
        PreparedStatement ps = connection.prepareStatement("select max(id) as id from product");
        ResultSet rs = ps.executeQuery();
        if (rs.next()) {
            // product id 설정 = product 테이블의 마지막 id + 1
            product.setId(rs.getLong("id") + 1);
        } else {
            throw new SQLException("product 테이블의 마지막 id 값을 찾아오지 못했습니다.");
        }

        ps = connection.prepareStatement("insert into product(id, title, image, link, lprice, myprice) values(?, ?, ?, ?, ?, ?)");
        ps.setLong(1, product.getId());
        ps.setString(2, product.getTitle());
        ps.setString(3, product.getImage());
        ps.setString(4, product.getLink());
        ps.setInt(5, product.getLprice());
        ps.setInt(6, product.getMyprice());

        // DB Query 실행
        ps.executeUpdate();

        // DB 연결 해제
        ps.close();
        connection.close();


        return new ProductResponseDto(product);
    }

    public List<ProductResponseDto> getProducts() throws SQLException {
        List<ProductResponseDto> products = new ArrayList<>();

        // DB 연결
        Connection connection = getConnection();

        // DB Query 작성 및 실행
        Statement stmt = connection.createStatement();
        ResultSet rs = stmt.executeQuery("select * from product");

        // DB Query 결과를 상품 객체 리스트로 변환
        while (rs.next()) {
            Product product = new Product();
            product.setId(rs.getLong("id"));
            product.setImage(rs.getString("image"));
            product.setLink(rs.getString("link"));
            product.setLprice(rs.getInt("lprice"));
            product.setMyprice(rs.getInt("myprice"));
            product.setTitle(rs.getString("title"));
            products.add(new ProductResponseDto(product));
        }

        // DB 연결 해제
        rs.close();
        connection.close();

        return products;
    }

    public Long updateProduct(Long id, ProductMypriceRequestDto requestDto) throws SQLException {

        // DB 연결
        Connection connection = getConnection();

        // DB Query 작성
        PreparedStatement ps = connection.prepareStatement("update product set myprice = ? where id = ?");
        ps.setInt(1, requestDto.getMyprice());
        ps.setLong(2, id);

        // DB Query 실행
        ps.executeUpdate();

        // DB 연결 해제
        ps.close();
        connection.close();

        return null;
    }

    public Product getProduct(Long id) throws SQLException {
        Product product = new Product();

        // DB 연결
        Connection connection = getConnection();

        // DB Query 작성
        PreparedStatement ps = connection.prepareStatement("select * from product where id = ?");
        ps.setLong(1, id);

        // DB Query 실행
        ResultSet rs = ps.executeQuery();
        if (rs.next()) {
            product.setId(rs.getLong("id"));
            product.setImage(rs.getString("image"));
            product.setLink(rs.getString("link"));
            product.setLprice(rs.getInt("lprice"));
            product.setMyprice(rs.getInt("myprice"));
            product.setTitle(rs.getString("title"));
        }

        // DB 연결 해제
        rs.close();
        ps.close();
        connection.close();

        return product;
    }

    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(dbUrl, username, password);
    }

}

 

 

 

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

  • JpaRepository<**"@Entity 클래스", "@Id 의 데이터 타입">**를 상속받는 interface 로 선언
    • 스프링 Data JPA 에 의해 자동으로 @Repository 가 추가됨
    • 아래 @Repository 역할 대부분을 자동으로 수행해 줌

 

 

📌🫘 최종 코드

 

더보기

[ProductController]

  • @RequiredArgsConstructor 어노테이션으로 의존성 주입 
  • HTTP Request 요청 온 것들을 받아서 ProductService로 넘김
  • 가져온 값들을 파라미터로 보내줌
package com.sparta.myselectshop.controller;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ProductController {

    private final ProductService productService;

    // 관심 상품 등록하기
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto) {
        // 응답 보내기
        return productService.createProduct(requestDto);
    }

    // 관심 상품 조회하기
    @GetMapping("/products")
    public List<ProductResponseDto> getProducts() {
        // 응답 보내기
        return productService.getProducts();
    }

    // 관심 상품 최저가 등록하기
    @PutMapping("/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) {
        // 응답 보내기 (업데이트된 상품 id)
        return productService.updateProduct(id, requestDto);
    }

}

 

[ProductService]

  • ProductController에 맞춰서 로직을 수행
  • DB에 접속하는 ProductRepository 와 연결되어 작업
  • saveAndFlush() :  db에 업데이트를 하는 flush가 아니라 쓰기 지연 SQL 저장소 내로 flush를 하는 과정
 

[JPA] save(), saveAll(), saveAndFlush() 차이

상황 > Card 객체를 DB에 insert 하기위해 JPA 메소드를 쓰려는데 insert 쿼리 메소드가 3개나 있다. 난 뭘 써야하나? 뭐가 다른걸까? 1개 저장 즉시 DB에 저장되지 않고 영속성 컨텍스트에 저장되었다가

velog.io

 

Spring jpa save(), saveAndFlush() 제대로 알고 쓰기

@Transactional 없이 save() vs saveAndFlush() public Member saveMember(MemberDTO memberDTO){ Member member = Member.create(memberDTO); member = memberRepository.save(member); member.setName("TaeHyun"); return member;// break point 설정 } 아래와 같

happyer16.tistory.com

 

[JPA] save 와 saveAndFlush의 차이

OS : MacOs Mojave DB : MySQL 5.7 DB Tool : Sequel Pro Framework : Spring Boot 2.0 맨밑에 결론있음 1. 준비 다음과 같은 member Entity를 준비했다. public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name =

ramees.tistory.com

  • getProducts() : findAll()을 이용하여 List 형태로 Product를 가져와서 list로 반환
  • updateProduct() : mtPrice 업데이트. JPA가 영속성에 의해 값을 추적해서 업데이트 
package com.sparta.myselectshop.service;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public ProductResponseDto createProduct(ProductRequestDto requestDto) {
        // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = productRepository.saveAndFlush(new Product(requestDto));

        return new ProductResponseDto(product);
    }

    @Transactional(readOnly = true)
    public List<ProductResponseDto> getProducts() {

        List<ProductResponseDto> list = new ArrayList<>();

        List<Product> productList = productRepository.findAll();
        for (Product product : productList) {
            list.add(new ProductResponseDto(product));
        }

        return list;
    }

    @Transactional
    public Long updateProduct(Long id, ProductMypriceRequestDto requestDto) {

        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
        );

        product.update(requestDto);

        return product.getId();
    }

}

 

[ProductRepository]

package com.sparta.myselectshop.repository;

import com.sparta.myselectshop.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

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

 

[Product]

package com.sparta.myselectshop.entity;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.naver.dto.ItemDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.*;

@Getter
@Setter
@Entity // DB 테이블 역할을 합니다.
@NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 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.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.myprice = 0;
    }

    public void update(ProductMypriceRequestDto requestDto) {
        this.myprice = requestDto.getMyprice();
    }
}

 

h2-console 에서도 브라우줘에도 제대로 뜬다 얏호!

 

📌 scheduler 추가

: 매일 새벽 1시에 관심 상품 목록 제목으로 검색해서, 최저가 정보를 업데이트

더보기

[Product]

 Timestamped를 상속, updateByItemDto() 생성

package com.sparta.myselectshop.entity;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.naver.dto.ItemDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.*;

@Getter
@Setter
@Entity // DB 테이블 역할을 합니다.
@NoArgsConstructor
public class Product extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 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.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.myprice = 0;
    }

    public void update(ProductMypriceRequestDto requestDto) {
        this.myprice = requestDto.getMyprice();
    }

    public void updateByItemDto(ItemDto itemDto) {
        this.lprice = itemDto.getLprice();
    }

}

 

[entity > Timestamped]

package com.sparta.myselectshop.entity;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.naver.dto.ItemDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.*;

@Getter
@Setter
@Entity // DB 테이블 역할을 합니다.
@NoArgsConstructor
public class Product extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 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.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.myprice = 0;
    }

    public void update(ProductMypriceRequestDto requestDto) {
        this.myprice = requestDto.getMyprice();
    }

    public void updateByItemDto(ItemDto itemDto) {
        this.lprice = itemDto.getLprice();
    }

}
@EnableJpaAuditing 어노테이션 추가해줘야한ㄷr!!!

 

 [scheduler > Scheduler]

package com.sparta.myselectshop.scheduler;

import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.naver.service.NaverApiService;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

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

@Slf4j
@Component
@RequiredArgsConstructor
public class Scheduler {

    private final NaverApiService naverApiService;
    private final ProductService productService;
    private final ProductRepository productRepository;

    // 초, 분, 시, 일, 월, 주 순서
    @Scheduled(cron = "0 0 1 * * *")
    public void updatePrice() throws InterruptedException {
        log.info("가격 업데이트 실행");
        List<Product> productList = productRepository.findAll();
        for (Product product : productList) {
            // 1초에 한 상품 씩 조회합니다 (NAVER 제한)
            TimeUnit.SECONDS.sleep(1);

            String title = product.getTitle();
            List<ItemDto> itemDtoList = naverApiService.searchItems(title);
            ItemDto itemDto = itemDtoList.get(0);

            // i 번째 관심 상품 정보를 업데이트합니다.
            Long id = product.getId();
            productService.updateBySearch(id, itemDto);
        }
    }
}
@EnableScheduling 어노테이션 삽입!

 

 [ProductService]

updateBySearching() 추가

package com.sparta.myselectshop.service;

import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public ProductResponseDto createProduct(ProductRequestDto requestDto) {
        // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = productRepository.saveAndFlush(new Product(requestDto));

        return new ProductResponseDto(product);
    }

    @Transactional(readOnly = true)
    public List<ProductResponseDto> getProducts() {

        List<ProductResponseDto> list = new ArrayList<>();

        List<Product> productList = productRepository.findAll();
        for (Product product : productList) {
            list.add(new ProductResponseDto(product));
        }

        return list;
    }

    @Transactional
    public Long updateProduct(Long id, ProductMypriceRequestDto requestDto) {

        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
        );

        product.update(requestDto);

        return product.getId();
    }

    @Transactional
    public void updateBySearch(Long id, ItemDto itemDto) {
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
        );
        product.updateByItemDto(itemDto);
    }
}

 

 

 


 

인증 & 인가

스프링 시큐리티에서 인증과 인가를 관리한다.

 

인증과 인가 개념 이해

[목차]

teamsparta.notion.site

📌 인증 VS 인가

인증 :  회원이 사이트에 로그인 하는 것

인가 :  사이트에서 접속하는 사람이 회원인지 비회원인지 가려서 정보 공개를 제한 하는 것

 

📌 인증의 방식

일반적으로 서버-클라이언트 구조로 되어있고, 실제로 이 두가지 요소는 아주 멀리 떨어져 있다. 그리고 Http 라는 프로토콜을 이용하여 통신하는데, 그 통신은 비연결성(Connectionless) 무상태(Stateless)로 이루어진다.

  • 비 연결성 : 서버와 클라이언트가 연결되어 있지 않음. 서버는 실제로 하나의 요청에 하나의 응답을 내버리고 연결을 끊어버린다.
  • 무상태 : 서버가 클라이언트의 상태를 저장하지 않음. 서버는 실제로 클라이언트가 직전에, 혹은 그 전에 어떠한 요청을 보냈는지 관심도 없고 전혀 알지 못한다.
🤔 그럼 우리가 보는 브라우저는 비연결성 상태인 것인데 어떻게 실시간 연결이 되는 것처럼 보일까?
url을 계층적으로 설계하고, 다음 요청에 대한 api url을 이전 계층에만 둔다면 연속적으로 사용하고 있다고 느껴진다.

 

📌 쿠키-세션 방식의 인증

서버가 특정 유저가 로그인 되었다는 상태를 저장하는 방식. 인증과 관련된 최소한의 정보는 저장해서 로그인을 유지시킨다.

출처 : 상단 URL

  1. 사용자가 로그인 요청을 보낸다.
  2. 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조하여 회원 여부를 확인한다.
  3. 실제 유저 테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 “세션 저장소”에 해당 유저가 로그인 되었다는 정보를 넣는다.
  4. 세션 저장소에서는 유저의 정보와는 관련 없는 난수인 session-id를 발급한다.
  5. 서버는 로그인 요청의 응답으로 session-id를 내준다.
  6. 클라이언트는 받은 session-id를 쿠키라는 저장소에 보관하고 앞으로의 요청마다  session-id를 같이 보낸다. (주로 HTTP header에 담아서 보낸다)
  7. 클라이언트의 요청에서 쿠키를 발견했다면 서버는 세션 저장소에서 쿠키를 검증한다.
  8. 만약 유저 정보를 받아왔다면 이 사용자는 로그인이 되어있 사용자이다.
  9. 이후에는 로그인 된 유저에 따른 응답을 내어준다.

 

📌JWT(JSON Web Token) 기반 인증

JWT란 인증에 필요한 정보들을 암호화시킨 토큰을 의미. JWT 기반 인증은 쿠키/세션 방식과 유사하게 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별한다.

출처 : 상단 URL

  1. 사용자가 로그인 요청을 보낸다.
  2. 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조하여 회원 여부를 확인한다.
  3. 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 유저의 정보를 JWT로 암호화 해서 내보낸다.
  4. 서버는 로그인 요청의 응답으로 JWT 토큰을 내준다.
  5. 클라이언트는 그 토큰을 저장소에 보관하고 앞으로의 요청마다 토큰을 같이 보낸다.
  6. 클라이언트의 요청에서 토큰을 발견했다면 서버는 토큰을 검증한다.
  7. 이후에는 로그인 된 유저에 따른 응답을 내준다.

 

🤔 JWT?

✔️ 말 그대로 인증에 필요한 정보들을 암호화시킨 토큰을 의미

실제로 이렇게 생겼고, 자세히 보시면 ’ . ’을 사용하여 세 부분으로 나뉘어져 있다.

위는 암호화 알고리즘에 의해 암호화 된 모양이고, 해독하면 아래와 같은 모습이다.

가운데에, 실제 유저의 정보가 들어있고, HEADER와 VERIFY SIGNATURE부분은 암호화 관련된 정보 양식이라고 생각하면 된다.

 

 


 

 

 📌MyselectShop에서 구현할 것

  1. ✔️ 키워드로 상품 검색하고 그 결과를 목록으로 보여주기
  2. ✔️ 관심 상품 등록하기
  3. ✔️ 관심 상품 조회하기
  4. ✔️ 관심 상품 최저가 등록하기
  5. 🆕 회원가입
  6. 🆕 로그인

 

 

 📌 Product API

 

기능 Method URL Request Response
메인페이지 GET /api/shop   index.html
키워드로 상품 검색하고 그 결과를 목록으로 보여주기
GET /api/search?query=검색어   [ { "title" : String, "
link" : String,
”image” : String,
"lprice" : int }, … ]
관심 상품 등록하기 POST /api/products { "title" : String,
”image” : String,
"link" : String,
"lprice" : int }
{ ”id” : Long
"title" : String,
"link" : String,
”image” : String,
"lprice" : int,
"myprice" : int }
관심 상품 조회하기 GET /api/products   [ { ”id” : Long "title" : String, "link" : String, ”image” : String, "lprice" : int, "myprice" : int }, … ]
관심 상품 최저가 등록하기 PUT /api/products/{id} {
"myprice"  : int
}
id

 

📌 HTML, JavaScript, CSS 준비

더보기

[resources > templates > signup.html]

회원 가입 Form 페이지

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<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="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap" rel="stylesheet">
    <link th:href="@{/style.css}" rel="stylesheet">
    <meta charset="UTF-8">
    <title>회원가입 페이지</title>
    <script>
        function onclickAdmin() {
            // Get the checkbox
            var checkBox = document.getElementById("admin-check");
            // Get the output text
            var box = document.getElementById("admin-token");

            // If the checkbox is checked, display the output text
            if (checkBox.checked == true){
                box.style.display = "block";
            } else {
                box.style.display = "none";
            }
        }
    </script>
</head>
<body>
<div id="login-form">
    <div id="login-title">Sign up Select Shop</div>

    <form method="POST" action="/api/user/signup">
        <div class="login-id-label">Username</div>
        <input type="text" name="username" placeholder="Username" class="login-input-box">

        <div class="login-id-label">Password</div>
        <input type="password" name="password" class="login-input-box">

        <div class="login-id-label">E-mail</div>
        <input type="text" name="email" placeholder="E-mail" class="login-input-box">

        <div>
            <input id="admin-check" type="checkbox" name="admin" onclick="onclickAdmin()" style="margin-top: 40px;">관리자
            <input id="admin-token" type="password" name="adminToken" placeholder="관리자 암호" class="login-input-box" style="display:none">
        </div>
        <button id="login-id-submit">회원 가입</button>
    </form>
</div>
</body>
</html>

 

[resources > templates > login.html]

회원 로그인 Form 페이지

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<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="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap" rel="stylesheet">
  <link th:href="@{/style.css}" rel="stylesheet">
  <meta charset="UTF-8">
  <title>로그인 페이지</title>
</head>
<body>
<div id="login-form">
  <div id="login-title">Log into Select Shop</div>
  <button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id=&redirect_uri=http://localhost:8080/api/user/kakao/callback&response_type=code'">
    카카오로 로그인하기
  </button>
  <button id="login-id-btn" onclick="location.href='/api/user/signup'">
    회원 가입하기
  </button>
  <form  method="POST" action="/api/user/login" >
    <div class="login-id-label">아이디</div>
    <input type="text" name="username" class="login-input-box">

    <div class="login-id-label">비밀번호</div>
    <input type="password" name="password" class="login-input-box">

    <button id="login-id-submit">로그인</button>
  </form>
  <div id="login-failed" style="display:none" class="alert alert-danger" role="alert">로그인에 실패하였습니다.</div>
</div>
</body>
<script>
  const href = location.href;
  const queryString = href.substring(href.indexOf("?")+1)
  if (queryString === 'error') {
    const errorDiv = document.getElementById('login-failed');
    errorDiv.style.display = 'block';
  }
</script>
</html>

 

[resources > templates > index.html]

로그인/회원가입 추가된 Shop 페이지

<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<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">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

  <link th:href="@{/style.css}" rel="stylesheet">
  <script type="text/javascript" th:src="@{/basic.js}"></script>
  <title>나만의 셀렉샵</title>
</head>
<body>
<div class="header" style="position:relative;">
  <!--headr 추가 부분-->
  <div th:if="${username != '' }">
    <div id="header-title-login-user">
      <span th:text="${username}"></span> 님의
    </div>
    <div id="header-title-select-shop">
      Select Shop
    </div>
  </div>
  <div th:unless="${username != ''}">
    <div id="header-title-select-shop">
      Select Shop
    </div>
    <a id="sign-text" href="/api/user/signup">
      회원가입
    </a>
    <a id="login-text" href="/api/user/login">
      로그인
    </a>
  </div>

  <!--/headr 추가 부분-->
</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">
  </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>

 

[resources > static > style.css]

* {
    font-family: 'Nanum Gothic', sans-serif;
}

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: 209px;
    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: 320px;
    height: 22px;
    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 {
    height: 255px;
    box-sizing: border-box;
    background-color: #15aabf;
    color: white;
    text-align: center;
    padding-top: 80px;
    /*padding: 50px;*/
    font-size: 45px;
    font-weight: bold;
}

#header-title-login-user {
    font-size: 36px;
    letter-spacing: -1.08px;
}

#header-title-select-shop {
    margin-top: 20px;
    font-size: 45px;
    letter-spacing: 1.1px;
}

#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: 11px;
    font-weight: normal;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: -0.75px;
    text-align: left;
    color: #343a40;
    margin-bottom: 10px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.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: 13.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: 48px;
}

.pagination {
    margin-top: 12px;
    margin-left: auto;
    margin-right: auto;
    width: 53%;
}

.folder-active {
    background-color: crimson !important;;
}

.none {
    display: none;
}

.folder-bar {
    width: 100%;
    overflow: hidden;
}

.folder-black, .folder-black:hover {
    color: #fff!important;
    background-color: #000!important;
}

.folder-bar .folder-button {
    white-space: normal;
}
.folder-bar .folder-bar-item {
    padding: 8px 16px;
    float: left;
    width: auto;
    border: none;
    display: block;
    outline: 0;
}

.folder-btn, .folder-button {
    border: none;
    display: inline-block;
    padding: 8px 16px;
    vertical-align: middle;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    background-color: inherit;
    text-align: center;
    cursor: pointer;
    white-space: nowrap;
}

#login-form {
    width: 538px;
    height: 710px;
    margin: 70px auto 141px auto;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    /*gap: 96px;*/
    padding: 56px 0 0;
    border-radius: 10px;
    box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.15);
    background-color: #ffffff;
}

#login-title {
    width: 303px;
    height: 32px;
    /*margin: 56px auto auto auto;*/
    flex-grow: 0;
    font-family: SpoqaHanSansNeo;
    font-size: 32px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: -0.96px;
    text-align: left;
    color: #212529;
}

#login-kakao-btn {
    border-width: 0;
    margin: 96px 0 8px;
    width: 393px;
    height: 62px;
    flex-grow: 0;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    /*margin: 0 0 8px;*/
    padding: 11px 12px;
    border-radius: 5px;
    background-color: #ffd43b;

    font-family: SpoqaHanSansNeo;
    font-size: 20px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    color: #414141;
}

#login-id-btn {
    width: 393px;
    height: 62px;
    flex-grow: 0;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    /*margin: 8px 0 0;*/
    padding: 11px 12px;
    border-radius: 5px;
    border: solid 1px #212529;
    background-color: #ffffff;

    font-family: SpoqaHanSansNeo;
    font-size: 20px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    color: #414141;
}

.login-input-box {
    border-width: 0;

    width: 370px !important;
    height: 52px;
    margin: 14px 0 0;
    border-radius: 5px;
    background-color: #e9ecef;
}

.login-id-label {
    /*width: 44.1px;*/
    /*height: 16px;*/
    width: 382px;
    padding-left: 11px;
    margin-top: 40px;
    /*margin: 0 337.9px 14px 11px;*/
    font-family: NotoSansCJKKR;
    font-size: 16px;
    font-weight: normal;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: -0.8px;
    text-align: left;
    color: #212529;
}

#login-id-submit {
    border-width: 0;
    width: 393px;
    height: 62px;
    flex-grow: 0;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    margin: 40px 0 0;
    padding: 11px 12px;
    border-radius: 5px;
    background-color: #15aabf;

    font-family: SpoqaHanSansNeo;
    font-size: 20px;
    font-weight: bold;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: normal;
    text-align: center;
    color: #ffffff;
}

#sign-text {
    position:absolute;
    top:48px;
    right:110px;
    font-size: 18px;
    font-family: SpoqaHanSansNeo;
    font-size: 18px;
    font-weight: 500;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: 0.36px;
    text-align: center;
    color: #ffffff;
}

#login-text {
    position:absolute;
    top:48px;
    right:50px;
    font-size: 18px;
    font-family: SpoqaHanSansNeo;
    font-size: 18px;
    font-weight: 500;
    font-stretch: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: 0.36px;
    text-align: center;
    color: #ffffff;
}

@media ( max-width: 768px ) {
    body {
        margin: 0px;
    }

    #search-result-box {
        margin-top: 15px;
    }

    .search-itemDto {
        width: 330px;
        display: flex;
        flex-direction: row;
        align-content: center;
        justify-content: space-around;
    }

    .search-itemDto-left img {
        width: 80px;
        height: 80px;
    }

    .search-itemDto-center {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-evenly;
    }

    .search-itemDto-center div {
        width: 190px;
        height: 23px;
        font-size: 13px;
        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: 17px;
        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: 14px;
        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: 14px;
        width: 14px;
        vertical-align: middle;
        margin-top: 26px;
        cursor: pointer;
        padding-right: 20px;
    }

    input#query {
        padding: 15px;
        width: 290px;
        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 {
        position: absolute;
        bottom: 0px;
        width: 333px;
        border-radius: 11px 11px 0px 0px;
    }

    .popup input {
        width: 278px !important;
        height: 39px;
        border-radius: 2px;
        border: solid 1.1px #dee2e6;
        margin-right: 9px;
        margin-bottom: 10px;
        padding-left: 10px;
    }

    .popup p {
        font-size: 14px;
    }

    .popup button.close {
        position: absolute;
        top: 15px;
        right: 15px;
        color: #adb5bd;
        background-color: #fff;
        font-size: 19px;
        border: none;
    }

    .popup button.cta {
        width: 319px;
        height: 43.9px;
        border-radius: 2px;
        background-color: #15aabf;
        border: none;
    }

    #search-area, #see-area {
        width: 330px;
        margin: auto;
    }

    .nav {
        width: 330px;
        margin: 30px auto;
        display: flex;
        align-items: center;
        justify-content: space-around;
    }

    .nav div {
        cursor: pointer;
    }

    .nav div.active {
        font-weight: 700;
    }

    .header {
        height: 255px;
        box-sizing: border-box;
        background-color: #15aabf;
        color: white;
        text-align: center;
        padding-top: 80px;
        /*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;*/
    /*    overflow: hidden;*/
    /*    white-space: nowrap;*/
    /*    text-overflow: ellipsis;*/
    /*}*/

    /*.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: 48px;*/
    /*}*/

    .none {
        display: none;
    }

    input#kakao-login {
        padding: 15px;
        width: 526px;
        border-radius: 2px;
        background-color: #e9ecef;
        border: none;

        background-image: url('images/kakao_login_medium_narrow.png');
        background-repeat: no-repeat;
        background-position: right 10px center;
        background-size: 20px 20px;
    }
}

.autocomplete {
    /*the container must be positioned relative:*/
    position: relative;
    display: inline-block;
}
input {
    border: 1px solid transparent;
    background-color: #f1f1f1;
    padding: 10px;
    font-size: 16px;
}
input[type=text] {
    background-color: #f1f1f1;
    /*width: 100%;*/
}
input[type=password] {
    background-color: #f1f1f1;
    /*width: 100%;*/
}
input[type=submit] {
    background-color: DodgerBlue;
    color: #fff;
}
.autocomplete-items {
    position: absolute;
    border: 1px solid #d4d4d4;
    border-bottom: none;
    border-top: none;
    z-index: 99;
    /*position the autocomplete items to be the same width as the container:*/
    top: 100%;
    left: 0;
    right: 0;
}
.autocomplete-items div {
    padding: 10px;
    cursor: pointer;
    background-color: #fff;
    border-bottom: 1px solid #d4d4d4;
}
.autocomplete-items div:hover {
    /*when hovering an item:*/
    background-color: #e9e9e9;
}
.autocomplete-active {
    /*when navigating through the items using the arrow keys:*/
    background-color: DodgerBlue !important;
    color: #ffffff;
}

.alert-danger {
    color: #721c24;
    background-color: #f8d7da;
    border-color: #f5c6cb;
}

.alert {
    width: 300px;
    margin-top: 22px;
    padding: 1.75rem 1.25rem;
    border: 1px solid transparent;
    border-radius: .25rem;
}

 

[ShopController]

package com.sparta.myselectshop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/api")
public class ShopController {

    @GetMapping("/shop")
    public ModelAndView shop() {
        ModelAndView modelAndView = new ModelAndView("index");
        modelAndView.addObject("username","");
        return modelAndView;
    }
}

 

 

 

📌 User API

기능 Method URL Request Response
회원가입 페이지 GET /api/user/signup   signup.html
회원가입 POST /api/user/signup POST Form 태그
{ "username" : String,
”password” : String,
”email : String,
”admin” : boolean,
”adminToken" : String }
redirect:/api/user/login
로그인 페이지 GET /api/user/login   login.html
로그인 POST /api/products POST Form 태그
{ "username" : String,
”password” : String }
redirect:/api/shop

 

 📌User 코드

더보기

[entity > User]

package com.sparta.myselectshop.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;

import jakarta.persistence.*;

@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    public User(String username, String password, String email, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
    }
}

 

[entity > UserRoleEnum]

✍️관리자 회원 가입 인가 방법 : ✔️관리자 가입 토큰 입력 필요

✔️ AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC

package com.sparta.myselectshop.entity;

public enum UserRoleEnum {
    // user의 권한을 알려주는 역할
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}

🚨  잠깐!

실제로 '관리자' 권한을 이렇게 엉성하게 부여해 주는 경우는 드물다. 해커가 해당 암호를 갈취하게 되면, 관리자 권한을 너무 쉽게 획득할 수 있게 되기 때문이다. 보통 현업에서는

 

1) '관리자' 권한을 부여할 수 있는 관리자 페이지 구현

2) 승인자에 의한 결재 과정 구현 → 관리자 권한 부여

 

로 나뉜다.

 

[UserRepository]

User entity랑 연결
package com.sparta.myselectshop.repository;

import com.sparta.myselectshop.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

 

[SignupRequestDto]

관리자인지 아닌지의 여부 판단

package com.sparta.myselectshop.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class SignupRequestDto {
    private String username;
    private String password;
    private String email;
    private boolean admin = false;
    private String adminToken = "";
}

 

[LoginRequestDto]

package com.sparta.myselectshop.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class LoginRequestDto {
    private String username;
    private String password;
}

 

[UserController]

url에 따른 페이지 반환

package com.sparta.myselectshop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/api/user")
public class UserController {
    // 팸플릿에 있는 html 파일만 반환하기 때문에 Controller 어노테이션을 사용 (RestController 어노테이션 X)

    @GetMapping("/signup")
    public ModelAndView signupPage() {
        return new ModelAndView("signup");
    }

    @GetMapping("/login")
    public ModelAndView loginPage() {
        return new ModelAndView("login");
    }

}

 

 

 


 

win + r -> dxdiag

오,,,, 이걸로 내 컴퓨터 사양을 다 알 수 있음!

'Sparta > What I Learned' 카테고리의 다른 글

22.12.30  (2) 2022.12.30
22.12.29  (0) 2022.12.29
22.12.27  (0) 2022.12.28
22.12.26  (0) 2022.12.28
22.12.25  (0) 2022.12.26