From a5bf9b36f74a874058d7700b20ca25d965a6b043 Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Fri, 17 Nov 2023 16:49:43 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20aws=20s3=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85=EA=B3=BC=20gitignore=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle | 2 ++ src/main/resources/application.yml | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 495e681..371c58e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ application-dev.yml application-prod.yml application-jwt.yml application-mail.yml +application-s3.yml diff --git a/build.gradle b/build.gradle index e4ef8d6..9e73624 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-mail' //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + //s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5b3e4b8..5bc9f8e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,4 +9,5 @@ spring: - prod include: - jwt - - mail \ No newline at end of file + - mail + - s3 \ No newline at end of file From cb6a0fd674dafea639a0f3051f71fc252df6e48b Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Fri, 17 Nov 2023 16:55:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20S3Config=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/backend/config/S3Config.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/backend/config/S3Config.java diff --git a/src/main/java/com/backend/config/S3Config.java b/src/main/java/com/backend/config/S3Config.java new file mode 100644 index 0000000..10526ed --- /dev/null +++ b/src/main/java/com/backend/config/S3Config.java @@ -0,0 +1,31 @@ +package com.backend.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +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 AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) + .build(); + } +} \ No newline at end of file From 980c4f83035daa675c31274b35d659547f25278a Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Fri, 17 Nov 2023 16:57:00 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EA=B3=BC=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C,=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/backend/config/SecurityConfig.java | 1 + .../auth/dto/request/JoinRequestDto.java | 3 +- .../domain/s3/controller/S3Controller.java | 29 ++++++++ .../backend/domain/s3/service/S3Service.java | 73 +++++++++++++++++++ .../com/backend/domain/user/entity/User.java | 9 ++- .../java/com/backend/error/ErrorCode.java | 6 +- 6 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/backend/domain/s3/controller/S3Controller.java create mode 100644 src/main/java/com/backend/domain/s3/service/S3Service.java diff --git a/src/main/java/com/backend/config/SecurityConfig.java b/src/main/java/com/backend/config/SecurityConfig.java index 12a0c45..b1f925e 100644 --- a/src/main/java/com/backend/config/SecurityConfig.java +++ b/src/main/java/com/backend/config/SecurityConfig.java @@ -61,6 +61,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(mvcMatcherBuilder.pattern("/v3/api-docs/**")).permitAll() .requestMatchers(mvcMatcherBuilder.pattern("/api/**")).permitAll() .requestMatchers(mvcMatcherBuilder.pattern("/mail/**")).permitAll() + .requestMatchers(mvcMatcherBuilder.pattern("/s3/create")).permitAll() .anyRequest().authenticated()) .exceptionHandling() .authenticationEntryPoint(entryPoint); diff --git a/src/main/java/com/backend/domain/auth/dto/request/JoinRequestDto.java b/src/main/java/com/backend/domain/auth/dto/request/JoinRequestDto.java index fca246c..78bdaed 100644 --- a/src/main/java/com/backend/domain/auth/dto/request/JoinRequestDto.java +++ b/src/main/java/com/backend/domain/auth/dto/request/JoinRequestDto.java @@ -6,13 +6,14 @@ import org.springframework.security.crypto.password.PasswordEncoder; public record JoinRequestDto(@NotEmpty String password, @NotEmpty String email, @NotEmpty String type, - @NotEmpty String typeName) { + @NotEmpty String typeName, @NotEmpty String proofImageUrl) { public User toEntity(PasswordEncoder passwordEncoder) { return User.builder() .email(email) .password(passwordEncoder.encode(this.password)) .type(GroupType.create(type)) .typeName(typeName) + .proofImageUrl(proofImageUrl) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/backend/domain/s3/controller/S3Controller.java b/src/main/java/com/backend/domain/s3/controller/S3Controller.java new file mode 100644 index 0000000..b7b738b --- /dev/null +++ b/src/main/java/com/backend/domain/s3/controller/S3Controller.java @@ -0,0 +1,29 @@ +package com.backend.domain.s3.controller; + +import com.backend.common.dto.ResponseDto; +import com.backend.domain.auth.dto.Login; +import com.backend.domain.auth.dto.LoginUser; +import com.backend.domain.s3.service.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/s3") +public class S3Controller { + + private final S3Service s3Service; + + @PostMapping(value = "/create") + public ResponseEntity uploadImage(@RequestPart MultipartFile file) { + return ResponseDto.created(s3Service.uploadImage((file))); + } + + @DeleteMapping("/delete") + public ResponseEntity deleteImage(@Login LoginUser loginUser, @RequestPart String fileName) { + s3Service.deleteImage(loginUser, fileName); + return ResponseDto.ok("이미지 삭제 성공"); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/domain/s3/service/S3Service.java b/src/main/java/com/backend/domain/s3/service/S3Service.java new file mode 100644 index 0000000..a2fb40f --- /dev/null +++ b/src/main/java/com/backend/domain/s3/service/S3Service.java @@ -0,0 +1,73 @@ +package com.backend.domain.s3.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.backend.domain.auth.dto.LoginUser; +import com.backend.domain.user.entity.User; +import com.backend.domain.user.repository.UserRepository; +import com.backend.error.ErrorCode; +import com.backend.error.exception.custom.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class S3Service { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + private final UserRepository userRepository; + + public String uploadImage(MultipartFile file) { + + log.info("multipartFile: {}", 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 BusinessException(ErrorCode.IMAGE_UPLOAD_FAIL); + } + //return amazonS3.getUrl(bucket, fileName).toString(); + return fileName; + } + + public void deleteImage(LoginUser loginUser, String fileName) { + User user = userRepository.findByEmail(loginUser.getEmail()) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + user.deleteProofImage(); + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } + + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + try { + return fileName.substring(fileName.lastIndexOf(".")); + } catch (StringIndexOutOfBoundsException e) { + throw new BusinessException(ErrorCode.INVALID_FILE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/domain/user/entity/User.java b/src/main/java/com/backend/domain/user/entity/User.java index ec49420..0e63fbb 100644 --- a/src/main/java/com/backend/domain/user/entity/User.java +++ b/src/main/java/com/backend/domain/user/entity/User.java @@ -27,15 +27,16 @@ public class User { private String refreshToken; - //todo. 대표 이미지 필드 추가 + private String proofImageUrl; @Builder - public User(String email, String password, GroupType type, String typeName, String refreshToken) { + public User(String email, String password, GroupType type, String typeName, String refreshToken, String proofImageUrl) { this.email = email; this.password = password; this.type = type; this.typeName = typeName; this.refreshToken = refreshToken; + this.proofImageUrl = proofImageUrl; } public void updateRefreshToken(String refreshToken) { @@ -45,4 +46,8 @@ public void updateRefreshToken(String refreshToken) { public void invalidateRefreshToken() { this.refreshToken = null; } + + public void deleteProofImage() { + this.proofImageUrl = null; + } } \ No newline at end of file diff --git a/src/main/java/com/backend/error/ErrorCode.java b/src/main/java/com/backend/error/ErrorCode.java index c2253d6..5ee15b5 100644 --- a/src/main/java/com/backend/error/ErrorCode.java +++ b/src/main/java/com/backend/error/ErrorCode.java @@ -15,7 +15,9 @@ public enum ErrorCode { ALREADY_EXIST_EMAIL(BAD_REQUEST, "이미 존재하는 이메일입니다."), INVALID_TOKEN(UNAUTHORIZED, "잘못된 토큰입니다."), INVALID_GROUP_TYPE(BAD_REQUEST, "잘못된 그룹 종류입니다."), - INVALID_PASSWORD(BAD_REQUEST, "잘못된 비밀번호입니다."); + INVALID_PASSWORD(BAD_REQUEST, "잘못된 비밀번호입니다."), + IMAGE_UPLOAD_FAIL(BAD_REQUEST, "이미지 업로드에 실패했습니다."), + INVALID_FILE(BAD_REQUEST, "잘못된 파일 형식입니다."); private final int code; private final String message; @@ -24,4 +26,4 @@ public enum ErrorCode { this.code = code.value(); this.message = message; } -} +} \ No newline at end of file From 8e539dbbc47558eaadf56d4636c857aa2b3a1e26 Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Fri, 17 Nov 2023 16:58:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20github=20action=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=97=90=20s3=20yml=20=EC=B6=94=EA=B0=80=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b751695..ec3243f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,6 +56,14 @@ jobs: DEV_SECRET_DIR_FILE_NAME: application-mail.yml run: echo $DEV_SECRET | base64 --decode >> $DEV_SECRET_DIR/$DEV_SECRET_DIR_FILE_NAME + # application-s3.yml + - name: Copy s3 secret + env: + DEV_SECRET: ${{ secrets.APPLICATION_S3_YML }} + DEV_SECRET_DIR: src/main/resources + DEV_SECRET_DIR_FILE_NAME: application-s3.yml + run: echo $DEV_SECRET | base64 --decode >> $DEV_SECRET_DIR/$DEV_SECRET_DIR_FILE_NAME + # ./gradlew 권한 설정 - name: ./gradlew 권한 설정 run: chmod +x ./gradlew From 3193be4d6317feb727629872ff6e2ad9e72e648b Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Fri, 17 Nov 2023 19:50:41 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20s3=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20api=20swagger=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/s3/controller/S3Controller.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/com/backend/domain/s3/controller/S3Controller.java b/src/main/java/com/backend/domain/s3/controller/S3Controller.java index b7b738b..7edef26 100644 --- a/src/main/java/com/backend/domain/s3/controller/S3Controller.java +++ b/src/main/java/com/backend/domain/s3/controller/S3Controller.java @@ -4,6 +4,11 @@ import com.backend.domain.auth.dto.Login; import com.backend.domain.auth.dto.LoginUser; import com.backend.domain.s3.service.S3Service; +import com.backend.error.dto.ErrorResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,11 +21,23 @@ public class S3Controller { private final S3Service s3Service; + @Operation(summary = "이미지 추가", description = "s3에 이미지를 추가합니다. 사용자가 업로드한 이미지 파일이 필요합니다.", + responses = { + @ApiResponse(responseCode = "204", description = "이미지 생성 성공, 이미지 파일 이름을 반환합니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) @PostMapping(value = "/create") public ResponseEntity uploadImage(@RequestPart MultipartFile file) { return ResponseDto.created(s3Service.uploadImage((file))); } + @Operation(summary = "이미지 추가", description = "해당하는 파일 이름의 이미지를 s3에서 삭제합니다. " + + "사용자의 AccessToken과 지우려는 파일 이름이 필요합니다.", + responses = { + @ApiResponse(responseCode = "200", description = "이미지 삭제 성공 메세지를 반환합니다."), + @ApiResponse(responseCode = "401", description = "토큰이 올바르지 않을 때 예외가 발생합니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) @DeleteMapping("/delete") public ResponseEntity deleteImage(@Login LoginUser loginUser, @RequestPart String fileName) { s3Service.deleteImage(loginUser, fileName);