Skip to content

Commit

Permalink
[feat] 사용자 프로필 관련 API 생성 (S3 Multipart 관련) (#26)
Browse files Browse the repository at this point in the history
* [chore] #23 add required dependency in build.gradle

* [feat] #23 s3 related setting

* [feat] #23 create exception and add exception in globalExceptionHandler

* [feat] #23 create user logic

* [chore] #23 delete unnecessary file (UserInfoService)
  • Loading branch information
chaewonni authored Jul 9, 2024
1 parent 9b9ffb1 commit 83089d5
Show file tree
Hide file tree
Showing 17 changed files with 398 additions and 7 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ dependencies {

// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// AWS S3
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import lombok.extern.slf4j.Slf4j;
import org.kkumulkkum.server.exception.AuthException;
import org.kkumulkkum.server.exception.AwsException;
import org.kkumulkkum.server.exception.BusinessException;
import org.kkumulkkum.server.exception.MeetingException;
import org.kkumulkkum.server.exception.code.AuthErrorCode;
import org.kkumulkkum.server.exception.code.AwsErrorCode;
import org.kkumulkkum.server.exception.code.BusinessErrorCode;
import org.kkumulkkum.server.exception.code.MeetingErrorCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;

@Slf4j
Expand All @@ -34,6 +37,14 @@ public ResponseEntity<AuthErrorCode> handleAuthException(AuthException e) {
.body(e.getErrorCode());
}

@ExceptionHandler(value = {AwsException.class})
public ResponseEntity<AwsErrorCode> handleAwsException(AwsException e) {
log.error("GlobalExceptionHandler catch AwsException : {}", e.getErrorCode().getMessage());
return ResponseEntity
.status(e.getErrorCode().getHttpStatus())
.body(e.getErrorCode());
}

// 도메인 관련된 에러가 아닐 경우
@ExceptionHandler(value = {BusinessException.class})
public ResponseEntity<BusinessErrorCode> handleBusinessException(BusinessException e) {
Expand All @@ -60,6 +71,13 @@ public ResponseEntity<BusinessErrorCode> handleException(MethodArgumentNotValidE
.body(BusinessErrorCode.INVALID_ARGUMENTS);
}

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<BusinessErrorCode> handleMaxSizeException(MaxUploadSizeExceededException e) {
return ResponseEntity
.status(BusinessErrorCode.PAYLOAD_TOO_LARGE.getHttpStatus())
.body(BusinessErrorCode.PAYLOAD_TOO_LARGE);
}

// 기본 예외
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<BusinessErrorCode> handleException(Exception e) {
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/org/kkumulkkum/server/config/AwsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.kkumulkkum.server.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class AwsConfig {

private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId";
private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey";

private final String accessKey;
private final String secretKey;
private final String regionString;

public AwsConfig(@Value("${aws-property.access-key}") final String accessKey,
@Value("${aws-property.secret-key}") final String secretKey,
@Value("${aws-property.aws-region}") final String regionString) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.regionString = regionString;
}

@Bean
public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() {
System.setProperty(AWS_ACCESS_KEY_ID, accessKey);
System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey);
return SystemPropertyCredentialsProvider.create();
}

@Bean
public Region getRegion() {
return Region.of(regionString);
}

@Bean
public S3Client getS3Client() {
return S3Client.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}
}
51 changes: 51 additions & 0 deletions src/main/java/org/kkumulkkum/server/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.kkumulkkum.server.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.kkumulkkum.server.annotation.UserId;
import org.kkumulkkum.server.dto.user.request.ImageUpdateDto;
import org.kkumulkkum.server.dto.user.response.UserDto;
import org.kkumulkkum.server.dto.user.response.UserNameDto;
import org.kkumulkkum.server.service.user.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
public class UserController {

private final UserService userService;

@PatchMapping("/users/me/image")
public ResponseEntity<Void> updateImage(
@UserId final Long userId,
@Valid @ModelAttribute final ImageUpdateDto imageUpdateDto
) {
userService.updateImage(userId, imageUpdateDto);
return ResponseEntity.ok().build();
}

@DeleteMapping("/users/me/image")
public ResponseEntity<Void> deleteImage(
@UserId final Long userId
) {
userService.deleteImage(userId);
return ResponseEntity.ok().build();
}

@PatchMapping("/users/me/name")
public ResponseEntity<UserNameDto> updateName(
@UserId final Long userId,
@Valid @RequestBody final UserNameDto userNameDto
) {
return ResponseEntity.ok().body(userService.updateName(userId, userNameDto));
}

@GetMapping("/users/me")
public ResponseEntity<UserDto> getUserInfo(
@UserId final Long userId
) {
return ResponseEntity.ok().body(userService.getUserInfo(userId));
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/kkumulkkum/server/domain/UserInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,16 @@ public UserInfo(String name,
this.user = user;
this.tardySum = 0L;
}

public void updateImage(String imageUrl) {
this.profileImg = imageUrl;
}

public void deleteImage() {
this.profileImg = null;
}

public void updateName(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.kkumulkkum.server.dto.user.request;

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

public record ImageUpdateDto(
@NotNull
MultipartFile image
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.kkumulkkum.server.dto.user.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record NameUpdateDto(
@NotBlank @Size(max = 5)
String name
) {
}
23 changes: 23 additions & 0 deletions src/main/java/org/kkumulkkum/server/dto/user/response/UserDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.kkumulkkum.server.dto.user.response;

import org.kkumulkkum.server.domain.UserInfo;

public record UserDto(
String name,
int level,
int promiseCount,
int tardyCount,
Long tardySum,
String profileImg
) {
public static UserDto from(UserInfo userInfo) {
return new UserDto(
userInfo.getName(),
userInfo.getLevel(),
userInfo.getPromiseCount(),
userInfo.getTardyCount(),
userInfo.getTardySum(),
userInfo.getProfileImg()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.kkumulkkum.server.dto.user.response;

import org.kkumulkkum.server.domain.UserInfo;

public record UserNameDto(
String name
) {
public static UserNameDto from(UserInfo userInfo) {
return new UserNameDto(userInfo.getName());
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/kkumulkkum/server/exception/AwsException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.kkumulkkum.server.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.kkumulkkum.server.exception.code.AwsErrorCode;

@Getter
@RequiredArgsConstructor
public class AwsException extends RuntimeException {
private final AwsErrorCode errorCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.kkumulkkum.server.exception.code;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum AwsErrorCode implements DefaultErrorCode {
// 400 Bad Request
INVALID_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, 40080, "이미지 확장자는 jpg, png, webp만 가능합니다."),
IMAGE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST,40081, "이미지 사이즈는 5MB를 넘을 수 없습니다."),

// 404 Not Found
NOT_FOUND_IMAGE(HttpStatus.NOT_FOUND, 40480, "삭제할 이미지를 찾을 수 없습니다."),
;

private HttpStatus httpStatus;
private int code;
private String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum BusinessErrorCode implements DefaultErrorCode {
// 400 Bad Request
BAD_REQUEST(HttpStatus.BAD_REQUEST,40000, "잘못된 요청입니다."),
INVALID_ARGUMENTS(HttpStatus.BAD_REQUEST, 40001, "인자의 형식이 올바르지 않습니다."),
PAYLOAD_TOO_LARGE(HttpStatus.BAD_REQUEST,40002,"최대 업로드 크기를 초과했습니다."),
// 404 Not Found
NOT_FOUND(HttpStatus.NOT_FOUND,40400, "요청한 정보를 찾을 수 없습니다."),
NOT_FOUND_END_POINT(HttpStatus.NOT_FOUND,40401, "요청한 엔드포인트를 찾을 수 없습니다."),
Expand Down
83 changes: 83 additions & 0 deletions src/main/java/org/kkumulkkum/server/external/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.kkumulkkum.server.external;

import org.kkumulkkum.server.config.AwsConfig;
import org.kkumulkkum.server.exception.AwsException;
import org.kkumulkkum.server.exception.code.AwsErrorCode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

@Component
public class S3Service {

private final String bucketName;
private final AwsConfig awsConfig;
private static final List<String> IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp");

public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AwsConfig awsConfig) {
this.bucketName = bucketName;
this.awsConfig = awsConfig;
}

public String uploadImage(String directoryPath, MultipartFile image) throws IOException {
final String extension = getFileExtension(image.getOriginalFilename());
final String key = directoryPath + generateImageFileName(extension);
final S3Client s3Client = awsConfig.getS3Client();

validateExtension(image);
validateFileSize(image);

PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(image.getContentType())
.contentDisposition("inline")
.build();

RequestBody requestBody = RequestBody.fromBytes(image.getBytes());
s3Client.putObject(request, requestBody);
return key;
}

public void deleteImage(String key) throws IOException {
final S3Client s3Client = awsConfig.getS3Client();

s3Client.deleteObject((DeleteObjectRequest.Builder builder) ->
builder.bucket(bucketName)
.key(key)
.build()
);
}

private String getFileExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1);
}

private String generateImageFileName(String extension) {
return UUID.randomUUID() + "." + extension;
}

private void validateExtension(MultipartFile image) {
String contentType = image.getContentType();
if (!IMAGE_EXTENSIONS.contains(contentType)) {
throw new AwsException(AwsErrorCode.INVALID_IMAGE_EXTENSION);
}
}

private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L;

private void validateFileSize(MultipartFile image) {
if (image.getSize() > MAX_FILE_SIZE) {
throw new AwsException(AwsErrorCode.IMAGE_SIZE_EXCEEDED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
import org.kkumulkkum.server.domain.UserInfo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {

Optional<UserInfo> findByUserId(Long id);
}
Loading

0 comments on commit 83089d5

Please sign in to comment.