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
59 changes: 59 additions & 0 deletions packages/assignment-5/src/refactoring/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from 'react';
import { CartPage } from './component/CartPage.tsx';
import { AdminPage } from './component/AdminPage.tsx';
import { Coupon, Product } from './types';
import { initialProducts, initialCoupons } from './data';
import React from 'react';



const App = () => {
const [products, setProducts] = useState<Product[]>(initialProducts);
const [coupons, setCoupons] = useState<Coupon[]>(initialCoupons);
const [isAdmin, setIsAdmin] = useState(false);

const handleProductUpdate = (updatedProduct: Product) => {
setProducts(prevProducts =>
prevProducts.map(p => p.id === updatedProduct.id ? updatedProduct : p)
);
};

const handleProductAdd = (newProduct: Product) => {
setProducts(prevProducts => [...prevProducts, newProduct]);
};

const handleCouponAdd = (newCoupon: Coupon) => {
setCoupons(prevCoupons => [...prevCoupons, newCoupon]);
};

return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-blue-600 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-2xl font-bold">쇼핑몰 관리 시스템</h1>
<button
onClick={() => setIsAdmin(!isAdmin)}
className="bg-white text-blue-600 px-4 py-2 rounded hover:bg-blue-100"
>
{isAdmin ? '장바구니 페이지로' : '관리자 페이지로'}
</button>
</div>
</nav>
<main className="container mx-auto mt-6">
{isAdmin ? (
<AdminPage
products={products}
coupons={coupons}
onProductUpdate={handleProductUpdate}
onProductAdd={handleProductAdd}
onCouponAdd={handleCouponAdd}
/>
) : (
<CartPage products={products} coupons={coupons}/>
)}
</main>
</div>
);
};

export default App;
159 changes: 159 additions & 0 deletions packages/assignment-5/src/refactoring/component/AdminPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { useState } from 'react';
import { Coupon, Discount, Product } from '../types';
import { AdminPageProps } from '../types/props/AdminPageProps';
import { Button } from '../component/common';
import { ProductForm, ProductCard } from '../component/product';
import { CouponForm, CouponList } from '../component/coupon';
import { toggleSetItem } from '../utils/set';

export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, onCouponAdd }: AdminPageProps) => {
const [openProductIds, setOpenProductIds] = useState<Set<string>>(new Set());
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [newDiscount, setNewDiscount] = useState<Discount>({ quantity: 0, rate: 0 });
const [newCoupon, setNewCoupon] = useState<Coupon>({
name: '',
code: '',
discountType: 'percentage',
discountValue: 0,
});
const [showNewProductForm, setShowNewProductForm] = useState(false);
const [newProduct, setNewProduct] = useState<Omit<Product, 'id'>>({
name: '',
price: 0,
stock: 0,
discounts: [],
});

const toggleProductAccordion = (productId: string) => {
setOpenProductIds(prev => toggleSetItem(prev, productId));
};

const handleEditProduct = (product: Product) => setEditingProduct({ ...product });

const handleProductNameUpdate = (productId: string, newName: string) => {
if (editingProduct && editingProduct.id === productId) {
setEditingProduct({ ...editingProduct, name: newName });
}
};

const handlePriceUpdate = (productId: string, newPrice: number) => {
if (editingProduct && editingProduct.id === productId) {
setEditingProduct({ ...editingProduct, price: newPrice });
}
};

const handleEditComplete = () => {
if (editingProduct) {
onProductUpdate(editingProduct);
setEditingProduct(null);
}
};

const handleStockUpdate = (productId: string, newStock: number) => {
const updatedProduct = products.find(p => p.id === productId);
if (updatedProduct) {
const newProduct = { ...updatedProduct, stock: newStock };
onProductUpdate(newProduct);
setEditingProduct(newProduct);
}
};

const handleAddDiscount = (productId: string) => {
const updatedProduct = products.find(p => p.id === productId);
if (updatedProduct && editingProduct) {
const newProduct = {
...updatedProduct,
discounts: [...updatedProduct.discounts, newDiscount],
};
onProductUpdate(newProduct);
setEditingProduct(newProduct);
setNewDiscount({ quantity: 0, rate: 0 });
}
};

const handleRemoveDiscount = (productId: string, index: number) => {
const updatedProduct = products.find(p => p.id === productId);
if (updatedProduct) {
const newProduct = {
...updatedProduct,
discounts: updatedProduct.discounts.filter((_, i) => i !== index),
};
onProductUpdate(newProduct);
setEditingProduct(newProduct);
}
};

const handleAddCoupon = () => {
onCouponAdd(newCoupon);
setNewCoupon({ name: '', code: '', discountType: 'percentage', discountValue: 0 });
};

const handleAddNewProduct = () => {
const productWithId = { ...newProduct, id: Date.now().toString() };
onProductAdd(productWithId);
setNewProduct({ name: '', price: 0, stock: 0, discounts: [] });
setShowNewProductForm(false);
};

return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">관리자 페이지</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h2 className="text-2xl font-semibold mb-4">상품 관리</h2>
<Button
onClick={() => setShowNewProductForm(!showNewProductForm)}
variant="primary"
className="mb-4"
>
{showNewProductForm ? '취소' : '새 상품 추가'}
</Button>

{showNewProductForm && (
<ProductForm
product={newProduct}
onChange={setNewProduct}
onSubmit={handleAddNewProduct}
onCancel={() => setShowNewProductForm(false)}
/>
)}

<div className="space-y-2">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
isOpen={openProductIds.has(product.id)}
isEditing={editingProduct?.id === product.id}
editingProduct={editingProduct}
newDiscount={newDiscount}
onToggle={() => toggleProductAccordion(product.id)}
onEdit={() => handleEditProduct(product)}
onChangeName={(value) => handleProductNameUpdate(product.id, value)}
onChangePrice={(value) => handlePriceUpdate(product.id, value)}
onChangeStock={(value) => handleStockUpdate(product.id, value)}
onAddDiscount={() => handleAddDiscount(product.id)}
onRemoveDiscount={(index) => handleRemoveDiscount(product.id, index)}
onChangeDiscountQuantity={(value) => setNewDiscount({ ...newDiscount, quantity: value })}
onChangeDiscountRate={(value) => setNewDiscount({ ...newDiscount, rate: value })}
onEditComplete={handleEditComplete}
/>
))}
</div>
</div>

<div>
<h2 className="text-2xl font-semibold mb-4">쿠폰 관리</h2>
<div className="bg-white p-4 rounded shadow space-y-4">
<CouponForm
coupon={newCoupon}
onChange={setNewCoupon}
onSubmit={handleAddCoupon}
/>
<CouponList coupons={coupons} />
</div>
</div>
</div>
</div>
);
};
172 changes: 172 additions & 0 deletions packages/assignment-5/src/refactoring/component/CartPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { useState } from 'react';
import { CartItem, Coupon, Product } from '../types';
import { Button, SelectField } from '../component/common';
import { CartPageProps } from '../types/props/CartItemCardProps';
import { getAppliedDiscount,getMaxDiscount,getRemainingStock, calculateTotal,addProductToCart,removeProductFromCart } from '../utils';

export const CartPage = ({ products, coupons }: CartPageProps) => {
const [cart, setCart] = useState<CartItem[]>([]);
const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null);


const addToCart = (product: Product) => {
const remaining = getRemainingStock(cart,product );
if (remaining <= 0) return;
setCart(prev => addProductToCart(prev, product));
};

const removeFromCart = (productId: string) => {
setCart(prev => removeProductFromCart(prev, productId));
};


const updateQuantity = (productId: string, newQuantity: number) => {
setCart(prevCart =>
prevCart
.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;
})
.filter((item): item is CartItem => item !== null)
);
};

const applyCoupon = (index: string) => {
const selected = coupons[parseInt(index)];
if (selected) setSelectedCoupon(selected);
};

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

return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">장바구니</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h2 className="text-2xl font-semibold mb-4">상품 목록</h2>
<div className="space-y-2">
{products.map(product => {
const remainingStock = getRemainingStock(cart, product);
return (
<div key={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)}
variant={remainingStock > 0 ? 'primary' : 'secondary'}
disabled={remainingStock <= 0}
className="w-full"
>
{remainingStock > 0 ? '장바구니에 추가' : '품절'}
</Button>
</div>
);
})}
</div>
</div>

<div>
<h2 className="text-2xl font-semibold mb-4">장바구니 내역</h2>
<div className="space-y-2">
{cart.map(item => {
const appliedDiscount = getAppliedDiscount(item);
return (
<div key={item.product.id} className="flex justify-between items-center bg-white p-3 rounded shadow">
<div>
<span className="font-semibold">{item.product.name}</span>
<br />
<span className="text-sm text-gray-600">
{item.product.price}원 x {item.quantity}
{appliedDiscount > 0 && (
<span className="text-green-600 ml-1">
({(appliedDiscount * 100).toFixed(0)}% 할인 적용)
</span>
)}
</span>
</div>
<div className="flex">
<Button
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
variant="secondary"
className="mr-1 px-2 py-1"
>
-
</Button>
<Button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
variant="secondary"
className="mr-1 px-2 py-1"
>
+
</Button>
<Button
onClick={() => removeFromCart(item.product.id)}
variant="danger"
className="px-2 py-1"
>
삭제
</Button>
</div>
</div>
);
})}
</div>

<div className="mt-6 bg-white p-4 rounded shadow">
<h2 className="text-2xl font-semibold mb-2">쿠폰 적용</h2>
<SelectField
label="쿠폰 선택"
value={coupons.findIndex(c => c.code === selectedCoupon?.code).toString()}
options={coupons.map((coupon, index) => ({
label: `${coupon.name} - ${coupon.discountType === 'amount' ? `${coupon.discountValue}원` : `${coupon.discountValue}%`}`,
value: index.toString(),
}))}
onChange={applyCoupon}
/>
{selectedCoupon && (
<p className="text-green-600">
적용된 쿠폰: {selectedCoupon.name} ({selectedCoupon.discountType === 'amount' ? `${selectedCoupon.discountValue}원` : `${selectedCoupon.discountValue}%`} 할인)
</p>
)}
</div>

<div className="mt-6 bg-white p-4 rounded shadow">
<h2 className="text-2xl font-semibold mb-2">주문 요약</h2>
<div className="space-y-1">
<p>상품 금액: {totalBeforeDiscount.toLocaleString()}원</p>
<p className="text-green-600">할인 금액: {totalDiscount.toLocaleString()}원</p>
<p className="text-xl font-bold">
최종 결제 금액: {totalAfterDiscount.toLocaleString()}원
</p>
</div>
</div>
</div>
</div>
</div>
);
};
Loading
Loading