diff --git a/jtoon-core/core-api/build.gradle b/jtoon-core/core-api/build.gradle index cfd400a..af066a2 100644 --- a/jtoon-core/core-api/build.gradle +++ b/jtoon-core/core-api/build.gradle @@ -38,6 +38,9 @@ dependencies { // OAuth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + // Jakarta tx + implementation 'org.springframework:spring-tx' + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/event/EventService.java b/jtoon-core/core-api/src/main/java/shop/jtoon/event/EventService.java new file mode 100644 index 0000000..89db87b --- /dev/null +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/event/EventService.java @@ -0,0 +1,74 @@ +package shop.jtoon.event; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import shop.jtoon.dto.ImagePublishData; +import shop.jtoon.event.domain.ImagePublish; +import shop.jtoon.event.service.EventDomainService; +import shop.jtoon.service.EventRedisService; +import shop.jtoon.webtoon.application.WebtoonClientService; +import shop.jtoon.webtoon.request.ImageEvent; +import shop.jtoon.webtoon.service.WebtoonDomainService; + +@Service +@RequiredArgsConstructor +public class EventService { + + private final EventDomainService eventDomainService; + private final WebtoonClientService webtoonClientService; + private final EventRedisService eventRedisService; + private final WebtoonDomainService webtoonDomainService; + + @Scheduled(cron = "0/10 * * * * *") + @Transactional + public void publish() { + LocalDateTime now = LocalDateTime.now(); + + List publishes = eventDomainService.readRecentEvent(now).stream() + .map(imagePublish -> { + webtoonClientService.upload(ImageEvent.toImageEvent(imagePublish.getImagePayload()).toImageUpload()); + imagePublish.updateStatus(); + + return imagePublish; + }) + .toList(); + + eventDomainService.update(publishes); + } + + @Scheduled(cron = "0/10 * * * * *") + @Transactional + public void eventExecute() { + List webtoonIds = new ArrayList<>(); + List publishes = eventRedisService.consume().stream() + .parallel() + .map(imagePublishData -> updateEvent(imagePublishData, webtoonIds)) + .filter(Objects::nonNull) + .map(ImageEvent::toImagePublish) + .toList(); + + eventDomainService.update(publishes); + webtoonDomainService.updateWebtoonStatus(webtoonIds); + } + + private ImageEvent updateEvent(ImagePublishData imagePublishData, List wetoonIds) { + ImageEvent imageEvent = ImageEvent.toImageEvent(imagePublishData); + + try { + webtoonClientService.upload(imageEvent.toImageUpload()); + wetoonIds.add(imagePublishData.id()); + + return null; + } catch (Exception e) { + return imageEvent; + } + } +} diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/EpisodeService.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/EpisodeService.java index 6a846d8..1444e02 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/EpisodeService.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/EpisodeService.java @@ -1,7 +1,6 @@ package shop.jtoon.webtoon.application; import static shop.jtoon.common.ImageType.*; -import static shop.jtoon.type.ErrorStatus.*; import java.util.List; @@ -10,17 +9,13 @@ import org.springframework.web.multipart.MultipartFile; import lombok.RequiredArgsConstructor; - -import shop.jtoon.dto.ImageUploadEvent; -import shop.jtoon.dto.MultiImageEvent; -import shop.jtoon.exception.InvalidRequestException; - +import shop.jtoon.webtoon.request.MultiImageEvent; import shop.jtoon.webtoon.domain.EpisodeMainInfo; import shop.jtoon.webtoon.domain.EpisodeSchema; import shop.jtoon.webtoon.entity.Webtoon; -import shop.jtoon.webtoon.presentation.WebtoonImageUploadEventListener; import shop.jtoon.webtoon.request.CreateEpisodeReq; import shop.jtoon.webtoon.request.GetEpisodesReq; +import shop.jtoon.webtoon.request.ImageEvent; import shop.jtoon.webtoon.request.MultiImagesReq; import shop.jtoon.webtoon.response.EpisodeInfoRes; import shop.jtoon.webtoon.response.EpisodeItemRes; @@ -44,18 +39,18 @@ public void createEpisode( Webtoon webtoon = episodeDomainService.readWebtoon(webtoonId, memberId, request.no()); MultiImageEvent mainUploadEvents = MultiImageEvent.builder() - .imageUploadEvents(mainImages.toMultiImageEvent(request, webtoon.getTitle())) + .imageEvents(mainImages.toMultiImageEvent(request, webtoon.getTitle())) .build(); - List mainUrls = mainUploadEvents.imageUploadEvents().stream() - .map(webtoonClientService::uploadUrl) + List mainUrls = mainUploadEvents.imageEvents().stream() + .map(imageEvent -> webtoonClientService.parseUrl(imageEvent.toImageUpload())) .toList(); - ImageUploadEvent thumbnailUploadEvent = request.toUploadImageDto( + ImageEvent thumbnailUploadEvent = request.toUploadImageDto( EPISODE_THUMBNAIL, webtoon.getTitle(), thumbnailImage - ).toImageUploadEvent(); - String thumbnailUrl = webtoonClientService.upload(thumbnailUploadEvent); + ).toImageEvent(); + String thumbnailUrl = webtoonClientService.parseUrl(thumbnailUploadEvent.toImageUpload()); EpisodeSchema episode = request.toEpisodeSchema(); episodeDomainService.createEpisode(episode, webtoon, mainUrls, thumbnailUrl); diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonClientService.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonClientService.java index a83180d..23ff415 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonClientService.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonClientService.java @@ -1,16 +1,10 @@ package shop.jtoon.webtoon.application; - - import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; import lombok.RequiredArgsConstructor; -import shop.jtoon.common.ImageType; -import shop.jtoon.dto.ImageUploadEvent; -import shop.jtoon.dto.UploadImageDto; +import shop.jtoon.dto.ImageUpload; import shop.jtoon.service.S3Service; -import shop.jtoon.webtoon.request.CreateWebtoonReq; @Service @RequiredArgsConstructor @@ -18,17 +12,11 @@ public class WebtoonClientService { private final S3Service s3Service; - public String upload(ImageType imageType, CreateWebtoonReq request, MultipartFile thumbnailImage) { - UploadImageDto uploadImageDto = request.toUploadImageDto(imageType, thumbnailImage); - - return s3Service.uploadImage(uploadImageDto); - } - - public String uploadUrl(ImageUploadEvent imageUpload) { + public String parseUrl(ImageUpload imageUpload) { return s3Service.uploadUrl(imageUpload.key()); } - public String upload(ImageUploadEvent imageUpload) { + public ImageUpload upload(ImageUpload imageUpload) { return s3Service.uploadImage(imageUpload); } diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonService.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonService.java index 1bf9e53..658cfef 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonService.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/application/WebtoonService.java @@ -5,16 +5,16 @@ import java.util.List; import java.util.Map; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import lombok.RequiredArgsConstructor; -import shop.jtoon.dto.ImageUploadEvent; +import shop.jtoon.service.EventRedisService; import shop.jtoon.webtoon.domain.WebtoonDetail; import shop.jtoon.webtoon.entity.enums.DayOfWeek; import shop.jtoon.webtoon.request.CreateWebtoonReq; import shop.jtoon.webtoon.request.GetWebtoonsReq; +import shop.jtoon.webtoon.request.ImageEvent; import shop.jtoon.webtoon.response.WebtoonInfoRes; import shop.jtoon.webtoon.response.WebtoonItemRes; import shop.jtoon.webtoon.service.WebtoonDomainService; @@ -25,20 +25,19 @@ public class WebtoonService { private final WebtoonClientService webtoonClientService; private final WebtoonDomainService webtoonDomainService; - private final ApplicationEventPublisher publisher; + private final EventRedisService eventRedisService; public void createWebtoon(Long memberId, MultipartFile thumbnailImage, CreateWebtoonReq request) { - ImageUploadEvent imageUploadEvent = request.toUploadImageDto(WEBTOON_THUMBNAIL, thumbnailImage) - .toImageUploadEvent(); - String thumbnailUrl = webtoonClientService.uploadUrl(imageUploadEvent); + ImageEvent imageEvent = request.toUploadImageDto(WEBTOON_THUMBNAIL, thumbnailImage) + .toImageEvent(); + String thumbnailUrl = webtoonClientService.parseUrl(imageEvent.toImageUpload()); webtoonDomainService.validateDuplicateTitle(request.title()); - webtoonDomainService.createWebtoon(memberId, + Long webtoonId = webtoonDomainService.createWebtoon(memberId, request.toWebtoonInfo(thumbnailUrl), request.toWebtoonGenres(), request.toWebtoonDayOfWeeks()); - - publisher.publishEvent(imageUploadEvent); + eventRedisService.publish(imageEvent.imagePublishData(webtoonId)); } public Map> getWebtoons(GetWebtoonsReq request) { diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/presentation/WebtoonImageUploadEventListener.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/presentation/WebtoonImageUploadEventListener.java index 120d0ce..dc2bb36 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/presentation/WebtoonImageUploadEventListener.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/presentation/WebtoonImageUploadEventListener.java @@ -3,10 +3,10 @@ import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; -import shop.jtoon.dto.ImageUploadEvent; -import shop.jtoon.dto.MultiImageEvent; +import shop.jtoon.dto.ImageUpload; import shop.jtoon.global.util.AsyncEventListener; import shop.jtoon.webtoon.application.WebtoonClientService; +import shop.jtoon.webtoon.request.MultiImageEvent; @Component @RequiredArgsConstructor @@ -15,15 +15,14 @@ public class WebtoonImageUploadEventListener { private final WebtoonClientService webtoonClientService; @AsyncEventListener - public void uploadImage(ImageUploadEvent imageUploadEvent) { - webtoonClientService.upload(imageUploadEvent); + public void uploadImage(ImageUpload imageUpload) { + webtoonClientService.upload(imageUpload); } @AsyncEventListener public void uploadMultiImages(MultiImageEvent multiImageEvent) { - multiImageEvent.imageUploadEvents().stream() + multiImageEvent.imageEvents().stream() .parallel() - .forEach(webtoonClientService::upload); + .forEach(imageEvent -> webtoonClientService.upload(imageEvent.toImageUpload())); } - } diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateEpisodeReq.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateEpisodeReq.java index ede1d59..34469c4 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateEpisodeReq.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateEpisodeReq.java @@ -11,11 +11,7 @@ import lombok.Builder; import shop.jtoon.common.FileName; import shop.jtoon.common.ImageType; -import shop.jtoon.dto.ImageUploadEvent; -import shop.jtoon.dto.UploadImageDto; import shop.jtoon.webtoon.domain.EpisodeSchema; -import shop.jtoon.webtoon.entity.Episode; -import shop.jtoon.webtoon.entity.Webtoon; @Builder public record CreateEpisodeReq( diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateWebtoonReq.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateWebtoonReq.java index e6f52b8..62917c3 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateWebtoonReq.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/CreateWebtoonReq.java @@ -1,6 +1,5 @@ package shop.jtoon.webtoon.request; -import java.util.List; import java.util.Set; import org.springframework.web.multipart.MultipartFile; @@ -11,13 +10,9 @@ import lombok.Builder; import shop.jtoon.common.FileName; import shop.jtoon.common.ImageType; -import shop.jtoon.dto.UploadImageDto; import shop.jtoon.webtoon.domain.WebtoonDayOfWeeks; import shop.jtoon.webtoon.domain.WebtoonGenres; import shop.jtoon.webtoon.domain.WebtoonInfo; -import shop.jtoon.webtoon.entity.DayOfWeekWebtoon; -import shop.jtoon.webtoon.entity.GenreWebtoon; -import shop.jtoon.webtoon.entity.Webtoon; import shop.jtoon.webtoon.entity.enums.AgeLimit; import shop.jtoon.webtoon.entity.enums.DayOfWeek; import shop.jtoon.webtoon.entity.enums.Genre; diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/ImageEvent.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/ImageEvent.java new file mode 100644 index 0000000..b6c8d4d --- /dev/null +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/ImageEvent.java @@ -0,0 +1,61 @@ +package shop.jtoon.webtoon.request; + +import java.time.LocalDateTime; + +import lombok.Builder; +import shop.jtoon.dto.ImagePublishData; +import shop.jtoon.dto.ImageUpload; +import shop.jtoon.event.domain.ImagePayload; +import shop.jtoon.event.domain.ImagePublish; +import shop.jtoon.event.entity.EventStatus; + +@Builder +public record ImageEvent( + String key, + byte[] data +) { + + public static ImageEvent toImageEvent(ImagePayload imagePayload) { + return ImageEvent.builder() + .key(imagePayload.key()) + .data(imagePayload.data()) + .build(); + } + + public static ImageEvent toImageEvent(ImagePublishData imagePublishData) { + return ImageEvent.builder() + .key(imagePublishData.key()) + .data(imagePublishData.data()) + .build(); + } + + public ImageUpload toImageUpload() { + return ImageUpload.builder() + .key(key) + .data(data) + .build(); + } + + public ImagePayload toImagePayload() { + return ImagePayload.builder() + .key(key) + .data(data) + .build(); + } + + public ImagePublish toImagePublish() { + return ImagePublish.builder() + .eventStatus(EventStatus.READY) + .imagePayload(this.toImagePayload()) + .publishDate(LocalDateTime.now()) + .build(); + } + + public ImagePublishData imagePublishData(Long id) { + return ImagePublishData.builder() + .id(id) + .key(key) + .data(data) + .build(); + } +} diff --git a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/MultiImageEvent.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImageEvent.java similarity index 59% rename from jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/MultiImageEvent.java rename to jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImageEvent.java index cf5af5c..52b3316 100644 --- a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/MultiImageEvent.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImageEvent.java @@ -1,4 +1,4 @@ -package shop.jtoon.dto; +package shop.jtoon.webtoon.request; import java.util.List; @@ -6,7 +6,7 @@ @Builder public record MultiImageEvent( - List imageUploadEvents + List imageEvents ) { } diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImagesReq.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImagesReq.java index 4bd938e..568b9be 100644 --- a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImagesReq.java +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/MultiImagesReq.java @@ -7,16 +7,15 @@ import org.springframework.web.multipart.MultipartFile; import lombok.Builder; -import shop.jtoon.dto.ImageUploadEvent; @Builder public record MultiImagesReq( List mainImages ) { - public List toMultiImageEvent(CreateEpisodeReq request, String webtoonTitle) { + public List toMultiImageEvent(CreateEpisodeReq request, String webtoonTitle) { return mainImages.stream() - .map(mainImage -> request.toUploadImageDto(EPISODE_MAIN, webtoonTitle, mainImage).toImageUploadEvent()) + .map(mainImage -> request.toUploadImageDto(EPISODE_MAIN, webtoonTitle, mainImage).toImageEvent()) .toList(); } } diff --git a/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/UploadImageDto.java b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/UploadImageDto.java new file mode 100644 index 0000000..507b15f --- /dev/null +++ b/jtoon-core/core-api/src/main/java/shop/jtoon/webtoon/request/UploadImageDto.java @@ -0,0 +1,35 @@ +package shop.jtoon.webtoon.request; + +import java.io.IOException; + +import org.springframework.web.multipart.MultipartFile; + +import lombok.Builder; +import shop.jtoon.common.FileName; +import shop.jtoon.common.ImageType; +import shop.jtoon.exception.NotFoundException; +import shop.jtoon.type.ErrorStatus; + +@Builder +public record UploadImageDto( + ImageType imageType, + String webtoonTitle, + FileName fileName, + MultipartFile image +) { + + public String toKey() { + return imageType.getPath(webtoonTitle, fileName.getValue()); + } + + public ImageEvent toImageEvent() { + try { + return ImageEvent.builder() + .key(toKey()) + .data(image.getBytes()) + .build(); + } catch (IOException exception) { + throw new NotFoundException(ErrorStatus.DATA_NOT_FOUND); + } + } +} diff --git a/jtoon-core/core-domain/build.gradle b/jtoon-core/core-domain/build.gradle index 1a777e9..6dda275 100644 --- a/jtoon-core/core-domain/build.gradle +++ b/jtoon-core/core-domain/build.gradle @@ -26,6 +26,12 @@ dependencies { // JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // Hibernate type parser + implementation 'io.hypersistence:hypersistence-utils-hibernate-60:3.3.1' + + // JSON + implementation 'com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.16.1' + // H2 implementation 'com.h2database:h2' diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/domain/ImagePayload.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/domain/ImagePayload.java new file mode 100644 index 0000000..d554605 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/domain/ImagePayload.java @@ -0,0 +1,11 @@ +package shop.jtoon.event.domain; + +import lombok.Builder; + +@Builder +public record ImagePayload( + String key, + byte[] data +) { + +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/domain/ImagePublish.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/domain/ImagePublish.java new file mode 100644 index 0000000..85a56bf --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/domain/ImagePublish.java @@ -0,0 +1,48 @@ +package shop.jtoon.event.domain; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.Builder; +import lombok.Getter; +import shop.jtoon.event.entity.Event; +import shop.jtoon.event.entity.EventStatus; + +@Getter +public class ImagePublish { + private Long id; + private LocalDateTime publishDate; + private ImagePayload imagePayload; + private EventStatus eventStatus; + + @Builder + public ImagePublish(ImagePayload imagePayload, EventStatus eventStatus, Long id, LocalDateTime publishDate) { + this.id = id; + this.imagePayload = imagePayload; + this.eventStatus = eventStatus; + this.publishDate = publishDate; + } + + public static Event toEvent(ImagePublish imagePublish) { + return Event.builder() + .id(imagePublish.id) + .status(imagePublish.eventStatus) + .payload(imagePublish.imagePayload) + .build(); + } + + public static List toImagePublishes(List events) { + return events.stream() + .map(event -> ImagePublish.builder() + .id(event.getId()) + .eventStatus(event.getStatus()) + .imagePayload(event.getPayload()) + .publishDate(event.getCreatedAt()) + .build()) + .toList(); + } + + public void updateStatus() { + this.eventStatus = EventStatus.OK; + } +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/entity/Event.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/entity/Event.java new file mode 100644 index 0000000..97eb82d --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/entity/Event.java @@ -0,0 +1,42 @@ +package shop.jtoon.event.entity; + +import org.hibernate.annotations.Type; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import shop.jtoon.common.BaseTimeEntity; +import shop.jtoon.event.domain.ImagePayload; + +@Getter +@Entity +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class Event extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + + @Enumerated(EnumType.STRING) + private EventStatus status; + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private ImagePayload payload; + + @Builder + public Event(Long id, EventStatus status, ImagePayload payload) { + this.status = status; + this.payload = payload; + } +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/entity/EventStatus.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/entity/EventStatus.java new file mode 100644 index 0000000..ba6df80 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/entity/EventStatus.java @@ -0,0 +1,6 @@ +package shop.jtoon.event.entity; + +public enum EventStatus { + READY, + OK +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventReader.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventReader.java new file mode 100644 index 0000000..f7588e2 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventReader.java @@ -0,0 +1,20 @@ +package shop.jtoon.event.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; +import shop.jtoon.event.entity.Event; + +@Repository +@RequiredArgsConstructor +public class EventReader { + + private final EventSearchRepository eventSearchRepository; + + public List readRecentEvent(LocalDateTime now) { + return eventSearchRepository.findByCreateAtBefore(now); + } +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventRepository.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventRepository.java new file mode 100644 index 0000000..a7a4d87 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventRepository.java @@ -0,0 +1,9 @@ +package shop.jtoon.event.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import shop.jtoon.event.entity.Event; + +public interface EventRepository extends JpaRepository { + +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventSearchRepository.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventSearchRepository.java new file mode 100644 index 0000000..00d70e8 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventSearchRepository.java @@ -0,0 +1,29 @@ +package shop.jtoon.event.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; +import shop.jtoon.event.entity.Event; +import shop.jtoon.event.entity.EventStatus; +import shop.jtoon.event.entity.QEvent; + +@Repository +@RequiredArgsConstructor +public class EventSearchRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List findByCreateAtBefore(LocalDateTime now) { + return jpaQueryFactory.selectFrom(QEvent.event) + .where( + QEvent.event.createdAt.before(now), + QEvent.event.status.eq(EventStatus.READY) + ) + .fetch(); + } +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventWriter.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventWriter.java new file mode 100644 index 0000000..25fc522 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/repository/EventWriter.java @@ -0,0 +1,26 @@ +package shop.jtoon.event.repository; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; +import shop.jtoon.event.domain.ImagePublish; +import shop.jtoon.event.entity.Event; + +@Repository +@RequiredArgsConstructor +public class EventWriter { + + private final EventRepository eventRepository; + + public void write(Event event) { + eventRepository.save(event); + } + + public void writeEvnets(List publishes) { + eventRepository.saveAll(publishes.stream() + .map(ImagePublish::toEvent) + .toList()); + } +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/event/service/EventDomainService.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/service/EventDomainService.java new file mode 100644 index 0000000..6666b32 --- /dev/null +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/event/service/EventDomainService.java @@ -0,0 +1,29 @@ +package shop.jtoon.event.service; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import shop.jtoon.event.domain.ImagePublish; +import shop.jtoon.event.repository.EventReader; +import shop.jtoon.event.repository.EventWriter; + +@Service +@RequiredArgsConstructor +public class EventDomainService { + + private final EventWriter eventWriter; + private final EventReader eventReader; + + public List readRecentEvent(LocalDateTime now) { + return ImagePublish.toImagePublishes(eventReader.readRecentEvent(now)); + } + + @Transactional + public void update(List publishes) { + eventWriter.writeEvnets(publishes); + } +} diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/entity/Webtoon.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/entity/Webtoon.java index 61daa8d..0372e92 100644 --- a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/entity/Webtoon.java +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/entity/Webtoon.java @@ -5,6 +5,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -16,6 +18,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import shop.jtoon.event.entity.EventStatus; import shop.jtoon.exception.InvalidRequestException; import shop.jtoon.common.BaseTimeEntity; import shop.jtoon.member.entity.Member; @@ -54,6 +57,10 @@ public class Webtoon extends BaseTimeEntity { @JoinColumn(name = "author_id", nullable = false) private Member author; + @Enumerated(EnumType.STRING) + @Column(name = "status") + private EventStatus eventStatus; + @Builder private Webtoon( String title, @@ -69,6 +76,7 @@ private Webtoon( this.thumbnailUrl = requireNonNull(thumbnailUrl, WEBTOON_THUMBNAIL_URL_IS_NULL.getMessage()); this.cookieCount = validateCookieCount(cookieCount); this.author = requireNonNull(author, WEBTOON_AUTHOR_IS_NULL.getMessage()); + this.eventStatus = EventStatus.READY; } public void validateAuthor(Long memberId) { diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonReader.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonReader.java index c89d6b8..75c75c7 100644 --- a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonReader.java +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonReader.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Repository; import lombok.RequiredArgsConstructor; +import shop.jtoon.event.entity.EventStatus; import shop.jtoon.exception.NotFoundException; import shop.jtoon.webtoon.domain.SearchWebtoon; import shop.jtoon.webtoon.entity.DayOfWeekWebtoon; @@ -28,7 +29,7 @@ public List search(SearchWebtoon search) { } public Webtoon read(Long webtoonId) { - return webtoonRepository.findById(webtoonId).orElseThrow(() -> new NotFoundException(WEBTOON_NOT_FOUND)); + return webtoonRepository.findByIdAndEventStatus(webtoonId, EventStatus.OK).orElseThrow(() -> new NotFoundException(WEBTOON_NOT_FOUND)); } public List readDayOfWebtoon(Webtoon webtoon) { diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonRepository.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonRepository.java index 4a480d7..abcd7c5 100644 --- a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonRepository.java +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonRepository.java @@ -1,10 +1,26 @@ package shop.jtoon.webtoon.repository; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import shop.jtoon.event.entity.EventStatus; import shop.jtoon.webtoon.entity.Webtoon; public interface WebtoonRepository extends JpaRepository { boolean existsByTitle(String title); + + Optional findByIdAndEventStatus(Long webtoonId, EventStatus eventStatus); + + @Modifying + @Query("update Webtoon set eventStatus = 'OK' where id = :webtoonId") + void updateStatus(Long webtoonId); + + @Modifying + @Query("update Webtoon set eventStatus = 'OK' where id in (:webtoonIds)") + void updateStatus(List webtoonIds); } diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonSearchRepository.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonSearchRepository.java index 12f3208..38b5141 100644 --- a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonSearchRepository.java +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonSearchRepository.java @@ -8,6 +8,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import shop.jtoon.event.entity.EventStatus; import shop.jtoon.member.entity.QMember; import shop.jtoon.util.DynamicQuery; import shop.jtoon.webtoon.entity.DayOfWeekWebtoon; @@ -26,6 +27,7 @@ public List findWebtoons(DayOfWeek dayOfWeek, String keyword) .join(QDayOfWeekWebtoon.dayOfWeekWebtoon.webtoon, QWebtoon.webtoon) .join(QWebtoon.webtoon.author, QMember.member) .where( + QWebtoon.webtoon.eventStatus.eq(EventStatus.OK), DynamicQuery.generateEq(dayOfWeek, QDayOfWeekWebtoon.dayOfWeekWebtoon.dayOfWeek::eq), DynamicQuery.generateEq(keyword, this::containsKeyword) ) diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonWriter.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonWriter.java index f5e31a7..8291a6f 100644 --- a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonWriter.java +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/repository/WebtoonWriter.java @@ -23,4 +23,8 @@ public void createWebtoon(Webtoon webtoon, List dayOfWeekWebto dayOfWeekWebtoonRepository.saveAll(dayOfWeekWebtoons); genreWebtoonRepository.saveAll(genreWebtoons); } + + public void update(List webtoonIds) { + webtoonRepository.updateStatus(webtoonIds); + } } diff --git a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/service/WebtoonDomainService.java b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/service/WebtoonDomainService.java index 6adf7a8..5d8ace5 100644 --- a/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/service/WebtoonDomainService.java +++ b/jtoon-core/core-domain/src/main/java/shop/jtoon/webtoon/service/WebtoonDomainService.java @@ -1,7 +1,6 @@ package shop.jtoon.webtoon.service; import static java.util.stream.Collectors.*; -import static shop.jtoon.type.ErrorStatus.*; import java.util.List; import java.util.Map; @@ -10,15 +9,15 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import shop.jtoon.exception.NotFoundException; +import lombok.extern.slf4j.Slf4j; import shop.jtoon.member.entity.Member; import shop.jtoon.member.repository.MemberReader; import shop.jtoon.webtoon.domain.SearchWebtoon; import shop.jtoon.webtoon.domain.WebtoonDayOfWeeks; import shop.jtoon.webtoon.domain.WebtoonDetail; -import shop.jtoon.webtoon.domain.WebtoonSchema; import shop.jtoon.webtoon.domain.WebtoonGenres; import shop.jtoon.webtoon.domain.WebtoonInfo; +import shop.jtoon.webtoon.domain.WebtoonSchema; import shop.jtoon.webtoon.entity.DayOfWeekWebtoon; import shop.jtoon.webtoon.entity.GenreWebtoon; import shop.jtoon.webtoon.entity.Webtoon; @@ -27,6 +26,7 @@ import shop.jtoon.webtoon.repository.WebtoonReader; import shop.jtoon.webtoon.repository.WebtoonWriter; +@Slf4j @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -42,7 +42,7 @@ public void validateDuplicateTitle(String title) { } @Transactional - public void createWebtoon(Long memberId, WebtoonInfo info, WebtoonGenres genres, WebtoonDayOfWeeks dayOfWeeks) { + public Long createWebtoon(Long memberId, WebtoonInfo info, WebtoonGenres genres, WebtoonDayOfWeeks dayOfWeeks) { Member member = memberReader.read(memberId); Webtoon webtoon = info.toWebtoonEntity(member); @@ -50,6 +50,8 @@ public void createWebtoon(Long memberId, WebtoonInfo info, WebtoonGenres genres, List genreWebtoons = genres.toGenreWebtoonEntity(webtoon); webtoonWriter.createWebtoon(webtoon, dayOfWeekWebtoons, genreWebtoons); + + return webtoon.getId(); } public Map> readWebtoons(SearchWebtoon search) { @@ -65,4 +67,9 @@ public WebtoonDetail readWebtoonDetail(Long webtoonId) { return WebtoonDetail.of(webtoon, dayOfWeeks, genres); } + + @Transactional + public void updateWebtoonStatus(List webtoonIds) { + webtoonWriter.update(webtoonIds); + } } diff --git a/jtoon-core/core-domain/src/test/java/shop/jtoon/ApplicationTest.java b/jtoon-core/core-domain/src/test/java/shop/jtoon/ApplicationTest.java new file mode 100644 index 0000000..86bd9f5 --- /dev/null +++ b/jtoon-core/core-domain/src/test/java/shop/jtoon/ApplicationTest.java @@ -0,0 +1,8 @@ +package shop.jtoon; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApplicationTest { + +} diff --git a/jtoon-core/core-domain/src/test/java/shop/jtoon/webtoon/WebtoonServiceTest.java b/jtoon-core/core-domain/src/test/java/shop/jtoon/webtoon/WebtoonServiceTest.java new file mode 100644 index 0000000..71e3a86 --- /dev/null +++ b/jtoon-core/core-domain/src/test/java/shop/jtoon/webtoon/WebtoonServiceTest.java @@ -0,0 +1,94 @@ +package shop.jtoon.webtoon; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import shop.jtoon.member.entity.Gender; +import shop.jtoon.member.entity.LoginType; +import shop.jtoon.member.entity.Member; +import shop.jtoon.member.entity.Role; +import shop.jtoon.member.repository.MemberRepository; +import shop.jtoon.webtoon.entity.Webtoon; +import shop.jtoon.webtoon.entity.enums.AgeLimit; +import shop.jtoon.webtoon.repository.DayOfWeekWebtoonRepository; +import shop.jtoon.webtoon.repository.GenreWebtoonRepository; +import shop.jtoon.webtoon.repository.WebtoonRepository; +import shop.jtoon.webtoon.repository.WebtoonWriter; +import shop.jtoon.webtoon.service.WebtoonDomainService; + +@EnableJpaAuditing +@DataJpaTest +class WebtoonServiceTest { + + @Autowired + WebtoonRepository webtoonRepository; + + @Autowired + MemberRepository memberRepository; + + @Autowired + DayOfWeekWebtoonRepository dayOfWeekWebtoonRepository; + + @Autowired + GenreWebtoonRepository genreWebtoonRepository; + + WebtoonDomainService webtoonDomainService; + + @BeforeEach + void init() { + WebtoonWriter webtoonWriter = new WebtoonWriter(webtoonRepository, dayOfWeekWebtoonRepository, + genreWebtoonRepository); + webtoonDomainService = new WebtoonDomainService(null, null, webtoonWriter, null); + } + + @Test + void update_bulk_query() { + + List members = new ArrayList<>(); + List webtoons = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + members.add(memberFixture()); + webtoons.add(webtoonFixture(members.get(i))); + } + + memberRepository.saveAll(members); + memberRepository.flush(); + webtoonRepository.saveAll(webtoons); + webtoonRepository.flush(); + + webtoonDomainService.updateWebtoonStatus(webtoons.stream().map(Webtoon::getId).toList()); + webtoonRepository.flush(); + + } + + Webtoon webtoonFixture(Member member) { + return Webtoon.builder() + .title(UUID.randomUUID().toString().substring(0, 10)) + .description("description") + .ageLimit(AgeLimit.AGE_12) + .thumbnailUrl("thumbnailUrl") + .cookieCount(4) + .author(member) + .build(); + } + + Member memberFixture() { + return Member.builder() + .name("n") + .role(Role.USER) + .phone(UUID.randomUUID().toString().substring(0, 10)) + .email(UUID.randomUUID().toString().substring(0, 10)) + .nickname(UUID.randomUUID().toString().substring(0, 10)) + .loginType(LoginType.KAKAO) + .password("1234") + .gender(Gender.MALE) + .build(); + } +} diff --git a/jtoon-core/core-domain/src/test/resources/application-test.yml b/jtoon-core/core-domain/src/test/resources/application-test.yml new file mode 100644 index 0000000..21c5766 --- /dev/null +++ b/jtoon-core/core-domain/src/test/resources/application-test.yml @@ -0,0 +1,34 @@ +spring: + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: +# format_sql: true +# show_sql: true + dialect: org.hibernate.dialect.MySQL5InnoDBDialect + use_sql_comments: true + jdbc: + batch_size: 50 + + datasource: + url: jdbc:h2:~/jtoon;MODE=MYSQL;DB_CLOSE_ON_EXIT=FALSE;maxQuerySizeToLog=999999; + username: sa + password: + driver-class-name: org.h2.Driver + hikari: + data-source-properties: + rewriteBatchedStatements: true + profileSQL: true + logger: Slf4JLogger + + +logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace + +logging.level.org.springframework.transaction.interceptor: trace diff --git a/jtoon-db/db-redis/src/main/java/shop/jtoon/dto/ImagePublishData.java b/jtoon-db/db-redis/src/main/java/shop/jtoon/dto/ImagePublishData.java new file mode 100644 index 0000000..8072f03 --- /dev/null +++ b/jtoon-db/db-redis/src/main/java/shop/jtoon/dto/ImagePublishData.java @@ -0,0 +1,12 @@ +package shop.jtoon.dto; + +import lombok.Builder; + +@Builder +public record ImagePublishData( + Long id, + String key, + byte[] data +) { + +} diff --git a/jtoon-db/db-redis/src/main/java/shop/jtoon/repository/ListRedisRepository.java b/jtoon-db/db-redis/src/main/java/shop/jtoon/repository/ListRedisRepository.java new file mode 100644 index 0000000..656ddfe --- /dev/null +++ b/jtoon-db/db-redis/src/main/java/shop/jtoon/repository/ListRedisRepository.java @@ -0,0 +1,41 @@ +package shop.jtoon.repository; + +import java.time.Duration; +import java.util.Date; +import java.util.List; + +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.hash.Jackson2HashMapper; +import org.springframework.stereotype.Repository; + +@Repository +public class ListRedisRepository { + + private final RedisTemplate redisTemplate; + private final ListOperations listOperations; + private final Jackson2HashMapper hashMapper; + + public ListRedisRepository(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + listOperations = redisTemplate.opsForList(); + hashMapper = new Jackson2HashMapper(false); + } + + public void push(String key, Object value, Duration timeout) { + listOperations.leftPush(key, hashMapper.toHash(value)); + redisTemplate.expire(key, timeout); + } + + public void delete(String key) { + redisTemplate.expireAt(key, new Date()); + } + + public List pop(String key, Long count) { + return listOperations.rightPop(key, count); + } + + public Long size(String key) { + return listOperations.size(key); + } +} \ No newline at end of file diff --git a/jtoon-db/db-redis/src/main/java/shop/jtoon/service/EventRedisService.java b/jtoon-db/db-redis/src/main/java/shop/jtoon/service/EventRedisService.java new file mode 100644 index 0000000..8324235 --- /dev/null +++ b/jtoon-db/db-redis/src/main/java/shop/jtoon/service/EventRedisService.java @@ -0,0 +1,31 @@ +package shop.jtoon.service; + +import java.time.Duration; +import java.util.List; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import shop.jtoon.dto.ImagePublishData; +import shop.jtoon.repository.ListRedisRepository; + +@Service +@RequiredArgsConstructor +public class EventRedisService { + + private static final String EVENT_KEY = "EVENT"; + private static final int EXPIRE_MINUTES = 1; + + private final ListRedisRepository listRedisRepository; + + public void publish(ImagePublishData imagePublishData) { + listRedisRepository.push(EVENT_KEY, imagePublishData, Duration.ofMinutes(EXPIRE_MINUTES)); + } + + public List consume() { + return listRedisRepository.pop(EVENT_KEY, listRedisRepository.size(EVENT_KEY)) + .stream() + .map(ImagePublishData.class::cast) + .toList(); + } +} diff --git a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/ImageUpload.java b/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/ImageUpload.java new file mode 100644 index 0000000..7966cb0 --- /dev/null +++ b/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/ImageUpload.java @@ -0,0 +1,11 @@ +package shop.jtoon.dto; + +import lombok.Builder; + +@Builder +public record ImageUpload( + String key, + byte[] data +) { + +} diff --git a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/ImageUploadEvent.java b/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/ImageUploadEvent.java deleted file mode 100644 index 67429a7..0000000 --- a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/ImageUploadEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package shop.jtoon.dto; - -import org.springframework.web.multipart.MultipartFile; - -import lombok.Builder; - -@Builder -public record ImageUploadEvent( - String key, - MultipartFile multipartFile -) { - -} diff --git a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/UploadImageDto.java b/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/UploadImageDto.java deleted file mode 100644 index ace65a4..0000000 --- a/jtoon-internal/s3-client/src/main/java/shop/jtoon/dto/UploadImageDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package shop.jtoon.dto; - -import org.springframework.web.multipart.MultipartFile; - -import lombok.Builder; -import shop.jtoon.common.FileName; -import shop.jtoon.common.ImageType; - -@Builder -public record UploadImageDto( - ImageType imageType, - String webtoonTitle, - FileName fileName, - MultipartFile image -) { - - public String toKey() { - return imageType.getPath(webtoonTitle, fileName.getValue()); - } - - public ImageUploadEvent toImageUploadEvent() { - return ImageUploadEvent.builder() - .key(toKey()) - .multipartFile(image) - .build(); - } -} diff --git a/jtoon-internal/s3-client/src/main/java/shop/jtoon/service/S3Service.java b/jtoon-internal/s3-client/src/main/java/shop/jtoon/service/S3Service.java index c2b10e8..d5f8e92 100644 --- a/jtoon-internal/s3-client/src/main/java/shop/jtoon/service/S3Service.java +++ b/jtoon-internal/s3-client/src/main/java/shop/jtoon/service/S3Service.java @@ -3,8 +3,7 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; -import shop.jtoon.dto.ImageUploadEvent; -import shop.jtoon.dto.UploadImageDto; +import shop.jtoon.dto.ImageUpload; import shop.jtoon.util.S3Manager; @Service @@ -17,12 +16,9 @@ public String uploadUrl(String key) { return s3Manager.uploadUrl(key); } - public String uploadImage(UploadImageDto dto) { - return s3Manager.uploadImage(dto.toKey(), dto.image()); - } - - public String uploadImage(ImageUploadEvent imageUpload) { - return s3Manager.uploadImage(imageUpload.key(), imageUpload.multipartFile()); + public ImageUpload uploadImage(ImageUpload imageUpload) { + s3Manager.uploadImage(imageUpload.key(), imageUpload.data()); + return imageUpload; } public void deleteImage(String imageUrl) { diff --git a/jtoon-internal/s3-client/src/main/java/shop/jtoon/util/S3Manager.java b/jtoon-internal/s3-client/src/main/java/shop/jtoon/util/S3Manager.java index fbb86c2..beb3e45 100644 --- a/jtoon-internal/s3-client/src/main/java/shop/jtoon/util/S3Manager.java +++ b/jtoon-internal/s3-client/src/main/java/shop/jtoon/util/S3Manager.java @@ -1,16 +1,13 @@ package shop.jtoon.util; -import java.io.IOException; +import java.io.ByteArrayInputStream; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; import io.awspring.cloud.s3.ObjectMetadata; import io.awspring.cloud.s3.S3Template; import lombok.RequiredArgsConstructor; -import shop.jtoon.exception.InvalidRequestException; -import shop.jtoon.type.ErrorStatus; @Component @RequiredArgsConstructor @@ -31,19 +28,13 @@ public String uploadUrl(String key) { return CLOUD_FRONT_URL + key; } - public String uploadImage(String key, MultipartFile file) { - try { + public void uploadImage(String key, byte[] file) { s3Template.upload( BUCKET, key, - file.getInputStream(), + new ByteArrayInputStream(file), ObjectMetadata.builder().contentType("image/png").build() ); - - return CLOUD_FRONT_URL + key; - } catch (IOException e) { - throw new InvalidRequestException(ErrorStatus.S3_UPLOAD_FAIL); - } } public void delete(String objectUrl) { diff --git a/jtoon-system/src/main/java/shop/jtoon/type/ErrorStatus.java b/jtoon-system/src/main/java/shop/jtoon/type/ErrorStatus.java index 479053d..1940f07 100644 --- a/jtoon-system/src/main/java/shop/jtoon/type/ErrorStatus.java +++ b/jtoon-system/src/main/java/shop/jtoon/type/ErrorStatus.java @@ -70,7 +70,7 @@ public enum ErrorStatus { EPISODE_OPENED_AT_IS_NULL("회차 공개일자 값이 NULL 입니다."), S3_UPLOAD_FAIL("S3 이미지 업로드에 실패했습니다."), - ; + DATA_NOT_FOUND("데이터 찾기 실패"); private final String message; }