Skip to content

Commit

Permalink
Merge pull request #96 from kakao-tech-campus-2nd-step3/Master
Browse files Browse the repository at this point in the history
9조 BE 코드리뷰 5회차
  • Loading branch information
leaf-upper authored Nov 10, 2024
2 parents 63506b1 + f0bd608 commit ddf1418
Show file tree
Hide file tree
Showing 44 changed files with 1,873 additions and 216 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies {
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Spring docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/helpmeCookies/Step3Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Step3Application {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.helpmeCookies.global.ApiResponse;

import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
@RequiredArgsConstructor
public class ApiResponse<T> {

private final int code;
private final String message;
private T data;

public static<T> ApiResponse<T> success(SuccessCode successCode, T data) {
return new ApiResponse<T>(successCode.getCode(), successCode.getMessage(),data);
}

public static ApiResponse<Void> success(SuccessCode successCode) {
return new ApiResponse<>(successCode.getCode(), successCode.getMessage(),null);
}

public static ApiResponse<Void> error(HttpStatus errorCode, String message) {
return new ApiResponse<>(errorCode.value(), message,null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.helpmeCookies.global.ApiResponse;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@AllArgsConstructor
@Getter
public enum SuccessCode {
OK(200, "OK"),
CREATED_SUCCESS(201, "저장에 성공했습니다"),
NO_CONTENT(204, "삭제에 성공했습니다.");

private final int code;
private final String message;

}
12 changes: 12 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/ProPertyConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.helpmeCookies.global.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import com.helpmeCookies.global.jwt.JwtProperties;

@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class ProPertyConfig {
}
40 changes: 40 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.helpmeCookies.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}



}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.helpmeCookies.global.exception;

import com.helpmeCookies.global.ApiResponse.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

Expand All @@ -10,8 +13,8 @@
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public String handleResourceNotFoundException() {
return "해당 리소스를 찾을 수 없습니다.";
public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(ResourceNotFoundException e) {
return ResponseEntity.badRequest().body(ApiResponse.error(HttpStatus.BAD_REQUEST, e.getMessage()));
}

@ExceptionHandler(DuplicateRequestException.class)
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.helpmeCookies.global.jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Component
@ConfigurationProperties(prefix = "jwt")
@Getter
@Setter
public class JwtProperties {
private String secret;
private long accessTokenExpireTime;
private long refreshTokenExpireTime;

public JwtProperties() {
}
}
11 changes: 6 additions & 5 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class JwtProvider implements InitializingBean {
private String secret = "4099a46b-39db-4860-a61b-2ae76ea24c43";
private long accessTokenExpireTime = 1800000; // 30 minutes;
private long refreshTokenExpireTime = 259200000; // 3 days;

private final JwtProperties jwtProperties;
private Key secretKey;
private static final String ROLE = "role";
private static final String IS_ACCESS_TOKEN = "isAccessToken";
Expand Down Expand Up @@ -92,7 +93,7 @@ private JwtUser claimsToJwtUser(Claims claims) {
}

private String generateToken(JwtUser jwtUser, boolean isAccessToken) {
long expireTime = isAccessToken ? accessTokenExpireTime : refreshTokenExpireTime;
long expireTime = isAccessToken ? jwtProperties.getAccessTokenExpireTime() : jwtProperties.getRefreshTokenExpireTime();
Date expireDate = new Date(System.currentTimeMillis() + expireTime);
return Jwts.builder()
.signWith(secretKey)
Expand All @@ -112,6 +113,6 @@ private Claims extractClaims(String rawToken) {

@Override
public void afterPropertiesSet() {
secretKey = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
secretKey = new SecretKeySpec(jwtProperties.getSecret().getBytes(), SignatureAlgorithm.HS256.getJcaName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;

private static final String AUTHORIZATION_HEADER = "Authorization";

@Override
Expand All @@ -46,6 +45,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtUser, null,
jwtUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
log.info("유효하지 않은 토큰 발생");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "토큰이 유효하지 않습니다.");
}

filterChain.doFilter(request, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/actuator/**",
"/v1/**",
"swagger-ui/**",
"/test/signup"
"/test/signup",
"/v1/artists/**"
).permitAll()
.anyRequest().authenticated()
).exceptionHandling((exception) -> exception
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.helpmeCookies.product.controller;

import com.helpmeCookies.global.ApiResponse.ApiResponse;
import com.helpmeCookies.global.ApiResponse.SuccessCode;
import com.helpmeCookies.global.jwt.JwtUser;
import com.helpmeCookies.product.dto.ImageUpload;
import static com.helpmeCookies.product.util.SortUtil.convertProductSort;

Expand All @@ -10,11 +13,18 @@
import com.helpmeCookies.product.dto.ProductResponse;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.service.ProductImageService;
import com.helpmeCookies.product.service.ProductLikeService;
import com.helpmeCookies.product.service.ProductService;
import com.helpmeCookies.product.util.ProductSort;
import com.helpmeCookies.review.dto.ReviewResponse;
import com.helpmeCookies.review.service.ReviewService;
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.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

Expand All @@ -27,6 +37,13 @@ public class ProductController implements ProductApiDocs {

private final ProductService productService;
private final ProductImageService productImageService;
private final ReviewService reviewService;
private final ProductLikeService productLikeService;

@PostMapping("/successTest")
public ResponseEntity<ApiResponse<Void>> saveTest() {
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK));
}

@PostMapping
public ResponseEntity<Void> saveProduct(@RequestBody ProductRequest productRequest) {
Expand Down Expand Up @@ -82,4 +99,36 @@ public ResponseEntity<ProductPage.Paging> getProductsByPage(

return ResponseEntity.ok(productService.getProductsByPage(query, pageable));
}

@GetMapping("/feed")
public ResponseEntity<ProductPage.Paging> getProductsWithRandomPaging(
@RequestParam(name = "size", required = false, defaultValue = "20") int size
) {
Pageable pageable = PageRequest.of(0, size);
return ResponseEntity.ok(productService.getProductsWithRandomPaging(pageable));
}

@GetMapping("/{productId}/reviews")
public ResponseEntity<ApiResponse<Page<ReviewResponse>>> getAllReviewsByProduct(
@PathVariable("productId") Long productId,
@PageableDefault(size = 7) Pageable pageable) {
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK,reviewService.getAllReviewByProduct(productId,pageable)));
}

@PostMapping("/{productId}/likes")
public ResponseEntity<ApiResponse<Void>> postProductLike(
@PathVariable("productId") Long productId,
@AuthenticationPrincipal JwtUser jwtUser) {
productLikeService.productLike(jwtUser.getId(), productId);
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK));
}

@DeleteMapping("/{productId}/likes")
public ResponseEntity<ApiResponse<Void>> deleteProductlike(
@PathVariable("productId") Long productId,
@AuthenticationPrincipal JwtUser jwtUser) {
productLikeService.deleteProductLike(jwtUser.getId(), productId);

return ResponseEntity.ok(ApiResponse.success(SuccessCode.NO_CONTENT));
}
}
8 changes: 7 additions & 1 deletion src/main/java/com/helpmeCookies/product/entity/Like.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;

@Entity
Expand All @@ -25,4 +24,11 @@ public class Like {
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;

protected Like(){};

public Like(User user, Product product) {
this.user = user;
this.product = product;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.helpmeCookies.product.repository;

import com.helpmeCookies.product.entity.Like;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.user.entity.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductLikeRepository extends JpaRepository<Like, Long> {
Optional<Like> findDistinctFirstByUserAndProduct(User user, Product product);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
"WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)",
nativeQuery = true) // Index 사용
Page<ProductSearch> findByNameWithIdx(@Param("query") String query, Pageable pageable);


@Query("SELECT p FROM Product p ORDER BY FUNCTION('RAND')")
Page<ProductSearch> findAllRandom(Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.helpmeCookies.product.service;

import com.helpmeCookies.product.entity.Like;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.repository.ProductLikeRepository;
import com.helpmeCookies.product.repository.ProductRepository;
import com.helpmeCookies.user.entity.User;
import com.helpmeCookies.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ProductLikeService {
private final ProductLikeRepository productLikeRepository;
private final UserRepository userRepository;
private final ProductRepository productRepository;

@Transactional
public void productLike(Long userId, Long productId) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 유저Id입니다." + userId));
Product product = productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 상품Id입니다." + productId));
Like like = new Like(user, product);
productLikeRepository.save(like);
}

@Transactional
public void deleteProductLike(Long userId, Long productId) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 유저Id입니다." + userId));
Product product = productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 상품Id입니다." + productId));

Like like = productLikeRepository.findDistinctFirstByUserAndProduct(user,product).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 상품 찜 항목입니다."));
productLikeRepository.delete(like);
}
}
Loading

0 comments on commit ddf1418

Please sign in to comment.