diff --git a/src/main/java/until/the/eternity/common/enums/ItemCategory.java b/src/main/java/until/the/eternity/common/enums/ItemCategory.java index 87801b67..6b8ab078 100644 --- a/src/main/java/until/the/eternity/common/enums/ItemCategory.java +++ b/src/main/java/until/the/eternity/common/enums/ItemCategory.java @@ -88,18 +88,18 @@ public enum ItemCategory { PAGE("페이지", "서적"), // 소모품 - POTION("포션", "소모폼"), - FOOD("음식", "소모폼"), - HERB("허브", "소모폼"), - DUNGEON_PASS("던전 통행증", "소모폼"), - ALBAN_TRAINING_STONE("알반 훈련석", "소모폼"), - GEM_STONE("개조석", "소모폼"), - JEWEL("보석", "소모폼"), - TRANSFORMATION_MEDAL("변신 메달", "소모폼"), - DYE_AMPULE("염색 앰플", "소모폼"), - SKETCH("스케치", "소모폼"), - FINZBEADS("핀즈비즈", "소모폼"), - ETC_CONSUMABLE("기타 소모품", "소모폼"), + POTION("포션", "소모품"), + FOOD("음식", "소모품"), + HERB("허브", "소모품"), + DUNGEON_PASS("던전 통행증", "소모품"), + ALBAN_TRAINING_STONE("알반 훈련석", "소모품"), + GEM_STONE("개조석", "소모품"), + JEWEL("보석", "소모품"), + TRANSFORMATION_MEDAL("변신 메달", "소모품"), + DYE_AMPULE("염색 앰플", "소모품"), + SKETCH("스케치", "소모품"), + FINZBEADS("핀즈비즈", "소모품"), + ETC_CONSUMABLE("기타 소모품", "소모품"), // 토템 TOTEM("토템", "토템"), diff --git a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfo.java b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfo.java index a351b9e0..810d2116 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfo.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfo.java @@ -1,8 +1,8 @@ package until.the.eternity.iteminfo.domain.entity; import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; -import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; @@ -14,15 +14,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ItemInfo { - @Id - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "sub_category", nullable = false) - private String subCategory; - - @Column(name = "top_category", nullable = false) - private String topCategory; + @EmbeddedId private ItemInfoId id; @Column(name = "description") private String description; @@ -53,4 +45,17 @@ public class ItemInfo { @Column(name = "max_alteration_count") private Byte maxAlterationCount; + + // Helper methods for backward compatibility + public String getName() { + return id != null ? id.getName() : null; + } + + public String getSubCategory() { + return id != null ? id.getSubCategory() : null; + } + + public String getTopCategory() { + return id != null ? id.getTopCategory() : null; + } } diff --git a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java new file mode 100644 index 00000000..9e8db8eb --- /dev/null +++ b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java @@ -0,0 +1,27 @@ +package until.the.eternity.iteminfo.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.io.Serializable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode +public class ItemInfoId implements Serializable { + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "sub_category", nullable = false) + private String subCategory; + + @Column(name = "top_category", nullable = false) + private String topCategory; +} diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java index e48eddf3..47ec58cf 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoJpaRepository.java @@ -4,11 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import until.the.eternity.iteminfo.domain.entity.ItemInfo; +import until.the.eternity.iteminfo.domain.entity.ItemInfoId; public interface ItemInfoJpaRepository - extends JpaRepository, JpaSpecificationExecutor { + extends JpaRepository, JpaSpecificationExecutor { - List findByTopCategory(String topCategory); + List findByIdTopCategory(String topCategory); - List findBySubCategory(String subCategory); + List findByIdSubCategory(String subCategory); } diff --git a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java index cee52700..1e8dc1a0 100644 --- a/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java +++ b/src/main/java/until/the/eternity/iteminfo/infrastructure/persistence/ItemInfoRepositoryPortImpl.java @@ -18,11 +18,11 @@ public List findAll() { @Override public List findByTopCategory(String topCategory) { - return jpaRepository.findByTopCategory(topCategory); + return jpaRepository.findByIdTopCategory(topCategory); } @Override public List findBySubCategory(String subCategory) { - return jpaRepository.findBySubCategory(subCategory); + return jpaRepository.findByIdSubCategory(subCategory); } } diff --git a/src/main/resources/db/migration/V10__alter_item_info_pk.sql b/src/main/resources/db/migration/V10__alter_item_info_pk.sql new file mode 100644 index 00000000..93ff8f44 --- /dev/null +++ b/src/main/resources/db/migration/V10__alter_item_info_pk.sql @@ -0,0 +1,3 @@ +ALTER TABLE item_info +DROP PRIMARY KEY, +ADD PRIMARY KEY (name, sub_category, top_category); \ No newline at end of file diff --git a/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java b/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java new file mode 100644 index 00000000..c75c8844 --- /dev/null +++ b/src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java @@ -0,0 +1,116 @@ +package until.the.eternity.iteminfo.application.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.util.List; +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 until.the.eternity.iteminfo.domain.entity.ItemInfo; +import until.the.eternity.iteminfo.domain.repository.ItemInfoRepositoryPort; +import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemCategoryResponse; +import until.the.eternity.iteminfo.interfaces.rest.dto.response.ItemInfoResponse; + +@ExtendWith(MockitoExtension.class) +class ItemInfoServiceTest { + + @Mock private ItemInfoRepositoryPort itemInfoRepository; + + @InjectMocks private ItemInfoService itemInfoService; + + @Test + @DisplayName("모든 아이템 카테고리를 조회하면 카테고리 목록을 반환한다") + void findItemCategories_should_return_all_categories() { + // when + List result = itemInfoService.findItemCategories(); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).extracting("subCategory").contains("한손 장비", "검", "활"); + assertThat(result).extracting("topCategory").contains("근거리 장비", "원거리 장비"); + } + + @Test + @DisplayName("모든 아이템 정보를 조회하면 아이템 목록을 반환한다") + void findAll_should_return_all_items() { + // given + ItemInfo item1 = createItemInfo("나뭇가지", "한손검", "무기"); + ItemInfo item2 = createItemInfo("숏소드", "한손검", "무기"); + when(itemInfoRepository.findAll()).thenReturn(List.of(item1, item2)); + + // when + List result = itemInfoService.findAll(); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting("name").containsExactly("나뭇가지", "숏소드"); + verify(itemInfoRepository).findAll(); + } + + @Test + @DisplayName("상위 카테고리로 아이템을 조회하면 해당 카테고리의 아이템 목록을 반환한다") + void findByTopCategory_should_return_items_by_top_category() { + // given + String topCategory = "소모품"; + ItemInfo item1 = createItemInfo("염색 앰플", "염색 앰플", topCategory); + ItemInfo item2 = createItemInfo("지정 색상 염색 앰플", "염색 앰플", topCategory); + when(itemInfoRepository.findByTopCategory(topCategory)).thenReturn(List.of(item1, item2)); + + // when + List result = itemInfoService.findByTopCategory(topCategory); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting("topCategory").containsOnly("소모품"); + assertThat(result).extracting("name").containsExactly("염색 앰플", "지정 색상 염색 앰플"); + verify(itemInfoRepository).findByTopCategory(topCategory); + } + + @Test + @DisplayName("하위 카테고리로 아이템을 조회하면 해당 카테고리의 아이템 목록을 반환한다") + void findBySubCategory_should_return_items_by_sub_category() { + // given + String topCategory = "소모품"; + String subCategory = "염색 앰플"; + ItemInfo item1 = createItemInfo("염색 앰플", subCategory, topCategory); + ItemInfo item2 = createItemInfo("지정 색상 염색 앰플", subCategory, topCategory); + when(itemInfoRepository.findBySubCategory(subCategory)).thenReturn(List.of(item1, item2)); + + // when + List result = itemInfoService.findBySubCategory(subCategory); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting("subCategory").containsOnly("염색 앰플"); + assertThat(result).extracting("name").containsExactly("염색 앰플", "지정 색상 염색 앰플"); + verify(itemInfoRepository).findBySubCategory(subCategory); + } + + @Test + @DisplayName("아이템 정보가 없으면 빈 목록을 반환한다") + void findAll_should_return_empty_list_when_no_data() { + // given + when(itemInfoRepository.findAll()).thenReturn(List.of()); + + // when + List result = itemInfoService.findAll(); + + // then + assertThat(result).isEmpty(); + verify(itemInfoRepository).findAll(); + } + + private ItemInfo createItemInfo(String name, String subCategory, String topCategory) { + ItemInfo itemInfo = mock(ItemInfo.class); + + when(itemInfo.getName()).thenReturn(name); + when(itemInfo.getSubCategory()).thenReturn(subCategory); + when(itemInfo.getTopCategory()).thenReturn(topCategory); + + return itemInfo; + } +}