MAKITTDocs

Cart Domain

docs/domain/cart.md

Cart Domain

개요

장바구니 생성, 상품 추가/수량 변경/삭제, 게스트→회원 전환, 정책 관리를 담당하는 커머스 도메인. 게스트와 회원 모두 장바구니를 사용할 수 있으며, DynamoDB TTL을 통해 만료된 장바구니가 자동 정리됩니다.

서버 패키지: com.makitt.core.domain.cart


엔티티 관계도

┌─────────────────────────────────────────────────────────┐
│  Shop                                                   │
│  ├── CartPolicy (장바구니 정책 — Shop과 PK 공유)         │
│  │                                                      │
│  └── Cart (장바구니)                                     │
│       ├── owner: Guest 또는 Customer                    │
│       └── CartItem (장바구니 항목 — Cart와 PK 공유)      │
│            ├── StoreItem 참조                            │
│            └── CartItemSnapshot (추가 시점 스냅샷)       │
└─────────────────────────────────────────────────────────┘

계층 요약:

계층역할예시
Cart장바구니 (소유자, 상태, TTL)cart_a1b2c3d4e5f6 (ACTIVE)
CartItem장바구니 내 상품 항목 (수량, 스냅샷)ci_x1y2z3w4v5u6 qty:2
CartItemSnapshot추가 시점의 상품 정보 스냅샷"Velvet Lip Tint - Rose Pink" ₩22,000
CartPolicyShop별 장바구니 정책 (TTL, 수량 제한, 병합 전략 등)게스트 허용, 최대 50개 라인

1. Cart

DynamoDB Entity

소스: makitt-core/.../cart/entity/Cart.java

필드타입설명
cartIdStringcart_{12자 UUID} 형식
shopIdString소속 Shop ID
ownerTypeCartOwnerTypeGUEST 또는 CUSTOMER
ownerIdStringGuest ID 또는 Customer ID
statusCartStatusACTIVE 또는 EXPIRED
itemCountInteger비정규화된 항목 수 (빠른 조회용)
ttlLongDynamoDB TTL (Unix 초). 자동 만료용
createdAtInstant생성 시각
updatedAtInstant최종 수정 시각

키 패턴

Key패턴예시
PKCART#{cartId}CART#cart_a1b2c3d4e5f6
SKMETADATAMETADATA
GSI1 PKSHOP#{shopId}SHOP#8e450dc8-...
GSI1 SKCART#{createdAtEpochMs}#{cartId}CART#1771392065711#cart_a1b2...
GSI2 PKCART_OWNER#{shopId}#{ownerType}#{ownerId}CART_OWNER#8e450dc8-...#CUSTOMER#cust_abc123
GSI2 SKCART#{status}#{cartId}CART#ACTIVE#cart_a1b2...
  • GSI1 (EntityLookupIndex): Shop별 장바구니 목록 조회
  • GSI2 (UniqueLookupIndex): 소유자별 활성 장바구니 조회 (CART#ACTIVE# prefix로 sortBeginsWith)

TTL 기본값

소유자TTL
Guest30일
Customer90일

팩토리 메서드

Cart.createForGuest(shopId, guestId) // 게스트 장바구니 생성 Cart.createForCustomer(shopId, customerId) // 회원 장바구니 생성

비즈니스 메서드

메서드설명
updateItemCount(newCount)항목 수 갱신 (새 Cart 반환)
expire()상태를 EXPIRED로 변경
transferToCustomer(customerId)게스트 → 회원 전환
extendTtl()TTL 갱신
isEmpty()항목 수 0 여부
isActive()ACTIVE 상태 여부

2. CartItem

DynamoDB Entity

소스: makitt-core/.../cart/entity/CartItem.java

필드타입설명
cartItemIdStringci_{12자 UUID} 형식
cartIdString부모 Cart ID
storeItemKeyStringStoreItem PK (STOREITEM#{storeItemId})
storeItemIdStringStoreItem ID
quantityInteger수량
displaySnapshotCartItemSnapshot추가 시점 상품 정보 스냅샷
addedAtInstant추가 시각
updatedAtInstant최종 수정 시각

키 패턴

Key패턴예시
PKCART#{cartId}CART#cart_a1b2c3d4e5f6 (Cart와 동일)
SKCARTITEM#{cartItemId}CARTITEM#ci_x1y2z3w4v5u6

Item Collection 패턴: Cart와 CartItem이 동일한 PK를 공유하여, SK begins_with CARTITEM#로 한 장바구니의 모든 항목을 효율적으로 조회합니다.

팩토리 메서드

CartItem.create(cartId, storeItemKey, storeItemId, quantity, displaySnapshot)

비즈니스 메서드

메서드설명
updateQuantity(newQuantity)수량 변경 (새 CartItem 반환)
updateSnapshot(newSnapshot)스냅샷 갱신
getLineTotal()snapshot.price × quantity 계산

3. CartItemSnapshot

내장 객체

소스: makitt-core/.../cart/entity/CartItemSnapshot.java

장바구니에 상품을 추가하는 시점의 상품 정보를 스냅샷으로 저장합니다. 이후 상품 정보가 변경되어도 장바구니에는 추가 시점의 정보가 유지됩니다.

필드타입설명
productNameString상품명
thumbnailString썸네일 이미지 URL
storeItemNameStringStoreItem 표시명 (옵션 조합명)
priceBigDecimal단가
compareAtPriceBigDecimal정가 (할인 표시용, nullable)

팩토리 메서드

CartItemSnapshot.of(productName, thumbnail, storeItemName, price, compareAtPrice) CartItemSnapshot.empty() // 빈 스냅샷

스냅샷 생성 시점

CartApplication.addItem()
  │
  ├─ StoreItemService → StoreItem 조회 (price, storeItemName)
  ├─ ProductService → Product 조회 (productName, thumbnail)
  │
  └─ CartItemSnapshot.of(productName, thumbnail, storeItemName, price, compareAtPrice)

4. CartPolicy

DynamoDB Entity

소스: makitt-core/.../cart/entity/policy/CartPolicy.java

Shop별 장바구니 운영 정책. Shop 엔티티와 PK를 공유합니다 (item collection).

키 패턴

Key패턴예시
PKSHOP#{shopId}SHOP#8e450dc8-... (Shop과 동일)
SKCART_POLICYCART_POLICY

A. 접근 & 유지 정책

필드타입기본값설명
guestCartEnabledBooleantrue게스트 장바구니 허용
cartTtlDaysInteger30장바구니 TTL (1–365일)
mergeStrategyMergeStrategyCOMBINE게스트→회원 전환 시 병합 전략
duplicateHandlingDuplicateHandlingSUM_QUANTITY병합 시 중복 항목 처리

B. 수량 & 라인 제한

필드타입기본값설명
maxItemQuantityIntegernull (무제한)항목당 최대 수량 (1–999)
maxCartLinesInteger50최대 라인 수 (1–999)
minOrderAmountEnabledBooleanfalse최소 주문 금액 적용 여부
minOrderAmountLongnull최소 주문 금액 (최소 화폐 단위)

C. 재고 & 가격 변동 처리

필드타입기본값설명
outOfStockHandlingOutOfStockHandlingBLOCK품절 상품 추가 처리
autoAdjustQuantityBooleantrue재고 부족 시 수량 자동 조정
priceChangeHandlingPriceChangeHandlingALWAYS_UPDATE가격 변동 처리
showPriceChangeBadgeBooleantrue가격 변동 배지 표시

D. 결제 규칙

필드타입기본값설명
requireLoginForCheckoutBooleanfalse결제 시 로그인 필수
collectGuestEmailBeforeCheckoutBooleantrue게스트 결제 전 이메일 수집
draftOrderExpiryDraftOrderExpiryHOURS_72임시 주문 만료 시간

팩토리 메서드

CartPolicy.createDefault(shopId) // 위 기본값으로 정책 생성

5. Enum 정의

CartStatus

설명
ACTIVE활성 장바구니 — 수정 가능
EXPIRED만료된 장바구니 — 더 이상 사용 불가

CartOwnerType

설명
GUEST비회원 (게스트 토큰)
CUSTOMER회원

MergeStrategy (게스트→회원 전환 시)

설명
COMBINE양쪽 장바구니 합침
LOGIN_USER회원 장바구니만 유지
GUEST게스트 장바구니만 유지

DuplicateHandling (병합 시 중복 항목)

설명
SUM_QUANTITY수량 합산
KEEP_HIGHER더 큰 수량 유지
KEEP_LOGIN_USER회원 쪽 수량 유지

OutOfStockHandling

설명
BLOCK품절 상품 추가 차단
ALLOW_WITH_NOTICE알림 표시 후 허용

PriceChangeHandling

설명
ALWAYS_UPDATE항상 최신 가격으로 갱신
CONFIRM_AT_CHECKOUT결제 시 가격 변동 확인

DraftOrderExpiry

시간
HOURS_2424시간
HOURS_7272시간
DAYS_77일 (168시간)

6. 아키텍처 패턴

6.1 Item Collection 패턴

Cart와 CartItem이 동일한 PK (CART#{cartId})를 공유합니다:

PK: CART#cart_abc123
  SK: METADATA           → Cart 엔티티
  SK: CARTITEM#ci_001    → CartItem #1
  SK: CARTITEM#ci_002    → CartItem #2
  SK: CARTITEM#ci_003    → CartItem #3
  • Cart 조회: PK = CART#{cartId}, SK = METADATA
  • CartItem 전체 조회: PK = CART#{cartId}, SK begins_with CARTITEM#
  • 개별 CartItem 조회: PK = CART#{cartId}, SK = CARTITEM#{cartItemId}

6.2 트랜잭션 패턴

Cart와 CartItem의 일관성을 위해 DynamoDB TransactWriteItems를 사용합니다:

새 항목 추가 시:
  TransactWriteItems:
    1. Cart 갱신 (itemCount + 1)
    2. CartItem 저장

6.3 중복 항목 처리

같은 StoreItem을 다시 추가하면 새 항목을 만들지 않고 기존 항목의 수량을 증가시킵니다:

addItemToCart(cart, item)
  ├─ 기존 항목 존재? → 수량 합산 (itemCount 유지)
  └─ 새 항목? → CartItem 저장 + itemCount 증가 (트랜잭션)

6.4 Display Snapshot 패턴

장바구니에 상품을 추가할 때, 그 시점의 상품 정보를 스냅샷으로 저장합니다. 이후 상품 가격이 변경되거나 삭제되어도 장바구니에서는 추가 시점의 정보를 표시할 수 있습니다.

6.5 GSI KEYS_ONLY 패턴

Cart의 GSI 조회 (소유자별 활성 장바구니 찾기)는 KEYS_ONLY projection을 사용합니다:

1. GSI2 쿼리 → PK/SK 키만 반환
2. Main Table getItem() → 전체 데이터 조회

7. 레이어별 클래스

Service (makitt-core)

클래스주요 메서드
CartServicegetOrCreateGuestCart, getOrCreateCustomerCart, findById, findActiveCartByOwner, addItemToCart, findCartItems, updateItemQuantity, removeItem, clearCart, deleteCartWithItems, expireCart
CartPolicyServicegetOrCreatePolicy, getPolicy, save, delete

Repository (makitt-core)

클래스설명
CartRepositoryCart/CartItem CRUD. 트랜잭션 쓰기 (saveCartWithItem, updateCartAndSaveItem), GSI2 소유자 조회, SK prefix 아이템 목록 조회
CartPolicyRepositoryCartPolicy CRUD (SHOP#{shopId}, CART_POLICY)

Application (makitt-application)

클래스의존성주요 메서드
CartApplicationCartService, StoreItemService, ProductServicegetOrCreateCart, getCart, getActiveCartByOwner, addItem, updateItemQuantity, removeItem, clearCart, deleteCart
CartPolicyApplicationCartPolicyServicegetCartPolicy, updateCartPolicy

Value Objects (makitt-core)

VO설명
CartResponseVoCart + items + subtotal 응답. fromEntity(Cart), fromEntity(Cart, List<CartItem>)
CartItemResponseVoCartItem + lineTotal 응답. fromEntity(CartItem)
CartItemSnapshotVo스냅샷 VO. fromEntity(CartItemSnapshot)
CartPolicyResponseVo정책 VO (enum → EnumDto 변환). fromEntity(CartPolicy)

8. Shop API

8.1 엔드포인트

모든 엔드포인트는 인증이 필요합니다 (ShopContext).

MethodPath설명인증
GET/shop/{shopId}/cart/summary장바구니 요약 (헤더 배지용)필수
GET/shop/{shopId}/cart장바구니 상세 (전체 항목 포함)필수
POST/shop/{shopId}/cart/items장바구니에 상품 추가필수

소스: makitt-shop-api/.../controller/CartController.java

8.2 장바구니 요약 API

GET /shop/{shopId}/cart/summary

헤더의 장바구니 아이콘 배지 등에 사용. 장바구니가 없으면 자동 생성합니다.

Response: CartSummaryResponse

{ "cartId": "cart_a1b2c3d4e5f6", "itemCount": 3 }

8.3 장바구니 상세 API

GET /shop/{shopId}/cart

장바구니의 전체 항목을 포함한 상세 정보. 장바구니가 없으면 자동 생성합니다.

Response: CartDetailResponse

{ "cartId": "cart_a1b2c3d4e5f6", "shopId": "8e450dc8-2434-4ab4-aa75-252bbb8ace42", "ownerType": { "name": "CUSTOMER", "description": "Registered customer" }, "status": { "name": "ACTIVE", "description": "Active cart - can be modified" }, "itemCount": 2, "items": [ { "cartItemId": "ci_x1y2z3w4v5u6", "cartId": "cart_a1b2c3d4e5f6", "storeItemId": "720f4dbe-8e57-49d6-be96-a17ed477cf8f", "quantity": 2, "productName": "Vitamin C Brightening Serum", "thumbnail": "https://cdn.dev.makitt.shop/products/main/2026/02/79514563.png", "storeItemName": "단품", "price": "35000", "compareAtPrice": null, "lineTotal": "70000", "addedAt": 1771392065711, "updatedAt": 1771392065711 } ], "subtotal": "70000", "createdAt": 1771392065711, "updatedAt": 1771396886123 }

Response 필드:

필드타입설명
cartIdString장바구니 ID
shopIdStringShop ID
ownerTypeEnumDtoGUEST 또는 CUSTOMER
statusEnumDtoACTIVE 또는 EXPIRED
itemCountInteger항목 수 (라인 수)
itemsList<CartItemDto>항목 목록
subtotalString (decimal)합계 금액
createdAtLong (epoch-ms)생성 시각
updatedAtLong (epoch-ms)최종 수정 시각

CartItemDto 필드:

필드타입설명
cartItemIdString항목 ID
cartIdString장바구니 ID
storeItemIdStringStoreItem ID
quantityInteger수량
productNameString상품명 (스냅샷)
thumbnailString썸네일 URL (스냅샷, nullable)
storeItemNameString옵션 조합명 (스냅샷)
priceString (decimal)단가 (스냅샷)
compareAtPriceString (decimal)정가 (스냅샷, nullable)
lineTotalString (decimal)price × quantity
addedAtLong (epoch-ms)추가 시각
updatedAtLong (epoch-ms)최종 수정 시각

8.4 장바구니 추가 API

POST /shop/{shopId}/cart/items

장바구니에 상품을 추가합니다. 두 가지 방식을 지원합니다.

Request: AddToCartRequest

{ "productId": "e91d3996-6623-4e56-b5c0-ccf631cb7154", "selections": [ "pvg_ample-ingredient:ing_niacinamide", "pvg_ample-ml:ml_10", "pvg_mask_offer-mask_qty:mask_1ea" ], "quantity": 1 }
필드타입필수설명
productIdStringO상품 ID
selectionsList<String>valueSelection 토큰 배열 (권장)
variantSelectionsMap<String, String>[Deprecated] variantGroupId → combinationKey 맵
quantityIntegerX수량 (기본값: 1)

selectionsvariantSelections 중 하나를 전달합니다. selections가 있으면 우선 사용됩니다.

내부 처리 흐름:

1. ShopContext에서 인증 정보 추출 (customerId 또는 guestId)
2. 활성 장바구니 조회 또는 자동 생성
3. StoreItem 해석:
   ├─ selections 있음 → StoreItemResolver.resolveFromSelections()
   │   ├─ 같은 pvgId의 selection 합산 (optionKey:valueKey|optionKey:valueKey)
   │   ├─ required=false PVG 누락 시 "pvgId-none" 자동 보충
   │   └─ canonical key 생성 → DynamoDB GSI3 조회
   └─ variantSelections 있음 → StoreItemResolver.resolve() (레거시)
4. CartItemSnapshot 생성 (StoreItem + Product에서 정보 수집)
5. 장바구니에 추가:
   ├─ 동일 StoreItem 기존 항목 존재 → 수량 합산
   └─ 새 항목 → CartItem 생성 + itemCount 증가 (트랜잭션)

Response: AddToCartResponse

{ "item": { "cartItemId": "ci_x1y2z3w4v5u6", "cartId": "cart_a1b2c3d4e5f6", "storeItemId": "720f4dbe-8e57-49d6-be96-a17ed477cf8f", "quantity": 1, "productName": "Ampoule Mask Combo Set", "thumbnail": "https://cdn.dev.makitt.shop/products/main/2026/02/abcdef01.png", "storeItemName": "나이아신아마이드 10ml / 마스크 1매", "price": "25000", "compareAtPrice": null, "lineTotal": "25000", "addedAt": 1771396886123, "updatedAt": 1771396886123 }, "cartId": "cart_a1b2c3d4e5f6", "cartItemCount": 3 }

8.5 에러 응답

모든 비즈니스 에러는 HTTP 400으로 반환됩니다.

상황에러 코드메시지
인증 필요error.auth.not_authenticated장바구니 기능을 사용하려면 로그인이 필요합니다
장바구니 없음error.cart.not.found장바구니를 찾을 수 없습니다
장바구니 만료error.cart.expired만료된 장바구니입니다
항목 없음error.cart.item.not.found장바구니 항목을 찾을 수 없습니다
StoreItem 미매칭error.product.store.item.not.found선택한 옵션 조합에 해당하는 상품을 찾을 수 없습니다
잘못된 selection 형식error.product.store.item.invalid.selections잘못된 선택 형식입니다

9. Domain Manifest

소스: makitt-shop-api/.../manifest/descriptor/CartDomainDescriptor.java

Builder가 API 연동 시 참조하는 리소스 정의입니다.

리소스타입Path설명
cartSummaryQuery GET/cart/summary장바구니 요약
cartDetailQuery GET/cart장바구니 상세
addToCartAction POST/cart/items상품 추가

GSI 인덱스 사용 현황

GSI인덱스명용도
GSI1EntityLookupIndexShop별 장바구니 목록
GSI2UniqueLookupIndex소유자별 활성 장바구니 조회