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
737 changes: 737 additions & 0 deletions src/basic/ARCHITECTURE.md

Large diffs are not rendered by default.

1,082 changes: 164 additions & 918 deletions src/basic/App.tsx

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions src/basic/components/entities/cart/CartItemRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { CartItem } from '../../../../types';

interface CartItemRowProps {
item: CartItem;
itemTotal: number;
discountRate: number;
onRemove: () => void;
onIncrease: () => void;
onDecrease: () => void;
}

export function CartItemRow({
item,
itemTotal,
discountRate,
onRemove,
onIncrease,
onDecrease
}: CartItemRowProps) {
return (
<div className="border-b pb-3 last:border-b-0">
<div className="flex justify-between items-start mb-2">
<h4 className="text-sm font-medium text-gray-900 flex-1">{item.product.name}</h4>
<button
onClick={onRemove}
className="text-gray-400 hover:text-red-500 ml-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<button
onClick={onDecrease}
className="w-6 h-6 rounded border border-gray-300 flex items-center justify-center hover:bg-gray-100"
>
<span className="text-xs">−</span>
</button>
<span className="mx-3 text-sm font-medium w-8 text-center">{item.quantity}</span>
<button
onClick={onIncrease}
className="w-6 h-6 rounded border border-gray-300 flex items-center justify-center hover:bg-gray-100"
>
<span className="text-xs">+</span>
</button>
</div>
<div className="text-right">
{discountRate > 0 && (
<span className="text-xs text-red-500 font-medium block">-{discountRate}%</span>
)}
<p className="text-sm font-medium text-gray-900">
{Math.round(itemTotal).toLocaleString()}원
</p>
</div>
</div>
</div>
);
}

45 changes: 45 additions & 0 deletions src/basic/components/entities/cart/CartSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
interface CartSummaryProps {
totals: {
totalBeforeDiscount: number;
totalAfterDiscount: number;
};
onCompleteOrder: () => void;
}

export function CartSummary({ totals, onCompleteOrder }: CartSummaryProps) {
const discountAmount = totals.totalBeforeDiscount - totals.totalAfterDiscount;

return (
<section className="bg-white rounded-lg border border-gray-200 p-4">
<h3 className="text-lg font-semibold mb-4">결제 정보</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">상품 금액</span>
<span className="font-medium">{totals.totalBeforeDiscount.toLocaleString()}원</span>
</div>
{discountAmount > 0 && (
<div className="flex justify-between text-red-500">
<span>할인 금액</span>
<span>-{discountAmount.toLocaleString()}원</span>
</div>
)}
<div className="flex justify-between py-2 border-t border-gray-200">
<span className="font-semibold">결제 예정 금액</span>
<span className="font-bold text-lg text-gray-900">{totals.totalAfterDiscount.toLocaleString()}원</span>
</div>
</div>

<button
onClick={onCompleteOrder}
className="w-full mt-4 py-3 bg-yellow-400 text-gray-900 rounded-md font-medium hover:bg-yellow-500 transition-colors"
>
{totals.totalAfterDiscount.toLocaleString()}원 결제하기
</button>

<div className="mt-3 text-xs text-gray-500 text-center">
<p>* 실제 결제는 이루어지지 않습니다</p>
</div>
</section>
);
}

35 changes: 35 additions & 0 deletions src/basic/components/entities/coupon/CouponCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Coupon } from '../../../../types';

interface CouponCardProps {
coupon: Coupon;
onDelete: () => void;
}

export function CouponCard({ coupon, onDelete }: CouponCardProps) {
return (
<div className="relative bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg p-4 border border-indigo-200">
<div className="flex justify-between items-start">
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{coupon.name}</h3>
<p className="text-sm text-gray-600 mt-1 font-mono">{coupon.code}</p>
<div className="mt-2">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-white text-indigo-700">
{coupon.discountType === 'amount'
? `${coupon.discountValue.toLocaleString()}원 할인`
: `${coupon.discountValue}% 할인`}
</span>
</div>
</div>
<button
onClick={onDelete}
className="text-gray-400 hover:text-red-600 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
);
}

41 changes: 41 additions & 0 deletions src/basic/components/entities/coupon/CouponSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Coupon } from '../../../../types';

interface CouponSelectProps {
coupons: Coupon[];
selectedCode: string | null;
onChange: (code: string | null) => void;
}

export function CouponSelect({ coupons, selectedCode, onChange }: CouponSelectProps) {
if (coupons.length === 0) {
return null;
}

return (
<section className="bg-white rounded-lg border border-gray-200 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-700">쿠폰 할인</h3>
<button className="text-xs text-blue-600 hover:underline">
쿠폰 등록
</button>
</div>
<select
className="w-full text-sm border border-gray-300 rounded px-3 py-2 focus:outline-none focus:border-blue-500"
value={selectedCode || ''}
onChange={(e) => {
onChange(e.target.value || null);
}}
>
<option value="">쿠폰 선택</option>
{coupons.map(coupon => (
<option key={coupon.code} value={coupon.code}>
{coupon.name} ({coupon.discountType === 'amount'
? `${coupon.discountValue.toLocaleString()}원`
: `${coupon.discountValue}%`})
</option>
))}
</select>
</section>
);
}

85 changes: 85 additions & 0 deletions src/basic/components/entities/product/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Product } from '../../../../types';

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

interface ProductCardProps {
product: ProductWithUI;
remainingStock: number;
formatPrice: (price: number, productId?: string) => string;
onAddToCart: () => void;
}

export function ProductCard({
product,
remainingStock,
formatPrice,
onAddToCart
}: ProductCardProps) {
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow">
{/* 상품 이미지 영역 (placeholder) */}
<div className="relative">
<div className="aspect-square bg-gray-100 flex items-center justify-center">
<svg className="w-24 h-24 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
{product.isRecommended && (
<span className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded">
BEST
</span>
)}
{product.discounts.length > 0 && (
<span className="absolute top-2 left-2 bg-orange-500 text-white text-xs px-2 py-1 rounded">
~{Math.max(...product.discounts.map(d => d.rate)) * 100}%
</span>
)}
</div>

{/* 상품 정보 */}
<div className="p-4">
<h3 className="font-medium text-gray-900 mb-1">{product.name}</h3>
{product.description && (
<p className="text-sm text-gray-500 mb-2 line-clamp-2">{product.description}</p>
)}

{/* 가격 정보 */}
<div className="mb-3">
<p className="text-lg font-bold text-gray-900">{formatPrice(product.price, product.id)}</p>
{product.discounts.length > 0 && (
<p className="text-xs text-gray-500">
{product.discounts[0].quantity}개 이상 구매시 할인 {product.discounts[0].rate * 100}%
</p>
)}
</div>

{/* 재고 상태 */}
<div className="mb-3">
{remainingStock <= 5 && remainingStock > 0 && (
<p className="text-xs text-red-600 font-medium">품절임박! {remainingStock}개 남음</p>
)}
{remainingStock > 5 && (
<p className="text-xs text-gray-500">재고 {remainingStock}개</p>
)}
</div>

{/* 장바구니 버튼 */}
<button
onClick={onAddToCart}
disabled={remainingStock <= 0}
className={`w-full py-2 px-4 rounded-md font-medium transition-colors ${
remainingStock <= 0
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-gray-900 text-white hover:bg-gray-800'
}`}
>
{remainingStock <= 0 ? '품절' : '장바구니 담기'}
</button>
</div>
</div>
);
}

52 changes: 52 additions & 0 deletions src/basic/components/entities/product/ProductRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Product } from '../../../../types';

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

interface ProductRowProps {
product: ProductWithUI;
formatPrice: (price: number, productId?: string) => string;
onEdit: () => void;
onDelete: () => void;
}

export function ProductRow({
product,
formatPrice,
onEdit,
onDelete
}: ProductRowProps) {
return (
<tr className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{product.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{formatPrice(product.price, product.id)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
product.stock > 10 ? 'bg-green-100 text-green-800' :
product.stock > 0 ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{product.stock}개
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">{product.description || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={onEdit}
className="text-indigo-600 hover:text-indigo-900 mr-3"
>
수정
</button>
<button
onClick={onDelete}
className="text-red-600 hover:text-red-900"
>
삭제
</button>
</td>
</tr>
);
}

Loading