Skip to content
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
14 changes: 2 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,10 @@ dependencies {
//객체 간 매핑 처리
implementation 'org.modelmapper:modelmapper:3.1.0'


implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4'
//객체 간 매핑 처리
implementation 'org.modelmapper:modelmapper:3.1.0'


// OAUTH2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// MOCKITO
testImplementation "org.mockito:mockito-core:3.+"

//WIREMOCK (외부 의존성 테스트용)
implementation 'org.wiremock.integrations:wiremock-spring-boot:3.3.0'
// S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

Expand All @@ -16,4 +18,11 @@ public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findByEmail(String email);

Optional<Account> findUserByLoginId(String loginId);

@Query("SELECT a.profileImage FROM Account a WHERE a.accountId = :accountId")
Optional<String> findProfileImageByAccountId(@Param("accountId") Long accountId);

@Query("update Account a set a.profileImage = :profileImage where a.accountId = :accountId")
@Modifying
void updateProfileImageByAccountId(@Param("profileImage") String profileImage, @Param("accountId") Long accountId);
}
23 changes: 23 additions & 0 deletions src/main/java/com/example/api/aws/controller/S3Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.api.aws.controller;

import com.example.api.aws.dto.UploadProfileRequest;
import com.example.api.aws.service.S3Service;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@AllArgsConstructor
@RestController
@RequestMapping("/api/v1")
public class S3Controller {
private final S3Service s3Service;

@PostMapping(value = "/upload/profile", consumes = "multipart/form-data")
public ResponseEntity<String> upload(@RequestParam("file") final MultipartFile file) {
UploadProfileRequest request = new UploadProfileRequest(1L, file);
return new ResponseEntity<>(s3Service.upload(request).path(), HttpStatus.OK);
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/example/api/aws/dto/OldKeyRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.api.aws.dto;

import jakarta.validation.constraints.NotBlank;

public record OldKeyRequest(@NotBlank String oldKey) {
}
8 changes: 8 additions & 0 deletions src/main/java/com/example/api/aws/dto/S3UploadRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.api.aws.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;

public record S3UploadRequest(@NotNull MultipartFile multipartFile, @NotBlank String key) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.api.aws.dto;

import jakarta.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;

public record UploadProfileRequest(
@NotNull Long userId,
@NotNull MultipartFile multipartFile) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.api.aws.dto;

import jakarta.validation.constraints.NotNull;

public record UploadProfileResponse(@NotNull String path) {
}
93 changes: 93 additions & 0 deletions src/main/java/com/example/api/aws/service/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.example.api.aws.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.example.api.account.repository.AccountRepository;
import com.example.api.aws.dto.S3UploadRequest;
import com.example.api.aws.dto.OldKeyRequest;
import com.example.api.aws.dto.UploadProfileRequest;
import com.example.api.aws.dto.UploadProfileResponse;
import com.example.api.exception.BusinessException;
import com.example.api.exception.ErrorCode;
import com.example.api.global.config.AmazonConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import java.io.IOException;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {
private final AmazonS3 amazonS3;
private final AmazonConfig amazonConfig;
private final AccountRepository accountRepository;

@Transactional
public UploadProfileResponse upload(@Validated final UploadProfileRequest request) {
// 업로드 파일이 null 이라면 기본 프로필로 초기화
if (initDefaultIfFileIsNull(request)) return new UploadProfileResponse(null);

Optional<String> userProfile = accountRepository.findProfileImageByAccountId(request.userId());
userProfile.ifPresent(oldKey -> remove(new OldKeyRequest(oldKey)));

String key = generateFileName(request);
String path = uploadToS3(new S3UploadRequest(request.multipartFile(), key));
accountRepository.updateProfileImageByAccountId(key, request.userId()); // S3 업로드 이후 사용자 테이블 프로필 값 업데이트

return new UploadProfileResponse(path);
}

private boolean initDefaultIfFileIsNull(final UploadProfileRequest request) {
if (request.multipartFile() == null || request.multipartFile().isEmpty()) {
accountRepository.updateProfileImageByAccountId(null, request.userId());
String oldKey = "user-uploads/" + request.userId() + "/profile.png";
remove(new OldKeyRequest(oldKey));
return true;
}
return false;
}

private String generateFileName(final UploadProfileRequest request) {
String contentType = request.multipartFile().getContentType();
String fileExtension = contentType != null && contentType.contains("/")
? "." + contentType.split("/")[1]
: ".png";
return String.format("user-uploads/%d/profile%s", request.userId(), fileExtension);
}

private String uploadToS3(final S3UploadRequest s3UploadRequest) {
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(s3UploadRequest.multipartFile().getSize());
metadata.setContentType(s3UploadRequest.multipartFile().getContentType());

amazonS3.putObject(
new PutObjectRequest(
amazonConfig.getBucket(),
s3UploadRequest.key(),
s3UploadRequest.multipartFile().getInputStream(),
metadata
)
);

return amazonS3.getUrl(amazonConfig.getBucket(), s3UploadRequest.key()).toString();

} catch (IOException e) {
throw new BusinessException(ErrorCode.FILE_UPLOAD_FAILED);
}
}

public void remove(final OldKeyRequest request) {
if (!amazonS3.doesObjectExist(amazonConfig.getBucket(), request.oldKey())) {
throw new AmazonS3Exception("Object " + request.oldKey() + " does not exist!");
}
amazonS3.deleteObject(amazonConfig.getBucket(), request.oldKey());
}
}
25 changes: 18 additions & 7 deletions src/main/java/com/example/api/domain/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@
import com.example.api.board.dto.update.UpdateOpenStatusRequest;
import com.example.api.board.dto.update.UpdateUserInfoRequest;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import java.util.Collection;

import static jakarta.persistence.FetchType.*;


@Entity
@Getter
@AttributeOverride(name = "createdDate", column = @Column(name = "ACCOUNT_REGISTERED_DATETIME"))
Expand Down Expand Up @@ -95,6 +88,24 @@ public Account(String loginId, String password, String email, String phoneNumber
this.roles = roles;
}

public Account(int age, boolean deleted, String email, String loginId, String name, Nationality nationality, String nickname, boolean openStatus, String password, String phoneNumber, String profileImage, Collection<UserRole> roles, String sex, float starPoint, int workCount) {
this.age = age;
this.deleted = deleted;
this.email = email;
this.loginId = loginId;
this.name = name;
this.nationality = nationality;
this.nickname = nickname;
this.openStatus = openStatus;
this.password = password;
this.phoneNumber = phoneNumber;
this.profileImage = profileImage;
this.roles = roles;
this.sex = sex;
this.starPoint = starPoint;
this.workCount = workCount;
}

public LoginUserRequest getLoginUser(){
return new LoginUserRequest(accountId);
}
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/com/example/api/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public enum ErrorCode {

BUSINESS_DOMAIN_EXCEPTION(HttpStatus.BAD_REQUEST, "-700", "비즈니스 도메인 에러"),

CONTRACT_EXCEPTION(HttpStatus.BAD_REQUEST, "-800", "계약 도메인 에러");
CONTRACT_EXCEPTION(HttpStatus.BAD_REQUEST, "-800", "계약 도메인 에러"),

FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"F500", "이미지 업로드 실패");

private final HttpStatus httpStatus;
private final String errorCode;
Expand All @@ -49,5 +51,4 @@ public enum ErrorCode {
this.errorCode = errorCodeResponse;
this.errorDescription = errorDescription;
}
}

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

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

@Configuration
@Getter
public class AmazonConfig {
private AWSCredentials awsCredentials;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;

@PostConstruct
public void init() {
this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
}

@Bean
public AmazonS3 amazonS3() {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}

@Bean
public AWSCredentialsProvider awsCredentialsProvider() {
return new AWSStaticCredentialsProvider(awsCredentials);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/example/api/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.api.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;

@Configuration
public class WebConfig {

@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
}
12 changes: 11 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,14 @@ app.oauth2.authorized-redirect-uris=*

# vendor
vendor.api.base-url=https://api.odcloud.kr/api/nts-businessman/v1/validate
vendor.api.service-key=${VENDOR_API_SERVICE-KEY}
vendor.api.service-key=${VENDOR_API_SERVICE-KEY}

cloud.aws.s3.bucket=danpat
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
cloud.aws.credentials.accessKey=${AWS_CREDENTIALS_ACCESSKEY}
cloud.aws.credentials.secretKey=${AWS_CREDENTIALS_SECRETKEY}

spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
Loading
Loading