Skip to content

Commit

Permalink
feat : cartStore 생성 후, 전역으로 데이터 처리하게 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
effozen committed Jan 16, 2025
1 parent 547171a commit ff7b379
Show file tree
Hide file tree
Showing 15 changed files with 190 additions and 117 deletions.
21 changes: 13 additions & 8 deletions src/basic/__tests__/basic.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { useState } from 'react';
import { describe, expect, test } from 'vitest';
import { describe, expect, test, beforeEach } from 'vitest';
import { act, fireEvent, render, renderHook, screen, within } from '@testing-library/react';
import { CartPage } from '@/pages/CartPage/';
import { AdminPage } from '@/pages/AdminPage/';
import { CartItem, Coupon, Product } from '@/shared/types/';
import { useCart, useCoupons, useProducts } from '@/features/hooks';
import { useCoupons, useProducts } from '@/features/hooks';
import * as cartUtils from '@/entities/cart/model/';
import { useCartStore } from '@/entities/cart';

beforeEach(() => {
useCartStore.setState({ cart: [], selectedCoupon: null });
});

const mockProducts: Product[] = [
{
Expand Down Expand Up @@ -415,7 +420,7 @@ describe('basic > ', () => {
});
});

describe('useCart > ', () => {
describe('useCartStore > ', () => {
const testProduct: Product = {
id: '1',
name: 'Test Product',
Expand All @@ -431,7 +436,7 @@ describe('basic > ', () => {
};

test('장바구니에 제품을 추가해야 합니다', () => {
const { result } = renderHook(() => useCart());
const { result } = renderHook(() => useCartStore());

act(() => {
result.current.addToCart(testProduct);
Expand All @@ -445,7 +450,7 @@ describe('basic > ', () => {
});

test('장바구니에서 제품을 제거해야 합니다', () => {
const { result } = renderHook(() => useCart());
const { result } = renderHook(() => useCartStore());

act(() => {
result.current.addToCart(testProduct);
Expand All @@ -456,7 +461,7 @@ describe('basic > ', () => {
});

test('제품 수량을 업데이트해야 합니다', () => {
const { result } = renderHook(() => useCart());
const { result } = renderHook(() => useCartStore());

act(() => {
result.current.addToCart(testProduct);
Expand All @@ -467,7 +472,7 @@ describe('basic > ', () => {
});

test('쿠폰을 적용해야지', () => {
const { result } = renderHook(() => useCart());
const { result } = renderHook(() => useCartStore());

act(() => {
result.current.applyCoupon(testCoupon);
Expand All @@ -477,7 +482,7 @@ describe('basic > ', () => {
});

test('합계를 정확하게 계산해야 합니다', () => {
const { result } = renderHook(() => useCart());
const { result } = renderHook(() => useCartStore());

act(() => {
result.current.addToCart(testProduct);
Expand Down
116 changes: 116 additions & 0 deletions src/refactoring/entities/cart/model/cartStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// useCartStore.ts
import { create } from 'zustand';
import { CartItem, Coupon, Product } from '@/shared/types';

interface CartState {
cart: CartItem[];
selectedCoupon: Coupon | null;
// 각종 액션들
addToCart: (product: Product) => void;
removeFromCart: (productId: string) => void;
updateQuantity: (productId: string, newQuantity: number) => void;
applyCoupon: (coupon: Coupon) => void;
getRemainingStock: (product: Product) => number;
calculateTotal: () => {
totalBeforeDiscount: number;
totalAfterDiscount: number;
totalDiscount: number;
};
}

export const useCartStore = create<CartState>((set, get) => ({
cart: [],
selectedCoupon: null,

// 남은 재고량 계산 함수
getRemainingStock: (product: Product): number => {
const { cart } = get();
const cartItem = cart.find((item) => item.product.id === product.id);
return product.stock - (cartItem?.quantity || 0);
},

// 카트에 상품 추가 함수
addToCart: (product: Product) => {
const { getRemainingStock, cart } = get();
const remainingStock = getRemainingStock(product);
if (remainingStock <= 0) return;

const existingItem = cart.find((item) => item.product.id === product.id);
if (existingItem) {
// 이미 카트에 있는 상품이라면 수량 업데이트 (최대 stock 값 제한)
const updatedCart = cart.map((item) =>
item.product.id === product.id ? { ...item, quantity: Math.min(item.quantity + 1, product.stock) } : item,
);
set({ cart: updatedCart });
} else {
// 처음 추가하는 상품이면 새 항목 추가
set({ cart: [...cart, { product, quantity: 1 }] });
}
},

// 카트에서 상품 제거 함수
removeFromCart: (productId: string) => {
set((state) => ({
cart: state.cart.filter((item) => item.product.id !== productId),
}));
},

// 카트 내 상품 수량 업데이트 함수
updateQuantity: (productId: string, newQuantity: number) => {
set((state) => ({
cart: state.cart
.map((item) => {
if (item.product.id === productId) {
const maxQuantity = item.product.stock;
const updatedQuantity = Math.max(0, Math.min(newQuantity, maxQuantity));
return updatedQuantity > 0 ? { ...item, quantity: updatedQuantity } : null;
}
return item;
})
// null 값은 카트에서 제거
.filter((item): item is CartItem => item !== null),
}));
},

// 쿠폰 적용 함수
applyCoupon: (coupon: Coupon) => {
set({ selectedCoupon: coupon });
},

// 총 결제 금액(할인 전/후, 할인액 계산) 함수
calculateTotal: () => {
const { cart, selectedCoupon } = get();
let totalBeforeDiscount = 0;
let totalAfterDiscount = 0;

cart.forEach((item) => {
const { price, discounts } = item.product;
const quantity = item.quantity;
totalBeforeDiscount += price * quantity;

// 상품 할인 적용: 주문 수량에 맞게 최대 할인율 적용
const discount = discounts.reduce((maxDiscount, d) => {
return quantity >= d.quantity && d.rate > maxDiscount ? d.rate : maxDiscount;
}, 0);
totalAfterDiscount += price * quantity * (1 - discount);
});

let totalDiscount = totalBeforeDiscount - totalAfterDiscount;

// 쿠폰 할인 적용
if (selectedCoupon) {
if (selectedCoupon.discountType === 'amount') {
totalAfterDiscount = Math.max(0, totalAfterDiscount - selectedCoupon.discountValue);
} else {
totalAfterDiscount *= 1 - selectedCoupon.discountValue / 100;
}
totalDiscount = totalBeforeDiscount - totalAfterDiscount;
}

return {
totalBeforeDiscount: Math.round(totalBeforeDiscount),
totalAfterDiscount: Math.round(totalAfterDiscount),
totalDiscount: Math.round(totalDiscount),
};
},
}));
1 change: 0 additions & 1 deletion src/refactoring/entities/cart/model/discounts/index.ts

This file was deleted.

7 changes: 4 additions & 3 deletions src/refactoring/entities/cart/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './discounts';
export * from './totals';
export * from './items';
export * from './discount';
export * from './caculate';
export * from './update';
export { useCartStore } from './cartStore';
1 change: 0 additions & 1 deletion src/refactoring/entities/cart/model/items/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/refactoring/entities/cart/model/totals/index.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/refactoring/features/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./useCart.ts";
export * from "./useCoupon.ts";
export * from "./useProduct.ts";
export * from './useCoupon';
export * from './useProduct';
97 changes: 0 additions & 97 deletions src/refactoring/features/hooks/useCart.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/refactoring/pages/CartPage/ui/CartPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Coupon, Product } from '@/shared/types/';
import { useCart } from '@/features/hooks';
import { getMaxDiscount } from '@/entities/cart';
import { getMaxDiscount, useCartStore } from '@/entities/cart';
import { getAppliedDiscount } from '@/entities/cart';

interface Props {
Expand All @@ -18,7 +17,7 @@ export function CartPage({ products, coupons }: Props) {
calculateTotal,
selectedCoupon,
getRemainingStock,
} = useCart();
} = useCartStore();

const { totalBeforeDiscount, totalAfterDiscount, totalDiscount } = calculateTotal();

Expand Down
1 change: 1 addition & 0 deletions src/refactoring/widgets/CartItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ui';
51 changes: 51 additions & 0 deletions src/refactoring/widgets/CartItem/ui/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getMaxDiscount, useCartStore } from '@/entities/cart';
import { Product } from '@/shared/types';

interface CartItemProps {
product: Product;
}

export function CartItem({ product }: CartItemProps) {
const { getRemainingStock, addToCart } = useCartStore();

const remainingStock = getRemainingStock(product);

return (
<div key={product.id} data-testid={`product-${product.id}`} className="bg-white p-3 rounded shadow">
<div className="flex justify-between items-center mb-2">
<span className="font-semibold">{product.name}</span>
<span className="text-gray-600">{product.price.toLocaleString()}</span>
</div>
<div className="text-sm text-gray-500 mb-2">
<span className={`font-medium ${remainingStock > 0 ? 'text-green-600' : 'text-red-600'}`}>
재고: {remainingStock}
</span>
{product.discounts.length > 0 && (
<span className="ml-2 font-medium text-blue-600">
최대 {(getMaxDiscount(product.discounts) * 100).toFixed(0)}% 할인
</span>
)}
</div>
{product.discounts.length > 0 && (
<ul className="list-disc list-inside text-sm text-gray-500 mb-2">
{product.discounts.map((discount, index) => (
<li key={index}>
{discount.quantity}개 이상: {(discount.rate * 100).toFixed(0)}% 할인
</li>
))}
</ul>
)}
<button
onClick={() => addToCart(product)}
className={`w-full px-3 py-1 rounded ${
remainingStock > 0
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
disabled={remainingStock <= 0}
>
{remainingStock > 0 ? '장바구니에 추가' : '품절'}
</button>
</div>
);
}
1 change: 1 addition & 0 deletions src/refactoring/widgets/CartItem/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CartItem } from './CartItem';

0 comments on commit ff7b379

Please sign in to comment.