Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,103 changes: 8 additions & 1,095 deletions src/basic/App.tsx

Large diffs are not rendered by default.

524 changes: 524 additions & 0 deletions src/basic/components/AdminPage.tsx

Large diffs are not rendered by default.

383 changes: 383 additions & 0 deletions src/basic/components/CartPage.tsx

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions src/basic/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Coupon, Product } from '../../types';

interface ProductWithUI extends Product {
description?: string;
isRecommended?: boolean;
}

export const initialProducts: ProductWithUI[] = [
{
id: 'p1',
name: '상품1',
price: 10000,
stock: 20,
discounts: [
{ quantity: 10, rate: 0.1 },
{ quantity: 20, rate: 0.2 }
],
description: '최고급 품질의 프리미엄 상품입니다.'
},
{
id: 'p2',
name: '상품2',
price: 20000,
stock: 20,
discounts: [
{ quantity: 10, rate: 0.15 }
],
description: '다양한 기능을 갖춘 실용적인 상품입니다.',
isRecommended: true
},
{
id: 'p3',
name: '상품3',
price: 30000,
stock: 20,
discounts: [
{ quantity: 10, rate: 0.2 },
{ quantity: 30, rate: 0.25 }
],
description: '대용량과 고성능을 자랑하는 상품입니다.'
}
];

export const initialCoupons: Coupon[] = [
{
name: '5000원 할인',
code: 'AMOUNT5000',
discountType: 'amount',
discountValue: 5000
},
{
name: '10% 할인',
code: 'PERCENT10',
discountType: 'percentage',
discountValue: 10
}
];

export type { ProductWithUI };
65 changes: 65 additions & 0 deletions src/basic/hooks/useCart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState, useCallback } from 'react';
import { CartItem, Coupon, Product } from '../../types';
import { useLocalStorage } from '../utils/hooks/useLocalStorage';
import {
addItemToCart,
removeItemFromCart,
updateCartItemQuantity,
calculateCartTotal,
getRemainingStock
} from '../models/cart';

/**
* 장바구니 관리 Hook
* 장바구니 상태 관리, 쿠폰 적용, 총액 계산 등 제공
*/
export function useCart() {
const [cart, setCart] = useLocalStorage<CartItem[]>('cart', []);
const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null);

const addToCart = useCallback((product: Product) => {
setCart(prevCart => addItemToCart(prevCart, product));
}, [setCart]);

const removeFromCart = useCallback((productId: string) => {
setCart(prevCart => removeItemFromCart(prevCart, productId));
}, [setCart]);

const updateQuantity = useCallback((productId: string, newQuantity: number) => {
setCart(prevCart => updateCartItemQuantity(prevCart, productId, newQuantity));
}, [setCart]);

const applyCoupon = useCallback((coupon: Coupon) => {
setSelectedCoupon(coupon);
}, []);

const removeCouponSelection = useCallback(() => {
setSelectedCoupon(null);
}, []);

const calculateTotal = useCallback(() => {
return calculateCartTotal(cart, selectedCoupon);
}, [cart, selectedCoupon]);

const getProductRemainingStock = useCallback((product: Product) => {
return getRemainingStock(product, cart);
}, [cart]);

const clearCart = useCallback(() => {
setCart([]);
setSelectedCoupon(null);
}, [setCart]);

return {
cart,
selectedCoupon,
addToCart,
removeFromCart,
updateQuantity,
applyCoupon,
removeCouponSelection,
calculateTotal,
getRemainingStock: getProductRemainingStock,
clearCart
};
}
27 changes: 27 additions & 0 deletions src/basic/hooks/useCoupons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useCallback } from 'react';
import { Coupon } from '../../types';
import { initialCoupons } from '../constants';
import { useLocalStorage } from '../utils/hooks/useLocalStorage';

/**
* 쿠폰 관리 Hook
* 쿠폰 목록 상태 관리 및 추가/삭제 기능 제공
*/
export function useCoupons() {
const [coupons, setCoupons] = useLocalStorage<Coupon[]>('coupons', initialCoupons);

const addCoupon = useCallback((newCoupon: Coupon) => {
setCoupons(prev => [...prev, newCoupon]);
return newCoupon;
}, [setCoupons]);

const removeCoupon = useCallback((couponCode: string) => {
setCoupons(prev => prev.filter(c => c.code !== couponCode));
}, [setCoupons]);

return {
coupons,
addCoupon,
removeCoupon
};
}
46 changes: 46 additions & 0 deletions src/basic/hooks/useProducts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { ProductWithUI, initialProducts } from '../constants';
import { useLocalStorage } from '../utils/hooks/useLocalStorage';

/**
* 상품 관리 Hook
* 상품 목록 상태 관리 및 CRUD 작업 제공
*/
export function useProducts() {
const [products, setProducts] = useLocalStorage<ProductWithUI[]>('products', initialProducts);

const addProduct = useCallback((newProduct: Omit<ProductWithUI, 'id'>) => {
const product: ProductWithUI = {
...newProduct,
id: `p${Date.now()}`
};
setProducts(prev => [...prev, product]);
return product;
}, [setProducts]);

const updateProduct = useCallback((productId: string, updates: Partial<ProductWithUI>) => {
setProducts(prev =>
prev.map(product =>
product.id === productId
? { ...product, ...updates }
: product
)
);
}, [setProducts]);

const deleteProduct = useCallback((productId: string) => {
setProducts(prev => prev.filter(p => p.id !== productId));
}, [setProducts]);

const updateProductStock = useCallback((productId: string, stock: number) => {
updateProduct(productId, { stock });
}, [updateProduct]);

return {
products,
addProduct,
updateProduct,
deleteProduct,
updateProductStock
};
}
123 changes: 123 additions & 0 deletions src/basic/models/cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { CartItem, Coupon, Product } from '../../types';

/**
* 장바구니 아이템에 적용 가능한 최대 할인율을 계산합니다.
* 대량 구매 시 추가 5% 할인이 적용됩니다.
*/
export function getMaxApplicableDiscount(item: CartItem, cart: CartItem[]): number {
const { discounts } = item.product;
const { quantity } = item;

// 상품 자체 할인율 계산
const baseDiscount = discounts.reduce((maxDiscount, discount) => {
return quantity >= discount.quantity && discount.rate > maxDiscount
? discount.rate
: maxDiscount;
}, 0);

// 대량 구매 보너스: 장바구니 내 10개 이상 구매 시 추가 5% 할인
const hasBulkPurchase = cart.some(cartItem => cartItem.quantity >= 10);
if (hasBulkPurchase) {
return Math.min(baseDiscount + 0.05, 0.5); // 최대 50% 할인
}

return baseDiscount;
}

/**
* 개별 장바구니 아이템의 할인 적용 후 총액을 계산합니다.
*/
export function calculateItemTotal(item: CartItem, cart: CartItem[]): number {
const { price } = item.product;
const { quantity } = item;
const discount = getMaxApplicableDiscount(item, cart);

return Math.round(price * quantity * (1 - discount));
}

/**
* 장바구니 전체의 할인 전/후 총액을 계산합니다.
*/
export function calculateCartTotal(
cart: CartItem[],
selectedCoupon: Coupon | null
): {
totalBeforeDiscount: number;
totalAfterDiscount: number;
totalDiscount: number;
} {
let totalBeforeDiscount = 0;
let totalAfterDiscount = 0;

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

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

return {
totalBeforeDiscount: Math.round(totalBeforeDiscount),
totalAfterDiscount: Math.round(totalAfterDiscount),
totalDiscount: Math.round(totalBeforeDiscount - totalAfterDiscount)
};
}

/**
* 상품의 남은 재고를 계산합니다.
*/
export function getRemainingStock(product: Product, cart: CartItem[]): number {
const cartItem = cart.find(item => item.product.id === product.id);
return product.stock - (cartItem?.quantity || 0);
}

/**
* 장바구니에 상품을 추가하거나 수량을 증가시킵니다.
*/
export function addItemToCart(cart: CartItem[], product: Product): CartItem[] {
const existingItem = cart.find(item => item.product.id === product.id);

if (existingItem) {
return cart.map(item =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}

return [...cart, { product, quantity: 1 }];
}

/**
* 장바구니에서 상품을 제거합니다.
*/
export function removeItemFromCart(cart: CartItem[], productId: string): CartItem[] {
return cart.filter(item => item.product.id !== productId);
}

/**
* 장바구니 아이템의 수량을 업데이트합니다.
*/
export function updateCartItemQuantity(
cart: CartItem[],
productId: string,
quantity: number
): CartItem[] {
if (quantity <= 0) {
return removeItemFromCart(cart, productId);
}

return cart.map(item =>
item.product.id === productId
? { ...item, quantity }
: item
);
}
24 changes: 24 additions & 0 deletions src/basic/utils/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 가격을 한국 원화 형식으로 포맷팅합니다.
*/
export function formatPrice(price: number): string {
return `₩${price.toLocaleString()}`;
}

/**
* 날짜를 YYYY-MM-DD 형식으로 포맷팅합니다.
*/
export function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}

/**
* 소수를 퍼센트 문자열로 변환합니다.
* @example formatPercentage(0.1) // "10%"
*/
export function formatPercentage(rate: number): string {
return `${Math.round(rate * 100)}%`;
}
23 changes: 23 additions & 0 deletions src/basic/utils/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';

/**
* 값 변경을 지연시키는 디바운스 Hook
* 지정된 시간 동안 값이 변경되지 않을 때만 업데이트됩니다.
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
// delay 후에 값을 업데이트하는 타이머 설정
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);

// 값이 변경되거나 컴포넌트가 언마운트되면 타이머 정리
return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
}
Loading