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
17 changes: 10 additions & 7 deletions src/main/java/in/koreatech/koin/global/auth/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import in.koreatech.koin.global.auth.exception.AuthException;
import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.global.auth.exception.AuthException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class JwtProvider {

@Value("${jwt.secret-key}")
private String secretKey;
private final String secretKey;
private final Long expirationTime;

@Value("${jwt.access-token.expiration-time}")
private Long expirationTime;
public JwtProvider(
@Value("${jwt.secret-key}") String secretKey,
@Value("${jwt.access-token.expiration-time}") Long expirationTime
) {
this.secretKey = secretKey;
this.expirationTime = expirationTime;
}

public String createToken(User user) {
if (user == null) {
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/in/koreatech/koin/global/config/AwsSesConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@
@Configuration
public class AwsSesConfig {

@Value("${aws.ses.access-key}")
private String accessKey;
private final String accessKey;
private final String secretKey;

@Value("${aws.ses.secret-key}")
private String secretKey;
public AwsSesConfig(
@Value("${aws.ses.access-key}") String accessKey,
@Value("${aws.ses.secret-key}") String secretKey
) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}

@Bean
public AmazonSimpleEmailServiceAsync amazonSimpleEmailServiceAsync() {
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/in/koreatech/koin/global/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
package in.koreatech.koin.global.config;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
import static software.amazon.awssdk.regions.Region.AP_NORTHEAST_2;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class S3Config {

private final String accessKey;
private final String secretKey;

public S3Config(
@Value("${s3.key}") String accessKey,
@Value("${s3.secret}") String secretKey
) {
Comment on lines +26 to +29

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

설정 파일에 담긴 정보를 @Value로 가져올 때 생성자를 통해 가져오는 이유가 있는 건가요??
필드를 final로 유지하여 강건성을 높이기 위해서라고 봐도 될까요??
JwtProvider 등은 필드에서 주입받고 있는데 서로 다른 용도인 것인지 궁금합니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

강건성 높이려고 맞습니다~
다른 애들 필드주입이였군요 -_- 확인하고 수정할게요

this.accessKey = accessKey;
this.secretKey = secretKey;
}

/**
* S3Presigner 사용 후 close()를 권장하므로, Builder 를 반환하여 필요 시 객체를 만들어 사용 후 close 되도록 구현.
*/
Expand All @@ -20,4 +40,16 @@ public S3Presigner.Builder s3PresignerBuilder() {
.credentialsProvider(InstanceProfileCredentialsProvider.create())
.region(AP_NORTHEAST_2);
}

@Bean
public AmazonS3 amazonS3ClientBuilder() {
ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setProtocol(Protocol.HTTPS);

return AmazonS3ClientBuilder.standard()
.withRegion(Regions.AP_NORTHEAST_2)
.withClientConfiguration(clientConfig)
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public class SwaggerConfig {

private final String serverUrl;

public SwaggerConfig(@Value("${swagger.server-url}") String serverUrl) {
public SwaggerConfig(
@Value("${swagger.server-url}") String serverUrl
) {
this.serverUrl = serverUrl;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@
@Service
public class SlackService {

@Value("${slack.notify_koin_event_url}")
private String slackUrl;
private final String slackUrl;
private final RestTemplate restTemplate;

private final RestTemplate restTemplate = new RestTemplate();
public SlackService(
@Value("${slack.notify_koin_event_url}") String slackUrl
) {
this.slackUrl = slackUrl;
this.restTemplate = new RestTemplate();
}

public void noticeEmailVerification(String email) {
SlackNotification slackNotification = SlackNotification.noticeEmailVerification(email, slackUrl);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package in.koreatech.koin.global.domain.upload.controller;


import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

import in.koreatech.koin.domain.user.model.UserType;
import static in.koreatech.koin.domain.user.model.UserType.OWNER;
import static in.koreatech.koin.domain.user.model.UserType.STUDENT;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.domain.upload.dto.UploadFileResponse;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlRequest;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlResponse;
import in.koreatech.koin.global.domain.upload.model.ImageUploadDomain;
Expand All @@ -34,18 +39,50 @@ public interface UploadApi {
}
)
@Operation(summary = "파일을 업로드할 수 있는 URL 생성", description = """
- `items`
- `lands`
- `circles`
- `market`
- `shops`
- `members`
- `owners`
{domain} 지원 목록
- items
- lands
- circles
- market
- shops
- members
- owners
""")
@PostMapping("/{domain}/upload/url")
ResponseEntity<UploadUrlResponse> getPresignedUrl(
@PathVariable ImageUploadDomain domain,
@RequestBody @Valid UploadUrlRequest request,
@Auth(permit = {UserType.OWNER, STUDENT}) Long memberId
);

@ApiResponses(
value = {
@ApiResponse(responseCode = "201"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "413", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "415", content = @Content(schema = @Schema(hidden = true))),
}
)
@Operation(summary = "단건 파일 업로드", description = """
{domain} 지원 목록
- items
- lands
- circles
- market
- shops
- members
- owners
""")
@PostMapping(
value = "/{domain}/upload/file",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
ResponseEntity<UploadFileResponse> uploadFile(
@PathVariable ImageUploadDomain domain,
@RequestPart MultipartFile multipartFile,
@Auth(permit = {OWNER, STUDENT}) Long memberId
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package in.koreatech.koin.global.domain.upload.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import in.koreatech.koin.domain.user.model.UserType;
import static in.koreatech.koin.domain.user.model.UserType.OWNER;
import static in.koreatech.koin.domain.user.model.UserType.STUDENT;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.domain.upload.dto.UploadFileResponse;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlRequest;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlResponse;
import in.koreatech.koin.global.domain.upload.model.ImageUploadDomain;
Expand All @@ -26,9 +31,23 @@ public class UploadController implements UploadApi {
public ResponseEntity<UploadUrlResponse> getPresignedUrl(
@PathVariable ImageUploadDomain domain,
@RequestBody @Valid UploadUrlRequest request,
@Auth(permit = {UserType.OWNER, STUDENT}) Long memberId
@Auth(permit = {OWNER, STUDENT}) Long memberId
) {
var response = uploadService.getPresignedUrl(domain, request);
return ResponseEntity.ok(response);
}

@PostMapping(
value = "/{domain}/upload/file",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
Comment on lines +42 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

각각 클라이언트, 서버가 보내는 값의 타입을 정의한다고 이해했는데

다른 메서드들과 달리 해당 메서드에만 잇는 이유가 있을까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swagger에 표현이 안되는 이슈가 있어서 MULTIPART_FORM_DATA를 다루는 값에 대해서 선언해줬습니다

)
public ResponseEntity<UploadFileResponse> uploadFile(
@PathVariable ImageUploadDomain domain,
@RequestPart MultipartFile multipartFile,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C

스프링3에서는 xml을 이용해서 최대 파일 크기를 제한하였는데,
부트에서는 properties를 이용해 설정이 가능하다고 알고 있어요
(참고: https://gksdudrb922.tistory.com/230)

적용해보면 어떨까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test propreties에 적용해서 추가했습니다~

@Auth(permit = {OWNER, STUDENT}) Long memberId
) {
var response = uploadService.uploadFile(domain, multipartFile);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package in.koreatech.koin.global.domain.upload.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(value = SnakeCaseStrategy.class)
public record UploadFileResponse(
@Schema(description = "첨부 파일 URL", example = "https://static.koreatech.in/1.png")
String fileUrl
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public record UploadUrlResponse(
""")
String preSignedUrl,

@Schema(description = "업로드한 파일을 가져올 때 사용하는 url", example = "https://static.koreatech.in/2023/09/01/uuid/example.png")
@Schema(description = "첨부 파일 URL", example = "https://static.koreatech.in/1.png")
String fileUrl,

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import in.koreatech.koin.global.domain.upload.dto.UploadFileResponse;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlRequest;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlResponse;
import in.koreatech.koin.global.domain.upload.model.ImageUploadDomain;
import in.koreatech.koin.global.s3.S3Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -28,6 +33,16 @@ public UploadUrlResponse getPresignedUrl(ImageUploadDomain domain, UploadUrlRequ
return s3Util.getUploadUrl(filePath);
}

public UploadFileResponse uploadFile(ImageUploadDomain domain, MultipartFile multipartFile) {
try {
var filePath = generateFilePath(domain.name(), Objects.requireNonNull(multipartFile.getOriginalFilename()));
return s3Util.uploadFile(filePath, multipartFile.getBytes());
} catch (Exception e) {
log.warn("파일 업로드중 문제가 발생했습니다. file: {} \n message: {}", multipartFile, e.getMessage());
throw new IllegalArgumentException("파일 업로드중 오류가 발생했습니다.");
}
}

private String generateFilePath(String domainName, String fileNameExt) {
var now = LocalDateTime.now(clock);
StringJoiner uploadPrefix = new StringJoiner("/");
Expand Down
21 changes: 20 additions & 1 deletion src/main/java/in/koreatech/koin/global/s3/S3Utils.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package in.koreatech.koin.global.s3;

import java.io.ByteArrayInputStream;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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 in.koreatech.koin.global.domain.upload.dto.UploadFileResponse;
import in.koreatech.koin.global.domain.upload.dto.UploadUrlResponse;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
Expand All @@ -17,18 +24,21 @@ public class S3Utils {

private static final int URL_EXPIRATION_MINUTE = 10;

private final S3Presigner.Builder presignerBuilder;
private final Clock clock;
private final String bucketName;
private final String domainUrlPrefix;
private final S3Presigner.Builder presignerBuilder;
private final AmazonS3 s3Client;

public S3Utils(
S3Presigner.Builder presignerBuilder,
AmazonS3 s3Client,
Clock clock,
@Value("${s3.bucket}") String bucketName,
@Value("${s3.custom_domain}") String domainUrlPrefix
) {
this.presignerBuilder = presignerBuilder;
this.s3Client = s3Client;
this.clock = clock;
this.bucketName = bucketName;
this.domainUrlPrefix = domainUrlPrefix;
Expand All @@ -52,4 +62,13 @@ public UploadUrlResponse getUploadUrl(String uploadFilePath) {
);
}
}

public UploadFileResponse uploadFile(String uploadFilePath, byte[] fileData) {
ObjectMetadata metaData = new ObjectMetadata();
metaData.setContentLength(fileData.length);
s3Client.putObject(
new PutObjectRequest(bucketName, uploadFilePath, new ByteArrayInputStream(fileData), metaData)
.withCannedAcl(CannedAccessControlList.PublicRead));
return new UploadFileResponse(domainUrlPrefix + uploadFilePath);
}
}
8 changes: 8 additions & 0 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ spring:
thymeleaf:
prefix: "classpath:/mail/"
suffix: ".html"
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB

server:
tomcat:
max-http-form-post-size: 10MB

logging:
level:
Expand Down
Loading