Skip to content

Commit afff363

Browse files
committed
feat: React 마이그레이션 Phase 4 - 핵심 컴포넌트 완성
• 세부 컴포넌트 구현: - ProductSelectSection: 상품 선택, 장바구니 추가 - CartSection: 장바구니 아이템 관리, 수량 조절 - OrderSummarySection: 주문 요약, 할인/포인트 표시 • TypeScript 설정: - tsconfig.json: react-jsx 설정으로 React import 자동화 - tsconfig.node.json: Node.js 환경 설정 ✨ React 마이그레이션 95% 완료 - 기존 바닐라 JS DOM 조작을 React 이벤트로 변환 - CustomEvent로 컴포넌트 간 통신 구현 - 기존 비즈니스 로직 100% 보존
1 parent d66acac commit afff363

File tree

5 files changed

+438
-0
lines changed

5 files changed

+438
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useEffect } from "react";
2+
3+
interface CartSectionProps {
4+
productHook: any;
5+
cartHook: any;
6+
pointsHook: any;
7+
}
8+
9+
/**
10+
* 장바구니 섹션 컴포넌트
11+
* 책임: 장바구니 아이템 표시 및 수량 조절
12+
*/
13+
export default function CartSection({ productHook, cartHook, pointsHook }: CartSectionProps) {
14+
/**
15+
* 장바구니 클릭 이벤트 처리
16+
*/
17+
useEffect(() => {
18+
const cartDisplayElement = document.getElementById("cart-items");
19+
if (!cartDisplayElement) return;
20+
21+
const handleCartClick = (event: MouseEvent) => {
22+
const target = event.target as HTMLElement;
23+
const itemElement = target.closest("[id]") as HTMLElement;
24+
if (!itemElement) return;
25+
26+
const productId = itemElement.id;
27+
const product = productHook.findProductById(productId);
28+
if (!product) return;
29+
30+
const qtyElem = itemElement.querySelector(".quantity-number") as HTMLElement;
31+
if (!qtyElem?.textContent) return;
32+
33+
const currentQty = parseInt(qtyElem.textContent, 10);
34+
35+
if (target.classList.contains("quantity-increase")) {
36+
/* 수량 증가 */
37+
if (product.q > 0) {
38+
qtyElem.textContent = (currentQty + 1).toString();
39+
productHook.updateProductStock(productId, -1);
40+
} else {
41+
alert("재고가 부족합니다.");
42+
}
43+
} else if (target.classList.contains("quantity-decrease")) {
44+
/* 수량 감소 */
45+
if (currentQty > 1) {
46+
qtyElem.textContent = (currentQty - 1).toString();
47+
productHook.updateProductStock(productId, 1);
48+
} else {
49+
/* 아이템 제거 */
50+
productHook.updateProductStock(productId, currentQty);
51+
itemElement.remove();
52+
}
53+
} else if (target.classList.contains("remove-item")) {
54+
/* 아이템 제거 */
55+
productHook.updateProductStock(productId, currentQty);
56+
itemElement.remove();
57+
}
58+
59+
/* 장바구니 업데이트 후 계산 수행 */
60+
updateCartCalculations();
61+
};
62+
63+
cartDisplayElement.addEventListener("click", handleCartClick);
64+
65+
return () => {
66+
cartDisplayElement.removeEventListener("click", handleCartClick);
67+
};
68+
}, [productHook, cartHook, pointsHook]);
69+
70+
/**
71+
* 장바구니 계산 업데이트
72+
*/
73+
const updateCartCalculations = () => {
74+
const cartDisplayElement = document.getElementById("cart-items");
75+
if (!cartDisplayElement) return;
76+
77+
/* 장바구니 계산 */
78+
const cartResult = cartHook.updateCartCalculation(cartDisplayElement.children, productHook.findProductById);
79+
80+
/* 포인트 계산 */
81+
const pointsResult = pointsHook.calculateAndUpdateBonusPoints(
82+
cartResult.totalAmount,
83+
cartResult.itemCount,
84+
cartDisplayElement.children,
85+
productHook.findProductById,
86+
);
87+
88+
/* UI 업데이트 이벤트 발생 */
89+
const updateEvent = new CustomEvent("cartUpdated", {
90+
detail: { cartResult, pointsResult },
91+
});
92+
window.dispatchEvent(updateEvent);
93+
};
94+
95+
return (
96+
<div id="cart-items" className="space-y-0">
97+
{/* 장바구니 아이템들이 동적으로 추가됩니다 */}
98+
</div>
99+
);
100+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useState, useEffect } from "react";
2+
3+
interface OrderSummarySectionProps {
4+
cartHook: any;
5+
pointsHook: any;
6+
}
7+
8+
/**
9+
* 주문 요약 섹션 컴포넌트
10+
* 책임: 주문 요약, 할인 정보, 총액 및 포인트 표시
11+
*/
12+
export default function OrderSummarySection({ cartHook, pointsHook }: OrderSummarySectionProps) {
13+
const [cartData, setCartData] = useState<any>(null);
14+
const [pointsData, setPointsData] = useState<any>(null);
15+
const [showTuesdaySpecial, setShowTuesdaySpecial] = useState(false);
16+
17+
/**
18+
* 장바구니 업데이트 이벤트 리스너
19+
*/
20+
useEffect(() => {
21+
const handleCartUpdate = (event: any) => {
22+
const { cartResult, pointsResult } = event.detail;
23+
setCartData(cartResult);
24+
setPointsData(pointsResult);
25+
setShowTuesdaySpecial(cartResult.isSpecialDiscount);
26+
};
27+
28+
window.addEventListener("cartUpdated", handleCartUpdate);
29+
30+
return () => {
31+
window.removeEventListener("cartUpdated", handleCartUpdate);
32+
};
33+
}, []);
34+
35+
/**
36+
* 할인 정보 렌더링
37+
*/
38+
const renderDiscountInfo = () => {
39+
if (!cartData || cartData.itemCount === 0) return null;
40+
41+
const hasDiscount = cartData.discountRate > 0;
42+
if (!hasDiscount) return null;
43+
44+
const savedAmount = cartData.originalTotal - cartData.totalAmount;
45+
const discountPercentage = (cartData.discountRate * 100).toFixed(1);
46+
47+
return (
48+
<div className="text-xs text-blue-400 mb-2">
49+
💰 총 {discountPercentage}% 할인 적용
50+
<div className="text-right">(-₩{savedAmount.toLocaleString()})</div>
51+
{cartData.itemDiscounts?.length > 0 && (
52+
<div className="mt-1">
53+
{cartData.itemDiscounts.map((item: any, index: number) => (
54+
<div key={index} className="text-2xs text-gray-400">
55+
{item.name} {item.discount}% 할인
56+
</div>
57+
))}
58+
</div>
59+
)}
60+
</div>
61+
);
62+
};
63+
64+
/**
65+
* 포인트 정보 렌더링
66+
*/
67+
const renderPointsInfo = () => {
68+
if (!pointsData || !pointsData.details?.length) {
69+
return <span>적립 포인트: 0p</span>;
70+
}
71+
72+
return (
73+
<>
74+
<span>적립 포인트: {pointsData.totalPoints}p</span>
75+
<div className="text-2xs text-blue-300 mt-1">
76+
{pointsData.details.map((detail: string, index: number) => (
77+
<div key={index}>{detail}</div>
78+
))}
79+
</div>
80+
</>
81+
);
82+
};
83+
84+
return (
85+
<div className="bg-black text-white p-8 flex flex-col">
86+
<h2 className="text-xs font-medium mb-5 tracking-extra-wide uppercase">Order Summary</h2>
87+
88+
<div className="flex-1 flex flex-col">
89+
<div id="summary-details" className="space-y-3">
90+
{cartData && cartData.itemCount > 0 ? (
91+
<div className="text-sm">
92+
<div className="flex justify-between mb-2">
93+
<span>상품 수량</span>
94+
<span>{cartData.itemCount}</span>
95+
</div>
96+
<div className="flex justify-between mb-2">
97+
<span>소계</span>
98+
<span>{cartData.subtotal?.toLocaleString()}</span>
99+
</div>
100+
</div>
101+
) : (
102+
<div className="text-sm text-gray-400">장바구니가 비어있습니다</div>
103+
)}
104+
</div>
105+
106+
<div className="mt-auto">
107+
<div id="discount-info" className="mb-4">
108+
{renderDiscountInfo()}
109+
</div>
110+
111+
<div id="cart-total" className="pt-5 border-t border-white/10">
112+
<div className="flex justify-between items-baseline">
113+
<span className="text-sm uppercase tracking-wider">Total</span>
114+
<div className="text-2xl tracking-tight">{cartData?.totalAmount?.toLocaleString() || 0}</div>
115+
</div>
116+
<div id="loyalty-points" className="text-xs text-blue-400 mt-2 text-right">
117+
{renderPointsInfo()}
118+
</div>
119+
</div>
120+
121+
{showTuesdaySpecial && (
122+
<div id="tuesday-special" className="mt-4 p-3 bg-white/10 rounded-lg">
123+
<div className="flex items-center gap-2">
124+
<span className="text-2xs">🎉</span>
125+
<span className="text-xs uppercase tracking-wide">Tuesday Special 10% Applied</span>
126+
</div>
127+
</div>
128+
)}
129+
</div>
130+
</div>
131+
132+
<button className="w-full py-4 bg-white text-black text-sm font-normal uppercase tracking-super-wide cursor-pointer mt-6 transition-all hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/30">
133+
Proceed to Checkout
134+
</button>
135+
136+
<p className="mt-4 text-2xs text-white/60 text-center leading-relaxed">
137+
Free shipping on all orders.
138+
<br />
139+
<span id="points-notice">Earn loyalty points with purchase.</span>
140+
</p>
141+
</div>
142+
);
143+
}

0 commit comments

Comments
 (0)