Skip to content

Commit

Permalink
Merge pull request #52 from kakao-tech-campus-2nd-step3/Master
Browse files Browse the repository at this point in the history
9조 BE 코드리뷰 3회차
  • Loading branch information
leaf-upper authored Oct 16, 2024
2 parents 5617e33 + 1583bb5 commit 0814a79
Show file tree
Hide file tree
Showing 58 changed files with 1,071 additions and 204 deletions.
12 changes: 7 additions & 5 deletions .github/workflows/main.yml → .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Java CI
name: Java CICD

on:
workflow_dispatch:
Expand All @@ -23,18 +23,18 @@ jobs:
- name: Create application.yaml
run: |
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_YAML }}" > src/main/resources/application.yaml
echo '${{ secrets.APPLICATION_YAML }}' > src/main/resources/application.yaml
cat src/main/resources/application.yaml
shell: bash

- name: Grant execute permission for Gradle Wrapper
run: chmod +x ./gradlew

- name: Build with Gradle
run: ./gradlew build
run: ./gradlew clean build -x test

- name: Test with Gradle
run: ./gradlew test
# - name: Test with Gradle
# run: ./gradlew test

# 경로 확인
- name: Verify JAR file
Expand Down Expand Up @@ -66,6 +66,8 @@ jobs:
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }} # EC2 인스턴스 pem key
port: ${{ secrets.REMOTE_SSH_PORT }} # 접속 포트(생략 시 22번 기본 사용)
script: |
echo '${{ secrets.APPLICATION_YAML }}' > test.yaml
cat test.yaml
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker pull ${{ secrets.DOCKER_USERNAME }}/katecam-backend:latest
docker stop katecam-backend
Expand Down
9 changes: 8 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

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

Expand All @@ -62,8 +67,10 @@ dependencies {
// Monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

//S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}
tasks.named('test') {
useJUnitPlatform()
}
20 changes: 20 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/QueryDSLConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.helpmeCookies.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
private final EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.helpmeCookies.global.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3 amazonS3() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey,secretKey);

return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.helpmeCookies.global.config;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 허용할 도메인 (모든 도메인 허용: "*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE") // 허용할 HTTP 메서드
.allowedHeaders("*") // 허용할 헤더
.allowCredentials(true); // 인증 정보 허용 여부
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.helpmeCookies.global.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import com.helpmeCookies.global.exception.user.ResourceNotFoundException;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public String handleResourceNotFoundException() {
return "Resource not found";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.helpmeCookies.global.exception.user;

public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException() {
super("Resource not found");
}
}
9 changes: 3 additions & 6 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@

@Component
public class JwtProvider implements InitializingBean {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expire-time}")
private long accessTokenExpireTime;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
private String secret = "4099a46b-39db-4860-a61b-2ae76ea24c43";
private long accessTokenExpireTime = 1800000; // 30 minutes;
private long refreshTokenExpireTime = 259200000; // 3 days;
private Key secretKey;
private static final String ROLE = "role";
private static final String IS_ACCESS_TOKEN = "isAccessToken";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/login", "/signup", "/", "/user",
"/api/auth/**",
"/swagger-ui/**",
"/actuator/**"
"/swagger-resources",
"/v3/api-docs/**",
"/actuator/**",
"/v1/**",
"swagger-ui/**"
).permitAll()
.anyRequest().authenticated()
);
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/com/helpmeCookies/global/utils/AwsS3FileUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.helpmeCookies.global.utils;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.helpmeCookies.product.dto.FileUploadResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

@Component
@RequiredArgsConstructor
public class AwsS3FileUtils {
private final AmazonS3 amazonS3;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

//다중파일 업로드후 url 반환
public List<FileUploadResponse> uploadMultiImages(List<MultipartFile> multipartFiles) {
List<FileUploadResponse> fileList = new ArrayList<>();

multipartFiles.forEach(file -> {
String fileName = createFileName(file.getOriginalFilename()); //파일 이름 난수화
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());

try (InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드 실패" + fileName);
}

fileList.add(new FileUploadResponse(amazonS3.getUrl(bucket,fileName).toString(),fileName));
});

return fileList;
}

public String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}

//TODO error handler 필요
public String getFileExtension(String fileName) {
try {
String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
//이미지 파일 확장자 목록
List<String> allowedExtensions = Arrays.asList(".jpg", ".jpeg", ".png");

if (!allowedExtensions.contains(extension)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미지 파일만 업로드가 가능합니다. 지원되지 않는 형식의 파일" + fileName);
}
return extension;
} catch (StringIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"잘못된 형식의 파일" + fileName + "입니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
package com.helpmeCookies.product.controller;

import com.helpmeCookies.product.dto.FileUploadResponse;
import com.helpmeCookies.product.dto.ProductImageResponse;
import com.helpmeCookies.product.dto.ProductRequest;
import com.helpmeCookies.product.dto.ProductResponse;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.service.ProductImageService;
import com.helpmeCookies.product.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

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

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}
private final ProductImageService productImageService;

@PostMapping
public ResponseEntity<Void> saveProduct(@RequestBody ProductRequest productRequest) {
Product product = productService.save(productRequest);
productService.save(productRequest);
return ResponseEntity.ok().build();
}

@PostMapping("/{productId}/images")
public ResponseEntity<ProductImageResponse> uploadImages(@PathVariable("productId") Long productId, List<MultipartFile> files) throws IOException {
List<FileUploadResponse> responses = productImageService.uploadMultiFiles(productId,files);
return ResponseEntity.ok(new ProductImageResponse(responses.stream().map(FileUploadResponse::photoUrl).toList()));
}

@GetMapping("/{productId}")
public ResponseEntity<ProductResponse> getProductInfo(@PathVariable("productId") Long productId) {
Product product = productService.find(productId);
Expand All @@ -36,6 +48,12 @@ public ResponseEntity<Void> editProductInfo(@PathVariable("productId") Long prod
return ResponseEntity.ok().build();
}

@PutMapping("/{productId}/images")
public ResponseEntity<Void> editImages(@PathVariable("productId") Long productId, List<MultipartFile> files) throws IOException {
productImageService.editImages(productId, files);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{productId}")
public ResponseEntity<Void> deleteProduct(@PathVariable("productId") Long productId) {
productService.delete(productId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.helpmeCookies.product.dto;

import com.helpmeCookies.product.entity.ProductImage;

public record FileUploadResponse(
String photoUrl,
String uuid
) {
public ProductImage toEntity(Long productId) {
return ProductImage.builder()
.productId(productId)
.photoUrl(photoUrl)
.uuid(uuid)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.helpmeCookies.product.dto;

import java.util.List;

public record ProductImageResponse(
List<String> urls
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public String getName() {
}

public static Category fromString(String name) {
System.out.println(name);
Category category = nameToCategoryMap.get(name);
if (category == null) {
throw new IllegalArgumentException(name + "에 해당하는 카테고리가 없습니다.");
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/com/helpmeCookies/product/entity/ProductImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Builder;

@Entity
public class ProductImage {
Expand All @@ -15,7 +14,15 @@ public class ProductImage {

private String photoUrl;

@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private Long productId;
private String uuid;

public ProductImage() {}

@Builder
public ProductImage(String photoUrl, Long productId, String uuid) {
this.photoUrl = photoUrl;
this.productId = productId;
this.uuid = uuid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.helpmeCookies.product.repository;

import com.helpmeCookies.product.entity.ProductImage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductImageRepository extends JpaRepository<ProductImage,Long> {
List<ProductImage> findAllByProductId(Long productId);
void deleteAllByProductId(Long productId);
}
Loading

0 comments on commit 0814a79

Please sign in to comment.