From ab6fd81a28011dd41900038dcedd55db36d48906 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 02:36:43 +0900 Subject: [PATCH 01/22] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../turip/favorite/controller/FavoriteFolderController.java | 2 -- .../turip/favorite/controller/FavoritePlaceController.java | 4 ---- .../stream/service/FavoriteFolderStreamServiceTest.java | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/controller/FavoriteFolderController.java b/backend/turip-app/src/main/java/turip/favorite/controller/FavoriteFolderController.java index 106a0613a..e588d65ad 100644 --- a/backend/turip-app/src/main/java/turip/favorite/controller/FavoriteFolderController.java +++ b/backend/turip-app/src/main/java/turip/favorite/controller/FavoriteFolderController.java @@ -29,7 +29,6 @@ import turip.favorite.controller.dto.response.FavoriteFoldersWithFavoriteStatusResponse; import turip.favorite.controller.dto.response.FavoriteFoldersWithPlaceCountResponse; import turip.favorite.service.FavoriteFolderService; -import turip.favorite.stream.service.FavoriteFolderStreamService; @RestController @RequiredArgsConstructor @@ -38,7 +37,6 @@ public class FavoriteFolderController { private final FavoriteFolderService favoriteFolderService; - private final FavoriteFolderStreamService favoriteFolderStreamService; @Operation( summary = "튜립 생성 api", diff --git a/backend/turip-app/src/main/java/turip/favorite/controller/FavoritePlaceController.java b/backend/turip-app/src/main/java/turip/favorite/controller/FavoritePlaceController.java index 730de5a05..1b24af104 100644 --- a/backend/turip-app/src/main/java/turip/favorite/controller/FavoritePlaceController.java +++ b/backend/turip-app/src/main/java/turip/favorite/controller/FavoritePlaceController.java @@ -31,8 +31,6 @@ import turip.favorite.controller.dto.response.FavoriteFolderWithFavoriteStatusResponse.FavoritePlacesWithPlaceDetailResponse; import turip.favorite.controller.dto.response.FavoritePlaceCountResponse; import turip.favorite.service.FavoritePlaceService; -import turip.favorite.stream.service.ActionType; -import turip.favorite.stream.service.FavoriteFolderStreamService; @RestController @RequiredArgsConstructor @@ -41,7 +39,6 @@ public class FavoritePlaceController { private final FavoritePlaceService favoritePlaceService; - private final FavoriteFolderStreamService favoriteFolderStreamService; @Operation( summary = "튜립 장소 추가 api", @@ -184,7 +181,6 @@ public ResponseEntity create( @RequestParam("placeId") Long placeId ) { FavoritePlaceResponse response = favoritePlaceService.create(account, favoriteFolderId, placeId); - favoriteFolderStreamService.sendFolderUpdateEvents(response.favoriteFolderId(), ActionType.PLACE_ADDED); return ResponseEntity.created(URI.create("/api/v1/turips/places/" + response.id())) .body(response); } diff --git a/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java index df7aa6650..20947469d 100644 --- a/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; @@ -32,6 +31,7 @@ import turip.common.exception.custom.ForbiddenException; import turip.common.exception.custom.NotFoundException; import turip.favorite.domain.FavoriteFolder; +import turip.favorite.domain.event.ActionType; import turip.favorite.service.FavoriteFolderAccountService; import turip.favorite.service.FavoriteFolderService; import turip.util.fixture.AccountFixture; From 26f676b2ef2e040f37751cab1d9a59a639f68c76 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 03:15:32 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{stream/service => domain/event}/ActionType.java | 9 +++++++-- .../dto/response/FolderUpdateStreamResponse.java | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) rename backend/turip-app/src/main/java/turip/favorite/{stream/service => domain/event}/ActionType.java (56%) diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/ActionType.java b/backend/turip-app/src/main/java/turip/favorite/domain/event/ActionType.java similarity index 56% rename from backend/turip-app/src/main/java/turip/favorite/stream/service/ActionType.java rename to backend/turip-app/src/main/java/turip/favorite/domain/event/ActionType.java index a9bd1d4aa..7e9e0c5b7 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/ActionType.java +++ b/backend/turip-app/src/main/java/turip/favorite/domain/event/ActionType.java @@ -1,11 +1,16 @@ -package turip.favorite.stream.service; +package turip.favorite.domain.event; public enum ActionType { // FavoritePlace 관련 PLACE_REORDERED, PLACE_ADDED, PLACE_DELETED, - NAME_CHANGED, + + // FavoriteFolder 관련 + FOLDER_NAME_CHANGED, + FOLDER_DELETED, + FOLDER_PLACE_CHANGED, + // Member 관련 MEMBER_JOINED, MEMBER_EXITED, diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/FolderUpdateStreamResponse.java b/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/FolderUpdateStreamResponse.java index acc45516d..7af8383cd 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/FolderUpdateStreamResponse.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/FolderUpdateStreamResponse.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.time.LocalDateTime; -import turip.favorite.stream.service.ActionType; +import turip.favorite.domain.event.ActionType; public record FolderUpdateStreamResponse( @JsonProperty("turipId") Long favoriteFolderId, From 8ad34de20026ad8c299d720144934e3f57293d56 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 03:24:10 +0900 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20SSE=20EventListener=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FavoriteFolderEventListener.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java new file mode 100644 index 000000000..5a3d33b23 --- /dev/null +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java @@ -0,0 +1,24 @@ +package turip.favorite.stream.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import turip.favorite.domain.event.FavoriteFolderUpdateEvent; + +@Component +@RequiredArgsConstructor +public class FavoriteFolderEventListener { + + private final FavoriteFolderStreamService favoriteFolderStreamService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleFolderUpdate(FavoriteFolderUpdateEvent event) { + favoriteFolderStreamService.sendFolderUpdateEvents( + event.favoriteFolderId(), + event.actionType() + ); + } +} From 009c128c9d20bf23b186ea37938054614ca257df Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 03:24:25 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20config=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/AsyncConfiguration.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java diff --git a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java new file mode 100644 index 000000000..1f14e0ef8 --- /dev/null +++ b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java @@ -0,0 +1,30 @@ +package turip.common.configuration; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@EnableAsync +public class AsyncConfiguration { + + @Bean(name = "sseEventExecutor") + public Executor sseEventExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("SSE-EVT-"); + + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + + executor.initialize(); + return executor; + } +} From 07ce5437da7de8c4a8e5dc1c2815074c13fa776e Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 04:21:50 +0900 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20sse=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=B0=9C=EC=86=A1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/FavoriteFolderUpdateEvent.java | 10 ++++ .../service/FavoriteFolderService.java | 8 ++++ .../service/FavoritePlaceService.java | 47 +++++++++++++++---- 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 backend/turip-app/src/main/java/turip/favorite/domain/event/FavoriteFolderUpdateEvent.java diff --git a/backend/turip-app/src/main/java/turip/favorite/domain/event/FavoriteFolderUpdateEvent.java b/backend/turip-app/src/main/java/turip/favorite/domain/event/FavoriteFolderUpdateEvent.java new file mode 100644 index 000000000..725d97bf7 --- /dev/null +++ b/backend/turip-app/src/main/java/turip/favorite/domain/event/FavoriteFolderUpdateEvent.java @@ -0,0 +1,10 @@ +package turip.favorite.domain.event; + +public record FavoriteFolderUpdateEvent( + Long favoriteFolderId, + ActionType actionType +) { + public static FavoriteFolderUpdateEvent of(Long favoriteFolderId, ActionType actionType) { + return new FavoriteFolderUpdateEvent(favoriteFolderId, actionType); + } +} diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java index 878f28d61..da12bee3c 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import turip.account.domain.Account; @@ -19,6 +20,8 @@ import turip.favorite.controller.dto.response.FavoriteFoldersWithPlaceCountResponse; import turip.favorite.domain.AccountRole; import turip.favorite.domain.FavoriteFolder; +import turip.favorite.domain.event.ActionType; +import turip.favorite.domain.event.FavoriteFolderUpdateEvent; import turip.favorite.repository.FavoriteFolderRepository; import turip.favorite.repository.FavoritePlaceRepository; import turip.place.domain.Place; @@ -32,6 +35,7 @@ public class FavoriteFolderService { private final FavoritePlaceRepository favoritePlaceRepository; private final PlaceRepository placeRepository; private final FavoriteFolderAccountService favoriteFolderAccountService; + private final ApplicationEventPublisher eventPublisher; @Transactional public void createDefaultFavoriteFolder(Account account) { @@ -102,6 +106,8 @@ public FavoriteFolderResponse updateName(Account account, Long favoriteFolderId, validateDuplicatedName(newName, account); favoriteFolder.rename(newName); + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.FOLDER_NAME_CHANGED)); + return FavoriteFolderResponse.of(favoriteFolder, account); } @@ -111,6 +117,8 @@ public void remove(Account account, Long favoriteFolderId) { validateRemovableFolder(account, favoriteFolder); favoritePlaceRepository.deleteAllByFavoriteFolder(favoriteFolder); favoriteFolderRepository.deleteById(favoriteFolderId); + + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.FOLDER_DELETED)); } private void validateRemovableFolder(Account account, FavoriteFolder favoriteFolder) { diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoritePlaceService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoritePlaceService.java index 9ae11a5d3..278bf2158 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoritePlaceService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoritePlaceService.java @@ -2,8 +2,10 @@ import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import turip.account.domain.Account; @@ -18,6 +20,8 @@ import turip.favorite.controller.dto.response.FavoritePlaceCountResponse; import turip.favorite.domain.FavoriteFolder; import turip.favorite.domain.FavoritePlace; +import turip.favorite.domain.event.ActionType; +import turip.favorite.domain.event.FavoriteFolderUpdateEvent; import turip.favorite.repository.FavoriteFolderRepository; import turip.favorite.repository.FavoritePlaceRepository; import turip.place.domain.Place; @@ -31,6 +35,7 @@ public class FavoritePlaceService { private final FavoriteFolderRepository favoriteFolderRepository; private final PlaceRepository placeRepository; private final FavoriteFolderAccountService favoriteFolderAccountService; + private final ApplicationEventPublisher eventPublisher; @Transactional public FavoritePlaceResponse create(Account account, Long favoriteFolderId, Long placeId) { @@ -46,26 +51,30 @@ public FavoritePlaceResponse create(Account account, Long favoriteFolderId, Long FavoritePlace favoritePlace = new FavoritePlace(favoriteFolder, place, maxOrder + 1); FavoritePlace savedFavoritePlace = favoritePlaceRepository.save(favoritePlace); + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.PLACE_ADDED)); + return FavoritePlaceResponse.from(savedFavoritePlace); } @Transactional public List updateFavoriteFolders(Account account, List favoriteFolderIds, - Long placeId - ) { - List requestFavoriteFolderIds = favoriteFolderIds.stream().distinct().toList(); - List favoriteFolders = favoriteFolderRepository.findAllById(requestFavoriteFolderIds); - validateMultiFolder(account, favoriteFolders, requestFavoriteFolderIds); + Long placeId) { + List requestIds = favoriteFolderIds.stream().distinct().toList(); + List requestFolders = favoriteFolderRepository.findAllById(requestIds); + validateMultiFolder(account, requestFolders, requestIds); Place place = getPlaceById(placeId); - List existingFavoritePlaces = favoritePlaceRepository.findAllByPlaceAndAccount(place, account); + List existingPlaces = favoritePlaceRepository.findAllByPlaceAndAccount(place, account); + + Set affectedFolderIds = calculateAffectedFolderIds(existingPlaces, requestIds); - deleteRemovedFavoritePlaces(existingFavoritePlaces, requestFavoriteFolderIds); - List createdFavoritePlaces = createFavoritePlaces(place, existingFavoritePlaces, favoriteFolders, - requestFavoriteFolderIds); + deleteRemovedFavoritePlaces(existingPlaces, requestIds); + List createdPlaces = createFavoritePlaces(place, existingPlaces, requestFolders, requestIds); - return convertToResultResponse(existingFavoritePlaces, createdFavoritePlaces, requestFavoriteFolderIds); + publishFolderUpdateEvents(affectedFolderIds); + + return convertToResultResponse(existingPlaces, createdPlaces, requestIds); } public FavoritePlacesWithPlaceDetailResponse findAllByFolder(Long favoriteFolderId) { @@ -102,6 +111,8 @@ public void updatePlaceOrder(Account account, Long favoriteFolderId, validateFavoritePlaceBelongsToFolder(favoritePlace, favoriteFolder); favoritePlace.updateFavoriteOrder(index + 1); } + + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.PLACE_REORDERED)); } @Transactional @@ -113,6 +124,8 @@ public void remove(Account account, Long favoriteFolderId, Long placeId) { FavoritePlace favoritePlace = getByFavoriteFolderAndPlace(favoriteFolder, place); favoritePlaceRepository.delete(favoritePlace); + + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.PLACE_DELETED)); } private FavoriteFolder getFavoriteFolderById(Long favoriteFolderId) { @@ -130,6 +143,20 @@ private FavoritePlace getFavoritePlaceById(Long favoritePlaceId) { .orElseThrow(() -> new NotFoundException(ErrorTag.FAVORITE_PLACE_NOT_FOUND)); } + private Set calculateAffectedFolderIds(List existingPlaces, List requestIds) { + Set ids = existingPlaces.stream() + .map(fp -> fp.getFavoriteFolder().getId()) + .collect(Collectors.toSet()); + ids.addAll(requestIds); + return ids; + } + + private void publishFolderUpdateEvents(Set folderIds) { + folderIds.forEach(folderId -> + eventPublisher.publishEvent(new FavoriteFolderUpdateEvent(folderId, ActionType.FOLDER_PLACE_CHANGED)) + ); + } + private void deleteRemovedFavoritePlaces(List existingFavoritePlaces, List requestFavoriteFolderIds) { List removedFavoritePlaces = existingFavoritePlaces.stream() From 2c9939877c88e7df4530502c2f6b3e85b5d23cf2 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 04:22:42 +0900 Subject: [PATCH 06/22] =?UTF-8?q?refactor:=20=EC=BB=A8=EB=B2=A4=EC=85=98?= =?UTF-8?q?=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=20=EC=B5=9C=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FavoriteFolderAccountRepository.java | 12 ++++----- .../service/FavoriteFolderAccountService.java | 9 +++++++ .../service/FavoriteFolderStreamService.java | 26 ++++--------------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java index 2c0a34a5c..f3238f1e5 100644 --- a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java +++ b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java @@ -11,16 +11,16 @@ public interface FavoriteFolderAccountRepository extends JpaRepository { - int countByFavoriteFolder(FavoriteFolder favoriteFolder); + @Modifying + @Query("UPDATE FavoriteFolderAccount ffa " + + "SET ffa.account = :newAccount " + + "WHERE ffa.account = :oldAccount") + void updateAccount(@Param("oldAccount") Account oldAccount, @Param("newAccount") Account newAccount); boolean existsByFavoriteFolderAndAccountAndAccountRole(FavoriteFolder favoriteFolder, Account account, AccountRole accountRole); boolean existsByFavoriteFolderAndAccount(FavoriteFolder favoriteFolder, Account account); - @Modifying - @Query("UPDATE FavoriteFolderAccount ffa " + - "SET ffa.account = :newAccount " + - "WHERE ffa.account = :oldAccount") - void updateAccount(@Param("oldAccount") Account oldAccount, @Param("newAccount") Account newAccount); + boolean existsByFavoriteFolderIdAndAccount(Long favoriteFolderId, Account account); } diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java index 8b640767d..5d2ad0205 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java @@ -40,4 +40,13 @@ public void validateMembership(Account account, FavoriteFolder favoriteFolder) { throw new ForbiddenException(ErrorTag.FORBIDDEN); } } + + public void validateMembership(Account account, Long favoriteFolderId) { + boolean isMember = favoriteFolderAccountRepository.existsByFavoriteFolderIdAndAccount(favoriteFolderId, + account); + + if (!isMember) { + throw new ForbiddenException(ErrorTag.FORBIDDEN); + } + } } diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java index 6b1cc00e5..30ee35b09 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java @@ -5,6 +5,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -13,15 +14,15 @@ import turip.account.domain.Member; import turip.common.exception.ErrorTag; import turip.common.exception.custom.InternalServerException; -import turip.favorite.domain.FavoriteFolder; +import turip.favorite.domain.event.ActionType; import turip.favorite.service.FavoriteFolderAccountService; -import turip.favorite.service.FavoriteFolderService; import turip.favorite.stream.controller.dto.response.ConnectStreamResponse; import turip.favorite.stream.controller.dto.response.FolderUpdateStreamResponse; import turip.favorite.stream.controller.dto.response.HeartbeatStreamResponse; @Slf4j @Service +@RequiredArgsConstructor public class FavoriteFolderStreamService { private static final Long DEFAULT_TIMEOUT = 3 * 60 * 1000L; // 3분 @@ -29,24 +30,12 @@ public class FavoriteFolderStreamService { private final Map> emitters = new ConcurrentHashMap<>(); private final Map> heartbeatSchedules = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler; + private final FavoriteFolderAccountService favoriteFolderAccountService; @Value("${sse.heartbeat.interval:30}") private Long heartbeatInterval; - private final FavoriteFolderService favoriteFolderService; - private final FavoriteFolderAccountService favoriteFolderAccountService; - - public FavoriteFolderStreamService( - FavoriteFolderService favoriteFolderService, - FavoriteFolderAccountService favoriteFolderAccountService, - ScheduledExecutorService scheduler - ) { - this.favoriteFolderService = favoriteFolderService; - this.favoriteFolderAccountService = favoriteFolderAccountService; - this.scheduler = scheduler; - } - public SseEmitter createEmitter(Long favoriteFolderId, Member member) { - validateIfMemberJoiningFavoriteFolder(favoriteFolderId, member); + favoriteFolderAccountService.validateMembership(member.getAccount(), favoriteFolderId); SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); String emitterKey = getEmitterKey(favoriteFolderId, member.getId()); @@ -69,11 +58,6 @@ public void sendFolderUpdateEvents(Long favoriteFolderId, ActionType actionType) .forEach(emitter -> sendFolderUpdateEvent(favoriteFolderId, actionType, emitter)); } - private void validateIfMemberJoiningFavoriteFolder(Long favoriteFolderId, Member member) { - FavoriteFolder favoriteFolder = favoriteFolderService.getById(favoriteFolderId); - favoriteFolderAccountService.validateMembership(member.getAccount(), favoriteFolder); - } - private String getEmitterKey(Long favoriteFolderId, Long memberId) { return favoriteFolderId + ":" + memberId + ":" + System.currentTimeMillis(); } From 7da7f5a16c84451a5624890ffe851ab8fa88baf1 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 04:39:11 +0900 Subject: [PATCH 07/22] Merge remote-tracking branch 'origin/feature/#574' into feature/#585 --- .../FavoriteFolderAccountRepository.java | 5 ++++ .../service/FavoriteFolderService.java | 28 ++++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java index 8d85a5a54..7e66bf47c 100644 --- a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java +++ b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java @@ -1,16 +1,21 @@ package turip.favorite.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 org.springframework.data.repository.query.Param; import turip.account.domain.Account; +import turip.account.domain.Member; import turip.favorite.domain.AccountRole; import turip.favorite.domain.FavoriteFolder; import turip.favorite.domain.FavoriteFolderAccount; public interface FavoriteFolderAccountRepository extends JpaRepository { + int countByFavoriteFolder(FavoriteFolder favoriteFolder); + Optional findByFavoriteFolderAndAccount(FavoriteFolder favoriteFolder, Account account); @Query("SELECT m FROM FavoriteFolderAccount ffa " + diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java index 2f05924cc..281562ea8 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java @@ -18,18 +18,15 @@ import turip.favorite.controller.dto.response.FavoriteFolderExitResponse; import turip.favorite.controller.dto.response.FavoriteFolderJoinResponse; import turip.favorite.controller.dto.response.FavoriteFolderMembersResponse; -import turip.favorite.controller.dto.response.FavoriteFolderExitResponse; -import turip.favorite.controller.dto.response.FavoriteFolderJoinResponse; import turip.favorite.controller.dto.response.FavoriteFolderResponse; import turip.favorite.controller.dto.response.FavoriteFolderWithFavoriteStatusResponse; import turip.favorite.controller.dto.response.FavoriteFoldersDetailResponse; import turip.favorite.controller.dto.response.FavoriteFoldersWithFavoriteStatusResponse; -import turip.favorite.controller.dto.response.FavoriteFoldersWithPlaceCountResponse; import turip.favorite.domain.AccountRole; import turip.favorite.domain.FavoriteFolder; +import turip.favorite.domain.FavoriteFolderAccount; import turip.favorite.domain.event.ActionType; import turip.favorite.domain.event.FavoriteFolderUpdateEvent; -import turip.favorite.domain.FavoriteFolderAccount; import turip.favorite.repository.FavoriteFolderRepository; import turip.favorite.repository.FavoritePlaceRepository; import turip.place.domain.Place; @@ -73,21 +70,22 @@ public FavoriteFolderJoinResponse joinMember(Long favoriteFolderId, Member membe return FavoriteFolderJoinResponse.from(favoriteFolderAccount); } - public FavoriteFolder getById(Long favoriteFolderId) { - return favoriteFolderRepository.findById(favoriteFolderId) - .orElseThrow(() -> new NotFoundException(ErrorTag.FAVORITE_FOLDER_NOT_FOUND)); - } - - public FavoriteFoldersWithPlaceCountResponse findAllByAccount(Account account) { - List favoriteFoldersWithPlaceCount = favoriteFolderRepository.findAllByAccountOrderByFavoriteFolderAccountIdAsc( + public FavoriteFoldersDetailResponse findAllByAccount(Account account) { + List favoriteFoldersWithPlaceCount = favoriteFolderRepository.findAllByAccountOrderByFavoriteFolderAccountIdAsc( account).stream() .map(favoriteFolder -> { int placeCount = favoritePlaceRepository.countByFavoriteFolder(favoriteFolder); - return FavoriteFolderWithPlaceCountResponse.of(favoriteFolder, account, placeCount); + int memberCount = favoriteFolderAccountService.countByFavoriteFolder(favoriteFolder); + return FavoriteFolderDetailResponse.of(favoriteFolder, account, placeCount, memberCount); }) .toList(); - return FavoriteFoldersWithPlaceCountResponse.from(favoriteFoldersWithPlaceCount); + return FavoriteFoldersDetailResponse.from(favoriteFoldersWithPlaceCount); + } + + public FavoriteFolder getById(Long favoriteFolderId) { + return favoriteFolderRepository.findById(favoriteFolderId) + .orElseThrow(() -> new NotFoundException(ErrorTag.FAVORITE_FOLDER_NOT_FOUND)); } public FavoriteFoldersWithFavoriteStatusResponse findAllWithFavoriteStatusByAccountId(Account account, @@ -150,9 +148,7 @@ public FavoriteFolderResponse updateName(Account account, Long favoriteFolderId, public void remove(Account account, Long favoriteFolderId) { FavoriteFolder favoriteFolder = getById(favoriteFolderId); validateRemovableFolder(account, favoriteFolder); - favoritePlaceRepository.deleteAllByFavoriteFolder(favoriteFolder); - favoriteFolderRepository.deleteById(favoriteFolderId); - + removeFavoriteFolderWithFavoritePlaces(favoriteFolderId, favoriteFolder); eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.FOLDER_DELETED)); } From 6f5f56cd746bffe056b9ade0aabe6783d0b6462c Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 04:46:20 +0900 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC,=20=EB=82=98=EA=B0=80=EA=B8=B0=20=EC=8B=9C=20SSE=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../turip/favorite/service/FavoriteFolderService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java index 281562ea8..d12f2060e 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java @@ -67,6 +67,9 @@ public FavoriteFolderJoinResponse joinMember(Long favoriteFolderId, Member membe FavoriteFolderAccount favoriteFolderAccount = favoriteFolderAccountService.findOrCreate(favoriteFolder, member.getAccount()); + + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.MEMBER_JOINED)); + return FavoriteFolderJoinResponse.from(favoriteFolderAccount); } @@ -157,16 +160,18 @@ public FavoriteFolderExitResponse exitFolder(Account account, Long favoriteFolde FavoriteFolder favoriteFolder = getByIdWithLock(favoriteFolderId); validateShareAndCustomFolder(favoriteFolder); favoriteFolderAccountService.validateMembership(account, favoriteFolder); + favoriteFolderAccountService.deleteByFavoriteFolderAndAccount(favoriteFolder, account); - boolean isDeleted = false; int remainingMemberCount = favoriteFolderAccountService.countByFavoriteFolder(favoriteFolder); if (remainingMemberCount == 0) { removeFavoriteFolderWithFavoritePlaces(favoriteFolderId, favoriteFolder); - isDeleted = true; + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.FOLDER_DELETED)); + return FavoriteFolderExitResponse.of(true); } - return FavoriteFolderExitResponse.of(isDeleted); + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.MEMBER_EXITED)); + return FavoriteFolderExitResponse.of(false); } private void validateRemovableFolder(Account account, FavoriteFolder favoriteFolder) { From 3d70c3c7829f9f478aa4ba5179afac7b04ba279b Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 05:17:51 +0900 Subject: [PATCH 09/22] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20SSE=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FavoriteFolderServiceTest.java | 62 ++++++++++++----- .../service/FavoritePlaceServiceTest.java | 68 +++++++++++++++++-- 2 files changed, 105 insertions(+), 25 deletions(-) diff --git a/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java index 4e67174a6..6db09b4ae 100644 --- a/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java @@ -19,6 +19,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import turip.account.domain.Account; import turip.account.domain.Member; import turip.account.domain.Role; @@ -38,6 +39,8 @@ import turip.favorite.domain.AccountRole; import turip.favorite.domain.FavoriteFolder; import turip.favorite.domain.FavoriteFolderAccount; +import turip.favorite.domain.event.ActionType; +import turip.favorite.domain.event.FavoriteFolderUpdateEvent; import turip.favorite.repository.FavoriteFolderRepository; import turip.favorite.repository.FavoritePlaceRepository; import turip.place.domain.Place; @@ -64,6 +67,9 @@ class FavoriteFolderServiceTest { @Mock private PlaceRepository placeRepository; + @Mock + private ApplicationEventPublisher eventPublisher; + @DisplayName("기본 장소 찜 폴더 생성 테스트") @Nested class CreateDefaultFavoriteFolder { @@ -238,7 +244,7 @@ void findAllWithFavoriteStatusByDeviceId2() { @Nested class UpdateName { - @DisplayName("찜 폴더의 이름을 변경할 수 있다") + @DisplayName("찜 폴더의 이름을 변경하고 실시간 알림 이벤트를 발행한다") @Test void updateName1() { // given @@ -262,7 +268,9 @@ void updateName1() { () -> assertThat(response.id()).isEqualTo(folderId), () -> assertThat(response.name()).isEqualTo(newName), () -> assertThat(response.accountId()).isEqualTo(accountId), - () -> assertThat(response.isDefault()).isFalse() + () -> assertThat(response.isDefault()).isFalse(), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(folderId, ActionType.FOLDER_NAME_CHANGED)) ); } @@ -402,9 +410,11 @@ void findMembersById1() { var response = favoriteFolderService.findMembersById(turipId, account); // then - assertThat(response.members()).hasSize(2); - assertThat(response.members().get(0).nickname()).isEqualTo("계정1"); - assertThat(response.members().get(1).nickname()).isEqualTo("계정2"); + assertAll( + () -> assertThat(response.members()).hasSize(2), + () -> assertThat(response.members().get(0).nickname()).isEqualTo("계정1"), + () -> assertThat(response.members().get(1).nickname()).isEqualTo("계정2") + ); } @DisplayName("찜폴더가 존재하지 않는 경우 NotFoundException을 발생시킨다") @@ -447,7 +457,7 @@ void findMembersById3() { @Nested class JoinFavoriteFolder { - @DisplayName("공유 찜폴더에 참여할 수 있다") + @DisplayName("공유 찜폴더에 참여하고 멤버 참여 이벤트를 발행한다") @Test void joinFavoriteFolder1() { // given @@ -475,7 +485,9 @@ void joinFavoriteFolder1() { () -> assertThat(response.id()).isEqualTo(favoriteFolderAccountId), () -> assertThat(response.favoriteFolderId()).isEqualTo(turipId), () -> assertThat(response.isShared()).isTrue(), - () -> assertThat(response.accountId()).isEqualTo(accountId) + () -> assertThat(response.accountId()).isEqualTo(accountId), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(turipId, ActionType.MEMBER_JOINED)) ); } @@ -535,7 +547,7 @@ void joinFavoriteFolder4() { @Nested class Remove { - @DisplayName("장소 찜 폴더를 삭제할 수 있다") + @DisplayName("장소 찜 폴더를 삭제하고 폴더 삭제 이벤트를 발행한다") @Test void remove1() { // given @@ -553,8 +565,12 @@ void remove1() { favoriteFolderService.remove(member, folderId); // then - verify(favoritePlaceRepository).deleteAllByFavoriteFolder(favoriteFolder); - verify(favoriteFolderRepository).deleteById(folderId); + assertAll( + () -> verify(favoritePlaceRepository).deleteAllByFavoriteFolder(favoriteFolder), + () -> verify(favoriteFolderRepository).deleteById(folderId), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(folderId, ActionType.FOLDER_DELETED)) + ); } @DisplayName("favoriteFolderId에 대한 회원이 존재하지 않는 경우 NotFoundException을 발생시킨다") @@ -639,7 +655,7 @@ void remove6() { @Nested class ExitFolder { - @DisplayName("공유 찜폴더에서 나갈 수 있다 (마지막 참여자가 아닌 경우)") + @DisplayName("마지막 참여자가 아닌 경우, 참여 정보만 삭제하고 멤버 탈퇴 이벤트를 발행한다") @Test void exitFolder1() { // given @@ -658,11 +674,16 @@ void exitFolder1() { FavoriteFolderExitResponse response = favoriteFolderService.exitFolder(account, folderId); // then - assertThat(response.isDeleted()).isFalse(); - verify(favoriteFolderAccountService).deleteByFavoriteFolderAndAccount(favoriteFolder, account); + assertAll( + () -> assertThat(response.isDeleted()).isFalse(), + () -> verify(favoriteFolderAccountService).deleteByFavoriteFolderAndAccount(favoriteFolder, + account), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(folderId, ActionType.MEMBER_EXITED)) + ); } - @DisplayName("공유 찜폴더에서 나갈 수 있다 (마지막 참여자인 경우 폴더 삭제)") + @DisplayName("마지막 참여자인 경우, 폴더를 삭제하고 폴더 삭제 이벤트를 발행한다") @Test void exitFolder2() { // given @@ -681,10 +702,15 @@ void exitFolder2() { FavoriteFolderExitResponse response = favoriteFolderService.exitFolder(account, folderId); // then - assertThat(response.isDeleted()).isTrue(); - verify(favoriteFolderAccountService).deleteByFavoriteFolderAndAccount(favoriteFolder, account); - verify(favoritePlaceRepository).deleteAllByFavoriteFolder(favoriteFolder); - verify(favoriteFolderRepository).deleteById(folderId); + assertAll( + () -> assertThat(response.isDeleted()).isTrue(), + () -> verify(favoriteFolderAccountService).deleteByFavoriteFolderAndAccount(favoriteFolder, + account), + () -> verify(favoritePlaceRepository).deleteAllByFavoriteFolder(favoriteFolder), + () -> verify(favoriteFolderRepository).deleteById(folderId), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(folderId, ActionType.FOLDER_DELETED)) + ); } @DisplayName("찜폴더가 존재하지 않는 경우 NotFoundException을 발생시킨다") diff --git a/backend/turip-app/src/test/java/turip/favorite/service/FavoritePlaceServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/service/FavoritePlaceServiceTest.java index d0c314d49..7ec2514dc 100644 --- a/backend/turip-app/src/test/java/turip/favorite/service/FavoritePlaceServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/service/FavoritePlaceServiceTest.java @@ -4,8 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; import java.util.List; import java.util.Optional; @@ -16,6 +18,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import turip.account.domain.Account; import turip.account.domain.Role; import turip.common.exception.ErrorTag; @@ -29,6 +32,8 @@ import turip.favorite.controller.dto.response.FavoritePlaceCountResponse; import turip.favorite.domain.FavoriteFolder; import turip.favorite.domain.FavoritePlace; +import turip.favorite.domain.event.ActionType; +import turip.favorite.domain.event.FavoriteFolderUpdateEvent; import turip.favorite.repository.FavoriteFolderRepository; import turip.favorite.repository.FavoritePlaceRepository; import turip.place.domain.Place; @@ -54,11 +59,14 @@ class FavoritePlaceServiceTest { @Mock private FavoriteFolderAccountService favoriteFolderAccountService; + @Mock + private ApplicationEventPublisher eventPublisher; + @DisplayName("장소 찜 생성 테스트") @Nested class Create { - @DisplayName("장소 찜을 생성할 수 있다") + @DisplayName("장소 찜을 생성하고 알림 이벤트를 발행한다") @Test void create1() { // given @@ -86,7 +94,9 @@ void create1() { assertAll( () -> assertThat(response.id()).isEqualTo(favoriteFolderId), () -> assertThat(response.favoriteFolderId()).isEqualTo(favoriteFolderId), - () -> assertThat(response.placeId()).isEqualTo(placeId) + () -> assertThat(response.placeId()).isEqualTo(placeId), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.PLACE_ADDED)) ); } @@ -247,12 +257,13 @@ void countByAccount1() { @Nested class UpdatePlaceOrder { - @DisplayName("장소 찜 순서를 변경할 수 있다") + @DisplayName("장소 찜 순서를 변경하고 알림 이벤트를 발행한다") @Test void updatePlaceOrder1() { // given Account account = AccountFixture.createUser(); - FavoriteFolder favoriteFolder = FavoriteFolderFixture.createDefaultFolder(); + Long favoriteFolderId = 1L; + FavoriteFolder favoriteFolder = FavoriteFolderFixture.createCustomFolderWithId(favoriteFolderId, "폴더"); FavoritePlace firstFavoritePlace = new FavoritePlace(1L, favoriteFolder, null, 1); FavoritePlace secondFavoritePlace = new FavoritePlace(2L, favoriteFolder, null, 2); given(favoriteFolderRepository.findById(favoriteFolder.getId())) @@ -269,7 +280,9 @@ void updatePlaceOrder1() { // then assertAll( () -> assertThat(firstFavoritePlace.getFavoriteOrder()).isEqualTo(2), - () -> assertThat(secondFavoritePlace.getFavoriteOrder()).isEqualTo(1) + () -> assertThat(secondFavoritePlace.getFavoriteOrder()).isEqualTo(1), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.PLACE_REORDERED)) ); } @@ -321,11 +334,48 @@ void updatePlaceOrder3() { } } + @DisplayName("여러 폴더의 장소 업데이트 테스트") + @Nested + class UpdateFavoriteFolders { + + @Test + @DisplayName("여러 폴더의 장소를 업데이트하면 영향을 받은 모든 폴더에 이벤트를 발행한다") + void updateFavoriteFolders_Success() { + // given + Account account = AccountFixture.createUser(); + Long placeId = 1L; + Place place = new Place(placeId, "장소", "url", "주소", 1, 1); + + // 기존 폴더: 1번 / 요청 폴더: 2번 -> 영향을 받은 폴더: 1, 2번 + Long existingFolderId = 1L; + Long requestFolderId = 2L; + FavoriteFolder existingFolder = FavoriteFolderFixture.createCustomFolderWithId(existingFolderId, "기존"); + FavoriteFolder requestFolder = FavoriteFolderFixture.createCustomFolderWithId(requestFolderId, "새요청"); + + FavoritePlace existingPlace = new FavoritePlace(10L, existingFolder, place, 1); + + given(placeRepository.findById(placeId)).willReturn(Optional.of(place)); + given(favoriteFolderRepository.findAllById(any())).willReturn(List.of(requestFolder)); + given(favoritePlaceRepository.findAllByPlaceAndAccount(place, account)).willReturn(List.of(existingPlace)); + + // when + favoritePlaceService.updateFavoriteFolders(account, List.of(requestFolderId), placeId); + + // then + assertAll( + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(existingFolderId, ActionType.FOLDER_PLACE_CHANGED)), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(requestFolderId, ActionType.FOLDER_PLACE_CHANGED)) + ); + } + } + @DisplayName("장소 찜 삭제 테스트") @Nested class Remove { - @DisplayName("장소 찜을 삭제할 수 있다") + @DisplayName("장소 찜을 삭제하고 알림 이벤트를 발행한다") @Test void remove1() { // given @@ -345,7 +395,11 @@ void remove1() { .willReturn(Optional.of(favoritePlace)); // when & then - assertDoesNotThrow(() -> favoritePlaceService.remove(account, favoriteFolderId, placeId)); + assertAll( + () -> assertDoesNotThrow(() -> favoritePlaceService.remove(account, favoriteFolderId, placeId)), + () -> verify(eventPublisher).publishEvent( + FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.PLACE_DELETED)) + ); } @DisplayName("폴더 소유자의 기기id와 요청자의 기기id가 같지 않은 경우 ForbiddenException을 발생시킨다") From 36744108fb586c040c3659453018c687e4100459 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 17 Feb 2026 06:03:10 +0900 Subject: [PATCH 10/22] =?UTF-8?q?feat:=20SSE=EC=97=90=EC=84=9C=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FavoriteFolderAccountRepository.java | 8 ++- .../service/FavoriteFolderAccountService.java | 4 ++ .../response/MemberUpdateStreamResponse.java | 30 +++++++++ .../service/FavoriteFolderEventListener.java | 18 ++++-- .../service/FavoriteFolderStreamService.java | 63 +++++++++++++++---- 5 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/MemberUpdateStreamResponse.java diff --git a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java index 7e66bf47c..170e46f11 100644 --- a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java +++ b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java @@ -15,7 +15,7 @@ public interface FavoriteFolderAccountRepository extends JpaRepository { int countByFavoriteFolder(FavoriteFolder favoriteFolder); - + Optional findByFavoriteFolderAndAccount(FavoriteFolder favoriteFolder, Account account); @Query("SELECT m FROM FavoriteFolderAccount ffa " + @@ -24,6 +24,12 @@ public interface FavoriteFolderAccountRepository extends JpaRepository findMembersByFavoriteFolder(@Param("favoriteFolder") FavoriteFolder favoriteFolder); + @Query("SELECT ffa FROM FavoriteFolderAccount ffa " + + "JOIN ffa.account a " + + "JOIN Member m ON m.account.id = a.id " + + "WHERE ffa.favoriteFolder.id = :favoriteFolderId") + List findMembersByFavoriteFolderId(@Param("favoriteFolderId") Long favoriteFolderId); + @Modifying @Query("UPDATE FavoriteFolderAccount ffa " + "SET ffa.account = :newAccount " + diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java index 5897b3fe4..5c5770fd4 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java @@ -62,6 +62,10 @@ public List findMembersByFavoriteFolder(FavoriteFolder favoriteFolder) { return favoriteFolderAccountRepository.findMembersByFavoriteFolder(favoriteFolder); } + public List findMembersByFavoriteFolder(Long favoriteFolderId) { + return favoriteFolderAccountRepository.findMembersByFavoriteFolderId(favoriteFolderId); + } + @Transactional public void deleteByFavoriteFolderAndAccount(FavoriteFolder favoriteFolder, Account account) { FavoriteFolderAccount favoriteFolderAccount = favoriteFolderAccountRepository.findByFavoriteFolderAndAccount( diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/MemberUpdateStreamResponse.java b/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/MemberUpdateStreamResponse.java new file mode 100644 index 000000000..117a0807c --- /dev/null +++ b/backend/turip-app/src/main/java/turip/favorite/stream/controller/dto/response/MemberUpdateStreamResponse.java @@ -0,0 +1,30 @@ +package turip.favorite.stream.controller.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; +import java.util.List; +import turip.account.domain.Member; +import turip.favorite.domain.event.ActionType; + +public record MemberUpdateStreamResponse( + @JsonProperty("turipId") Long favoriteFolderId, + @JsonProperty("action") ActionType actionType, + int memberCount, + List members, + LocalDateTime timestamp +) { + public static MemberUpdateStreamResponse of(Long favoriteFolderId, ActionType actionType, List members) { + return new MemberUpdateStreamResponse( + favoriteFolderId, + actionType, + members.size(), + members.stream() + .map(member -> new MemberNicknameResponse(member.getAccount().getNickname())) + .toList(), + LocalDateTime.now() + ); + } + + public record MemberNicknameResponse(String nickname) { + } +} diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java index 5a3d33b23..5095debe9 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; +import turip.favorite.domain.event.ActionType; import turip.favorite.domain.event.FavoriteFolderUpdateEvent; @Component @@ -13,12 +14,17 @@ public class FavoriteFolderEventListener { private final FavoriteFolderStreamService favoriteFolderStreamService; - @Async + @Async("sseEventExecutor") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void handleFolderUpdate(FavoriteFolderUpdateEvent event) { - favoriteFolderStreamService.sendFolderUpdateEvents( - event.favoriteFolderId(), - event.actionType() - ); + public void handleFavoriteFolderUpdateEvent(FavoriteFolderUpdateEvent event) { + Long folderId = event.favoriteFolderId(); + ActionType action = event.actionType(); + + if (action == ActionType.MEMBER_JOINED || action == ActionType.MEMBER_EXITED) { + favoriteFolderStreamService.sendMemberUpdateEvents(folderId, action); + return; + } + + favoriteFolderStreamService.sendFolderUpdateEvents(folderId, action); } } diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java index 30ee35b09..59f0ab1e3 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java @@ -1,5 +1,8 @@ package turip.favorite.stream.service; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; @@ -19,6 +22,7 @@ import turip.favorite.stream.controller.dto.response.ConnectStreamResponse; import turip.favorite.stream.controller.dto.response.FolderUpdateStreamResponse; import turip.favorite.stream.controller.dto.response.HeartbeatStreamResponse; +import turip.favorite.stream.controller.dto.response.MemberUpdateStreamResponse; @Slf4j @Service @@ -47,15 +51,38 @@ public SseEmitter createEmitter(Long favoriteFolderId, Member member) { } public void sendFolderUpdateEvents(Long favoriteFolderId, ActionType actionType) { + Map folderEmitters = validateAndGetFolderEmitters( + favoriteFolderId); + FolderUpdateStreamResponse response = FolderUpdateStreamResponse.of(favoriteFolderId, actionType); + + log.info(SSE_LOG_PREFIX + "폴더 업데이트 이벤트 전송 시작, folderId: {}, 연결된 사용자 수: {}", favoriteFolderId, + folderEmitters.size()); + + new ArrayList<>(folderEmitters.values()).forEach(emitter -> + sendFolderUpdateEvent(favoriteFolderId, response, emitter) + ); + } + + public void sendMemberUpdateEvents(Long favoriteFolderId, ActionType actionType) { + Map folderEmitters = validateAndGetFolderEmitters( + favoriteFolderId); + List members = favoriteFolderAccountService.findMembersByFavoriteFolder(favoriteFolderId); + MemberUpdateStreamResponse response = MemberUpdateStreamResponse.of(favoriteFolderId, actionType, members); + + log.info(SSE_LOG_PREFIX + "멤버 업데이트 이벤트 전송 시작, folderId: {}, action: {}", favoriteFolderId, actionType); + + new ArrayList<>(folderEmitters.values()).forEach(emitter -> + sendMemberUpdateEvent(favoriteFolderId, response, emitter) + ); + } + + private Map validateAndGetFolderEmitters(Long favoriteFolderId) { Map folderEmitters = emitters.get(favoriteFolderId); if (folderEmitters == null || folderEmitters.isEmpty()) { log.info(SSE_LOG_PREFIX + "폴더에 연결된 사용자 없음, folderId: {}", favoriteFolderId); - return; + return Collections.emptyMap(); } - log.info(SSE_LOG_PREFIX + "폴더 업데이트 이벤트 전송 시작, folderId: {}, 연결된 사용자 수: {}", favoriteFolderId, - folderEmitters.size()); - folderEmitters.values() - .forEach(emitter -> sendFolderUpdateEvent(favoriteFolderId, actionType, emitter)); + return folderEmitters; } private String getEmitterKey(Long favoriteFolderId, Long memberId) { @@ -111,10 +138,8 @@ private void sendConnectEvent(Long favoriteFolderId, SseEmitter emitter, String } } - private void sendFolderUpdateEvent(Long favoriteFolderId, ActionType actionType, SseEmitter emitter) { + private void sendFolderUpdateEvent(Long favoriteFolderId, FolderUpdateStreamResponse response, SseEmitter emitter) { try { - FolderUpdateStreamResponse response = FolderUpdateStreamResponse.of(favoriteFolderId, actionType); - SseEventBuilder event = SseEmitter.event() .id(String.valueOf(System.currentTimeMillis())) .name(StreamEventType.FOLDER_UPDATE.getName()) @@ -131,6 +156,24 @@ private void sendFolderUpdateEvent(Long favoriteFolderId, ActionType actionType, } } + private void sendMemberUpdateEvent(Long favoriteFolderId, MemberUpdateStreamResponse response, SseEmitter emitter) { + try { + SseEventBuilder event = SseEmitter.event() + .id(String.valueOf(System.currentTimeMillis())) + .name(StreamEventType.MEMBER_UPDATE.getName()) + .data(response); + + emitter.send(event); + } catch (Exception e) { + try { + log.error(SSE_LOG_PREFIX + "멤버 업데이트 전송 실패, folderId: {}", favoriteFolderId, e); + emitter.completeWithError(e); + } catch (Exception completeException) { + log.error(SSE_LOG_PREFIX + "completeWithError 실패, folderId: {}", favoriteFolderId, completeException); + } + } + } + private void removeEmitter(Long favoriteFolderId, String emitterKey) { emitters.computeIfPresent(favoriteFolderId, (key, folderEmitters) -> { folderEmitters.remove(emitterKey); @@ -142,10 +185,6 @@ private void removeEmitter(Long favoriteFolderId, String emitterKey) { }); } - private void sendMemberUpdateEvent() { - // TODO: 구현 필요 - } - private void startHeartbeat(String emitterKey, SseEmitter emitter) { ScheduledFuture scheduledFuture = scheduler.scheduleAtFixedRate( () -> sendHeartbeatEvent(emitter), From e2c4a60276255926697638fadb6d55184ee5883c Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 00:32:37 +0900 Subject: [PATCH 11/22] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FavoriteFolderAccountService.java | 8 ++++---- .../service/FavoriteFolderService.java | 18 ++++++++++++++---- .../service/FavoriteFolderStreamService.java | 4 +++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java index 5c5770fd4..3f178db07 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java @@ -58,10 +58,6 @@ public void validateMembership(Account account, Long favoriteFolderId) { } } - public List findMembersByFavoriteFolder(FavoriteFolder favoriteFolder) { - return favoriteFolderAccountRepository.findMembersByFavoriteFolder(favoriteFolder); - } - public List findMembersByFavoriteFolder(Long favoriteFolderId) { return favoriteFolderAccountRepository.findMembersByFavoriteFolderId(favoriteFolderId); } @@ -77,4 +73,8 @@ public void deleteByFavoriteFolderAndAccount(FavoriteFolder favoriteFolder, Acco public int countByFavoriteFolder(FavoriteFolder favoriteFolder) { return favoriteFolderAccountRepository.countByFavoriteFolder(favoriteFolder); } + + public boolean isMember(FavoriteFolder favoriteFolder, Account account) { + return favoriteFolderAccountRepository.existsByFavoriteFolderAndAccount(favoriteFolder, account); + } } diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java index d12f2060e..cf244c3a6 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java @@ -122,10 +122,9 @@ public FavoriteFolderDetailResponse findById(Long favoriteFolderId, Account acco } public FavoriteFolderMembersResponse findMembersById(Long favoriteFolderId, Account account) { - FavoriteFolder favoriteFolder = getById(favoriteFolderId); - favoriteFolderAccountService.validateMembership(account, favoriteFolder); - - List members = favoriteFolderAccountService.findMembersByFavoriteFolder(favoriteFolder); + validateFolderExists(favoriteFolderId); + favoriteFolderAccountService.validateMembership(account, favoriteFolderId); + List members = favoriteFolderAccountService.findMembersByFavoriteFolder(favoriteFolderId); return FavoriteFolderMembersResponse.of(members); } @@ -174,6 +173,17 @@ public FavoriteFolderExitResponse exitFolder(Account account, Long favoriteFolde return FavoriteFolderExitResponse.of(false); } + public void validateFolderExists(Long favoriteFolderId) { + if (!favoriteFolderRepository.existsById(favoriteFolderId)) { + throw new NotFoundException(ErrorTag.FAVORITE_FOLDER_NOT_FOUND); + } + } + + public void validateFolderMembership(Long favoriteFolderId, Member member) { + FavoriteFolder favoriteFolder = getById(favoriteFolderId); + favoriteFolderAccountService.validateMembership(member.getAccount(), favoriteFolder); + } + private void validateRemovableFolder(Account account, FavoriteFolder favoriteFolder) { if (favoriteFolder.isDefault()) { throw new BadRequestException(ErrorTag.DEFAULT_FAVORITE_FOLDER_OPERATION_NOT_ALLOWED); diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java index 59f0ab1e3..d945a388c 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java @@ -19,6 +19,7 @@ import turip.common.exception.custom.InternalServerException; import turip.favorite.domain.event.ActionType; import turip.favorite.service.FavoriteFolderAccountService; +import turip.favorite.service.FavoriteFolderService; import turip.favorite.stream.controller.dto.response.ConnectStreamResponse; import turip.favorite.stream.controller.dto.response.FolderUpdateStreamResponse; import turip.favorite.stream.controller.dto.response.HeartbeatStreamResponse; @@ -34,12 +35,13 @@ public class FavoriteFolderStreamService { private final Map> emitters = new ConcurrentHashMap<>(); private final Map> heartbeatSchedules = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler; + private final FavoriteFolderService favoriteFolderService; private final FavoriteFolderAccountService favoriteFolderAccountService; @Value("${sse.heartbeat.interval:30}") private Long heartbeatInterval; public SseEmitter createEmitter(Long favoriteFolderId, Member member) { - favoriteFolderAccountService.validateMembership(member.getAccount(), favoriteFolderId); + favoriteFolderService.validateFolderMembership(favoriteFolderId, member); SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); String emitterKey = getEmitterKey(favoriteFolderId, member.getId()); From 7c924f99f82e7cacf7604fe7809659aecd6a7101 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 00:32:56 +0900 Subject: [PATCH 12/22] =?UTF-8?q?refactor:=20=EC=83=88=EB=A1=AD=EA=B2=8C?= =?UTF-8?q?=20=EC=B0=B8=EC=97=AC=ED=95=9C=20=ED=9A=8C=EC=9B=90=EC=9D=BC=20?= =?UTF-8?q?=EB=95=8C=EB=A7=8C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/turip/favorite/service/FavoriteFolderService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java index cf244c3a6..b56a70dc9 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java @@ -65,10 +65,12 @@ public FavoriteFolderJoinResponse joinMember(Long favoriteFolderId, Member membe FavoriteFolder favoriteFolder = getById(favoriteFolderId); validateShareAndCustomFolder(favoriteFolder); + boolean isAlreadyJoined = favoriteFolderAccountService.isMember(favoriteFolder, member.getAccount()); FavoriteFolderAccount favoriteFolderAccount = favoriteFolderAccountService.findOrCreate(favoriteFolder, member.getAccount()); - - eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.MEMBER_JOINED)); + if (!isAlreadyJoined) { + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.MEMBER_JOINED)); + } return FavoriteFolderJoinResponse.from(favoriteFolderAccount); } From bbb415709ebd68a5e2d263b103e8da78240c8a0e Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 00:41:56 +0900 Subject: [PATCH 13/22] =?UTF-8?q?test:=20=EC=BD=94=EB=93=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FavoriteFolderServiceTest.java | 19 ++++++++++--------- .../FavoriteFolderStreamServiceTest.java | 7 +------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java index 6db09b4ae..0ec1a075b 100644 --- a/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java @@ -33,6 +33,7 @@ import turip.favorite.controller.dto.request.FavoriteFolderRequest; import turip.favorite.controller.dto.response.FavoriteFolderExitResponse; import turip.favorite.controller.dto.response.FavoriteFolderJoinResponse; +import turip.favorite.controller.dto.response.FavoriteFolderMembersResponse; import turip.favorite.controller.dto.response.FavoriteFolderResponse; import turip.favorite.controller.dto.response.FavoriteFoldersDetailResponse; import turip.favorite.controller.dto.response.FavoriteFoldersWithFavoriteStatusResponse; @@ -394,20 +395,20 @@ void findMembersById1() { // given Long turipId = 1L; Account account = AccountFixture.createUser(); - FavoriteFolder favoriteFolder = FavoriteFolderFixture.createCustomFolderWithId(turipId, "함께 튜립"); Account memberAccount1 = new Account(2L, Role.USER, "계정1"); Account memberAccount2 = new Account(3L, Role.USER, "계정2"); Member member1 = new Member(1L, memberAccount1, "test1@example.com", false); Member member2 = new Member(2L, memberAccount2, "test2@example.com", false); - given(favoriteFolderRepository.findById(turipId)) - .willReturn(Optional.of(favoriteFolder)); - given(favoriteFolderAccountService.findMembersByFavoriteFolder(favoriteFolder)) + given(favoriteFolderRepository.existsById(turipId)) + .willReturn(true); + given(favoriteFolderAccountService.findMembersByFavoriteFolder(turipId)) .willReturn(List.of(member1, member2)); // when - var response = favoriteFolderService.findMembersById(turipId, account); + FavoriteFolderMembersResponse response = favoriteFolderService.findMembersById(turipId, + account); // then assertAll( @@ -424,8 +425,8 @@ void findMembersById2() { Long nonExistentTuripId = 999L; Account account = AccountFixture.createUser(); - given(favoriteFolderRepository.findById(nonExistentTuripId)) - .willReturn(Optional.empty()); + given(favoriteFolderRepository.existsById(nonExistentTuripId)) + .willReturn(false); // when & then assertThatThrownBy(() -> favoriteFolderService.findMembersById(nonExistentTuripId, account)) @@ -441,8 +442,8 @@ void findMembersById3() { Account nonMemberAccount = AccountFixture.createUser(); FavoriteFolder favoriteFolder = FavoriteFolderFixture.createCustomFolderWithId(turipId, "비공개 튜립"); - given(favoriteFolderRepository.findById(turipId)) - .willReturn(Optional.of(favoriteFolder)); + given(favoriteFolderRepository.existsById(turipId)) + .willReturn(true); willThrow(new ForbiddenException(ErrorTag.FORBIDDEN)) .given(favoriteFolderAccountService).validateMembership(nonMemberAccount, favoriteFolder); diff --git a/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java index 20947469d..e39456101 100644 --- a/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java @@ -30,12 +30,10 @@ import turip.common.exception.ErrorTag; import turip.common.exception.custom.ForbiddenException; import turip.common.exception.custom.NotFoundException; -import turip.favorite.domain.FavoriteFolder; import turip.favorite.domain.event.ActionType; import turip.favorite.service.FavoriteFolderAccountService; import turip.favorite.service.FavoriteFolderService; import turip.util.fixture.AccountFixture; -import turip.util.fixture.FavoriteFolderFixture; import turip.util.fixture.MemberFixture; @ExtendWith(MockitoExtension.class) @@ -81,12 +79,9 @@ void createEmitter2() { Long folderId = 1L; Account account = AccountFixture.createUser(); Member member = MemberFixture.createCustomMember(account, "test@example.com", false); - FavoriteFolder favoriteFolder = FavoriteFolderFixture.createCustomFolderWithId(folderId, "테스트 폴더"); - given(favoriteFolderService.getById(folderId)).willReturn(favoriteFolder); willThrow(new ForbiddenException(ErrorTag.FORBIDDEN)) - .given(favoriteFolderAccountService).validateMembership(account, favoriteFolder); - + .given(favoriteFolderAccountService).validateMembership(account, folderId); // when & then assertThatThrownBy(() -> favoriteFolderStreamService.createEmitter(folderId, member)) .isInstanceOf(ForbiddenException.class) From b4b9857d8e43f1f405e9409bde1aa36c5ff80995 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 01:12:48 +0900 Subject: [PATCH 14/22] =?UTF-8?q?test:=20=EC=BD=94=EB=93=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FavoriteFolderAccountRepository.java | 2 +- .../service/FavoriteFolderStreamService.java | 12 ++++- .../service/FavoriteFolderServiceTest.java | 44 ++++++++++++++++--- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java index 170e46f11..ae912e512 100644 --- a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java +++ b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java @@ -24,7 +24,7 @@ public interface FavoriteFolderAccountRepository extends JpaRepository findMembersByFavoriteFolder(@Param("favoriteFolder") FavoriteFolder favoriteFolder); - @Query("SELECT ffa FROM FavoriteFolderAccount ffa " + + @Query("SELECT m FROM FavoriteFolderAccount ffa " + "JOIN ffa.account a " + "JOIN Member m ON m.account.id = a.id " + "WHERE ffa.favoriteFolder.id = :favoriteFolderId") diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java index d945a388c..65d22f30a 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java @@ -8,7 +8,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -27,7 +26,6 @@ @Slf4j @Service -@RequiredArgsConstructor public class FavoriteFolderStreamService { private static final Long DEFAULT_TIMEOUT = 3 * 60 * 1000L; // 3분 @@ -40,6 +38,16 @@ public class FavoriteFolderStreamService { @Value("${sse.heartbeat.interval:30}") private Long heartbeatInterval; + public FavoriteFolderStreamService( + FavoriteFolderService favoriteFolderService, + FavoriteFolderAccountService favoriteFolderAccountService, + ScheduledExecutorService scheduler + ) { + this.favoriteFolderService = favoriteFolderService; + this.favoriteFolderAccountService = favoriteFolderAccountService; + this.scheduler = scheduler; + } + public SseEmitter createEmitter(Long favoriteFolderId, Member member) { favoriteFolderService.validateFolderMembership(favoriteFolderId, member); SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); diff --git a/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java index 0ec1a075b..c187e1199 100644 --- a/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/service/FavoriteFolderServiceTest.java @@ -3,8 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import java.util.List; @@ -33,7 +35,6 @@ import turip.favorite.controller.dto.request.FavoriteFolderRequest; import turip.favorite.controller.dto.response.FavoriteFolderExitResponse; import turip.favorite.controller.dto.response.FavoriteFolderJoinResponse; -import turip.favorite.controller.dto.response.FavoriteFolderMembersResponse; import turip.favorite.controller.dto.response.FavoriteFolderResponse; import turip.favorite.controller.dto.response.FavoriteFoldersDetailResponse; import turip.favorite.controller.dto.response.FavoriteFoldersWithFavoriteStatusResponse; @@ -407,8 +408,7 @@ void findMembersById1() { .willReturn(List.of(member1, member2)); // when - FavoriteFolderMembersResponse response = favoriteFolderService.findMembersById(turipId, - account); + var response = favoriteFolderService.findMembersById(turipId, account); // then assertAll( @@ -440,12 +440,11 @@ void findMembersById3() { // given Long turipId = 1L; Account nonMemberAccount = AccountFixture.createUser(); - FavoriteFolder favoriteFolder = FavoriteFolderFixture.createCustomFolderWithId(turipId, "비공개 튜립"); given(favoriteFolderRepository.existsById(turipId)) .willReturn(true); willThrow(new ForbiddenException(ErrorTag.FORBIDDEN)) - .given(favoriteFolderAccountService).validateMembership(nonMemberAccount, favoriteFolder); + .given(favoriteFolderAccountService).validateMembership(nonMemberAccount, turipId); // when & then assertThatThrownBy(() -> favoriteFolderService.findMembersById(turipId, nonMemberAccount)) @@ -475,6 +474,7 @@ void joinFavoriteFolder1() { given(favoriteFolderRepository.findById(turipId)) .willReturn(Optional.of(favoriteFolder)); + given(favoriteFolderAccountService.isMember(favoriteFolder, account)).willReturn(false); given(favoriteFolderAccountService.findOrCreate(favoriteFolder, account)) .willReturn(favoriteFolderAccount); @@ -492,6 +492,40 @@ void joinFavoriteFolder1() { ); } + @DisplayName("이미 참여한 공유 찜폴더에 참여요청시 멤버 참여 이벤트를 발행하지 않는다") + @Test + void joinFavoriteFolder_alreadyJoined() { + // given + Long turipId = 1L; + Long accountId = 1L; + Long favoriteFolderAccountId = 1L; + Member member = MemberFixture.createMember(); + Account account = member.getAccount(); + FavoriteFolder favoriteFolder = FavoriteFolderFixture.createCustomFolderWithId(turipId, "함께 튜립"); + favoriteFolder.shareFolder(); + FavoriteFolderAccount favoriteFolderAccount = new FavoriteFolderAccount( + favoriteFolderAccountId, favoriteFolder, account, AccountRole.MEMBER + ); + + given(favoriteFolderRepository.findById(turipId)) + .willReturn(Optional.of(favoriteFolder)); + given(favoriteFolderAccountService.isMember(favoriteFolder, account)).willReturn(true); + given(favoriteFolderAccountService.findOrCreate(favoriteFolder, account)) + .willReturn(favoriteFolderAccount); + + // when + FavoriteFolderJoinResponse response = favoriteFolderService.joinMember(turipId, member); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(favoriteFolderAccountId), + () -> assertThat(response.favoriteFolderId()).isEqualTo(turipId), + () -> assertThat(response.isShared()).isTrue(), + () -> assertThat(response.accountId()).isEqualTo(accountId), + () -> verify(eventPublisher, never()).publishEvent(any()) + ); + } + @DisplayName("찜폴더가 존재하지 않는 경우 NotFoundException을 발생시킨다") @Test void joinFavoriteFolder2() { From a2be5c81651b26ff5bae28aefbb0b0142f313b1a Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 02:12:16 +0900 Subject: [PATCH 15/22] =?UTF-8?q?test:=20=EC=BD=94=EB=93=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FavoriteFolderStreamServiceTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java b/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java index e39456101..e309194e8 100644 --- a/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/stream/service/FavoriteFolderStreamServiceTest.java @@ -31,7 +31,6 @@ import turip.common.exception.custom.ForbiddenException; import turip.common.exception.custom.NotFoundException; import turip.favorite.domain.event.ActionType; -import turip.favorite.service.FavoriteFolderAccountService; import turip.favorite.service.FavoriteFolderService; import turip.util.fixture.AccountFixture; import turip.util.fixture.MemberFixture; @@ -45,9 +44,6 @@ class FavoriteFolderStreamServiceTest { @Mock private FavoriteFolderService favoriteFolderService; - @Mock - private FavoriteFolderAccountService favoriteFolderAccountService; - @Mock private ScheduledExecutorService scheduler; @@ -63,8 +59,9 @@ void createEmitter1() { Account account = AccountFixture.createUser(); Member member = MemberFixture.createCustomMember(account, "test@example.com", false); - given(favoriteFolderService.getById(folderId)) - .willThrow(new NotFoundException(ErrorTag.FAVORITE_FOLDER_NOT_FOUND)); + willThrow(new NotFoundException(ErrorTag.FAVORITE_FOLDER_NOT_FOUND)) + .given(favoriteFolderService) + .validateFolderMembership(folderId, member); // when & then assertThatThrownBy(() -> favoriteFolderStreamService.createEmitter(folderId, member)) @@ -81,7 +78,9 @@ void createEmitter2() { Member member = MemberFixture.createCustomMember(account, "test@example.com", false); willThrow(new ForbiddenException(ErrorTag.FORBIDDEN)) - .given(favoriteFolderAccountService).validateMembership(account, folderId); + .given(favoriteFolderService) + .validateFolderMembership(folderId, member); + // when & then assertThatThrownBy(() -> favoriteFolderStreamService.createEmitter(folderId, member)) .isInstanceOf(ForbiddenException.class) From c008e4be6a8872780b1a41606264b8dc58af114a Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 02:26:43 +0900 Subject: [PATCH 16/22] =?UTF-8?q?refactor:=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EB=B0=96=EC=97=90=EC=84=9C=20lazy=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=AC=B8=EC=A0=9C=20=EB=B0=9C=EC=83=9D=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/FavoriteFolderAccountRepository.java | 12 +++--------- .../FavoriteFolderAccountRepositoryTest.java | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java index ae912e512..63528a2a5 100644 --- a/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java +++ b/backend/turip-app/src/main/java/turip/favorite/repository/FavoriteFolderAccountRepository.java @@ -18,15 +18,9 @@ public interface FavoriteFolderAccountRepository extends JpaRepository findByFavoriteFolderAndAccount(FavoriteFolder favoriteFolder, Account account); - @Query("SELECT m FROM FavoriteFolderAccount ffa " + - "JOIN ffa.account a " + - "JOIN Member m ON m.account.id = a.id " + - "WHERE ffa.favoriteFolder = :favoriteFolder") - List findMembersByFavoriteFolder(@Param("favoriteFolder") FavoriteFolder favoriteFolder); - - @Query("SELECT m FROM FavoriteFolderAccount ffa " + - "JOIN ffa.account a " + - "JOIN Member m ON m.account.id = a.id " + + @Query("SELECT m FROM Member m " + + "JOIN FETCH m.account a " + + "JOIN FavoriteFolderAccount ffa ON ffa.account.id = a.id " + "WHERE ffa.favoriteFolder.id = :favoriteFolderId") List findMembersByFavoriteFolderId(@Param("favoriteFolderId") Long favoriteFolderId); diff --git a/backend/turip-app/src/test/java/turip/favorite/repository/FavoriteFolderAccountRepositoryTest.java b/backend/turip-app/src/test/java/turip/favorite/repository/FavoriteFolderAccountRepositoryTest.java index 93fe8ab9a..88e8c9da2 100644 --- a/backend/turip-app/src/test/java/turip/favorite/repository/FavoriteFolderAccountRepositoryTest.java +++ b/backend/turip-app/src/test/java/turip/favorite/repository/FavoriteFolderAccountRepositoryTest.java @@ -52,7 +52,7 @@ void findMembersByFavoriteFolder1() { favoriteFolderAccountRepository.save(new FavoriteFolderAccount(folder, account2, AccountRole.MEMBER)); // when - List members = favoriteFolderAccountRepository.findMembersByFavoriteFolder(folder); + List members = favoriteFolderAccountRepository.findMembersByFavoriteFolderId(folder.getId()); // then assertThat(members).hasSize(2); @@ -67,7 +67,7 @@ void findMembersByFavoriteFolder2() { FavoriteFolder folder = favoriteFolderRepository.save(FavoriteFolder.customFolderOf("빈 폴더")); // when - List members = favoriteFolderAccountRepository.findMembersByFavoriteFolder(folder); + List members = favoriteFolderAccountRepository.findMembersByFavoriteFolderId(folder.getId()); // then assertThat(members).isEmpty(); From 950828ef5027ca50338623012e11a06417d6b8da Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 02:52:03 +0900 Subject: [PATCH 17/22] =?UTF-8?q?refactor:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=EB=A0=88=EB=93=9C=20=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/AsyncConfiguration.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java index 1f14e0ef8..376af10ec 100644 --- a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java +++ b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java @@ -1,30 +1,37 @@ package turip.common.configuration; import java.util.concurrent.Executor; -import java.util.concurrent.ThreadPoolExecutor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -@Configuration +@Slf4j @EnableAsync +@Configuration public class AsyncConfiguration { @Bean(name = "sseEventExecutor") public Executor sseEventExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(8); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); executor.setQueueCapacity(100); executor.setThreadNamePrefix("SSE-EVT-"); - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setRejectedExecutionHandler((r, executorInstance) -> log.warn( + "[SSE-ThreadPool] 이벤트 전송 거부됨 - Thread pool 포화 상태 (현재 활성 스레드: {}, 잔여 큐: {})", + executorInstance.getActiveCount(), + executorInstance.getQueue().size() + ) + ); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); + return executor; } } From 9dd5924d0a32b5e1abfde57f0f1fb2c05666aeae Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 03:15:57 +0900 Subject: [PATCH 18/22] =?UTF-8?q?refactor:=20=EC=9E=94=EC=97=AC=20?= =?UTF-8?q?=ED=81=90=20=EA=B3=84=EC=82=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/turip/common/configuration/AsyncConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java index 376af10ec..60dcdbcde 100644 --- a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java +++ b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java @@ -21,9 +21,9 @@ public Executor sseEventExecutor() { executor.setThreadNamePrefix("SSE-EVT-"); executor.setRejectedExecutionHandler((r, executorInstance) -> log.warn( - "[SSE-ThreadPool] 이벤트 전송 거부됨 - Thread pool 포화 상태 (현재 활성 스레드: {}, 잔여 큐: {})", + "[SSE-ThreadPool] 이벤트 전송 거부됨 - Thread pool 포화 상태 (현재 활성 스레드: {}, 잔여 큐 용량: {})", executorInstance.getActiveCount(), - executorInstance.getQueue().size() + executorInstance.getQueue().remainingCapacity() ) ); From 89692ae8dd06edae248b5ffb950f7f49b3ae034e Mon Sep 17 00:00:00 2001 From: eunseongu Date: Thu, 19 Feb 2026 03:16:33 +0900 Subject: [PATCH 19/22] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/turip/common/configuration/AsyncConfiguration.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java index 60dcdbcde..a95bbc3de 100644 --- a/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java +++ b/backend/turip-app/src/main/java/turip/common/configuration/AsyncConfiguration.java @@ -30,8 +30,6 @@ public Executor sseEventExecutor() { executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); - executor.initialize(); - return executor; } } From 875c3d7d7cb5b354fa52a377e67ac3730f60b2eb Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 24 Feb 2026 11:27:18 +0900 Subject: [PATCH 20/22] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=9B=84=20emitter=20=EC=A0=95=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stream/service/FavoriteFolderEventListener.java | 5 +++++ .../stream/service/FavoriteFolderStreamService.java | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java index 5095debe9..3f1ac8f8c 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderEventListener.java @@ -25,6 +25,11 @@ public void handleFavoriteFolderUpdateEvent(FavoriteFolderUpdateEvent event) { return; } + if (action == ActionType.FOLDER_DELETED) { + favoriteFolderStreamService.closeAllEmittersForFolder(folderId); + return; + } + favoriteFolderStreamService.sendFolderUpdateEvents(folderId, action); } } diff --git a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java index d945a388c..d235f12fd 100644 --- a/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java +++ b/backend/turip-app/src/main/java/turip/favorite/stream/service/FavoriteFolderStreamService.java @@ -78,6 +78,19 @@ public void sendMemberUpdateEvents(Long favoriteFolderId, ActionType actionType) ); } + public void closeAllEmittersForFolder(Long favoriteFolderId) { + Map folderEmitters = emitters.remove(favoriteFolderId); + if (folderEmitters != null) { + new ArrayList<>(folderEmitters.values()).forEach(emitter -> { + try { + emitter.complete(); + } catch (Exception e) { + log.warn(SSE_LOG_PREFIX + "Emitter close 실패", e); + } + }); + } + } + private Map validateAndGetFolderEmitters(Long favoriteFolderId) { Map folderEmitters = emitters.get(favoriteFolderId); if (folderEmitters == null || folderEmitters.isEmpty()) { From 411b83abc76e577309939ca63daea833faa22855 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 24 Feb 2026 14:25:21 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../favorite/service/FavoriteFolderAccountService.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java index 55370189e..9f91f1ea2 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderAccountService.java @@ -42,11 +42,7 @@ public void validateOwnership(Account account, FavoriteFolder favoriteFolder) { } public void validateMembership(Account account, FavoriteFolder favoriteFolder) { - boolean isMember = favoriteFolderAccountRepository.existsByFavoriteFolderAndAccount(favoriteFolder, account); - - if (!isMember) { - throw new ForbiddenException(ErrorTag.FORBIDDEN); - } + validateMembership(account, favoriteFolder.getId()); } public void validateMembership(Account account, Long favoriteFolderId) { From 24c5a17d3e70296ee21ad149199b33dbca3d7c57 Mon Sep 17 00:00:00 2001 From: eunseongu Date: Tue, 3 Mar 2026 14:07:52 +0900 Subject: [PATCH 22/22] =?UTF-8?q?faet:=20=EA=B3=B5=EC=9C=A0=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EC=97=90=20=EC=9D=B4=EB=AF=B8=20=EC=B0=B8=EC=97=AC?= =?UTF-8?q?=ED=95=9C=20=EB=A9=A4=EB=B2=84=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=8D=98=EC=A7=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/turip/common/exception/ErrorTag.java | 1 + .../turip/favorite/service/FavoriteFolderService.java | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/turip-app/src/main/java/turip/common/exception/ErrorTag.java b/backend/turip-app/src/main/java/turip/common/exception/ErrorTag.java index feaee813e..6726c7437 100644 --- a/backend/turip-app/src/main/java/turip/common/exception/ErrorTag.java +++ b/backend/turip-app/src/main/java/turip/common/exception/ErrorTag.java @@ -51,6 +51,7 @@ public enum ErrorTag { FAVORITE_PLACE_IN_FOLDER_CONFLICT("해당 폴더에 이미 찜한 장소입니다."), LOGIN_ID_CONFLICT("중복 아이디가 존재합니다."), CONTENT_SAVE_CONFLICT("이미 등록된 컨텐츠입니다."), + FAVORITE_FOLDER_ALREADY_JOINED("해당 공유 찜 폴더에 이미 참여한 멤버입니다."), // 500 Internal Server Error INTERNAL_SERVER_ERROR("서버에서 예기치 못한 에러가 발생했습니다."), diff --git a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java index 25f20efd3..c1e404147 100644 --- a/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java +++ b/backend/turip-app/src/main/java/turip/favorite/service/FavoriteFolderService.java @@ -70,11 +70,13 @@ public FavoriteFolderJoinResponse joinMember(Long favoriteFolderId, Member membe validateShareAndCustomFolder(favoriteFolder); boolean isAlreadyJoined = favoriteFolderAccountService.isFolderMember(member.getAccount(), favoriteFolder); + if (isAlreadyJoined) { + throw new ConflictException(ErrorTag.FAVORITE_FOLDER_ALREADY_JOINED); + } + FavoriteFolderAccount favoriteFolderAccount = favoriteFolderAccountService.findOrCreate(favoriteFolder, member.getAccount()); - if (!isAlreadyJoined) { - eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.MEMBER_JOINED)); - } + eventPublisher.publishEvent(FavoriteFolderUpdateEvent.of(favoriteFolderId, ActionType.MEMBER_JOINED)); return FavoriteFolderJoinResponse.from(favoriteFolderAccount); }