Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] S3 이미지 관리 기능을 추가하고 사용자 도메인과 연동 #17

Merged
merged 5 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ application-dev.yml
application-prod.yml
application-jwt.yml
application-mail.yml
application-s3.yml
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/backend/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
1 change: 1 addition & 0 deletions src/main/java/com/backend/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/backend/domain/s3/controller/S3Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 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.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@RequestMapping("/s3")
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<String> 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<String> deleteImage(@Login LoginUser loginUser, @RequestPart String fileName) {
s3Service.deleteImage(loginUser, fileName);
return ResponseDto.ok("이미지 삭제 성공");
}
}
73 changes: 73 additions & 0 deletions src/main/java/com/backend/domain/s3/service/S3Service.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
9 changes: 7 additions & 2 deletions src/main/java/com/backend/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -45,4 +46,8 @@ public void updateRefreshToken(String refreshToken) {
public void invalidateRefreshToken() {
this.refreshToken = null;
}

public void deleteProofImage() {
this.proofImageUrl = null;
}
}
6 changes: 4 additions & 2 deletions src/main/java/com/backend/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,4 +26,4 @@ public enum ErrorCode {
this.code = code.value();
this.message = message;
}
}
}
3 changes: 2 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ spring:
- prod
include:
- jwt
- mail
- mail
- s3
Loading