Project MySelectShop - JPA (3)

박영준·2022년 12월 5일
0

Java

목록 보기
24/111

전체적인 패키지 및 파일 생성

1. controller

1) FolderController

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 * * *")    //새벽 1시에 매번 자동으로 이 메소드가 실행됨 --> 구글링
    public void updatePrice() throws InterruptedException {
        log.info("가격 업데이트 실행");
        List<Product> productList = productRepository.findAll();    //productRepository 로 모든 product 를 가져온다
        for (Product product : productList) {   //for 문 돌면서,
            // 1초에 한 상품 씩 조회합니다 (NAVER 제한)
            TimeUnit.SECONDS.sleep(1);

            //product 에서 title 을 가져온다
            String title = product.getTitle();
            //title 을 통해서, naverApiService 를 사용해서 itemDtoList 를 가져온다
            List<ItemDto> itemDtoList = naverApiService.searchItems(title);
            //가장 상단에 있는 item 을 가져온다
            ItemDto itemDto = itemDtoList.get(0);

            //i 번째 관심 상품 정보를 업데이트합니다.
            //product 의 id 도 가져와서, 가장 상단에 있는 item 과 id 를 통해 update 를 실시
            Long id = product.getId();
            productService.updateBySearch(id, itemDto);
        }
    }
}

2) 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.entity.Product;
import com.sparta.myselectshop.service.ProductService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor     //의존성 주입
public class ProductController {

    //HTTP request 를 받아서,  Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
    private final ProductService productService;

    //관심상품 추가 할 때 토큰 보내기
    // 관심 상품 등록하기
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, HttpServletRequest request) {
        // 응답 보내기
        return productService.createProduct(requestDto, request);
    }

    //관심상품 조회 할 때 토큰 보내기
    // 관심 상품 조회하기
    @GetMapping("/products")
    public Page<Product> getProducts(
            @RequestParam("page") int page,     //쿼리 방식으로 데이터를 가져옴
            @RequestParam("size") int size,
            @RequestParam("sortBy") String sortBy,
            @RequestParam("isAsc") boolean isAsc,
            HttpServletRequest request
    ) {
        // 응답 보내기
            //page-1: 페이지 객체에서는 0번 인덱스가 1 페이지
        return productService.getProducts(request, page-1, size, sortBy, isAsc);
    }

    //관심상품 최저가 추가 할 때 토큰 보내기
    // 관심 상품 최저가 등록하기
    @PutMapping("/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto, HttpServletRequest request) {
        // 응답 보내기 (업데이트된 상품 id)
        //request: 토큰을 가져와야하므로 넣었음
        return productService.updateProduct(id, requestDto, request);
    }

    //관심상품에 폴더 추가 구현
    // 상품에 폴더 추가
    @PostMapping("/products/{productId}/folder")
    public Long addFolder(
            @PathVariable Long productId,
            @RequestParam Long folderId,
            HttpServletRequest request
    ) {
        Product product = productService.addFolder(productId, folderId, request);
        return product.getId();
    }

}

3) ShopController

//회원이 저장한 폴더 조회
package com.sparta.myselectshop.controller;

import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.repository.UserRepository;
import com.sparta.myselectshop.service.FolderService;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

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

    private final FolderService folderService;
    private final JwtUtil jwtUtil;
    private final UserRepository userRepository;

    @GetMapping("/shop")
    public ModelAndView shop() {
        return new ModelAndView("index");
    }

    //저장한 folder 를 가지고 오는 부분
    // 로그인 한 유저가 메인페이지를 요청할 때 가지고있는 폴더를 반환
    @GetMapping("/user-folder")
    //request: 누가 로그인 했는지 알기위해
    public String getUserInfo(Model model, HttpServletRequest request) {

        model.addAttribute("folders", folderService.getFolders(request));

        //비동기적으로, index.html 의 folder 의 한 부분을 바꿔치기
        return "index :: #fragment";
    }

    // 로그인 한 유저가 메인페이지를 요청할 때 유저의 이름 반환
    @GetMapping("/user-info")
    @ResponseBody
    public String getUserName(HttpServletRequest request) {
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            return user.getUsername();
        }else {
            return "fail";
        }
    }

}

4) UserController

//로그인/회원가입 페이지
package com.sparta.myselectshop.controller;

import com.sparta.myselectshop.dto.LoginRequestDto;
import com.sparta.myselectshop.dto.SignupRequestDto;
import com.sparta.myselectshop.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {

    private final UserService userService;

    @GetMapping("/signup")
    public ModelAndView signupPage() {
        return new ModelAndView("signup");  //url 로 /signup 이 들어오면, signup 페이지를 반환
    }

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

    //회원가입 구현
    @PostMapping("/signup")
    public String signup(SignupRequestDto signupRequestDto) {
        userService.signup(signupRequestDto);
        return "redirect:/api/user/login";
    }

    //로그인 구현
    @ResponseBody
    @PostMapping("/login")
    //@RequestBody: ajax 에서 body 쪽으로 값이 넘어오기때문에 써주었음
    //HttpServletResponse response: HttpRequest 에서 Header 가 넘어와 받아오는 것처럼,
        //Client 쪽으로 반환할 때는 response 객체를 반환하고, 반환 할 response 객체의 Header 쪽에 우리가 만든 Token 을 넣기위해
    public String login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
        //login() 메소드에 loginRequestDto, response 를 넣는다
        userService.login(loginRequestDto, response);
        return "success";
    }

}

2. dto

1) FolderRequestDto

//폴더 생성
package com.sparta.myselectshop.dto;

import lombok.Getter;

import java.util.List;

@Getter
public class FolderRequestDto {
    List<String> folderNames;   //folder 의 이름을 List 형식으로 받아주는 객체
}

2) LoginRequestDto

package com.sparta.myselectshop.dto;

import lombok.Getter;
import lombok.Setter;

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

3) ProductMypriceRequestDto

package com.sparta.myselectshop.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductMypriceRequestDto {
    private int myprice;
}

4) ProductRequestDto

package com.sparta.myselectshop.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductRequestDto {
    // 관심상품명
    private String title;
    // 관심상품 썸네일 image URL
    private String image;
    // 관심상품 구매링크 URL
    private String link;
    // 관심상품의 최저가
    private int lprice;
}

5) ProductResponseDto

package com.sparta.myselectshop.dto;

import com.sparta.myselectshop.entity.Product;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ProductResponseDto {
    private Long id;
    private String title;
    private String link;
    private String image;
    private int lprice;
    private int myprice;

    public ProductResponseDto(Product product) {
        this.id = product.getId();
        this.title = product.getTitle();
        this.link = product.getLink();
        this.image = product.getImage();
        this.lprice = product.getLprice();
        this.myprice = product.getMyprice();
    }
}

6) 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;  //admin 인지 아닌지 확인
    private String adminToken = "";
}

3. entity

1) Folder

//폴더 생성
package com.sparta.myselectshop.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
public class Folder {

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

    @Column(nullable = false)
    private String name;

    @ManyToOne
    @JoinColumn(name = "USER_ID", nullable = false)
    private User user;

    public Folder(String name, User user) {
        this.name = name;
        this.user = user;
    }
}

2) 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 jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

@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;    //itemDto 의 lprice 값을 가져온 것을 기존의 lprice 쪽으로 update

    @Column(nullable = false)
    private int myprice;

    @Column(nullable = false)   
    private Long userId;        //userId 를 넣는다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)

    //관심상품에 폴더 추가 구현
    //ManyToMany 가 단방향으로 걸려있음
        //문제점?  우리가 직접 만든 테이블이 아니라, JPA 가 N:N 으로 풀어주기 위해 중간 테이블을 만듦
        //왜 문제인가?  중간 테이블에는 추가 정보를 넣을 수 없음, 예상하지 못한 쿼리들이 JPA 에서 자동으로 나갈 수 있음
        //해결법?  OneToMany, OneToMany, ManyToOne 으로 풀어서 사용
    @ManyToMany
    private List<Folder> folderList = new ArrayList<>();

    public Product(ProductRequestDto requestDto, Long userId) {
        this.title = requestDto.getTitle();
        this.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.myprice = 0;
        this.userId = userId;   //생성자를 통해서 userId 도 같이 넣는다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
    }

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

    public void updateByItemDto(ItemDto itemDto) {
        //itemDto 의 lprice 값을 가져와서,
        this.lprice = itemDto.getLprice();
    }

    //관심상품에 폴더 추가 구현
    public void addFolder(Folder folder) {  //Folder 를 받아와서 folderList 안에 넣음
        this.folderList.add(folder);
    }

}

3) Timestamped

package com.sparta.myselectshop.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    private LocalDateTime modifiedAt;
}

4) User

//폴더 생성
package com.sparta.myselectshop.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@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;

    @OneToMany
    List<Folder> folders = new ArrayList<>();

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

}

5) UserRoleEnum

package com.sparta.myselectshop.entity;

public enum UserRoleEnum {
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}

//관리자 회원 가입 인가 방법
    //로그인 할 때는 ADMIN 표시를 하고
    //회원가입 할 때는 미리 서버쪽에서 가지고 있는 키 값을 클라이언트쪽에서 등록해야 ADMIN 사용자로 등록 가능
        //'관리자 가입 토큰' 입력 필요 --> AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC

    //but, 실제로 '관리자' 권한을 이렇게 엉성하게 부여해 주는 경우는 드물다.(해커가 해당 암호를 갈취하게 되면, 관리자 권한을 너무 쉽게 획득하니까)

    //보통 현업에서는
        //방법 1. '관리자' 권한을 부여할 수 있는 관리자 페이지 구현
        //방법 2. 승인자에 의한 결재 과정 구현 → 관리자 권한 부여

4. jwt

jwtUtil

package com.sparta.myselectshop.jwt;


import com.sparta.myselectshop.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j
@Component  //빈 등록을 위해
@RequiredArgsConstructor
public class JwtUtil {  //빈이 등록됐다는 '나뭇잎 모양' 확인 가능

    //토큰 생성에 필요한 값

    //Authorization: Bearer <JWT> 에서 Header 에 들어가는 KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";

    // 사용자 권한 값의 KEY
        //사용자 권한도 Token 안에 넣는데, 이를 가져올 때 사용하는 KEY 값
    public static final String AUTHORIZATION_KEY = "auth";

    // Token 식별자
        //Token 을 만들 때, 앞에 들어가는 부분
    private static final String BEARER_PREFIX = "Bearer ";

    // 토큰 만료시간
        //밀리세컨드 기준. 60 * 1000L는 1분. 60 * 60 * 1000L는 1시간
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    @Value("${jwt.secret.key}") //@Value() 안에 application.properties 에 넣어둔 KEY 값(jwt.secret.key=7ZWt7ZW0O...pA=)을 넣으면, 가져올 수 있음
    private String secretKey;
    private Key key;    //Key 객체: Token을 만들 때 넣어줄 KEY 값
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct  //처음에 객체가 생성될 떄, 초기화하는 함수 --> 구글링
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);   //Base64로 인코딩되어 있는 것을, 값을 가져와서 디코드하고, byte 배열로 반환
        key = Keys.hmacShaKeyFor(bytes);    //반환된 bytes 를 hmacShaKeyFor() 메서드를 사용해서 Key 객체에 넣기
    }

    //Header 에서 Token 가져오기
    public String resolveToken(HttpServletRequest request) {
        //HttpServletRequest request 객체 안에 들어간 Token 값을 가져옴
        //() 안에는 어떤 KEY 를 가져올지 정할 수 있음(여기선 AUTHORIZATION_HEADER 안에 있는 KEY 의 값을 가져옴)
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        //그리고, 가져온 코드가 있는지, BEARER_PREFIX 로 시작하는지 확인
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            //substring() 메소드를 사용해서, 앞의 일곱 글자를 지워줌 --> 앞의 일곱 글자는 Token 과 관련 없는 String 값이므로
            return bearerToken.substring(7);
        }
        return null;
    }

    //JWT 생성
    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +      //Token 앞에 들어가는 부분임
                //실제로 만들어지는 부분
                Jwts.builder()
                        //setSubject 라는 공간 안에 username 를 넣는다
                        .setSubject(username)
                        //claim 이라는 공간 안에 AUTHORIZATION_KEY 사용자의 권한을 넣고(이 권한을 가져올 때는 지정해둔 auth KEY 값을 사용해서 넣음)
                        .claim(AUTHORIZATION_KEY, role)
                        //이 Token 을 언제까지 유효하게 가져갈건지. date: 위의 Date date = new Date() 에서 가져온 부분
                        //getTime(): 현재 시간
                        //+ TOKEN_TIME: 우리가 지정해 둔 시간(TOKEN_TIME = 60 * 60 * 1000L)을 더한다 = 즉, 지금 기준으로 언제까지 가능한지 결정해줌
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        //Token 이 언제 만들어졌는지 넣는 부분
                        .setIssuedAt(date)
                        //secret key 를 사용해서 만든 key 객체와
                        //어떤 암호화 알고리즘을 사용해서 암호화할것인지 지정(SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256)
                        .signWith(key, signatureAlgorithm)
                        //반환
                        .compact();
    }

    //JWT 검증
    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            //Jwts: 위에서 JWT 생성 시 사용했던 것
            //parserBuilder(): 검증 방법
            //setSigningKey(key): Token 을 만들 때 사용한 KEY
            //parseClaimsJws(token): 어떤 Token 을 검증할 것인지
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);  //이 코드만으로 내부적으로 검증 가능
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }


    //JWT 에서 사용자 정보 가져오기
    // 토큰에서 사용자 정보 가져오기 --> 위에서 validateToken() 으로 토큰을 검증했기에 이 코드를 사용할 수 있는 것
    public Claims getUserInfoFromToken(String token) {
        //getBody(): Token?? 안에 들어있는 정보를 가져옴
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

}

5. repository

1) FolderRepository

//폴더 생성
package com.sparta.myselectshop.repository;

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

import java.util.List;

public interface FolderRepository extends JpaRepository<Folder, Long> {

    List<Folder> findAllByUser(User user);

    //중복 폴더 생성 이슈 해결
    List<Folder> findAllByUserAndNameIn(User user, List<String> names);
}

2) ProductRepository

package com.sparta.myselectshop.repository;

import com.sparta.myselectshop.entity.Product;
import org.springframework.data.domain.Page;    //패키지도 확인하도록 주의
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long> {

    //UserId 를 통해, userId 가 동일한 Product 를 가져온다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
    //Page 객체로 받는다
    Page<Product> findAllByUserId(Long userId, Pageable pageable);

    //Product 의 id 와 userId 가 일치하는 Product 를 가져온다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
    Optional<Product> findByIdAndUserId(Long id, Long userId);
    Page<Product> findAll(Pageable pageable);

    //폴더 별 관심상품 조회
    Page<Product> findAllByUserIdAndFolderList_Id(Long userId, Long folderId, Pageable pageable);

    //같은 폴더 내에 상품 중복 생성 이슈 해결
    Optional<Product> findByIdAndFolderList_Id(Long productId, Long folderId);
}

3) UserRepository

//회원가입 구현
//UserRepository 가 필요한 이유?  user 를 검증하려면 UserRepository 에 접근해서 DB 에 갔다와야 함
package com.sparta.myselectshop.repository;

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

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    // 회원 중복 확인
    Optional<User> findByUsername(String username);
}

6. 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 * * *")    //새벽 1시에 매번 자동으로 이 메소드가 실행됨 --> 구글링
    public void updatePrice() throws InterruptedException {
        log.info("가격 업데이트 실행");
        List<Product> productList = productRepository.findAll();    //productRepository 로 모든 product 를 가져온다
        for (Product product : productList) {   //for 문 돌면서,
            // 1초에 한 상품 씩 조회합니다 (NAVER 제한)
            TimeUnit.SECONDS.sleep(1);

            //product 에서 title 을 가져온다
            String title = product.getTitle();
            //title 을 통해서, naverApiService 를 사용해서 itemDtoList 를 가져온다
            List<ItemDto> itemDtoList = naverApiService.searchItems(title);
            //가장 상단에 있는 item 을 가져온다
            ItemDto itemDto = itemDtoList.get(0);

            //i 번째 관심 상품 정보를 업데이트합니다.
            //product 의 id 도 가져와서, 가장 상단에 있는 item 과 id 를 통해 update 를 실시
            Long id = product.getId();
            productService.updateBySearch(id, itemDto);
        }
    }
}

7. service

1) FolderService

package com.sparta.myselectshop.service;

import com.sparta.myselectshop.entity.Folder;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.repository.FolderRepository;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class FolderService {

    private final ProductRepository productRepository;  //폴더 별 관심상품 조회
    private final FolderRepository folderRepository;    //폴더 생성
    private final UserRepository userRepository;    //폴더 생성
    private final JwtUtil jwtUtil;    //폴더 생성

    //폴더 생성
    // 로그인한 회원에 폴더들 등록
    @Transactional
    public List<Folder> addFolders(List<String> folderNames, HttpServletRequest request) {

        // Request 에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if (token != null) {

            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
            //NameIn: where 조건문 안에 in 으로 여러 조건을 사용했던 것처럼
            List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);  //중복 폴더 생성 이슈 해결

            //생기게 될 여러가지 folder 를 넣어줄 folderList 생성
            List<Folder> folderList = new ArrayList<>();

            //folderNames 이 List 형식이므로(List<String> folderNames), for 문을 돌림
            for (String folderName : folderNames) {
                // 이미 생성한 폴더가 아닌 경우만 폴더 생성
                //Client 쪽에서 받아온 folderName(addFolders(List<String> folderNames, HttpServletRequest request)에서) 와 회원이 이미 생성한 폴더들(existFolderList)
                if (!isExistFolderName(folderName, existFolderList)) {  //중복 폴더 생성 이슈 해결
                    //folderName: 가지고온 folder 이름 중 하나
                    //user: 로그인 한 유저 하나
                    //이렇게 하나씩 넣어서 하나의 folder 를 만듦
                    Folder folder = new Folder(folderName, user);
                    //이렇게 만든 folder 를 위의 folderList 에 하나씩 넣어준다
                    folderList.add(folder);
                }
            }

            return folderRepository.saveAll(folderList);
        } else {
            return null;
        }
    }

    //폴더 조회
    // 로그인한 회원이 등록된 모든 폴더 조회
    @Transactional(readOnly = true)
    public List<Folder> getFolders(HttpServletRequest request) {

        // 사용자의 정보를 가져온다
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //확인한 user 객체를 넣어서 모두 찾고, List 형식으로 반환
            return folderRepository.findAllByUser(user);

        } else {
            return null;
        }
    }

    //폴더 별 관심상품 조회
    @Transactional(readOnly = true)
    public Page<Product> getProductsInFolder(Long folderId, int page, int size, String sortBy, boolean isAsc, HttpServletRequest request) {

        // 페이징 처리
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);

        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            System.out.println("FolderService.getProductsInFolder");  //실제 프로젝트 만들 때, 이런 부분은 지워주기
            System.out.println("folderId = " + folderId); 
            System.out.println("user = " + user.getId()); 

            //우리가 선택한 폴더 안의 product 를 가져온다
            //findAllByUserIdAndFolderList_Id   --> Product.java 에서 @ManyToMany 가 쓰인 부분
                //UserId: 로그인한 유저 아이디
                //FolderList_Id: 우리가 선택한 폴더의 아이디
            return productRepository.findAllByUserIdAndFolderList_Id(user.getId(), folderId, pageable);

        } else {
            return null;
        }
    }

    //중복 폴더 생성 이슈 해결
    //회원이 이미 생성한 폴더들(existFolderList) 를 for 문으로 돌면서
    private boolean isExistFolderName(String folderName, List<Folder> existFolderList) {
        //기존 폴더 리스트에서 folder name 이 있는지?
        for (Folder existFolder : existFolderList) {
            //회원이 이미 생성한 폴더들(existFolderList) 안에 있는 폴더의 Name 과 Client 쪽에서 받아온 folderName(addFolders(List<String> folderNames, HttpServletRequest request)에서)가 일치하는지 확인
            if (existFolder.getName().equals(folderName)) {
                //일치하면 true 반환
                return true;
            }
        }
        //일치하지 않으면 false 반환
        return false;
    }

}

2) 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.Folder;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.repository.FolderRepository;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class ProductService {

    private final FolderRepository folderRepository;    //관심상품에 폴더 추가 구현
    private final ProductRepository productRepository;  //의존성 주입
    private final UserRepository userRepository;  //의존성 주입
    private final JwtUtil jwtUtil;  //의존성 주입

    //관심상품 추가 할 때 토큰 보내기
    //등록하기
    @Transactional
    public ProductResponseDto createProduct(ProductRequestDto requestDto, HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        //JWT 안에 있는 정보를 담는 Claims 객체
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 추가 가능
        if (token != null) {
            //validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
            if (jwtUtil.validateToken(token)) {
                //true 라면, 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            //false 라면,
            } else {
                //해당 메시지 반환
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            //claims.getSubject(): 우리가 넣어두었던 username 가져오기
            //findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 요청받은 DTO 로 DB에 저장할 객체 만들기
            Product product = productRepository.saveAndFlush(new Product(requestDto, user.getId()));

            return new ProductResponseDto(product);
        //토큰이 null 이라면(Client 에게서 Token 이 넘어오지 않은 경우),
        } else {
            //null 을 반환
            return null;
        }
    }

    //관심상품 조회 할 때 토큰 보내기
    //조회하기 (Token 을 통해 검증한 다음, 검증된 사람만 조회 가능)
    @Transactional(readOnly = true)
    //page: page-1 한 숫자
    public Page<Product> getProducts(HttpServletRequest request,
                                     int page, int size, String sortBy, boolean isAsc) {
        // 페이징 처리
            //true 면 오름차순, false 면 내림자순
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        //direction: 오름차순/내림차순
        //sortBy: 어떤 것을 기준으로 오름차순/내림차순 판단할 것인지
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);  //Pageable --> 구글링

        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);  //의존성 주입 필요
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //사용자 권한 가져와서,
                //ADMIN 이면 전체 조회
                //USER 면 본인이 추가한 부분만 조회
            //user: 위의 User 객체에서 가져온 것
            //userRoleEnum: 가져온 권한을 담는 곳
            UserRoleEnum userRoleEnum = user.getRole();
            System.out.println("role = " + userRoleEnum);

            Page<Product> products;

            //사용자 권한이 USER 일 경우
            if (userRoleEnum == UserRoleEnum.USER) {
                //UserId 가 동일한 product 를 가져와서 products 에 담는다
                //id 와 pageable 을 통해 products 를 찾음
                products = productRepository.findAllByUserId(user.getId(), pageable);
            //사용자 권한이 ADMIN 일 경우
            } else {
                //상관없이 모든걸 다 가져온다(findAll())
                //pageable: 페이징 처리가 필요하므로 넣음
                products = productRepository.findAll(pageable);
            }
            //반환
            return products;

        } else {
            return null;
        }
    }

    //관심상품 최저가 추가 할 때 토큰 보내기
    @Transactional
    //Long id: product 의 id --> update 하기 위해서는 먼저 어떤 product 인지 확인하고, 그 product 의 myprice 를 update
    public Long updateProduct(Long id, ProductMypriceRequestDto requestDto, HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 최저가 업데이트 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //product 안에 userid 가 추가돼서, productid(id) 와 userid(user.getId()) 둘 다 필요
            //내가 가지고 온 productid 이면서, 그 product 가 동일한 userid 를 가지고 있는지까지 확인
                //즉, 현재 로그인한 user 가 선택한 product 가 맞는지 확인
            Product product = productRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
            );

            //동일하다면, update() 메서드 사용
            product.update(requestDto);

            //product 의 id 반환
            return product.getId();

        } else {
            return null;
        }
    }

    @Transactional      //설정해둔 myprice 값 보다 수정된 lprice 값이 작다면, '최저가' 표시가 뜨도록 js 에서 설정되어 있음
    public void updateBySearch(Long id, ItemDto itemDto) {
        //가지고 온 id 로 product 가 있는지 없는지 확인부터 한다
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 상품은 존재하지 않습니다.")
        );
        //그리고 itemDto 를 넣어서 update 를 실시
        product.updateByItemDto(itemDto);
    }

    //관심상품에 폴더 추가 구현
    @Transactional
    public Product addFolder(Long productId, Long folderId, HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 최저가 업데이트 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            //addFolder(Long productId, Long folderId, HttpServletRequest request)에 따라서, 각 인자를 확인

            // 1) 상품을 조회합니다.
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new NullPointerException("해당 상품 아이디가 존재하지 않습니다."));

            // 2) 관심상품을 조회합니다.
            Folder folder = folderRepository.findById(folderId)
                    .orElseThrow(() -> new NullPointerException("해당 폴더 아이디가 존재하지 않습니다."));

            // 3) 조회한 폴더와 관심상품이 모두 로그인한 회원의 소유인지 확인합니다.
            Long loginUserId = user.getId();
            //product 의 UserId 와 우리가 User 객체에서 가져온 UserId 가 동일한지 확인
            //folder 의 User 의 Id 와 우리가 User 객체에서 가져온 UserId 동일한지 확인
            if (!product.getUserId().equals(loginUserId) || !folder.getUser().getId().equals(loginUserId)) {
                //둘 中 하나라고 동일하지 않으면 예외 처리가 발생
                throw new IllegalArgumentException("회원님의 관심상품이 아니거나, 회원님의 폴더가 아닙니다~^^");
            }

            //같은 폴더 내에 상품 중복 생성 해결
            // 중복확인
            Optional<Product> overlapFolder = productRepository.findByIdAndFolderList_Id(product.getId(), folder.getId());

            //중복된 게 존재하면
            if(overlapFolder.isPresent()) {
                //예외 처리하기
                throw new IllegalArgumentException("중복된 폴더입니다.");
            }

            //중복된게 존재하지 않는다면
            // 4) 상품에 폴더를 추가합니다.
            product.addFolder(folder);

            return product;
        } else {
            return null;
        }
    }

}

3) UserService

package com.sparta.myselectshop.service;

import com.sparta.myselectshop.dto.LoginRequestDto;
import com.sparta.myselectshop.dto.SignupRequestDto;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.repository.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class UserService {

    //회원가입 구현

    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;  //의존성 주입(DI) --> jwtUtil.class 에서 @Component 로 빈을 등록했기때문에 가능
    private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";   //검증을 위한 Token? admin?

    @Transactional
    //SignupRequestDto 에서 가져온 username, password, email 을 확인
    public void signup(SignupRequestDto signupRequestDto) {
        String username = signupRequestDto.getUsername();
        String password = signupRequestDto.getPassword();
        String email = signupRequestDto.getEmail();

        // 회원 중복 확인
        //userRepository 에서 username 으로 실제 유저가 있는지 없는지 확인
        Optional<User> found = userRepository.findByUsername(username);  //userRepository 에 구현하기
        //중복된 유저가 존재한다면,
        if (found.isPresent()) {
            //해당 메시지 보내기
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // 사용자 ROLE(권한) 확인
        UserRoleEnum role = UserRoleEnum.USER;
        //SignupRequestDto 에서 admin 이 true 라면(admin 으로 로그인을 시도하려고 하는구나),
        if (signupRequestDto.isAdmin()) {
            //들어온 Token 값과 위의 검증을 위한 Token 값(위쪽에 AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC)이 일치하는지 확인
            if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
                //일치하지 않으면, 해당 메시지를 보낸다
                throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
            }
            //일치하면, user 를 admin 타입으로 바꾼다
            role = UserRoleEnum.ADMIN;
        }

        //가져온 username, password, email, role(UserRoleEnum)를 넣어서 저장(save)
        User user = new User(username, password, email, role);
        userRepository.save(user);
    }

    //로그인 구현

    @Transactional(readOnly = true)
    //로그인이 되면, LoginRequestDto 로 username, password 가 넘어온다
    public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
        String username = loginRequestDto.getUsername();
        String password = loginRequestDto.getPassword();

        // 사용자 확인
        //username 을 통해 확인.
        //있다면, User 객체에 담긴다
        User user = userRepository.findByUsername(username).orElseThrow(
                //없다면, 해당 메시지 보내기
                () -> new IllegalArgumentException("등록된 사용자가 없습니다.")
        );
        // 비밀번호 확인
        //User 객체에 들어있던 Password 와 가지고 온 Password(String password = loginRequestDto.getPassword() 에 있는) 가 일치하는지 확인
        //일치하지 않는다면, 해당 메시지 보내기
        if(!user.getPassword().equals(password)){
            throw  new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        //response 에 addHeader() 를 사용해서 Header 쪽에 값을 넣는데
            //AUTHORIZATION_HEADER: KEY 값
            //createToken(user.getUsername(), user.getRole()): Username(이름), Role(권한)을 넣어서 토큰을 만든다
                //위의 User 객체에서 유저 정보를 가져왔기 떄문에 사용 가능한 것
            //jwtUtil 를 사용하기 위해, 위에서 의존성 주입을 해줘야 함
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername(), user.getRole()));
    }
}

application.properties

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=

spring.thymeleaf.cache=false

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=
profile
개발자로 거듭나기!

0개의 댓글