Skip to content

Commit

Permalink
feat: 아이템 목록 조회 기능 구현 (#41)
Browse files Browse the repository at this point in the history
* refactor: ResponseStatus + DTO 방식으로 변경

* feat: 아이템, 인벤토리 Entity 생성

* feat: 아이템 목록 조회 API 구현

* test: containsExactly 검증으로 수정

* test: 아이템 목록 조회 Service 테스트

* test: 인벤토리 아이템 목록 조회 Repository 테스트

* feat: Stream 유틸 클래스 생성 및 적용

* fix: ItemFixture를 통한 아이템 생성 시 build() 추가

* test: 구매하지 않은 아이템 목록 조회 Repository 테스트

* feat: MethodArgumentTypeMismatchException handler 추가

* test: 아이템 목록 조회 Controller 테스트

* fix: Mapper 생성자 접근 레벨 private으로 변경

* feat: ItemType 생성 및 적용

* refactor: 잘못된 요청 타입 에러 메시지 상수화
  • Loading branch information
kmebin authored Nov 6, 2023
1 parent db829cb commit 0201932
Show file tree
Hide file tree
Showing 27 changed files with 757 additions and 15 deletions.
31 changes: 31 additions & 0 deletions src/main/java/com/moabam/api/application/ItemService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.moabam.api.application;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.domain.entity.Item;
import com.moabam.api.domain.entity.enums.ItemType;
import com.moabam.api.domain.repository.InventorySearchRepository;
import com.moabam.api.domain.repository.ItemSearchRepository;
import com.moabam.api.dto.ItemMapper;
import com.moabam.api.dto.ItemsResponse;

import lombok.RequiredArgsConstructor;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {

private final ItemSearchRepository itemSearchRepository;
private final InventorySearchRepository inventorySearchRepository;

public ItemsResponse getItems(Long memberId, ItemType type) {
List<Item> purchasedItems = inventorySearchRepository.findItems(memberId, type);
List<Item> notPurchasedItems = itemSearchRepository.findNotPurchasedItems(memberId, type);

return ItemMapper.toItemsResponse(purchasedItems, notPurchasedItems);
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/Inventory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.moabam.api.domain.entity;

import static java.util.Objects.*;

import org.hibernate.annotations.ColumnDefault;

import com.moabam.global.common.entity.BaseTimeEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "inventory", indexes = @Index(name = "idx_member_id", columnList = "member_id"))
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Inventory extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Column(name = "member_id", updatable = false, nullable = false)
private Long memberId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id", updatable = false, nullable = false)
private Item item;

@Column(name = "is_default", nullable = false)
@ColumnDefault("false")
private boolean isDefault;

@Builder
private Inventory(Long memberId, Item item, boolean isDefault) {
this.memberId = requireNonNull(memberId);
this.item = requireNonNull(item);
this.isDefault = isDefault;
}
}
90 changes: 90 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/Item.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.moabam.api.domain.entity;

import static com.moabam.global.error.model.ErrorMessage.*;
import static java.util.Objects.*;

import org.hibernate.annotations.ColumnDefault;

import com.moabam.api.domain.entity.enums.ItemCategory;
import com.moabam.api.domain.entity.enums.ItemType;
import com.moabam.global.common.entity.BaseTimeEntity;
import com.moabam.global.error.exception.BadRequestException;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "item")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Enumerated(value = EnumType.STRING)
@Column(name = "type", nullable = false)
private ItemType type;

@Enumerated(value = EnumType.STRING)
@Column(name = "category", nullable = false)
private ItemCategory category;

@Column(name = "name", nullable = false)
private String name;

@Column(name = "image", nullable = false)
private String image;

@Column(name = "bug_price", nullable = false)
@ColumnDefault("0")
private int bugPrice;

@Column(name = "golden_bug_price", nullable = false)
@ColumnDefault("0")
private int goldenBugPrice;

@Column(name = "unlock_level", nullable = false)
@ColumnDefault("1")
private int unlockLevel;

@Builder
private Item(ItemType type, ItemCategory category, String name, String image, int bugPrice, int goldenBugPrice,
Integer unlockLevel) {
this.type = requireNonNull(type);
this.category = requireNonNull(category);
this.name = requireNonNull(name);
this.image = requireNonNull(image);
this.bugPrice = validatePrice(bugPrice);
this.goldenBugPrice = validatePrice(goldenBugPrice);
this.unlockLevel = validateLevel(requireNonNullElse(unlockLevel, 1));
}

private int validatePrice(int price) {
if (price < 0) {
throw new BadRequestException(INVALID_PRICE);
}

return price;
}

private int validateLevel(int level) {
if (level < 1) {
throw new BadRequestException(INVALID_LEVEL);
}

return level;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.moabam.api.domain.entity.enums;

public enum ItemCategory {

SKIN;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.moabam.api.domain.entity.enums;

public enum ItemType {

MORNING,
NIGHT;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.moabam.api.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.moabam.api.domain.entity.Inventory;

public interface InventoryRepository extends JpaRepository<Inventory, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.moabam.api.domain.repository;

import static com.moabam.api.domain.entity.QInventory.*;
import static com.moabam.api.domain.entity.QItem.*;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.moabam.api.domain.entity.Item;
import com.moabam.api.domain.entity.enums.ItemType;
import com.moabam.global.common.util.DynamicQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class InventorySearchRepository {

private final JPAQueryFactory jpaQueryFactory;

public List<Item> findItems(Long memberId, ItemType type) {
return jpaQueryFactory.selectFrom(inventory)
.join(inventory.item, item)
.where(
DynamicQuery.generateEq(memberId, inventory.memberId::eq),
DynamicQuery.generateEq(type, inventory.item.type::eq))
.orderBy(inventory.createdAt.desc())
.select(item)
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.moabam.api.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.moabam.api.domain.entity.Item;

public interface ItemRepository extends JpaRepository<Item, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.moabam.api.domain.repository;

import static com.moabam.api.domain.entity.QInventory.*;
import static com.moabam.api.domain.entity.QItem.*;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.moabam.api.domain.entity.Item;
import com.moabam.api.domain.entity.enums.ItemType;
import com.moabam.global.common.util.DynamicQuery;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class ItemSearchRepository {

private final JPAQueryFactory jpaQueryFactory;

public List<Item> findNotPurchasedItems(Long memberId, ItemType type) {
return jpaQueryFactory.selectFrom(item)
.leftJoin(inventory)
.on(inventory.item.id.eq(item.id))
.where(
DynamicQuery.generateEq(type, item.type::eq),
DynamicQuery.generateEq(memberId, this::filterByMemberId))
.orderBy(
item.unlockLevel.asc(),
item.bugPrice.asc(),
item.goldenBugPrice.asc(),
item.name.asc())
.fetch();
}

private BooleanExpression filterByMemberId(Long memberId) {
return inventory.memberId.isNull()
.or(inventory.memberId.ne(memberId));
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/moabam/api/dto/BugMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class BugMapper {

public static BugResponse toBugResponse(Bug bug) {
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/com/moabam/api/dto/ItemMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.moabam.api.dto;

import java.util.List;

import com.moabam.api.domain.entity.Item;
import com.moabam.global.common.util.StreamUtils;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ItemMapper {

public static ItemResponse toItemResponse(Item item) {
return ItemResponse.builder()
.id(item.getId())
.type(item.getType().name())
.category(item.getCategory().name())
.name(item.getName())
.image(item.getImage())
.level(item.getUnlockLevel())
.bugPrice(item.getBugPrice())
.goldenBugPrice(item.getGoldenBugPrice())
.build();
}

public static ItemsResponse toItemsResponse(List<Item> purchasedItems, List<Item> notPurchasedItems) {
return ItemsResponse.builder()
.purchasedItems(StreamUtils.map(purchasedItems, ItemMapper::toItemResponse))
.notPurchasedItems(StreamUtils.map(notPurchasedItems, ItemMapper::toItemResponse))
.build();
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/moabam/api/dto/ItemResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.moabam.api.dto;

import lombok.Builder;

@Builder
public record ItemResponse(
Long id,
String type,
String category,
String name,
String image,
int level,
int bugPrice,
int goldenBugPrice
) {

}
13 changes: 13 additions & 0 deletions src/main/java/com/moabam/api/dto/ItemsResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moabam.api.dto;

import java.util.List;

import lombok.Builder;

@Builder
public record ItemsResponse(
List<ItemResponse> purchasedItems,
List<ItemResponse> notPurchasedItems
) {

}
7 changes: 3 additions & 4 deletions src/main/java/com/moabam/api/dto/ProductMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import java.util.List;

import com.moabam.api.domain.entity.Product;
import com.moabam.global.common.util.StreamUtils;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ProductMapper {

public static ProductResponse toProductResponse(Product product) {
Expand All @@ -22,9 +23,7 @@ public static ProductResponse toProductResponse(Product product) {

public static ProductsResponse toProductsResponse(List<Product> products) {
return ProductsResponse.builder()
.products(products.stream()
.map(ProductMapper::toProductResponse)
.toList())
.products(StreamUtils.map(products, ProductMapper::toProductResponse))
.build();
}
}
Loading

0 comments on commit 0201932

Please sign in to comment.