Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
227febd
Merge pull request #2 from pable91/feature/round2/round2
pable91 Feb 18, 2026
0cd9724
feat : (브랜드 도메인) 도메인 객체 & jpa 엔티티 생성
Feb 19, 2026
4bcf401
test : (브랜드 도메인) 생성 테스트 코드 작성
Feb 19, 2026
2a63a28
feat : (브랜드 도메인) 브랜드 수정 비즈니스 코드 작성
Feb 19, 2026
3821d0c
refactor : user 패키지 생성
Feb 19, 2026
054c9eb
test : (브랜드 도메인) BrandService 테스트 코드 작성
Feb 19, 2026
cc5b091
refactor : BaseEntity id 초기화 부분 제거
Feb 20, 2026
5c3816d
chore : 클로드 md 파일 수정
Feb 20, 2026
686da5d
feat : (상품 도메인) 도메인 객체 & DB 엔티티 추가
Feb 20, 2026
0f473a7
test : (상품 도메인) 도메인 객체 테스트 코드 작성
Feb 20, 2026
41f3527
refactor : (브랜드 도메인) record -> class 변경
Feb 20, 2026
2dba98d
feat : (상품 도메인) repository 인터페이스 & 구현 클래스 작성
Feb 20, 2026
57ff003
feat : (브랜드 도메인) 브랜드 존재 여부 로직 & 테스트 코드 작성
Feb 21, 2026
502f010
feat : (상품 도메인) 상품 등록 도메인 서비스 로직 작성
Feb 21, 2026
a29112b
test : (브랜드 도메인) 브랜드 존재하지 않을 때 테스트 코드 작성
Feb 21, 2026
78df4b7
test : (상품 도메인) 상품 생성 테스트 코드 작성
Feb 21, 2026
aaa87a6
feat : (상품 도메인) 상품 수량 감소 비즈니스 로직 추가
Feb 21, 2026
7b3a195
remove : (브랜드 도메인) 브랜드 정보 update 할 때 불필요한 파라미터 제거
Feb 21, 2026
5bc2ad5
test : (상품 도메인) 상품 수량 감소시 상품이 존재하지 않으면, 예외 던지는 테스트 코드 작성
Feb 22, 2026
5530876
feat : 상품 목록 조건+페이징 조회 코드 작성
Feb 22, 2026
19873ec
test : 상품 목록 조건+페이징 조회시 브랜드 존재하지 않으면, 예외 던지는 테스트 코드 작성
Feb 22, 2026
41300ac
feat : 차감 수량 파라미터가 양수가 아닌 경우 예외를 던지는 비즈니스 & 테스트 코드 작성
Feb 22, 2026
7e27a70
refactor : (상품 도메인) null 방어 코드 추가
Feb 22, 2026
4284c43
refactor : (상품 도메인) 상품 목록 조회시 null 방어 코드 추가
Feb 22, 2026
14c12c9
feat : (좋아요 도메인) Product 도메인에 likeCount 필드 및 테스트 코드 추가
Feb 23, 2026
758e774
refactor : ProductTest 코드 중복 코드 정리
Feb 23, 2026
d266b58
feat : (좋아요 도메인) 도메인 및 jpa 엔티티 코드 작성
Feb 23, 2026
b339b4d
feat : (상품 도메인) 좋아요 증감 메서드 및 테스트코드 작성
Feb 23, 2026
787b69b
feat : (좋아요 도메인) 좋아요 예외 케이스 및 테스트 코드 작성
Feb 23, 2026
3530cda
feat : (좋아요 도메인) 좋아요 증감 도메인 서비스 코드 작성
Feb 23, 2026
3211c09
feat : (좋아요 도메인) 좋아요 기능 멱등성을 위한 예외 추가 및 테스트 코드 작성
Feb 23, 2026
76c3d16
feat : (좋아요 도메인) LikeFacade 코드 작성
Feb 23, 2026
827fe4c
test : (좋아요 도메인) null 파라미터 추가
Feb 23, 2026
177fff1
refactor : Product 도메인 서비스에 존재하던 BrandValidator를 삭제
Feb 23, 2026
29400a4
feat : 상품 상세 조회 facade 로직 & 응답 dto 추가
Feb 23, 2026
76713b8
test : 브랜드 update 메서드 테스트 코드 추가
Feb 23, 2026
a235eb0
feat : (오더 도메인) 도메인 객체 & DB 엔티티 객체 생성
Feb 24, 2026
b3393be
test : (오더 도메인) 오더 도메인 객체 테스트 작성
Feb 24, 2026
fb1d1ed
feat : (오더 도메인) Order, OrderItem 도메인 객체 및 DB 엔티티 작성
Feb 24, 2026
009ae72
refactor : 불변 데이터(=필드)에 대한 db 방어 코드 추가
Feb 24, 2026
fa21c2f
test : OrderItem, OrderStatusHistory 도메인 테스트 코드 작성
Feb 24, 2026
0823fee
feat : (주문 도메인) Order, OrderItem, OrderStatusHistory repository 생성
Feb 26, 2026
5484c83
feat : (주문 도메인) facade 주문 로직 작성
Feb 26, 2026
3e925df
fix : 상품 목록을 조회하는 로직 추가
Feb 26, 2026
44c263a
rename : 의미에 맞게 find get 메서드명 변경
Feb 26, 2026
908b09f
test : (상품 도메인) 상품 목록 조회 테스트 코드 추가
Feb 26, 2026
4af8071
refactor : (주문 도메인) 주문 비즈니스 로직을 facade -> service 계층으로 이동
Feb 26, 2026
a9b71dd
refactor : orderItem, orderStatusHistory의 service들을 따로 분리하도록 수정
Feb 26, 2026
e725232
test : orderItem 도메인 테스트 코드 작성
Feb 26, 2026
099ecd9
test : (좋아요 도메인) like 테스트 코드 작성
Feb 26, 2026
3f94e66
feat : interface 레이어 작성 & application 레이어에서 사용하는 command dto 작성
Feb 26, 2026
073de4d
refactor : 중복되는 예외 메시지를 관리하는 클래스 생성
Feb 26, 2026
a55e62e
refactor : primitive타입을 Money VO로 변경
Feb 27, 2026
72d3834
test : 좋아요, 주문, 상품 E2E 테스트 작성
Feb 28, 2026
66efe5b
refactor : Order, OrderItem, OrderStatusHistory 도메인들에 애그리거트 도입
Mar 2, 2026
f04258a
refactor : Order, OrderItem, OrderStatusHistory 엔티티에도 애그리거트 도입 + 저장, …
Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@ infrastructure/ → JpaRepository 구현체
support/error/ → CoreException, ErrorType (에러 코드 enum)
```

- Repository 패턴: domain에 인터페이스, infrastructure에 구현체
- 본 프로젝트는 레이어드 아키텍처를 따르며, DIP (의존성 역전 원칙) 을 준수합니다.
- Repository 패턴: domain에 인터페이스, infrastructure에 구현체
- API request, response DTO와 응용 레이어의 DTO는 분리해 작성
- Facade 패턴: application 레이어에서 여러 도메인 서비스 조합
- API 버전닝: `/api/v1/` 경로 기반
- 글로벌 예외 처리: `ApiControllerAdvice`에서 `CoreException` → `ApiResponse` 변환

### BaseEntity (modules/jpa)

모든 엔티티의 부모 클래스. Auto-increment ID, `createdAt`/`updatedAt`/`deletedAt` 자동 관리, `delete()`/`restore()` 소프트 삭제 지원.
#### 도메인 & 객체 설계 전략
- 도메인 객체는 비즈니스 규칙을 캡슐화해야 합니다.
- 애플리케이션 서비스는 서로 다른 도메인을 조립해, 도메인 로직을 조정하여 기능을 제공해야 합니다.
- 규칙이 여러 서비스에 나타나면 도메인 객체에 속할 가능성이 높습니다.
- 각 기능에 대한 책임과 결합도에 대해 개발자의 의도를 확인하고 개발을 진행합니다.

## Testing

Expand Down Expand Up @@ -118,3 +122,5 @@ support/error/ → CoreException, ErrorType (에러 코드 enum)
- 불필요한 코드 제거 및 품질 개선
- 객체지향적 코드 작성, 성능 최적화
- 모든 테스트 케이스가 통과해야 함


Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.loopers.application.like;

import com.loopers.domain.like.LikeService;
import com.loopers.domain.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class LikeFacade {

private final LikeService likeService;
private final ProductService productService;

public void toggleLike(Long productId, Long userId) {
if (likeService.isLiked(productId, userId)) {
likeService.unlike(productId, userId);
productService.decreaseLikeCount(productId);
} else {
likeService.like(productId, userId);
productService.increaseLikeCount(productId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.application.order;

import java.util.Map;

public record OrderCommand(
Long userId,
Map<Long, Integer> productQuantities
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.loopers.application.order;

import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderItemSpec;
import com.loopers.domain.order.OrderService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductService;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class OrderFacade {

private final ProductService productService;
private final OrderService orderService;

public OrderInfo order(OrderCommand command) {
List<Long> productIds = new ArrayList<>(command.productQuantities().keySet());
List<Product> products = productService.getByIds(productIds);

products.forEach(product ->
productService.decreaseStock(product.getId(), command.productQuantities().get(product.getId())));

List<OrderItemSpec> itemSpecs = products.stream()
.map(product -> new OrderItemSpec(
product.getId(),
product.getPrice(),
command.productQuantities().get(product.getId())
))
.toList();

// 도메인 서비스에 주문 애그리거트 생성/저장 위임
Order savedOrder = orderService.placeOrder(command.userId(), itemSpecs);

return OrderInfo.from(savedOrder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.application.order;

import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderStatus;
import java.time.ZonedDateTime;

public record OrderInfo(
Long id,
Long userId,
OrderStatus status,
Integer totalPrice,
ZonedDateTime orderDt
) {
public static OrderInfo from(Order order) {
return new OrderInfo(
order.getId(),
order.getRefUserId(),
order.getStatus(),
order.getTotalPrice().value(),
order.getOrderDt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.loopers.application.product;

import java.util.Map;

public record CreateProductCommand(
Map<Long, ProductItem> products
) {
public record ProductItem(
String name,
Integer price,
Integer stock
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.loopers.application.product;

import com.loopers.domain.brand.Brand;
import com.loopers.domain.brand.BrandService;
import com.loopers.domain.product.CreateProductRequest;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductSearchCondition;
import com.loopers.domain.product.ProductService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class ProductFacade {

private final BrandService brandService;
private final ProductService productService;

public List<ProductInfo> createProducts(CreateProductCommand command) {
command.products().keySet().forEach(brandService::getById);

Map<Long, CreateProductRequest> domainRequest = new HashMap<>();
command.products().forEach((brandId, item) -> {
domainRequest.put(brandId, new CreateProductRequest(
item.name(),
item.price(),
item.stock()
));
});

List<Product> products = productService.createProducts(domainRequest);
return products.stream()
.map(product -> {
Brand brand = brandService.getById(product.getRefBrandId());
return ProductInfo.of(product, brand);
})
.toList();
}

@Transactional(readOnly = true)
public ProductInfo getProduct(Long productId) {
Product product = productService.getById(productId);
Brand brand = brandService.getById(product.getRefBrandId());
return ProductInfo.of(product, brand);
}

@Transactional(readOnly = true)
public List<ProductInfo> getProducts(ProductSearchCommand command) {
if (command.hasBrandId()) {
brandService.getById(command.brandId());
}

ProductSearchCondition condition = new ProductSearchCondition(
command.brandId(),
command.sortType(),
command.page(),
command.size()
);

return productService.findProducts(condition).stream()
.map(product -> {
Brand brand = brandService.getById(product.getRefBrandId());
return ProductInfo.of(product, brand);
})
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.loopers.application.product;

import com.loopers.domain.brand.Brand;
import com.loopers.domain.product.Product;

public record ProductInfo(
Long id,
String name,
Integer price,
Integer stock,
Integer likeCount,
Long brandId,
String brandName,
String brandDescription
) {

public static ProductInfo of(Product product, Brand brand) {
return new ProductInfo(
product.getId(),
product.getName(),
product.getPrice().value(),
product.getStock(),
product.getLikeCount(),
brand.getId(),
brand.getName(),
brand.getDescription()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.loopers.application.product;

import com.loopers.domain.product.ProductSortType;

public record ProductSearchCommand(
Long brandId,
ProductSortType sortType,
int page,
int size
) {
public boolean hasBrandId() {
return brandId != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.loopers.domain.brand;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorMessage;
import com.loopers.support.error.ErrorType;

/**
* 브랜드 도메인 객체
*/
public class Brand {

private final Long id;
private String name;
private String description;

private Brand(Long id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}

public static Brand create(Long id, String name, String description) {
validateName(name);

return new Brand(id, name, description);
}

private static void validateName(String name) {
if (name == null || name.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, ErrorMessage.Brand.BRAND_NAME_REQUIRED);
}
}

public void update(String name, String description) {
validateName(name);

this.name = name;
this.description = description;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public String getDescription() {
return description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.loopers.domain.brand;

import java.util.Optional;

public interface BrandRepository {

Brand create(Brand brand);
Brand update(Brand brand);
Optional<Brand> findById(Long id);
boolean existsById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.loopers.domain.brand;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorMessage;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BrandService {

private final BrandRepository brandRepository;

public Brand create(String name, String description) {
Brand brand = Brand.create(null, name, description);
return brandRepository.create(brand);
}

public Brand update(Long id, String name, String description) {
Brand brand = getById(id);
brand.update(name, description);
return brandRepository.update(brand);
}

public Brand getById(Long id) {
return brandRepository.findById(id)
.orElseThrow(() -> new CoreException(ErrorType.BAD_REQUEST, ErrorMessage.Brand.BRAND_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.loopers.domain.common;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorMessage;
import com.loopers.support.error.ErrorType;

/**
* 금액 Value Object
*/
public record Money(Integer value) {

public static final Money ZERO = new Money(0);

public Money {
if (value == null || value < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, ErrorMessage.Money.AMOUNT_INVALID);
}
}

public Money add(Money other) {
return new Money(this.value + other.value);
}

public Money multiply(int multiplier) {
return new Money(this.value * multiplier);
}
}
Loading