From 4ba0314a95a9747920b1150f91d2864399f8a374 Mon Sep 17 00:00:00 2001 From: bum0w0 Date: Sat, 13 Jun 2026 21:51:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EA=B7=BC=EC=B2=98=EA=B8=B0=EB=85=90?= =?UTF-8?q?=ED=92=88=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=9C=A0=EC=A0=80=EC=9D=98=20isWishlisted=20null?= =?UTF-8?q?=20=EC=98=A4=EB=B0=98=ED=99=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 찜한 기념품이 없는 로그인 유저도 emptySet으로 처리되어 isWishlisted가 false 대신 null로 반환되던 문제 수정 - SouvenirService: 비로그인 시 wishlistedIds를 emptySet → null로 변경 - SouvenirNearbyResponse: isEmpty() 체크 → null 체크로 변경하여 비로그인(null)과 찜 0개 로그인 유저(false)를 올바르게 구분 --- .../general/service/GeneralRecommendationService.java | 8 +++++++- .../domain/souvenir/dto/SouvenirNearbyResponse.java | 2 +- .../souzip/domain/souvenir/service/SouvenirService.java | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/souzip/domain/recommend/general/service/GeneralRecommendationService.java b/src/main/java/com/souzip/domain/recommend/general/service/GeneralRecommendationService.java index b33f5f49..a504cf4e 100644 --- a/src/main/java/com/souzip/domain/recommend/general/service/GeneralRecommendationService.java +++ b/src/main/java/com/souzip/domain/recommend/general/service/GeneralRecommendationService.java @@ -11,6 +11,8 @@ import com.souzip.domain.recommend.general.dto.GeneralRecommendationStatsDto; import com.souzip.domain.recommend.general.repository.GeneralRecommendationRepositoryCustom; import com.souzip.domain.souvenir.entity.Souvenir; +import com.souzip.domain.user.entity.User; +import com.souzip.domain.user.repository.UserRepository; import com.souzip.domain.wishlist.repository.WishlistRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,6 +36,7 @@ public class GeneralRecommendationService { private final FileStorage fileStorage; private final WishlistRepository wishlistRepository; private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; public List getTop10ByCategory(String categoryName, @Nullable String authorizationHeader) { List souvenirs = generalRecommendationRepository.findTop10ByCategoryRecent(categoryName); @@ -171,7 +174,10 @@ private boolean isEmptyToken(String token) { private String parseUserIdFromToken(String token) { try { Long userId = jwtTokenProvider.getUserIdFromToken(token); - return userId != null ? String.valueOf(userId) : null; + if (userId == null) return null; + return userRepository.findById(userId) + .map(User::getUserId) + .orElse(null); } catch (Exception e) { log.debug("Failed to parse token", e); return null; diff --git a/src/main/java/com/souzip/domain/souvenir/dto/SouvenirNearbyResponse.java b/src/main/java/com/souzip/domain/souvenir/dto/SouvenirNearbyResponse.java index 0a58ee20..9d0c979c 100644 --- a/src/main/java/com/souzip/domain/souvenir/dto/SouvenirNearbyResponse.java +++ b/src/main/java/com/souzip/domain/souvenir/dto/SouvenirNearbyResponse.java @@ -86,7 +86,7 @@ public static SouvenirNearbyResponse fromObjectArray( String imageUrl = generateImageUrl(thumbnail, urlGenerator); long wishlistCount = ((Number) row[INDEX_WISHLIST_COUNT]).longValue(); - Boolean isWishlisted = wishlistedIds.isEmpty() ? null : wishlistedIds.contains(id); + Boolean isWishlisted = wishlistedIds != null ? wishlistedIds.contains(id) : null; return new SouvenirNearbyResponse( id, diff --git a/src/main/java/com/souzip/domain/souvenir/service/SouvenirService.java b/src/main/java/com/souzip/domain/souvenir/service/SouvenirService.java index 7437554b..49a54341 100644 --- a/src/main/java/com/souzip/domain/souvenir/service/SouvenirService.java +++ b/src/main/java/com/souzip/domain/souvenir/service/SouvenirService.java @@ -63,7 +63,7 @@ public SouvenirNearbyListResponse getNearbySouvenirs( String userUuid = userId != null ? findUserUuid(userId) : null; Set wishlistedIds = userUuid != null ? wishlistRepository.findSouvenirIdsByUserUserId(userUuid) - : Collections.emptySet(); + : null; List list = results.stream() .map(row -> SouvenirNearbyResponse.fromObjectArray( From 01f9c80f50eeb3552ad3f430000e639266923353 Mon Sep 17 00:00:00 2001 From: bum0w0 Date: Sat, 13 Jun 2026 21:52:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20SouvenirServiceTest=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 --- .../souvenir/service/SouvenirServiceTest.java | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 src/test/java/com/souzip/domain/souvenir/service/SouvenirServiceTest.java diff --git a/src/test/java/com/souzip/domain/souvenir/service/SouvenirServiceTest.java b/src/test/java/com/souzip/domain/souvenir/service/SouvenirServiceTest.java new file mode 100644 index 00000000..c6699564 --- /dev/null +++ b/src/test/java/com/souzip/domain/souvenir/service/SouvenirServiceTest.java @@ -0,0 +1,352 @@ +package com.souzip.domain.souvenir.service; + +import com.souzip.adapter.storage.file.NcpStorage; +import com.souzip.application.file.FileModifyService; +import com.souzip.application.file.FileQueryService; +import com.souzip.application.file.required.FileStorage; +import com.souzip.auth.adapter.security.jwt.JwtTokenProvider; +import com.souzip.domain.category.entity.Category; +import com.souzip.domain.exchangerate.service.ExchangeRateService; +import com.souzip.domain.souvenir.dto.*; +import com.souzip.domain.souvenir.entity.Purpose; +import com.souzip.domain.souvenir.entity.Souvenir; +import com.souzip.domain.souvenir.repository.SouvenirRepository; +import com.souzip.domain.souvenir.vo.PriceInfo; +import com.souzip.domain.user.entity.User; +import com.souzip.domain.user.repository.UserRepository; +import com.souzip.domain.wishlist.repository.WishlistRepository; +import com.souzip.shared.exception.BusinessException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SouvenirServiceTest { + + @Mock + private SouvenirRepository souvenirRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private WishlistRepository wishlistRepository; + + @Mock + private NcpStorage ncpStorage; + + @Mock + private JwtTokenProvider jwtTokenProvider; + + @Mock + private FileQueryService fileQueryService; + + @Mock + private FileStorage fileStorage; + + @Mock + private FileModifyService fileModifyService; + + @Mock + private ExchangeRateService exchangeRateService; + + @InjectMocks + private SouvenirService souvenirService; + + private static final Long SOUVENIR_ID = 1L; + private static final Long USER_PK = 1L; + private static final String USER_UUID = "550e8400-e29b-41d4-a716-446655440000"; + private static final String BEARER_TOKEN = "Bearer valid.jwt.token"; + private static final String JWT_TOKEN = "valid.jwt.token"; + + private Object[] buildRow(long id) { + return new Object[]{ + id, + "기념품", + "SOUVENIR_BASIC", + "GIFT", + 10000, + 120000, + "$", + null, + new BigDecimal("37.123456"), + new BigDecimal("127.123456"), + "테스트 주소", + 5L + }; + } + + @DisplayName("비로그인 시 isWishlisted는 null이다") + @Test + void getNearbySouvenirs_isWishlisted_null_when_not_logged_in() { + // given + List rows = Collections.singletonList(buildRow(1L)); + given(souvenirRepository.findNearbySouvenirs(anyDouble(), anyDouble(), anyDouble())) + .willReturn(rows); + + // when + SouvenirNearbyListResponse result = souvenirService.getNearbySouvenirs(37.0, 127.0, 5000, null); + + // then + assertThat(result.souvenirs().get(0).isWishlisted()).isNull(); + } + + @DisplayName("로그인 유저가 찜 목록이 비어있으면 isWishlisted는 false이다") + @Test + void getNearbySouvenirs_isWishlisted_false_when_logged_in_with_empty_wishlist() { + // given + User user = mock(User.class); + given(user.getUserId()).willReturn(USER_UUID); + given(jwtTokenProvider.getUserIdFromToken(JWT_TOKEN)).willReturn(USER_PK); + given(userRepository.findById(USER_PK)).willReturn(Optional.of(user)); + given(wishlistRepository.findSouvenirIdsByUserUserId(USER_UUID)).willReturn(Collections.emptySet()); + List rows = Collections.singletonList(buildRow(1L)); + given(souvenirRepository.findNearbySouvenirs(anyDouble(), anyDouble(), anyDouble())) + .willReturn(rows); + + // when + SouvenirNearbyListResponse result = souvenirService.getNearbySouvenirs(37.0, 127.0, 5000, BEARER_TOKEN); + + // then + assertThat(result.souvenirs().get(0).isWishlisted()).isFalse(); + } + + @DisplayName("로그인 유저가 해당 기념품을 찜했으면 isWishlisted는 true이다") + @Test + void getNearbySouvenirs_isWishlisted_true_when_souvenir_wishlisted() { + // given + User user = mock(User.class); + given(user.getUserId()).willReturn(USER_UUID); + given(jwtTokenProvider.getUserIdFromToken(JWT_TOKEN)).willReturn(USER_PK); + given(userRepository.findById(USER_PK)).willReturn(Optional.of(user)); + given(wishlistRepository.findSouvenirIdsByUserUserId(USER_UUID)).willReturn(Set.of(1L)); + List rows = Collections.singletonList(buildRow(1L)); + given(souvenirRepository.findNearbySouvenirs(anyDouble(), anyDouble(), anyDouble())) + .willReturn(rows); + + // when + SouvenirNearbyListResponse result = souvenirService.getNearbySouvenirs(37.0, 127.0, 5000, BEARER_TOKEN); + + // then + assertThat(result.souvenirs().get(0).isWishlisted()).isTrue(); + } + + @DisplayName("비로그인으로 기념품 상세를 조회하면 isOwned와 isWishlisted는 false이다") + @Test + void getSouvenir_not_logged_in_success() { + // given + Souvenir souvenir = mock(Souvenir.class); + User owner = mock(User.class); + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.of(souvenir)); + given(fileQueryService.findByEntity(any(), eq(SOUVENIR_ID))).willReturn(Collections.emptyList()); + given(souvenir.getUser()).willReturn(owner); + given(wishlistRepository.countBySouvenirId(SOUVENIR_ID)).willReturn(0L); + given(exchangeRateService.createPriceResponse(any(), any())).willReturn(null); + + // when + SouvenirDetailResponse result = souvenirService.getSouvenir(SOUVENIR_ID, null); + + // then + assertThat(result.isOwned()).isFalse(); + assertThat(result.isWishlisted()).isFalse(); + } + + @DisplayName("로그인 유저가 자신의 기념품을 조회하면 isOwned는 true이다") + @Test + void getSouvenir_owner_can_see_isOwned_true() { + // given + Souvenir souvenir = mock(Souvenir.class); + User owner = mock(User.class); + given(jwtTokenProvider.getUserIdFromToken(JWT_TOKEN)).willReturn(USER_PK); + given(userRepository.findById(USER_PK)).willReturn(Optional.of(owner)); + given(owner.getUserId()).willReturn(USER_UUID); + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.of(souvenir)); + given(fileQueryService.findByEntity(any(), eq(SOUVENIR_ID))).willReturn(Collections.emptyList()); + given(souvenir.getUser()).willReturn(owner); + given(souvenir.isOwnedBy(USER_UUID)).willReturn(true); + given(wishlistRepository.existsByUserUserIdAndSouvenirId(USER_UUID, SOUVENIR_ID)).willReturn(false); + given(wishlistRepository.countBySouvenirId(SOUVENIR_ID)).willReturn(0L); + given(exchangeRateService.createPriceResponse(any(), any())).willReturn(null); + + // when + SouvenirDetailResponse result = souvenirService.getSouvenir(SOUVENIR_ID, BEARER_TOKEN); + + // then + assertThat(result.isOwned()).isTrue(); + } + + @DisplayName("존재하지 않는 기념품을 조회하면 예외가 발생한다") + @Test + void getSouvenir_not_found_throws_exception() { + // given + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> souvenirService.getSouvenir(SOUVENIR_ID, null)) + .isInstanceOf(BusinessException.class); + } + + @DisplayName("기념품 삭제에 성공한다") + @Test + void deleteSouvenir_success() { + // given + Souvenir souvenir = mock(Souvenir.class); + User owner = mock(User.class); + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.of(souvenir)); + given(souvenir.getUser()).willReturn(owner); + given(owner.getId()).willReturn(USER_PK); + + // when + souvenirService.deleteSouvenir(SOUVENIR_ID, USER_PK); + + // then + verify(souvenir).delete(); + verify(fileModifyService).deleteByEntity(any(), eq(SOUVENIR_ID)); + } + + @DisplayName("인증 없이 기념품을 삭제하면 예외가 발생한다") + @Test + void deleteSouvenir_unauthorized_when_user_id_is_null() { + // when & then + assertThatThrownBy(() -> souvenirService.deleteSouvenir(SOUVENIR_ID, null)) + .isInstanceOf(BusinessException.class); + + verify(souvenirRepository, never()).findByIdWithUser(any()); + } + + @DisplayName("존재하지 않는 기념품을 삭제하면 예외가 발생한다") + @Test + void deleteSouvenir_not_found_throws_exception() { + // given + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> souvenirService.deleteSouvenir(SOUVENIR_ID, USER_PK)) + .isInstanceOf(BusinessException.class); + } + + @DisplayName("소유자가 아닌 유저가 기념품을 삭제하면 예외가 발생한다") + @Test + void deleteSouvenir_forbidden_when_not_owner() { + // given + Long otherUserId = 999L; + Souvenir souvenir = mock(Souvenir.class); + User owner = mock(User.class); + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.of(souvenir)); + given(souvenir.getUser()).willReturn(owner); + given(owner.getId()).willReturn(otherUserId); + + // when & then + assertThatThrownBy(() -> souvenirService.deleteSouvenir(SOUVENIR_ID, USER_PK)) + .isInstanceOf(BusinessException.class); + + verify(souvenir, never()).delete(); + } + + @DisplayName("기념품 등록에 성공한다") + @Test + void createSouvenirV2_success() { + // given + User user = mock(User.class); + SouvenirRequest request = new SouvenirRequest( + "기념품", 10000, "JPY", "설명", "주소", null, + new BigDecimal("35.0"), new BigDecimal("139.0"), + Category.SOUVENIR_BASIC, Purpose.GIFT, "JP" + ); + PriceData priceData = new PriceData( + PriceInfo.of(10000, "JPY"), 90000, "¥", PriceInfo.of(90000, "KRW") + ); + given(userRepository.findById(USER_PK)).willReturn(Optional.of(user)); + given(exchangeRateService.calculatePriceData(any(), any(), any())).willReturn(priceData); + given(exchangeRateService.createPriceResponse(any(), any())).willReturn(null); + + // when + SouvenirResponse result = souvenirService.createSouvenirV2(request, USER_PK, Collections.emptyList()); + + // then + assertThat(result.name()).isEqualTo("기념품"); + verify(souvenirRepository).save(any(Souvenir.class)); + } + + @DisplayName("인증 없이 기념품을 등록하면 예외가 발생한다") + @Test + void createSouvenirV2_unauthorized_when_user_id_is_null() { + // given + SouvenirRequest request = new SouvenirRequest( + "기념품", 10000, "JPY", "설명", "주소", null, + new BigDecimal("35.0"), new BigDecimal("139.0"), + Category.SOUVENIR_BASIC, Purpose.GIFT, "JP" + ); + + // when & then + assertThatThrownBy(() -> souvenirService.createSouvenirV2(request, null, Collections.emptyList())) + .isInstanceOf(BusinessException.class); + + verify(souvenirRepository, never()).save(any()); + } + + @DisplayName("기념품 수정에 성공한다") + @Test + void updateSouvenirV2_success() { + // given + Souvenir souvenir = mock(Souvenir.class); + User owner = mock(User.class); + SouvenirRequest request = new SouvenirRequest( + "수정 기념품", 20000, "USD", "수정 설명", "수정 주소", null, + new BigDecimal("35.0"), new BigDecimal("139.0"), + Category.FOOD_SNACK, Purpose.PERSONAL, "US" + ); + PriceData priceData = new PriceData( + PriceInfo.of(20000, "USD"), 28000000, "$", PriceInfo.of(28000000, "KRW") + ); + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.of(souvenir)); + given(souvenir.getUser()).willReturn(owner); + given(owner.getId()).willReturn(USER_PK); + given(exchangeRateService.calculatePriceData(any(), any(), any())).willReturn(priceData); + given(exchangeRateService.createPriceResponse(any(), any())).willReturn(null); + + // when + souvenirService.updateSouvenirV2(SOUVENIR_ID, request, USER_PK); + + // then + verify(souvenir).updateV2(eq(request), any(), any(), any(), any()); + } + + @DisplayName("소유자가 아닌 유저가 기념품을 수정하면 예외가 발생한다") + @Test + void updateSouvenirV2_forbidden_when_not_owner() { + // given + Long otherUserId = 999L; + Souvenir souvenir = mock(Souvenir.class); + User owner = mock(User.class); + SouvenirRequest request = new SouvenirRequest( + "수정 기념품", 20000, "USD", "수정 설명", "수정 주소", null, + new BigDecimal("35.0"), new BigDecimal("139.0"), + Category.FOOD_SNACK, Purpose.PERSONAL, "US" + ); + given(souvenirRepository.findByIdWithUser(SOUVENIR_ID)).willReturn(Optional.of(souvenir)); + given(souvenir.getUser()).willReturn(owner); + given(owner.getId()).willReturn(otherUserId); + + // when & then + assertThatThrownBy(() -> souvenirService.updateSouvenirV2(SOUVENIR_ID, request, USER_PK)) + .isInstanceOf(BusinessException.class); + + verify(souvenir, never()).updateV2(any(), any(), any(), any(), any()); + } +}