Skip to content

Commit

Permalink
[BE] feat: 이미지 업로드 기능 추가 (#929) (#930)
Browse files Browse the repository at this point in the history
* feat: UploadFile 엔티티, 도메인 추가

* feat: StorageClient R2 구현체 추가

* feat: ImageFileUploadService 추가

* fix: UploadFile의 상태 변경 로직 수정

- ASSIGNED, ATTACHED 상태 변경 시 예외 던지지 않도록 변경
- 리뉴얼 상태로 변경 시 주인 검사 로직 추가

* feat: UploadFileStatusChangeService 추가

* feat: 관리자 이미지 업로드 API 추가

* feat: AsyncSchoolUploadImagesStatusChangeEventListener 추가

* feat: AsyncArtistUploadImagesStatusChangeEventListener 추가

* feat: AsyncFestivalUploadImagesStatusChangeEventListener 추가

* feat: 서브모듈 업데이트

* chore: R2Config 메서드 인자로 설정 모두 받도록 변경

* feat: 서브모듈 업데이트

* refactor: R2Config 제거 및 R2StorageClient에서 설정들 의존 받도록 변경

* chore: 서브모듈 업데이트
  • Loading branch information
seokjin8678 authored May 7, 2024
1 parent ead933c commit 929dac6
Show file tree
Hide file tree
Showing 49 changed files with 1,833 additions and 14 deletions.
4 changes: 4 additions & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ val jjwtVersion = "0.12.5"
val logbackSlackAppenderVersion = "1.4.0"
val cucumberVersion = "7.13.0"
val firebaseVersion = "8.1.0"
val awsS3Version = "2.25.40"

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
Expand Down Expand Up @@ -82,6 +83,9 @@ dependencies {

// Micrometer
runtimeOnly("io.micrometer:micrometer-registry-prometheus")

// AWS S3
implementation("software.amazon.awssdk:s3:${awsS3Version}")
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.admin.dto.upload;

import java.net.URI;

public record AdminImageUploadV1Response(
URI uploadUri
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.festago.admin.presentation.v1;

import com.festago.admin.dto.upload.AdminImageUploadV1Response;
import com.festago.upload.application.ImageFileUploadService;
import com.festago.upload.domain.FileOwnerType;
import com.festago.upload.dto.FileUploadResult;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/admin/api/v1/upload/images")
@RequiredArgsConstructor
@Hidden
public class AdminUploadV1Controller {

private final ImageFileUploadService imageFileUploadService;

@PostMapping
public ResponseEntity<AdminImageUploadV1Response> uploadImage(
@RequestPart MultipartFile image,
@RequestParam(required = false) Long ownerId,
@RequestParam(required = false) FileOwnerType ownerType
) {
FileUploadResult result = imageFileUploadService.upload(image, ownerId, ownerType);
return ResponseEntity.ok()
.body(new AdminImageUploadV1Response(result.uploadUri()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.artist.dto.event;

import com.festago.artist.domain.Artist;

public record ArtistCreatedEvent(
Artist artist
) {

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.festago.artist.dto.event;

public record ArtistDeleteEvent(
public record ArtistDeletedEvent(
Long artistId
) {

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.artist.dto.event;

import com.festago.artist.domain.Artist;

public record ArtistUpdatedEvent(
Artist artist
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum ErrorCode {
OAUTH2_INVALID_CODE("잘못된 OAuth2 Code 입니다."),
OPEN_ID_NOT_SUPPORTED_SOCIAL_TYPE("해당 OpenId 제공자는 지원되지 않습니다."),
OPEN_ID_INVALID_TOKEN("잘못된 OpenID 토큰입니다."),
NOT_SUPPORT_FILE_EXTENSION("해당 파일의 확장자는 허용되지 않습니다."),

// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
Expand Down Expand Up @@ -81,6 +82,7 @@ public enum ErrorCode {
TICKET_SEQUENCE_DATA_ERROR("입장 순서 값의 데이터 정합성에 문제가 발생했습니다."),
OAUTH2_INVALID_REQUEST("알 수 없는 OAuth2 에러가 발생했습니다."),
OPEN_ID_PROVIDER_NOT_RESPONSE("OpenID 제공자 서버에 문제가 발생했습니다."),
FILE_UPLOAD_ERROR("파일 업로드 중 에러가 발생했습니다."),
;

private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class FestivalQueryInfoEventListener {
@EventListener
@Transactional(propagation = Propagation.MANDATORY)
public void festivalCreatedEventHandler(FestivalCreatedEvent event) {
FestivalQueryInfo festivalQueryInfo = FestivalQueryInfo.create(event.festivalId());
FestivalQueryInfo festivalQueryInfo = FestivalQueryInfo.create(event.festival().getId());
festivalInfoRepository.save(festivalQueryInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public Long createFestival(FestivalCreateCommand command) {
Festival festival = command.toEntity(school);
validate(festival);
festivalRepository.save(festival);
eventPublisher.publishEvent(new FestivalCreatedEvent(festival.getId()));
eventPublisher.publishEvent(new FestivalCreatedEvent(festival));
return festival.getId();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import com.festago.festival.domain.FestivalDuration;
import com.festago.festival.domain.validator.FestivalUpdateValidator;
import com.festago.festival.dto.command.FestivalUpdateCommand;
import com.festago.festival.dto.event.FestivalUpdatedEvent;
import com.festago.festival.repository.FestivalRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -17,6 +19,7 @@ public class FestivalUpdateService {

private final FestivalRepository festivalRepository;
private final List<FestivalUpdateValidator> validators;
private final ApplicationEventPublisher eventPublisher;

/**
* 강제로 수정할 일이 필요할 수 있으므로, 시작일이 과거여도 예외를 발생하지 않음
Expand All @@ -27,5 +30,6 @@ public void updateFestival(Long festivalId, FestivalUpdateCommand command) {
festival.changePosterImageUrl(command.posterImageUrl());
festival.changeFestivalDuration(new FestivalDuration(command.startDate(), command.endDate()));
validators.forEach(validator -> validator.validate(festival));
eventPublisher.publishEvent(new FestivalUpdatedEvent(festival));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.festago.festival.dto.event;

import com.festago.festival.domain.Festival;

public record FestivalCreatedEvent(
Long festivalId
Festival festival
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.festival.dto.event;

import com.festago.festival.domain.Festival;

public record FestivalUpdatedEvent(
Festival festival
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void makeMockFestivals() {
for (Festival festival : festivals) {
List<Stage> stages = stageRepository.saveAll(mockStagesGenerator.generate(festival));
stageArtistRepository.saveAll(mockStageArtistsGenerator.generate(stages, artists));
eventPublisher.publishEvent(new FestivalCreatedEvent(festival.getId()));
eventPublisher.publishEvent(new FestivalCreatedEvent(festival));
for (Stage stage : stages) {
eventPublisher.publishEvent(new StageCreatedEvent(stage));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import com.festago.school.domain.School;
import com.festago.school.dto.SchoolCreateCommand;
import com.festago.school.dto.SchoolUpdateCommand;
import com.festago.school.dto.evnet.SchoolCreatedEvent;
import com.festago.school.dto.evnet.SchoolUpdatedEvent;
import com.festago.school.repository.SchoolRepository;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -17,10 +20,12 @@
public class SchoolCommandService {

private final SchoolRepository schoolRepository;
private final ApplicationEventPublisher eventPublisher;

public Long createSchool(SchoolCreateCommand command) {
validateCreate(command);
School school = schoolRepository.save(command.toDomain());
eventPublisher.publishEvent(new SchoolCreatedEvent(school));
return school.getId();
}

Expand All @@ -46,6 +51,7 @@ public void updateSchool(Long schoolId, SchoolUpdateCommand command) {
school.changeRegion(command.region());
school.changeLogoUrl(command.logoUrl());
school.changeBackgroundImageUrl(command.backgroundImageUrl());
eventPublisher.publishEvent(new SchoolUpdatedEvent(school));
}

private void validateUpdate(School school, SchoolUpdateCommand command) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.festago.school.application;

import com.festago.school.domain.validator.SchoolDeleteValidator;
import com.festago.school.dto.evnet.SchoolDeletedEvent;
import com.festago.school.repository.SchoolRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -14,9 +16,11 @@ public class SchoolDeleteService {

private final SchoolRepository schoolRepository;
private final List<SchoolDeleteValidator> validators;
private final ApplicationEventPublisher eventPublisher;

public void deleteSchool(Long schoolId) {
validators.forEach(validator -> validator.validate(schoolId));
schoolRepository.deleteById(schoolId);
eventPublisher.publishEvent(new SchoolDeletedEvent(schoolId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.school.dto.evnet;

import com.festago.school.domain.School;

public record SchoolCreatedEvent(
School school
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.festago.school.dto.evnet;

public record SchoolDeletedEvent(
Long schoolId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.school.dto.evnet;

import com.festago.school.domain.School;

public record SchoolUpdatedEvent(
School school
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.festago.upload.application;

import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import com.festago.common.util.Validator;
import com.festago.upload.domain.FileExtension;
import com.festago.upload.domain.FileOwnerType;
import com.festago.upload.domain.StorageClient;
import com.festago.upload.domain.UploadFile;
import com.festago.upload.dto.FileUploadResult;
import com.festago.upload.repository.UploadFileRepository;
import com.festago.upload.util.FileNameExtensionParser;
import jakarta.annotation.Nullable;
import java.util.EnumSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@Service
@RequiredArgsConstructor
// 명시적으로 @Transactional 사용하지 않음
public class ImageFileUploadService {

private static final int MAX_FILE_SIZE = 2_000_000; // 2MB
private static final Set<FileExtension> ALLOW_IMAGE_EXTENSION = EnumSet.of(FileExtension.JPG, FileExtension.PNG);

private final StorageClient storageClient;
private final UploadFileRepository uploadFileRepository;

public FileUploadResult upload(MultipartFile image, @Nullable Long ownerId, @Nullable FileOwnerType ownerType) {
validate(image);
UploadFile uploadImage = storageClient.storage(image);
if (ownerId != null && ownerType != null) {
uploadImage.changeAssigned(ownerId, ownerType);
}

uploadFileRepository.save(uploadImage);

return new FileUploadResult(uploadImage.getId(), uploadImage.getUploadUri());
}

private void validate(MultipartFile image) {
validateSize(image.getSize());
validateExtension(image.getOriginalFilename());
}

private void validateSize(long imageSize) {
Validator.maxValue(imageSize, MAX_FILE_SIZE, "imageSize");
}

private void validateExtension(String imageName) {
Validator.notBlank(imageName, "imageName");
String extension = FileNameExtensionParser.parse(imageName);
for (FileExtension allowExtension : ALLOW_IMAGE_EXTENSION) {
if (allowExtension.match(extension)) {
return;
}
}
log.info("허용되지 않은 확장자에 대한 이미지 업로드 요청이 있습니다. fileName={}, extension={}", imageName, extension);
throw new BadRequestException(ErrorCode.NOT_SUPPORT_FILE_EXTENSION);
}
}
Loading

0 comments on commit 929dac6

Please sign in to comment.