스프링 프로젝트로 다양한 상품을 구매/판매하는 커머스 서비스를 개발하고자 한다. 구현할 기능들은 다음과 같다.
ERD는 다음과 같다.
CustomerController
@PostMapping("/signUp")
public ResponseEntity<String> signUp(@RequestBody CustomerSignUpForm customerSignUpForm) {
customerService.signUp(customerSignUpForm.toServiceForm());
return ResponseEntity.ok(SIGNUP_SUCCESS);
}
CustomerService
@Override
public void signUp(CustomerSignUpServiceForm customerSignUpServiceForm) {
Optional<Customer> optionalCustomer =
customerRepository.findByEmail(customerSignUpServiceForm.getEmail());
optionalCustomer.ifPresent(it -> {
throw new CustomException(ErrorCode.ALREADY_SIGNUP_EMAIL);
});
if (!isValidPassword(customerSignUpServiceForm.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD);
}
if (!isValidPhone(customerSignUpServiceForm.getPhone())) {
throw new CustomException(ErrorCode.INVALID_PHONE);
}
String encPassword = BCrypt.hashpw(customerSignUpServiceForm.getPassword(), BCrypt.gensalt());
Customer customer = Customer.builder()
.email(customerSignUpServiceForm.getEmail())
.password(encPassword)
.phone(customerSignUpServiceForm.getPhone())
.address(customerSignUpServiceForm.getAddress())
.point(0)
.deletedYn(false)
.build();
customerRepository.save(customer);
}
Controller에서 받는 dto와 Service에서 사용하는 dto를 분리시켰다. 회원가입시 이미 등록된 이메일이 있는지 확인한다. 비밀번호 유효성 검사, 연락처 형식 유효성 검사를 하고, 비밀번호는 암호화해서 데이터베이스에 저장한다. 기본 적립금은 0원으로 저장된다.
CustomerController
@PostMapping("/signIn")
public ResponseEntity<String> signIn(@RequestBody CustomerSignInForm customerSignInForm) {
return ResponseEntity.ok(customerService.signIn(customerSignInForm.toServiceForm()));
}
CustomerService
@Override
public String signIn(CustomerSignInServiceForm form) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
Optional<Customer> optionalCustomer =
customerRepository.findByEmail(form.getEmail());
if (optionalCustomer.isEmpty() || !encoder.matches(form.getPassword(),
optionalCustomer.get().getPassword())) {
throw new CustomException(ErrorCode.LOGIN_CHECK_FAIL);
}
Customer customer = optionalCustomer.get();
return provider.createToken(customer.getEmail(), customer.getId(), UserType.CUSTOMER);
}
JwtTokenProvider
public String createToken(String email, Long id, UserType userType) {
Claims claims = Jwts.claims().setSubject(email).setId(id.toString());
claims.put(USERTYPE, userType.toString());
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + TOKEN_EXPIRE))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
String username = claims.getSubject();
String userType = claims.get("userType", String.class);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + userType));
UserDetails userDetails = new User(username, "", authorities);
return new UsernamePasswordAuthenticationToken(userDetails, "",
userDetails.getAuthorities());
}
public boolean validateToken(String token) {
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claimsJws.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
public UserVo getUserVo(String token) {
Claims claims = Jwts.parser().setSigningKey(secretKey)
.parseClaimsJws(token.substring(TOKEN_PREFIX.length())).getBody();
return new UserVo(Long.parseLong(Objects.requireNonNull(claims.getId())),
claims.getSubject());
}
해당 이메일을 가진 Customer 데이터베이스를 조회하고, 암호화된 비밀번호를 비교한다. 해당 이메일을 가진 Customer가 존재하지 않거나 비밀번호가 일치하지 않을 경우, LOGIN_CHECK_FAIL 에러가 난다. 로그인에 성공하면 Customer id, email, Customer Role 정보를 가진 JWT 토큰이 발급된다. Seller로 로그인한 경우에는 JWT 토큰에 Seller Role 정보가 담긴다.
ProductController
@PostMapping
@PreAuthorize("hasRole('SELLER')")
public ResponseEntity<String> registerProduct(
@RequestHeader(name = TOKEN_HEADER) String token,
@RequestBody ProductRegisterForm productRegisterForm) {
productService.register(token, productRegisterForm.toServiceForm());
return ResponseEntity.ok(REGISTER_PRODUCT_SUCCESS);
}
ProductService
@Override
public void register(String token, ProductRegisterServiceForm form) {
if (isStringEmpty(form.getName()) || form.getPrice() <= 0 || isStringEmpty(
form.getDescription()) || form.getQuantity() <= 0) {
throw new CustomException(ErrorCode.INVALID_PRODUCT_REGISTER);
}
UserVo vo = provider.getUserVo(token);
Seller seller = sellerRepository.findByEmail(vo.getEmail())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
Category category = categoryRepository.findById(form.getCategoryId())
.orElseThrow(() -> new CustomException(ErrorCode.CATEGORY_NOT_FOUND));
Product product = Product.builder()
.category(category)
.seller(seller)
.name(form.getName())
.price(form.getPrice())
.description(form.getDescription())
.quantity(form.getQuantity())
.image(form.getImage())
.orderedCnt(0)
.deletedYn(false)
.build();
productRepository.save(product);
}
Seller인 사용자가 상품 등록한다. Customer로 로그인해서 발급 받은 JWT 토큰을 가지고 상품 등록을 하면 403 응답이 발생한다. 상품 등록시 상품 이름이 비어있거나, 상품 가격이 0 이하이거나, 상품 설명이 비어있거나, 상품 수량이 0 이하이거나, 카테고리가 존재하지 않으면 Custom Error가 발생한다. 기본 주문 횟수는 0으로 저장된다.