diff --git a/src/main/java/com/stoury/controller/ExceptionController.java b/src/main/java/com/stoury/controller/ExceptionController.java index e355ae9..4dc40ad 100644 --- a/src/main/java/com/stoury/controller/ExceptionController.java +++ b/src/main/java/com/stoury/controller/ExceptionController.java @@ -8,6 +8,7 @@ import com.stoury.exception.feed.FeedCreateException; import com.stoury.exception.feed.FeedSearchException; import com.stoury.exception.feed.FeedUpdateException; +import com.stoury.exception.graphiccontent.GraphicContentsException; import com.stoury.exception.location.GeocodeApiException; import com.stoury.exception.member.MemberCreateException; import com.stoury.exception.member.MemberDeleteException; @@ -39,10 +40,10 @@ public ResponseEntity handle404(RuntimeException ex) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(ex.getMessage())); } - @ExceptionHandler(value = {GeocodeApiException.class}) + @ExceptionHandler(value = {GeocodeApiException.class, GraphicContentsException.class}) public ResponseEntity handle500(RuntimeException ex) { log.error(ex.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of(ex.getMessage())); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of("Some error occur.")); } @ExceptionHandler(value = {NotAuthorizedException.class}) diff --git a/src/main/java/com/stoury/event/EventHandlers.java b/src/main/java/com/stoury/event/EventHandlers.java index 86e9392..cda1bbc 100644 --- a/src/main/java/com/stoury/event/EventHandlers.java +++ b/src/main/java/com/stoury/event/EventHandlers.java @@ -5,42 +5,46 @@ import com.stoury.domain.Member; import com.stoury.domain.Tag; import com.stoury.exception.feed.FeedSearchException; +import com.stoury.exception.graphiccontent.GraphicContentsException; import com.stoury.projection.FeedResponseEntity; import com.stoury.repository.FeedRepository; import com.stoury.service.storage.StorageService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import org.springframework.web.multipart.MultipartFile; import java.nio.file.Paths; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +@Slf4j @Component @RequiredArgsConstructor public class EventHandlers { private final StorageService storageService; private final FeedRepository feedRepository; - @Async - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void onFileSaveEventHandler(GraphicSaveEvent graphicSaveEvent) { - MultipartFile fileToSave = graphicSaveEvent.getFileToSave(); + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + public void onFeedCreationFailEventHandler(GraphicSaveEvent graphicSaveEvent) { String path = graphicSaveEvent.getPath(); - storageService.saveFileAtPath(fileToSave, Paths.get(path)); + storageService.deleteFileAtPath(Paths.get(path)); } @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onFileDeleteEventHandler(GraphicDeleteEvent graphicDeleteEvent) { String path = graphicDeleteEvent.getPath(); - storageService.deleteFileAtPath(Paths.get(path)); + try { + storageService.deleteFileAtPath(Paths.get(path)); + } catch (GraphicContentsException exception) { + log.error(exception.getMessage()); + } } @Async diff --git a/src/main/java/com/stoury/service/FeedService.java b/src/main/java/com/stoury/service/FeedService.java index 6c058ea..7bbcd43 100644 --- a/src/main/java/com/stoury/service/FeedService.java +++ b/src/main/java/com/stoury/service/FeedService.java @@ -17,6 +17,7 @@ import com.stoury.repository.LikeRepository; import com.stoury.repository.MemberRepository; import com.stoury.service.location.LocationService; +import com.stoury.service.storage.StorageService; import com.stoury.utils.FileUtils; import com.stoury.utils.JsonMapper; import com.stoury.utils.SupportedFileType; @@ -32,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -43,6 +45,7 @@ public class FeedService { @Value("${path-prefix}") public String pathPrefix; + private final StorageService storageService; private final FeedRepository feedRepository; private final MemberRepository memberRepository; private final LikeRepository likeRepository; @@ -76,10 +79,12 @@ private List saveGraphicContents(List graphicCont for (int i = 0; i < graphicContents.size(); i++) { MultipartFile file = graphicContents.get(i); - GraphicSaveEvent event = publishNewFileEvent(file); - String contentPath = event.getPath(); + String path = FileUtils.createFilePath(file, pathPrefix); - graphicContentList.add(new GraphicContent(contentPath, i)); + graphicContentList.add(new GraphicContent(path, i)); + + storageService.saveFileAtPath(file, Paths.get(path)); + eventPublisher.publishEvent(new GraphicSaveEvent(this, file, path)); // NOSONAR } return graphicContentList; @@ -97,13 +102,6 @@ private Set getOrCreateTags(Set tagNames) { .collect(Collectors.toSet()); } - private GraphicSaveEvent publishNewFileEvent(MultipartFile file) { - String path = FileUtils.createFilePath(file, pathPrefix); - GraphicSaveEvent event = new GraphicSaveEvent(this, file, path); - eventPublisher.publishEvent(event); // NOSONAR - return event; - } - private void validate(Long writerId, FeedCreateRequest feedCreateRequest, List graphicContents) { if (writerId == null) { throw new FeedCreateException("Writer id cannot be null"); @@ -149,8 +147,10 @@ private FeedResponse toFeedResponse(FeedResponseEntity feedResponseEntity) { SimpleMemberResponse writer = new SimpleMemberResponse(feedResponseEntity.getWriterId(), feedResponseEntity.getWriterUsername()); List graphicContents = - jsonMapper.stringJsonToObject(feedResponseEntity.getGraphicContentPaths(), new TypeReference>() {}); - Set tags = jsonMapper.stringJsonToObject(feedResponseEntity.getTagNames(), new TypeReference>() {}); + jsonMapper.stringJsonToObject(feedResponseEntity.getGraphicContentPaths(), new TypeReference>() { + }); + Set tags = jsonMapper.stringJsonToObject(feedResponseEntity.getTagNames(), new TypeReference>() { + }); LocationResponse location = new LocationResponse(feedResponseEntity.getCity(), feedResponseEntity.getCountry()); @@ -167,7 +167,11 @@ protected FeedResponse updateFeed(Long feedId, FeedUpdateRequest feedUpdateReque feed.updateTags(getOrCreateTags(feedUpdateRequest.tagNames())); feed.deleteSelectedGraphics(feedUpdateRequest.deleteGraphicContentSequence()); - publishDeleteFileEvents(beforeDeleteGraphicContents, feed.getGraphicContents()); + List toDeleteGraphicContents = beforeDeleteGraphicContents.stream() + .filter(graphicContent -> !feed.getGraphicContents().contains(graphicContent)) + .toList(); + + publishDeleteFileEvents(toDeleteGraphicContents); eventPublisher.publishEvent(new FeedResponseUpdateEvent(this, feed.getId())); return FeedResponse.from(feed, likeRepository.getCountByFeedId(feed.getId().toString())); @@ -185,12 +189,10 @@ public FeedResponse updateFeedIfOwner(Long feedId, FeedUpdateRequest feedUpdateR return updateFeed(feedId, feedUpdateRequest); } - private void publishDeleteFileEvents(List beforeDeleteGraphicContents, - List afterDeleteGraphicContents) { - for (GraphicContent beforeDeleteGraphicContent : beforeDeleteGraphicContents) { - if (!afterDeleteGraphicContents.contains(beforeDeleteGraphicContent)) { - eventPublisher.publishEvent(new GraphicDeleteEvent(this, beforeDeleteGraphicContent.getPath())); - } + private void publishDeleteFileEvents(List toDeleteGraphicContents) { + for (GraphicContent toDeleteGraphicContent : toDeleteGraphicContents) { + String path = toDeleteGraphicContent.getPath(); + eventPublisher.publishEvent(new GraphicDeleteEvent(this, path)); } } @@ -198,6 +200,8 @@ protected void deleteFeed(Long feedId) { Feed feed = feedRepository.findById(Objects.requireNonNull(feedId)) .orElseThrow(FeedSearchException::new); feedRepository.delete(feed); + + publishDeleteFileEvents(feed.getGraphicContents()); eventPublisher.publishEvent(new FeedResponseDeleteEvent(this, feedId)); } @@ -215,10 +219,10 @@ public void deleteFeedIfOwner(Long feedId, Long memberId) { @Transactional(readOnly = true) public FeedResponse getFeed(Long feedId) { - Feed feed = feedRepository.findById(Objects.requireNonNull(feedId)) + Long feedIdNonNull = Objects.requireNonNull(feedId); + FeedResponseEntity feedResponseEntity = feedRepository.findFeedResponseById(feedIdNonNull) .orElseThrow(FeedSearchException::new); - long likes = likeRepository.getCountByFeedId(feed.getId().toString()); - return FeedResponse.from(feed, likes); + return toFeedResponse(feedResponseEntity); } } diff --git a/src/test/groovy/com/stoury/service/FeedServiceTest.groovy b/src/test/groovy/com/stoury/service/FeedServiceTest.groovy index d061364..70d45fc 100644 --- a/src/test/groovy/com/stoury/service/FeedServiceTest.groovy +++ b/src/test/groovy/com/stoury/service/FeedServiceTest.groovy @@ -19,12 +19,14 @@ import com.stoury.repository.FeedRepository import com.stoury.repository.LikeRepository import com.stoury.repository.MemberRepository import com.stoury.service.location.LocationService +import com.stoury.service.storage.StorageService import com.stoury.utils.JsonMapper import org.springframework.context.ApplicationEventPublisher import org.springframework.mock.web.MockMultipartFile import spock.lang.Specification class FeedServiceTest extends Specification { + def storageService = Mock(StorageService) def memberRepository = Mock(MemberRepository) def tagService = Mock(TagService) def feedRepository = Mock(FeedRepository) @@ -32,7 +34,7 @@ class FeedServiceTest extends Specification { def eventPublisher = Mock(ApplicationEventPublisher) def locationService = Mock(LocationService) def jsonMapper = new JsonMapper(new ObjectMapper()) - def feedService = new FeedService(feedRepository, memberRepository, likeRepository, + def feedService = new FeedService(storageService, feedRepository, memberRepository, likeRepository, tagService, locationService, eventPublisher, jsonMapper) def writer = Mock(Member) @@ -132,12 +134,13 @@ class FeedServiceTest extends Specification { def "피드 삭제 성공"() { given: def writer = new Member(id: 1) - def feed = new Feed(id: 1, member: writer) + def feed = new Feed(id: 1, member: writer, graphicContents: [new GraphicContent("/gc1", 0)]) feedRepository.findById(_) >> Optional.of(feed) when: feedService.deleteFeedIfOwner(feed.id, writer.id) then: 1 * feedRepository.delete(_) + 1 * eventPublisher.publishEvent(_ as GraphicDeleteEvent) 1 * eventPublisher.publishEvent(_ as FeedResponseDeleteEvent) }