diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9b0a02 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Spring-JPA-study +D-Coding 백엔드 스터디 + +## 1주차 과제 제출(23-01-21) +스프링 입문 강의 정리 + +## 2~4주차 과제 +- 간단한 ‘당근마켓’ 벡엔드 구현해보기 +### 요구 사항 + +- 회원가입/로그인 기능 +- 상품등록 기능 +- 상품페이지 기능 +- 마이 페이지(나의 당근 페이지) 기능 + +### 제한 사항 + +- Spring 또는 Spring Boot Framework를 사용해야합니다. +- JPA를 사용해야합니다. (Spring data jpa 활용 가능) +- 인증/인가 방식은 JWT & Spring Security 를 활용해야합니다. + +### 2주차 + +- 요구사항대로 당근마켓 ERD 그려보기 +- 요구사항 API 구현 (가능한 만큼) +- jwt, security 이용하여 로그인, 회원가입 기능 구현 + + +![dcoding-erd](https://user-images.githubusercontent.com/85729858/215239770-a2ac0b3a-cd5b-443d-b7a3-d026ce9ae5c5.png) + + +### 3주차 + +- 피드백 반영하여 카테고리를 enum으로 처리하도록 하고, createdDate와 updatedDate추가 +- 로그인 기능 리펙토링 +- 회원가입 기능 추가 +- 상품 관련 엔티티, 레포지토리, 서비스 추가 +- 상태, 카테고리 enum추가 + +![image](https://user-images.githubusercontent.com/85729858/216071539-42dc3af6-452e-464f-9a68-c52364c5fc3f.png) + + +### 4주차 + +- 상품등록 기능 구현 +- 상품조회 기능 구현 +- 좋아요 등록, 삭제 기능 구현 +- 유저 프로필 수정 기능 추가 diff --git a/build.gradle b/build.gradle index 15b77ef..ee53417 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.0.1' + id 'org.springframework.boot' version '2.7.8' id 'io.spring.dependency-management' version '1.1.0' } group = 'com.dku' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { @@ -27,6 +27,15 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + + runtimeOnly 'com.h2database:h2' + //implementation 'com.auth0:java-jwt:3.18.2' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java index ef164c9..1bef475 100644 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ b/src/main/java/com/dku/springstudy/SpringStudyApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SpringStudyApplication { diff --git a/src/main/java/com/dku/springstudy/config/SecurityConfig.java b/src/main/java/com/dku/springstudy/config/SecurityConfig.java new file mode 100644 index 0000000..65e9db0 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package com.dku.springstudy.config; + +import com.dku.springstudy.jwt.JwtAuthenticationFilter; +import com.dku.springstudy.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; + +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .httpBasic().disable() + .formLogin().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/v1/**").permitAll() + //.antMatchers("/api/v1/").hasRole("USER") + //.anyRequest().authenticated() + .anyRequest().permitAll() + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/BaseTimeEntity.java b/src/main/java/com/dku/springstudy/domain/BaseTimeEntity.java new file mode 100644 index 0000000..673b74c --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/BaseTimeEntity.java @@ -0,0 +1,23 @@ +package com.dku.springstudy.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +public class BaseTimeEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime lastModifiedDate; +} diff --git a/src/main/java/com/dku/springstudy/domain/like/Likes.java b/src/main/java/com/dku/springstudy/domain/like/Likes.java new file mode 100644 index 0000000..25c4216 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/like/Likes.java @@ -0,0 +1,36 @@ +package com.dku.springstudy.domain.like; + +import com.dku.springstudy.domain.BaseTimeEntity; +import com.dku.springstudy.domain.product.Product; +import com.dku.springstudy.domain.user.User; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@NoArgsConstructor +@Getter +public class Likes extends BaseTimeEntity { + @Id + @GeneratedValue + private Long id; + private Long productInfo; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Builder + public Likes(Long productInfo, User user, Product product) { + this.productInfo = productInfo; + this.user = user; + this.product = product; + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/like/controller/LikeController.java b/src/main/java/com/dku/springstudy/domain/like/controller/LikeController.java new file mode 100644 index 0000000..dad9713 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/like/controller/LikeController.java @@ -0,0 +1,45 @@ +package com.dku.springstudy.domain.like.controller; + +import com.dku.springstudy.domain.like.Likes; +import com.dku.springstudy.domain.like.service.LikeService; +import com.dku.springstudy.domain.product.Product; +import com.dku.springstudy.domain.product.service.ProductService; +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/like") +public class LikeController { + private final LikeService likeService; + private final UserService userService; + private final ProductService productService; + + @PostMapping("/{productId}/addLike") + public ResponseEntity addLike(@AuthenticationPrincipal UserDetails userDetails, + @PathVariable("productId") Long productId) { + User user = userService.findUser(userDetails.getUsername()); + Product product = productService.findProduct(productId); + + likeService.addLike(new Likes(productId, user, product)); + productService.addLikeCount(product); + + return ResponseEntity.status(HttpStatus.OK).body("success"); + } + + @DeleteMapping("/{productId}/deleteLike") + public ResponseEntity deleteLike(@AuthenticationPrincipal UserDetails userDetails, + @PathVariable("productId") Long productId) { + User user = userService.findUser(userDetails.getUsername()); + likeService.deleteLike(productId, user.getId()); + productService.deleteLikeCount(productService.findProduct(productId)); + + return ResponseEntity.status(HttpStatus.OK).body("success"); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/like/repository/LikeRepository.java b/src/main/java/com/dku/springstudy/domain/like/repository/LikeRepository.java new file mode 100644 index 0000000..b98b17e --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/like/repository/LikeRepository.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.domain.like.repository; + +import com.dku.springstudy.domain.like.Likes; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeRepository extends JpaRepository { + void deleteByProductIdAndUserId(Long productId, Long userId); + // void deleteByProductId(productId); +} diff --git a/src/main/java/com/dku/springstudy/domain/like/service/LikeService.java b/src/main/java/com/dku/springstudy/domain/like/service/LikeService.java new file mode 100644 index 0000000..f11c97a --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/like/service/LikeService.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.domain.like.service; + +import com.dku.springstudy.domain.like.Likes; +import com.dku.springstudy.domain.like.repository.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LikeService { + private final LikeRepository likeRepository; + + @Transactional + public Likes addLike(Likes likes){ + return likeRepository.save(likes); + } + + @Transactional + public void deleteLike(Long productId, Long userId){ + likeRepository.deleteByProductIdAndUserId(productId, userId); + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/product/Category.java b/src/main/java/com/dku/springstudy/domain/product/Category.java new file mode 100644 index 0000000..7d1abcd --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/Category.java @@ -0,0 +1,36 @@ +package com.dku.springstudy.domain.product; + +import lombok.Getter; + +@Getter +public enum Category { + /* + ‘디지털기기’, ‘생활가전’, ‘가구/인테리어’, ‘유아동’, ‘생활/가공식품’, + ‘유아도서’, ‘스포츠/레저’, ‘여성잡화’, ‘여성의류’, ‘남성패션/잡화’, + ‘게임/취미’, ‘뷰티/미용’, ‘반려동물용품’, ‘도서/티켓/음반’, ‘식물’, + ‘기타 중고물품’, ‘중고차’ + */ + DIGITAL_DEVICE("디지털기기"), + HOME_ELECTRONIC("생활가전"), + FURNITURE_INTERIOR("가구/인테리어"), + CHILD("유아동"), + LIVING_PROCESSED_FOODS("생활/가공식품"), + CHILD_BOOK("유아도서"), + SPORTS_LEISURE("스포츠/레저"), + WOMAN_GOODS("여성잡화"), + WOMAN_CLOTHES("여성의류"), + MAN_FASHION_GOODS("남성패션/잡화"), + GAME_HOBBY("게임/취미"), + BEAUTY("뷰티/미용"), + PET_GOODS("반려동물용품"), + BOOK_TICKET_RECORD("도서/티켓/음반"), + PLANT("식물"), + USED_ETC("기타 중고물품"), + USED_CAR("중고차"); + + private final String korCategory; + + Category(String korCategory) { + this.korCategory = korCategory; + } +} diff --git a/src/main/java/com/dku/springstudy/domain/product/Product.java b/src/main/java/com/dku/springstudy/domain/product/Product.java new file mode 100644 index 0000000..5ebf9ab --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/Product.java @@ -0,0 +1,61 @@ +package com.dku.springstudy.domain.product; + +import com.dku.springstudy.domain.BaseTimeEntity; +import com.dku.springstudy.domain.user.User; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +public class Product extends BaseTimeEntity { + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String productName; + private String productImgUrl; + private Integer cost; + private String contents; + @ColumnDefault("0") + private Integer likeCount; + + @Enumerated(EnumType.STRING) + private ProductStatus status; + + @Enumerated(EnumType.STRING) + private Category category; + + @Builder + public Product(String productName, Category category, Integer cost, String contents, + String productImgUrl, User user, ProductStatus productStatus, Integer likeCount) { + this.productName = productName; + this.category = category; + this.cost = cost; + this.contents = contents; + this.likeCount = likeCount; + this.productImgUrl = productImgUrl; + this.status = productStatus; + this.user = user; + } + + public void addLikeCount() { + this.likeCount++; + } + + public void deleteLikeCount() { + this.likeCount--; + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/product/ProductStatus.java b/src/main/java/com/dku/springstudy/domain/product/ProductStatus.java new file mode 100644 index 0000000..81dd814 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/ProductStatus.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.domain.product; + +import lombok.Getter; + +@Getter +public enum ProductStatus { + ON_SALE("판매중"), SOLD_OUT("판매완료"), RESERVED("예약중"); + + private final String korStatus; + + ProductStatus(String korStatus) { + this.korStatus = korStatus; + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/product/controller/ProductController.java b/src/main/java/com/dku/springstudy/domain/product/controller/ProductController.java new file mode 100644 index 0000000..64d3907 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/controller/ProductController.java @@ -0,0 +1,46 @@ +package com.dku.springstudy.domain.product.controller; + + +import com.dku.springstudy.domain.product.dto.ProductListResponseDTO; +import com.dku.springstudy.domain.product.dto.ProductRegisterRequestDTO; +import com.dku.springstudy.domain.product.dto.ProductResponseDTO; +import com.dku.springstudy.domain.product.service.ProductService; +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/product") +public class ProductController { + private final ProductService productService; + private final UserService userService; + + @PostMapping("/create") + public ResponseEntity create(@Valid @RequestBody ProductRegisterRequestDTO requestDTO, + @AuthenticationPrincipal UserDetails userDetails) { + User findUser = userService.findUser(userDetails.getUsername()); + Long id = productService.save(requestDTO, findUser); + + return ResponseEntity.status(HttpStatus.OK).body(id); + } + + @GetMapping("{productId}") + public ResponseEntity getProduct(@AuthenticationPrincipal UserDetails userDetails, + @PathVariable("productId") Long productId) { + User loginUser = userService.findUser(userDetails.getUsername()); + ProductResponseDTO view = productService.findByProductId(productId); + List otherList = productService.findByUserId(loginUser.getId()); + + return new ResponseEntity<>(new ProductListResponseDTO(view, otherList), HttpStatus.OK); + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/product/dto/ProductListResponseDTO.java b/src/main/java/com/dku/springstudy/domain/product/dto/ProductListResponseDTO.java new file mode 100644 index 0000000..c6dc473 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/dto/ProductListResponseDTO.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.domain.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ProductListResponseDTO { + private ProductResponseDTO selectProduct; + private List otherProductList; +} diff --git a/src/main/java/com/dku/springstudy/domain/product/dto/ProductRegisterRequestDTO.java b/src/main/java/com/dku/springstudy/domain/product/dto/ProductRegisterRequestDTO.java new file mode 100644 index 0000000..cc9c4d1 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/dto/ProductRegisterRequestDTO.java @@ -0,0 +1,46 @@ +package com.dku.springstudy.domain.product.dto; + +import com.dku.springstudy.domain.product.Category; +import com.dku.springstudy.domain.product.Product; +import com.dku.springstudy.domain.product.ProductStatus; +import com.dku.springstudy.domain.user.User; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@NoArgsConstructor +public class ProductRegisterRequestDTO { + @NonNull + private String productName; + @NonNull + private Category category; + @NonNull + private Integer cost; + @NonNull + private String contents; + @NonNull + private String productImgUrl; + + @Builder + public ProductRegisterRequestDTO(String productName, Category category, Integer cost, String contents, String productImgUrl) { + this.productName = productName; + this.category = category; + this.cost = cost; + this.contents = contents; + this.productImgUrl = productImgUrl; + } + + public Product toEntity(User user) { + return Product.builder() + .productName(this.getProductName()) + .category(this.getCategory()) + .cost(this.getCost()) + .contents(this.getContents()) + .productImgUrl(this.getProductImgUrl()) + .productStatus(ProductStatus.ON_SALE) + .user(user) + .build(); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/product/dto/ProductResponseDTO.java b/src/main/java/com/dku/springstudy/domain/product/dto/ProductResponseDTO.java new file mode 100644 index 0000000..dc0c17b --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/dto/ProductResponseDTO.java @@ -0,0 +1,28 @@ +package com.dku.springstudy.domain.product.dto; + +import com.dku.springstudy.domain.product.Product; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ProductResponseDTO { + private Long productId; + private String productName; + private Integer cost; + private String contents; + private Long userId; + //like추가 + private Integer likeCount; + + public ProductResponseDTO(Product product) { + this.productId = product.getId(); + this.productName = product.getProductName(); + this.cost = product.getCost(); + this.contents = product.getContents(); + this.userId = product.getUser().getId(); + this.likeCount = product.getLikeCount(); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/product/repository/ProductRepository.java b/src/main/java/com/dku/springstudy/domain/product/repository/ProductRepository.java new file mode 100644 index 0000000..c28287e --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/repository/ProductRepository.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.domain.product.repository; + +import com.dku.springstudy.domain.product.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ProductRepository extends JpaRepository { + Optional> findProductByUserId(Long userId); +} diff --git a/src/main/java/com/dku/springstudy/domain/product/service/ProductService.java b/src/main/java/com/dku/springstudy/domain/product/service/ProductService.java new file mode 100644 index 0000000..eb18f79 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/service/ProductService.java @@ -0,0 +1,59 @@ +package com.dku.springstudy.domain.product.service; + +import com.dku.springstudy.domain.product.Product; +import com.dku.springstudy.domain.product.dto.ProductRegisterRequestDTO; +import com.dku.springstudy.domain.product.dto.ProductResponseDTO; +import com.dku.springstudy.domain.product.repository.ProductRepository; +import com.dku.springstudy.domain.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ProductService { + private final ProductRepository productRepository; + + @Transactional + public Long save(ProductRegisterRequestDTO requestDTO, User user) { + Product product = requestDTO.toEntity(user); + productRepository.save(product); + + return product.getId(); + } + + public ProductResponseDTO findByProductId(Long productId) { + Product product = productRepository.findById(productId).orElseThrow(); + + return new ProductResponseDTO(product); + } + + public List findByUserId(Long userId) { + List products = productRepository.findProductByUserId(userId).orElseGet(ArrayList::new); + + return products.stream() + .map(ProductResponseDTO::new) + .collect(Collectors.toList()); + } + + public Product findProduct(Long id) { + return productRepository.findById(id).orElseThrow(() -> new UsernameNotFoundException("no id")); + } + + @Transactional + public void addLikeCount(Product product) { + product.addLikeCount(); + } + + @Transactional + public void deleteLikeCount(Product product) { + product.deleteLikeCount(); + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/user/Role.java b/src/main/java/com/dku/springstudy/domain/user/Role.java new file mode 100644 index 0000000..a7c98a7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/Role.java @@ -0,0 +1,5 @@ +package com.dku.springstudy.domain.user; + +public enum Role { + USER, ADMIN +} diff --git a/src/main/java/com/dku/springstudy/domain/user/User.java b/src/main/java/com/dku/springstudy/domain/user/User.java new file mode 100644 index 0000000..73cdba1 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/User.java @@ -0,0 +1,55 @@ +package com.dku.springstudy.domain.user; + +import com.dku.springstudy.domain.BaseTimeEntity; +import com.dku.springstudy.domain.like.Likes; +import com.dku.springstudy.domain.product.Product; +import lombok.*; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseTimeEntity { + + @Id + @GeneratedValue + private Long id; + + @OneToMany(mappedBy = "user") + private List products = new ArrayList<>(); + + @OneToMany(mappedBy = "user") + private List likes = new ArrayList<>(); + + private String email; + private String password; + private String username; + private String phoneNumber; + private String nickname; + private String profileImgUrl; + + @Enumerated(EnumType.STRING) + private Role role; + + @Builder + public User(String email, String password, String username, String phoneNumber, String nickname, String profileImgUrl) { + this.email = email; + this.password = password; + this.username = username; + this.phoneNumber = phoneNumber; + this.nickname = nickname; + this.profileImgUrl = profileImgUrl; + this.role = Role.USER; + } + + public void updateNickName(String nickname) { + this.nickname = nickname; + } + + public void updateProfileImg(String profileImgUrl) { + this.profileImgUrl = profileImgUrl; + } +} diff --git a/src/main/java/com/dku/springstudy/domain/user/controller/UserController.java b/src/main/java/com/dku/springstudy/domain/user/controller/UserController.java new file mode 100644 index 0000000..1ef4846 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/controller/UserController.java @@ -0,0 +1,50 @@ +package com.dku.springstudy.domain.user.controller; + + +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.dto.LoginRequestDto; +import com.dku.springstudy.domain.user.dto.SignUpRequestDTO; +import com.dku.springstudy.domain.user.dto.UserProfileUpdateRequestDTO; +import com.dku.springstudy.domain.user.service.UserService; +import com.dku.springstudy.jwt.TokenDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class UserController { + private final UserService userService; + + @PostMapping("/signup") + public ResponseEntity signUp(@Valid @RequestBody SignUpRequestDTO requestDTO) { + userService.signUp(requestDTO); + + return ResponseEntity.status(HttpStatus.CREATED).body(null); + } + + @PostMapping("/login") + public TokenDto login(@Valid @RequestBody LoginRequestDto loginRequestDto) { + String email = loginRequestDto.getEmail(); + String password = loginRequestDto.getPassword(); + return userService.login(email, password); + } + + @PostMapping("/profile") + public ResponseEntity updateProfile(@AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody UserProfileUpdateRequestDTO requestDTO) { + User loginUser = userService.findUser(userDetails.getUsername()); + Long id = userService.updateProfile(loginUser, requestDTO); + + return ResponseEntity.status(HttpStatus.OK).body(id); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/user/dto/LoginRequestDto.java b/src/main/java/com/dku/springstudy/domain/user/dto/LoginRequestDto.java new file mode 100644 index 0000000..2a5b8ec --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/dto/LoginRequestDto.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.domain.user.dto; + +import lombok.Data; + +@Data +public class LoginRequestDto { + private String email; + private String password; +} diff --git a/src/main/java/com/dku/springstudy/domain/user/dto/SignUpRequestDTO.java b/src/main/java/com/dku/springstudy/domain/user/dto/SignUpRequestDTO.java new file mode 100644 index 0000000..78c44ba --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/dto/SignUpRequestDTO.java @@ -0,0 +1,31 @@ +package com.dku.springstudy.domain.user.dto; + +import com.dku.springstudy.domain.user.User; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@NoArgsConstructor +public class SignUpRequestDTO { + @NonNull + private String email; + @NonNull + private String password; + @NonNull + private String username; + @NonNull + private String phoneNumber; + @NonNull + private String nickname; + + public User toEntity() { + return User.builder() + .email(email) + .password(password) + .username(username) + .phoneNumber(phoneNumber) + .nickname(nickname) + .build(); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/user/dto/UserProfileUpdateRequestDTO.java b/src/main/java/com/dku/springstudy/domain/user/dto/UserProfileUpdateRequestDTO.java new file mode 100644 index 0000000..0fd9a93 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/dto/UserProfileUpdateRequestDTO.java @@ -0,0 +1,10 @@ +package com.dku.springstudy.domain.user.dto; + +import lombok.Data; + +@Data +public class UserProfileUpdateRequestDTO { + private String nickname; + private String profileImgUrl; + +} diff --git a/src/main/java/com/dku/springstudy/domain/user/repository/UserRepository.java b/src/main/java/com/dku/springstudy/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..304784f --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.dku.springstudy.domain.user.repository; + +import com.dku.springstudy.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/dku/springstudy/domain/user/service/CustomUserDetailsService.java b/src/main/java/com/dku/springstudy/domain/user/service/CustomUserDetailsService.java new file mode 100644 index 0000000..6cb7d4a --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/service/CustomUserDetailsService.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.domain.user.service; + +import com.dku.springstudy.jwt.SecurityUserDetails; +import com.dku.springstudy.domain.user.User; + +import com.dku.springstudy.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("해당하는 유저를 찾을 수 없습니다.")); + return new SecurityUserDetails(user); + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/user/service/UserService.java b/src/main/java/com/dku/springstudy/domain/user/service/UserService.java new file mode 100644 index 0000000..cb54434 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/service/UserService.java @@ -0,0 +1,68 @@ +package com.dku.springstudy.domain.user.service; + +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.dto.SignUpRequestDTO; +import com.dku.springstudy.domain.user.dto.UserProfileUpdateRequestDTO; +import com.dku.springstudy.domain.user.repository.UserRepository; +import com.dku.springstudy.jwt.JwtTokenProvider; +import com.dku.springstudy.jwt.TokenDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.validation.Valid; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class UserService { + private final AuthenticationManagerBuilder authenticationManagerBuilder; + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public TokenDto login(String email, String password) { + // 1. Login ID/PW 를 기반으로 Authentication 객체 생성 + // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password); + + // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분 + // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행 + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + + // 3. 인증 정보를 기반으로 JWT 토큰 생성 + + return jwtTokenProvider.generateToken(authentication); + } + + @Transactional + public Long signUp(@Valid SignUpRequestDTO requestDTO) { + String bcryptPassword = passwordEncoder.encode(requestDTO.getPassword()); + requestDTO.setPassword(bcryptPassword); + User user = requestDTO.toEntity(); + userRepository.save(user); + + return user.getId(); + } + + public User findUser(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("No user")); + } + + @Transactional + public Long updateProfile(User user, UserProfileUpdateRequestDTO requestDTO) { + user.updateNickName(requestDTO.getNickname()); + user.updateProfileImg(requestDTO.getProfileImgUrl()); + userRepository.save(user); + + return user.getId(); + } + +} diff --git a/src/main/java/com/dku/springstudy/jwt/JwtAuthenticationFilter.java b/src/main/java/com/dku/springstudy/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9942825 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,42 @@ +package com.dku.springstudy.jwt; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + // 1. Request Header 에서 JWT 토큰 추출 + String token = resolveToken((HttpServletRequest) request); + + // 2. validateToken 으로 토큰 유효성 검사 + if (token != null && jwtTokenProvider.validateToken(token)) { + // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장 + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + } + // Request Header 에서 토큰 정보 추출 + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/dku/springstudy/jwt/JwtTokenProvider.java b/src/main/java/com/dku/springstudy/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..16f4a44 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/JwtTokenProvider.java @@ -0,0 +1,110 @@ +package com.dku.springstudy.jwt; + + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + + +@Slf4j +@Component +public class JwtTokenProvider { + + private final Key key; + + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public TokenDto generateToken(Authentication authentication) { + // 권한 가져오기 + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + // Access Token 생성 + Date accessTokenExpiresIn = new Date(now + 86400000); + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim("auth", authorities) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + // Refresh Token 생성 + //1일: 24 * 60 * 60 * 1000 = 86400000 + String refreshToken = Jwts.builder() + .setExpiration(new Date(now + 86400000)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + return TokenDto.builder() + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드 + public Authentication getAuthentication(String accessToken) { + // 토큰 복호화 + Claims claims = parseClaims(accessToken); + + if (claims.get("auth") == null) { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + // 클레임에서 권한 정보 가져오기 + Collection authorities = + Arrays.stream(claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + // UserDetails 객체를 만들어서 Authentication 리턴 + UserDetails principal = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + // 토큰 정보를 검증하는 메서드 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/src/main/java/com/dku/springstudy/jwt/SecurityUserDetails.java b/src/main/java/com/dku/springstudy/jwt/SecurityUserDetails.java new file mode 100644 index 0000000..6ec4fa7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/SecurityUserDetails.java @@ -0,0 +1,62 @@ +package com.dku.springstudy.jwt; + + +import com.dku.springstudy.domain.user.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + + +public class SecurityUserDetails implements UserDetails { + private User user; + + public SecurityUserDetails(User user) { + this.user = user; + } + + public User getUser() { + return user; + } + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + String roles = user.getRole().toString(); + for (String role : roles.split(",")) { + authorities.add(() -> role); + } + return authorities; + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/dku/springstudy/jwt/TokenDto.java b/src/main/java/com/dku/springstudy/jwt/TokenDto.java new file mode 100644 index 0000000..0737d70 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/TokenDto.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.jwt; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@AllArgsConstructor +public class TokenDto { + //grantType은 JWT 대한 인증 타입 + private String grantType; + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/dku/springstudy/testInit.java b/src/main/java/com/dku/springstudy/testInit.java new file mode 100644 index 0000000..1b44c75 --- /dev/null +++ b/src/main/java/com/dku/springstudy/testInit.java @@ -0,0 +1,56 @@ +package com.dku.springstudy; + +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.dto.SignUpRequestDTO; +import com.dku.springstudy.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import javax.persistence.EntityManager; + +@Component +@RequiredArgsConstructor +public class testInit { + private final InitService initService; + + @PostConstruct + public void init() { + initService.dbInit1(); + + } + + @Component + @Transactional + @RequiredArgsConstructor + static class InitService { + private final UserRepository userRepository; + + private final EntityManager em; + private final PasswordEncoder passwordEncoder; + + public void dbInit1() { + + User user = createUser("qwe", "123", "username", "01012341234", "usernick"); + em.persist(user); + + } + + private User createUser(String email, String password, String username, String phoneNumber, String nickname) { + SignUpRequestDTO requestDTO = new SignUpRequestDTO(); + String bcryptPassword = passwordEncoder.encode(password); + + requestDTO.setEmail(email); + requestDTO.setPassword(bcryptPassword); + requestDTO.setUsername(username); + requestDTO.setPhoneNumber(phoneNumber); + requestDTO.setNickname(nickname); + User user = requestDTO.toEntity(); + + return user; + } + + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..c822a97 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:tcp://localhost/~/test + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + #show_sql: true + format_sql: true +logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace + +jwt: + secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa