From 98d5c6492ccef2be8d0563f3ecf0a215bdfea91a Mon Sep 17 00:00:00 2001 From: Jong1 <44349716+donsonioc2010@users.noreply.github.com> Date: Sun, 17 Sep 2023 02:11:42 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=ED=8C=8C=EC=9D=BC=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=EA=B8=B0=EB=8A=A5)=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs : 최초 실행시 Sample Data 넣는 방법 추가 * style : 오탈자수정 * feat : Naver Object Storage Properties기능 구현 - Notion 설정 방법 추가 - ObjectStorage 설정값 추가 - NaverObjectStorageProperties Bean 생성 구현 * chore : Server Port 분리 - Local, Dev모두 8080인것 확인하여 변경 - local : 8080, dev : 80번포트 변경 * chore : 파일관련 의존성 및 설정 추가 - bucket-name 추가 - 파일 관리시 Bucket-Name필요하나 누락으로 인한 추가 - Properties 속성 추가 - Dependency 의존성 추가 - AWS-SDK추가 : ObjectStorage 파일 Management - Apache Common IO 추가 : 유틸리티 사용을 하고자 추가 * feat : File사용 관련 Common Exception 생성 - Upload Exception - Delete Exception * feat : 파일 업로드 기능 구현완료 - FileDeleteException 불필요 의존성 제거 - application.yml 불필요 속성 삭제 - FileIO작업관련 Exception - Enum추가 - Exception 추N - File Upload관련 예제 추가 - NaverController, upload-form.html - ObjectStorage기능 구현 - NaverObjectStorageConfig Bean 구현 - NaverObjectStorageUsageType Enum 구현 - Type을 통한 분류 - NaverObjectStorageUtil 공통 파일업로드 기능 구현 * refactor : 오탈자 및 누락 속성 추가 --- .../picasso/server/api/HomeController.java | 2 +- .../api/naver_test/NaverController.java | 70 ++++++++++ Api/src/main/resources/application.yml | 11 +- .../resources/templates/test/upload-form.html | 20 +++ Common/build.gradle | 6 + .../config/ConfigurationPropertiesConfig.java | 8 +- .../config/NaverObjectStorageConfig.java | 56 ++++++++ .../common/exception/FileDeleteException.java | 14 ++ .../common/exception/FileIOException.java | 14 ++ .../common/exception/FileUploadException.java | 14 ++ .../common/exception/GlobalException.java | 3 + .../NaverObjectStorageProperties.java | 19 +++ .../util/NaverObjectStorageUsageType.java | 14 ++ .../common/util/NaverObjectStorageUtil.java | 124 ++++++++++++++++++ .../src/main/resources/application-common.yml | 8 +- Domain/src/main/resources/data.sql | 10 ++ 16 files changed, 384 insertions(+), 9 deletions(-) create mode 100644 Api/src/main/java/picasso/server/api/naver_test/NaverController.java create mode 100644 Api/src/main/resources/templates/test/upload-form.html create mode 100644 Common/src/main/java/picasso/server/common/config/NaverObjectStorageConfig.java create mode 100644 Common/src/main/java/picasso/server/common/exception/FileDeleteException.java create mode 100644 Common/src/main/java/picasso/server/common/exception/FileIOException.java create mode 100644 Common/src/main/java/picasso/server/common/exception/FileUploadException.java create mode 100644 Common/src/main/java/picasso/server/common/properties/NaverObjectStorageProperties.java create mode 100644 Common/src/main/java/picasso/server/common/util/NaverObjectStorageUsageType.java create mode 100644 Common/src/main/java/picasso/server/common/util/NaverObjectStorageUtil.java create mode 100644 Domain/src/main/resources/data.sql diff --git a/Api/src/main/java/picasso/server/api/HomeController.java b/Api/src/main/java/picasso/server/api/HomeController.java index 01284ec3..2c8dd227 100644 --- a/Api/src/main/java/picasso/server/api/HomeController.java +++ b/Api/src/main/java/picasso/server/api/HomeController.java @@ -10,6 +10,6 @@ public class HomeController { @GetMapping("/") public String index() { - return "/index"; + return "index"; } } diff --git a/Api/src/main/java/picasso/server/api/naver_test/NaverController.java b/Api/src/main/java/picasso/server/api/naver_test/NaverController.java new file mode 100644 index 00000000..f1077cc5 --- /dev/null +++ b/Api/src/main/java/picasso/server/api/naver_test/NaverController.java @@ -0,0 +1,70 @@ +package picasso.server.api.naver_test; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; +import picasso.server.common.properties.NaverObjectStorageProperties; +import picasso.server.common.util.NaverObjectStorageUsageType; +import picasso.server.common.util.NaverObjectStorageUtil; + +import java.util.ArrayList; +import java.util.List; +/* +TODO : 기능개발 완료이후 삭제해야 할 파일 + */ + +@Slf4j +@RequiredArgsConstructor +@Controller +@RequestMapping("/naver") +public class NaverController { + private final NaverObjectStorageProperties naverObjectStorageProperties; + private final NaverObjectStorageUtil naverObjectStorageUtil; + + @GetMapping("/properties-test") + @ResponseBody + public ResponseEntity naverTest() { + return ResponseEntity.ok( + naverObjectStorageProperties.getEndPoint() + " " + + naverObjectStorageProperties.getRegionName() + ); + } + + @GetMapping("/file-upload-test-form") + public String uploadTest() { + return "test/upload-form"; + } + + @PostMapping("/file-upload-test") + @ResponseBody + public ResponseEntity fileUploadTest( + @RequestPart(value = "profile", required = false) MultipartFile profile, + @RequestPart(value = "paint", required = false) MultipartFile paint + ) { + List filePath = new ArrayList<>(); + if (!profile.isEmpty()) + filePath.add( + naverObjectStorageUtil.storageFileUpload( + NaverObjectStorageUsageType.PROFILE, profile + ) + ); + if (!paint.isEmpty()) + filePath.add( + naverObjectStorageUtil.storageFileUpload( + NaverObjectStorageUsageType.PAINT, paint + ) + ); + if (filePath.isEmpty()) + filePath.add("업로드한 파일이 없음"); + + return ResponseEntity.ok(filePath); + } + +} diff --git a/Api/src/main/resources/application.yml b/Api/src/main/resources/application.yml index 562782cb..6f4785fb 100644 --- a/Api/src/main/resources/application.yml +++ b/Api/src/main/resources/application.yml @@ -4,7 +4,7 @@ spring: group: local: - infra-local - - commons-local + - common-local - domain-local dev: - infra-dev @@ -18,9 +18,6 @@ spring: static-path-pattern: static/** servlet: multipart: - file-size-threshold: 1MB -# TODO : MultiPart 활용시 경로 변경 필요 - location: ~/template-project/upload max-file-size: 20MB max-request-size: 200MB web: @@ -31,7 +28,6 @@ spring: server: - port: 8080 shutdown: graceful servlet: session: @@ -45,6 +41,8 @@ spring: devtools: livereload: enabled: true +server: + port: 8080 test-message : local-test @@ -53,5 +51,8 @@ spring: config: activate: on-profile: dev +server: + port: 80 + test-message : dev-test diff --git a/Api/src/main/resources/templates/test/upload-form.html b/Api/src/main/resources/templates/test/upload-form.html new file mode 100644 index 00000000..2acb886d --- /dev/null +++ b/Api/src/main/resources/templates/test/upload-form.html @@ -0,0 +1,20 @@ + + + + + +

Upload Test Page

+
+ 업로드 대상 파일 (프로필) :
+ 업로드 대상 파일 (미술품게시판) :
+ +
+ + + \ No newline at end of file diff --git a/Common/build.gradle b/Common/build.gradle index e6df4500..9e8e670a 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -6,4 +6,10 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-aop' api 'org.springframework.boot:spring-boot-starter-validation' + + //File + api 'commons-io:commons-io:2.13.0' + + // Amazon AWS Java SDK s3 + api 'com.amazonaws:aws-java-sdk-s3:1.12.530' } \ No newline at end of file diff --git a/Common/src/main/java/picasso/server/common/config/ConfigurationPropertiesConfig.java b/Common/src/main/java/picasso/server/common/config/ConfigurationPropertiesConfig.java index 738b2fde..7aec71b9 100644 --- a/Common/src/main/java/picasso/server/common/config/ConfigurationPropertiesConfig.java +++ b/Common/src/main/java/picasso/server/common/config/ConfigurationPropertiesConfig.java @@ -2,9 +2,13 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; +import picasso.server.common.properties.NaverObjectStorageProperties; @Configuration -@EnableConfigurationProperties({ -}) +@EnableConfigurationProperties( + { + NaverObjectStorageProperties.class + } +) public class ConfigurationPropertiesConfig { } diff --git a/Common/src/main/java/picasso/server/common/config/NaverObjectStorageConfig.java b/Common/src/main/java/picasso/server/common/config/NaverObjectStorageConfig.java new file mode 100644 index 00000000..5f2c6d31 --- /dev/null +++ b/Common/src/main/java/picasso/server/common/config/NaverObjectStorageConfig.java @@ -0,0 +1,56 @@ +package picasso.server.common.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import picasso.server.common.properties.NaverObjectStorageProperties; + +@Slf4j +@RequiredArgsConstructor +@Configuration +public class NaverObjectStorageConfig { + private final NaverObjectStorageProperties naverObjectStorageProperties; + + @Bean + public AmazonS3 storageObject() { + log.info("Create NaverObjectStorageConfig Bean"); + return AmazonS3ClientBuilder.standard() + .withEndpointConfiguration(this.getEndpointConfig()) + .withCredentials(this.getCredentialsProvier()) + .build(); + } + + private AwsClientBuilder.EndpointConfiguration getEndpointConfig() { + log.info( + "Create EndPoint Object >>> EndPoint : {}, RegionName : {}", + this.naverObjectStorageProperties.getEndPoint(), + this.naverObjectStorageProperties.getRegionName() + ); + + return new AwsClientBuilder.EndpointConfiguration( + this.naverObjectStorageProperties.getEndPoint(), + this.naverObjectStorageProperties.getRegionName() + ); + } + + private AWSStaticCredentialsProvider getCredentialsProvier() { + log.info( + "Create CredentialsProvider >>> AccessKey : {}, SecretKey : {}", + this.naverObjectStorageProperties.getAccessKey(), + this.naverObjectStorageProperties.getSecretKey() + ); + + return new AWSStaticCredentialsProvider( + new BasicAWSCredentials( + naverObjectStorageProperties.getAccessKey(), + naverObjectStorageProperties.getSecretKey() + ) + ); + } +} diff --git a/Common/src/main/java/picasso/server/common/exception/FileDeleteException.java b/Common/src/main/java/picasso/server/common/exception/FileDeleteException.java new file mode 100644 index 00000000..1f5fb6f3 --- /dev/null +++ b/Common/src/main/java/picasso/server/common/exception/FileDeleteException.java @@ -0,0 +1,14 @@ +package picasso.server.common.exception; + +import static picasso.server.common.exception.GlobalException.FILE_DELETE_ERROR; + +public class FileDeleteException extends BaseException { + + public static final BaseException EXCEPTION = new FileDeleteException(); + + private FileDeleteException() { + super(FILE_DELETE_ERROR); + } + +} + diff --git a/Common/src/main/java/picasso/server/common/exception/FileIOException.java b/Common/src/main/java/picasso/server/common/exception/FileIOException.java new file mode 100644 index 00000000..1fb331b1 --- /dev/null +++ b/Common/src/main/java/picasso/server/common/exception/FileIOException.java @@ -0,0 +1,14 @@ +package picasso.server.common.exception; + +import static picasso.server.common.exception.GlobalException.FILE_IO_ERROR; + +public class FileIOException extends BaseException { + + public static final BaseException EXCEPTION = new FileIOException(); + + private FileIOException() { + super(FILE_IO_ERROR); + } + +} + diff --git a/Common/src/main/java/picasso/server/common/exception/FileUploadException.java b/Common/src/main/java/picasso/server/common/exception/FileUploadException.java new file mode 100644 index 00000000..5e2d8dc6 --- /dev/null +++ b/Common/src/main/java/picasso/server/common/exception/FileUploadException.java @@ -0,0 +1,14 @@ +package picasso.server.common.exception; + +import static picasso.server.common.exception.GlobalException.FILE_UPLOAD_ERROR; + +public class FileUploadException extends BaseException { + + public static final BaseException EXCEPTION = new FileUploadException(); + + private FileUploadException() { + super(FILE_UPLOAD_ERROR); + } + +} + diff --git a/Common/src/main/java/picasso/server/common/exception/GlobalException.java b/Common/src/main/java/picasso/server/common/exception/GlobalException.java index 2637300d..a407be1c 100644 --- a/Common/src/main/java/picasso/server/common/exception/GlobalException.java +++ b/Common/src/main/java/picasso/server/common/exception/GlobalException.java @@ -14,6 +14,9 @@ public enum GlobalException implements BaseErrorCode { METHOD_ARGUMENT_ERROR(BAD_REQUEST.value(), "메서드 인자가 유효하지 않거나 @Valid를 통과하지 못하여 발생하는 예외입니다."), INTERNAL_SERVER_ERRORS(INTERNAL_SERVER_ERROR.value(), "서버 내부 오류입니다."), DATE_FORMAT_ERROR(BAD_REQUEST.value(), "날짜 형식을 확인해주세요."), + FILE_UPLOAD_ERROR(INTERNAL_SERVER_ERROR.value(), "파일 업로드 중 오류가 발생하였습니다"), + FILE_DELETE_ERROR(INTERNAL_SERVER_ERROR.value(), "파일 삭제 중 오류가 발생하였습니다"), + FILE_IO_ERROR(INTERNAL_SERVER_ERROR.value(), "파일 변환 중 오류가 발생하였습니다") ; private final Integer statusCode; diff --git a/Common/src/main/java/picasso/server/common/properties/NaverObjectStorageProperties.java b/Common/src/main/java/picasso/server/common/properties/NaverObjectStorageProperties.java new file mode 100644 index 00000000..632ecbae --- /dev/null +++ b/Common/src/main/java/picasso/server/common/properties/NaverObjectStorageProperties.java @@ -0,0 +1,19 @@ +package picasso.server.common.properties; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ToString +@AllArgsConstructor +@ConfigurationProperties(prefix = "naver.storage") +public class NaverObjectStorageProperties { + + private String endPoint; + private String regionName; + private String accessKey; + private String secretKey; + private String bucketName; +} diff --git a/Common/src/main/java/picasso/server/common/util/NaverObjectStorageUsageType.java b/Common/src/main/java/picasso/server/common/util/NaverObjectStorageUsageType.java new file mode 100644 index 00000000..7ca19355 --- /dev/null +++ b/Common/src/main/java/picasso/server/common/util/NaverObjectStorageUsageType.java @@ -0,0 +1,14 @@ +package picasso.server.common.util; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum NaverObjectStorageUsageType { + PROFILE("profile") + , PAINT("paint"); + + + private String path; +} diff --git a/Common/src/main/java/picasso/server/common/util/NaverObjectStorageUtil.java b/Common/src/main/java/picasso/server/common/util/NaverObjectStorageUtil.java new file mode 100644 index 00000000..21e7a0f3 --- /dev/null +++ b/Common/src/main/java/picasso/server/common/util/NaverObjectStorageUtil.java @@ -0,0 +1,124 @@ +package picasso.server.common.util; + +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import picasso.server.common.exception.FileDeleteException; +import picasso.server.common.exception.FileIOException; +import picasso.server.common.exception.FileUploadException; +import picasso.server.common.properties.NaverObjectStorageProperties; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + + +@Slf4j +@RequiredArgsConstructor +@Component +public class NaverObjectStorageUtil { + private final AmazonS3 storageObject; + private final NaverObjectStorageProperties naverObjectStorageProperties; + private final Environment environment; + + /** + * 사용처, MultipartFile만 제공하면 업로드가 가능하다. + * + * @param usageType + * @param file + * @return + * @throws FileUploadException + */ + public String storageFileUpload(NaverObjectStorageUsageType usageType, MultipartFile file) throws FileUploadException { + try { + String filePath = getPath(usageType, getFileUUIDNameByMultipartFile(file)); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getBytes().length); + + uploadFile( + usageType, filePath, file.getInputStream(), metadata + ); + log.info("File Upload : {}", filePath); + return filePath; + } catch (IOException e) { + throw FileIOException.EXCEPTION; + } catch (Exception e) { + log.error(e.getMessage()); + throw FileUploadException.EXCEPTION; + } + } + + /** + * 파일 삭제를 진행할 경우 사용한다 + * + * @param filePath + * @throws FileDeleteException + */ + public void storageFileDelete(String filePath) throws FileDeleteException { + try { + storageObject.deleteObject( + naverObjectStorageProperties.getBucketName(), + filePath + ); + log.info("File Delete : {}", filePath); + } catch (Exception e) { + throw FileDeleteException.EXCEPTION; + } + } + + + /** + * 실제 File을 NaverObject Storage에 Upload + * + * @param usageType + * @param filePath + * @param uploadTarget + * @param metadata + */ + private void uploadFile(NaverObjectStorageUsageType usageType, String filePath, InputStream uploadTarget, ObjectMetadata metadata) { + storageObject.putObject( + new PutObjectRequest( + naverObjectStorageProperties.getBucketName(), + filePath, + uploadTarget, + metadata + ).withCannedAcl(CannedAccessControlList.PublicRead) + ); + } + + /** + * UUID.png 등으로 제작하기 위함 + * + * @param file + * @return + */ + private String getFileUUIDNameByMultipartFile(MultipartFile file) { + return UUID.randomUUID() + + "." + + FilenameUtils.getExtension(file.getOriginalFilename()); + } + + /** + * ObjectStorage에 저장될 File Path + * + * @param usageType + * @param fileName + * @return + */ + private String getPath(NaverObjectStorageUsageType usageType, String fileName) { + return environment.getProperty("spring.profiles.active", "default") + "/" + + usageType.getPath() + "/" + + fileName; + } + + +} diff --git a/Common/src/main/resources/application-common.yml b/Common/src/main/resources/application-common.yml index bbda7cf7..761cc112 100644 --- a/Common/src/main/resources/application-common.yml +++ b/Common/src/main/resources/application-common.yml @@ -1,8 +1,14 @@ +naver: + storage: + end-point: https://kr.object.ncloudstorage.com + region-name: kr-standard + bucket-name: picasso-bucket + +--- spring: config: activate: on-profile: common-local - --- spring: config: diff --git a/Domain/src/main/resources/data.sql b/Domain/src/main/resources/data.sql new file mode 100644 index 00000000..47af9e12 --- /dev/null +++ b/Domain/src/main/resources/data.sql @@ -0,0 +1,10 @@ +insert into t_test(name, param1,param2,param3,param4) +values ('aaaa', 'a','b','c','d'); +insert into t_test(name, param1,param2,param3,param4) +values ('dddd', 'a','b','c','d'); +insert into t_test(name, param1,param2,param3,param4) +values ('cccc', 'a','b','c','d'); +insert into t_test(name, param1,param2,param3,param4) +values ('dddd', 'a','b','c','d'); +insert into t_test(name, param1,param2,param3,param4) +values ('ffff', 'a','b','c','d');