Skip to content

Commit

Permalink
Merge pull request #66 from backendoori/feature-image-enhancement
Browse files Browse the repository at this point in the history
🚀 이미지 관련 validation 추가와 각 상황에 맞는 테스트 코드 작성
  • Loading branch information
Sehee-Lee-01 authored Jan 12, 2024
2 parents abbf7f0 + 68dd084 commit 31ffa64
Show file tree
Hide file tree
Showing 29 changed files with 626 additions and 165 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.backendoori.ootw.avatar.controller;

import java.util.List;
import com.backendoori.ootw.avatar.dto.AvatarItemRequest;
import com.backendoori.ootw.avatar.dto.AvatarItemResponse;
import com.backendoori.ootw.avatar.service.AvatarItemService;
Expand All @@ -8,6 +9,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
Expand All @@ -22,11 +24,16 @@ public class AvatarItemController {
private final AvatarItemService appearanceService;

@PostMapping
public ResponseEntity<AvatarItemResponse> uploadImage(@RequestPart @Image MultipartFile file,
@RequestPart @Valid AvatarItemRequest request) {
AvatarItemResponse avatarItem = appearanceService.uploadItem(file, request);
public ResponseEntity<AvatarItemResponse> upload(@RequestPart @Image MultipartFile file,
@RequestPart @Valid AvatarItemRequest request) {
AvatarItemResponse avatarItem = appearanceService.upload(file, request);

return ResponseEntity.status(HttpStatus.CREATED).body(avatarItem);
}

@GetMapping
public ResponseEntity<List<AvatarItemResponse>> getAll() {
return ResponseEntity.status(HttpStatus.OK).body(appearanceService.getList());
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.backendoori.ootw.avatar.domain;

import static com.backendoori.ootw.avatar.validation.AvatarImageValidator.validateImage;
import static com.backendoori.ootw.avatar.validation.AvatarImageValidator.validateItemType;
import static com.backendoori.ootw.avatar.validation.AvatarImageValidator.validateSex;

import com.backendoori.ootw.avatar.dto.AvatarItemRequest;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -32,9 +36,14 @@ public class AvatarItem {
private ItemType itemType;

@Column(name = "sex", nullable = false, columnDefinition = "varchar(10)")
@Enumerated(EnumType.STRING)
private Sex sex;

private AvatarItem(String image, String type, String sex) {
validateImage(image);
validateItemType(type);
validateSex(sex);

this.image = image;
this.itemType = ItemType.valueOf(type);
this.sex = Sex.valueOf(sex);
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/backendoori/ootw/avatar/domain/ItemType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.backendoori.ootw.avatar.domain;

import java.util.Arrays;

public enum ItemType {
HAIR, TOP, PANTS, ACCESSORY, SHOES, BACKGROUND
HAIR, TOP, PANTS, ACCESSORY, SHOES, BACKGROUND;

public static boolean checkValue(String itemType) {
return Arrays.stream(ItemType.values())
.anyMatch(e -> e.name().equals(itemType));
}

}
10 changes: 9 additions & 1 deletion src/main/java/com/backendoori/ootw/avatar/domain/Sex.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.backendoori.ootw.avatar.domain;

import java.util.Arrays;

public enum Sex {
MALE, FEMALE
MALE, FEMALE;

public static boolean checkValue(String sex) {
return Arrays.stream(Sex.values())
.anyMatch(e -> e.name().equals(sex));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import com.backendoori.ootw.avatar.domain.AvatarItem;

public record AvatarItemResponse(
Long avatarItemId,
String type,
String sex,
String url
) {

public static AvatarItemResponse from(AvatarItem avatarItem) {
return new AvatarItemResponse(avatarItem.getItemType().name(),
return new AvatarItemResponse(
avatarItem.getId(),
avatarItem.getItemType().name(),
avatarItem.getSex().name(),
avatarItem.getImage());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.backendoori.ootw.avatar.service;

import java.util.List;
import com.backendoori.ootw.avatar.domain.AvatarItem;
import com.backendoori.ootw.avatar.dto.AvatarItemRequest;
import com.backendoori.ootw.avatar.dto.AvatarItemResponse;
import com.backendoori.ootw.avatar.repository.AvatarItemRepository;
import com.backendoori.ootw.common.image.ImageFile;
import com.backendoori.ootw.common.image.ImageService;
import com.backendoori.ootw.common.image.exception.SaveException;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -16,11 +20,25 @@ public class AvatarItemService {
private final ImageService imageService;
private final AvatarItemRepository avatarItemRepository;

public AvatarItemResponse uploadItem(MultipartFile file, AvatarItemRequest requestDto) {
String url = imageService.uploadImage(file);
AvatarItem savedItem = avatarItemRepository.save(AvatarItem.create(requestDto, url));
@Transactional
public AvatarItemResponse upload(MultipartFile file, AvatarItemRequest requestDto) {
ImageFile imageFile = imageService.upload(file);
try {
String url = imageFile.url();
AvatarItem savedItem = avatarItemRepository.save(AvatarItem.create(requestDto, url));

return AvatarItemResponse.from(savedItem);
return AvatarItemResponse.from(savedItem);
} catch (Exception e) {
imageService.delete(imageFile.fileName());
throw new SaveException();
}
}

public List<AvatarItemResponse> getList() {
return avatarItemRepository.findAll()
.stream()
.map(AvatarItemResponse::from)
.toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.backendoori.ootw.avatar.validation;

import com.backendoori.ootw.avatar.domain.ItemType;
import com.backendoori.ootw.avatar.domain.Sex;
import io.jsonwebtoken.lang.Assert;

public class AvatarImageValidator {

private static final String NO_IMAGE_URL_MESSAGE = "아바타 이미지 url이 존재하지 않습니다.";
private static final String ITEM_TYPE_ESSENTIAL = "아바타 타입은 반드시 포함되어야 합니다.";
private static final String INVALID_ITEM_TYPE_MESSAGE = "해당 단어가 아바타 이미지 타입이 존재하지 않습니다.";
private static final String SEX_ESSENTIAL = "성별은 반드시 포함되어야 합니다.";
private static final String INVALID_WORD_MESSAGE = "해당 단어가 프로젝트 내 성별 분류 체계에 존재하지 않습니다.";

public static void validateSex(String sex) {
Assert.notNull(sex, SEX_ESSENTIAL);
Assert.isTrue(!sex.isBlank(), SEX_ESSENTIAL);
Assert.isTrue(Sex.checkValue(sex), INVALID_WORD_MESSAGE);
}

public static void validateItemType(String type) {
Assert.notNull(type, ITEM_TYPE_ESSENTIAL);
Assert.isTrue(!type.isBlank(), ITEM_TYPE_ESSENTIAL);
Assert.isTrue(ItemType.checkValue(type), INVALID_ITEM_TYPE_MESSAGE);
}

public static void validateImage(String image) {
Assert.notNull(image, NO_IMAGE_URL_MESSAGE);
Assert.isTrue(!image.isBlank(), NO_IMAGE_URL_MESSAGE);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.backendoori.ootw.common.image;

public record ImageFile(
String url,
String fileName
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

public interface ImageService {

String uploadImage(MultipartFile file);
ImageFile upload(MultipartFile file);

void delete(String fileName);

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.backendoori.ootw.common.image;

import static com.backendoori.ootw.common.image.exception.ImageException.IMAGE_ROLLBACK_FAIL_MESSAGE;
import static com.backendoori.ootw.common.image.exception.ImageException.IMAGE_UPLOAD_FAIL_MESSAGE;
import static com.backendoori.ootw.common.validation.ImageValidator.validateImage;

import java.io.InputStream;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import com.backendoori.ootw.common.image.exception.ImageException;
import com.backendoori.ootw.config.MiniOConfig;
import com.backendoori.ootw.exception.ImageUploadException;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -27,7 +32,8 @@ public class MiniOImageServiceImpl implements ImageService {
private Path path;

@Override
public String uploadImage(MultipartFile file) {
public ImageFile upload(MultipartFile file) {
validateImage(file);
try {
path = Path.of(file.getOriginalFilename());
InputStream inputStream = file.getInputStream();
Expand All @@ -39,28 +45,36 @@ public String uploadImage(MultipartFile file) {
.contentType(contentType)
.build();
minioClient.putObject(args);
return new ImageFile(getUrl(), path.toString());
} catch (Exception e) {
throw new ImageUploadException();
throw new ImageException(IMAGE_UPLOAD_FAIL_MESSAGE);
}
}

return getUrl();
@Override
public void delete(String fileName) {
try {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(miniOConfig.getBucket())
.object(fileName)
.build());
} catch (Exception e) {
throw new ImageException(IMAGE_ROLLBACK_FAIL_MESSAGE);
}
}

private String getUrl() {
String url = null;
try {
url = minioClient.getPresignedObjectUrl(
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(miniOConfig.getBucket())
.object(path.toString())
.expiry(DURATION, TimeUnit.HOURS)
.build());
} catch (Exception e) {
throw new ImageUploadException();
throw new ImageException(IMAGE_UPLOAD_FAIL_MESSAGE);
}

return url;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.backendoori.ootw.common.image.exception;

import com.backendoori.ootw.exception.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class ImageControllerAdvice {

private static final String IMAGE_RELATED_EXCEPTION = "업로드 요청 중 문제가 발생했습니다.";

@ExceptionHandler(ImageException.class)
public ResponseEntity<ErrorResponse> handleImageUploadException(ImageException e) {
log.error("error message : {}", e.getMessage());
ErrorResponse errorResponse = new ErrorResponse(IMAGE_RELATED_EXCEPTION);

return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
.body(errorResponse);
}

@ExceptionHandler(SaveException.class)
public ResponseEntity<ErrorResponse> handleSaveException(SaveException e) {
log.error("error message : {}", e.getMessage());
ErrorResponse errorResponse = new ErrorResponse(IMAGE_RELATED_EXCEPTION);

return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
.body(errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.backendoori.ootw.common.image.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ImageException extends RuntimeException {

public static final String IMAGE_UPLOAD_FAIL_MESSAGE = "이미지 업로드 중 예외가 발생했습니다.";
public static final String IMAGE_ROLLBACK_FAIL_MESSAGE = "이미지 롤백 중 예외가 발생했습니다.";

private final String message;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.backendoori.ootw.common.image.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class SaveException extends RuntimeException {

private static final String DEFAULT_MESSAGE = "이미지 업로드 후 저장 로직에서 예외가 발생했습니다.";

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@

@Target(value = ElementType.PARAMETER)
@Retention(value = RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ImageValidator.class)
@Constraint(validatedBy = ImageAnnotationValidator.class)
public @interface Image {

String message = "유효하지 않은 이미지를 업로드하였습니다. 다른 이미지를 업로드 해주세요";
String message = "유효하지 않은 이미지를 업로드하였습니다.";

String message() default message;

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

boolean ignoreCase() default false;

}
Loading

0 comments on commit 31ffa64

Please sign in to comment.