Skip to content

Commit 7932840

Browse files
committed
feat: add basic file include type in advaced
1 parent fea365d commit 7932840

File tree

20 files changed

+1314
-0
lines changed

20 files changed

+1314
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// components/CartItem.ts
2+
import { createElement } from '../utils/dom.js';
3+
import { formatProductPrice, formatProductName } from '../utils/formatters.js';
4+
import type { Product } from '../types/index.js';
5+
6+
export function CartItem(product: Product): HTMLElement {
7+
const itemDiv = createElement(
8+
'div',
9+
'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0',
10+
);
11+
itemDiv.id = product.id;
12+
13+
const price = formatProductPrice(product);
14+
const productName = formatProductName(product);
15+
16+
itemDiv.innerHTML = `
17+
<div class="w-20 h-20 bg-gradient-black relative overflow-hidden">
18+
<div class="absolute top-1/2 left-1/2 w-[60%] h-[60%] bg-white/10 -translate-x-1/2 -translate-y-1/2 rotate-45"></div>
19+
</div>
20+
<div>
21+
<h3 class="text-base font-normal mb-1 tracking-tight">${productName}</h3>
22+
<p class="text-xs text-gray-500 mb-0.5 tracking-wide">PRODUCT</p>
23+
<p class="text-xs text-black mb-3">${price}</p>
24+
<div class="flex items-center gap-4">
25+
<button class="quantity-change w-6 h-6 border border-black bg-white text-sm flex items-center justify-center transition-all hover:bg-black hover:text-white" data-product-id="${product.id}" data-change="-1">−</button>
26+
<span class="quantity-number text-sm font-normal min-w-[20px] text-center tabular-nums">1</span>
27+
<button class="quantity-change w-6 h-6 border border-black bg-white text-sm flex items-center justify-center transition-all hover:bg-black hover:text-white" data-product-id="${product.id}" data-change="1">+</button>
28+
</div>
29+
</div>
30+
<div class="text-right">
31+
<div class="text-lg mb-2 tracking-tight tabular-nums">${price}</div>
32+
<a class="remove-item text-2xs text-gray-500 uppercase tracking-wider cursor-pointer transition-colors border-b border-transparent hover:text-black hover:border-black" data-product-id="${product.id}">Remove</a>
33+
</div>
34+
`;
35+
36+
return itemDiv;
37+
}

src/advanced/components/Header.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// components/Header.ts
2+
import { createElement } from '../utils/dom.js';
3+
4+
export function Header(): HTMLElement {
5+
const header = createElement('div', 'mb-8');
6+
header.innerHTML = `
7+
<h1 class="text-xs font-medium tracking-extra-wide uppercase mb-2">🛒 Hanghae Online Store</h1>
8+
<div class="text-5xl tracking-tight leading-none">Shopping Cart</div>
9+
<p id="item-count" class="text-sm text-gray-500 font-normal mt-3">🛍️ 0 items in cart</p>
10+
`;
11+
return header;
12+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// components/HelpModal.ts
2+
import { createElement } from '../utils/dom.js';
3+
4+
export function HelpModal(): { toggleButton: HTMLElement; overlay: HTMLElement } {
5+
const toggleButton = createElement(
6+
'button',
7+
'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50',
8+
);
9+
toggleButton.innerHTML = `
10+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
12+
</svg>
13+
`;
14+
15+
const overlay = createElement(
16+
'div',
17+
'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300',
18+
);
19+
20+
const panel = createElement(
21+
'div',
22+
'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300',
23+
);
24+
panel.innerHTML = `
25+
<button class="absolute top-4 right-4 text-gray-500 hover:text-black" onclick="document.querySelector('.fixed.inset-0').classList.add('hidden'); this.parentElement.classList.add('translate-x-full')">
26+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
28+
</svg>
29+
</button>
30+
<h2 class="text-xl font-bold mb-4">📖 이용 안내</h2>
31+
<div class="mb-6">
32+
<h3 class="text-base font-bold mb-3">💰 할인 정책</h3>
33+
<div class="space-y-3">
34+
<div class="bg-gray-100 rounded-lg p-3">
35+
<p class="font-semibold text-sm mb-1">개별 상품</p>
36+
<p class="text-gray-700 text-xs pl-2">
37+
• 키보드 10개↑: 10%<br>
38+
• 마우스 10개↑: 15%<br>
39+
• 모니터암 10개↑: 20%<br>
40+
• 스피커 10개↑: 25%
41+
</p>
42+
</div>
43+
<div class="bg-gray-100 rounded-lg p-3">
44+
<p class="font-semibold text-sm mb-1">전체 수량</p>
45+
<p class="text-gray-700 text-xs pl-2">• 30개 이상: 25%</p>
46+
</div>
47+
<div class="bg-gray-100 rounded-lg p-3">
48+
<p class="font-semibold text-sm mb-1">특별 할인</p>
49+
<p class="text-gray-700 text-xs pl-2">
50+
• 화요일: +10%<br>
51+
• ⚡번개세일: 20%<br>
52+
• 💝추천할인: 5%
53+
</p>
54+
</div>
55+
</div>
56+
</div>
57+
<div class="mb-6">
58+
<h3 class="text-base font-bold mb-3">🎁 포인트 적립</h3>
59+
<div class="space-y-3">
60+
<div class="bg-gray-100 rounded-lg p-3">
61+
<p class="font-semibold text-sm mb-1">기본</p>
62+
<p class="text-gray-700 text-xs pl-2">• 구매액의 0.1%</p>
63+
</div>
64+
<div class="bg-gray-100 rounded-lg p-3">
65+
<p class="font-semibold text-sm mb-1">추가</p>
66+
<p class="text-gray-700 text-xs pl-2">
67+
• 화요일: 2배<br>
68+
• 키보드+마우스: +50p<br>
69+
• 풀세트: +100p<br>
70+
• 10개↑: +20p / 20개↑: +50p / 30개↑: +100p
71+
</p>
72+
</div>
73+
</div>
74+
</div>
75+
<div class="border-t border-gray-200 pt-4 mt-4">
76+
<p class="text-xs font-bold mb-1">💡 TIP</p>
77+
<p class="text-2xs text-gray-600 leading-relaxed">
78+
• 화요일 대량구매 = MAX 혜택<br>
79+
• ⚡+💝 중복 가능<br>
80+
• 상품4 = 품절
81+
</p>
82+
</div>
83+
`;
84+
85+
// 이벤트 핸들러
86+
toggleButton.onclick = function () {
87+
overlay.classList.toggle('hidden');
88+
panel.classList.toggle('translate-x-full');
89+
};
90+
91+
overlay.onclick = function (e) {
92+
if (e.target === overlay) {
93+
overlay.classList.add('hidden');
94+
panel.classList.add('translate-x-full');
95+
}
96+
};
97+
98+
overlay.appendChild(panel);
99+
100+
return { toggleButton, overlay };
101+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// components/LeftColumn.ts
2+
import { createElement } from '../utils/dom.js';
3+
import { ProductContainer } from './ProductContainer.js';
4+
5+
export function LeftColumn(): HTMLElement {
6+
const column = createElement(
7+
'div',
8+
'bg-white border border-gray-200 p-8 overflow-y-auto',
9+
);
10+
11+
const container = ProductContainer();
12+
column.appendChild(container);
13+
14+
const cart = createElement('div');
15+
cart.id = 'cart-items';
16+
column.appendChild(cart);
17+
18+
return column;
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// components/OrderSummary.ts
2+
import { createElement } from '../utils/dom.js';
3+
4+
export function OrderSummary(): HTMLElement {
5+
const column = createElement('div', 'bg-black text-white p-8 flex flex-col');
6+
column.innerHTML = `
7+
<h2 class="text-xs font-medium mb-5 tracking-extra-wide uppercase">Order Summary</h2>
8+
<div class="flex-1 flex flex-col">
9+
<div id="summary-details" class="space-y-3"></div>
10+
<div class="mt-auto">
11+
<div id="discount-info" class="mb-4"></div>
12+
<div id="cart-total" class="pt-5 border-t border-white/10">
13+
<div class="flex justify-between items-baseline">
14+
<span class="text-sm uppercase tracking-wider">Total</span>
15+
<div class="text-2xl tracking-tight">₩0</div>
16+
</div>
17+
<div id="loyalty-points" class="text-xs text-blue-400 mt-2 text-right">적립 포인트: 0p</div>
18+
</div>
19+
<div id="tuesday-special" class="mt-4 p-3 bg-white/10 rounded-lg hidden">
20+
<div class="flex items-center gap-2">
21+
<span class="text-2xs">🎉</span>
22+
<span class="text-xs uppercase tracking-wide">Tuesday Special 10% Applied</span>
23+
</div>
24+
</div>
25+
</div>
26+
</div>
27+
<button class="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">
28+
Proceed to Checkout
29+
</button>
30+
<p class="mt-4 text-2xs text-white/60 text-center leading-relaxed">
31+
Free shipping on all orders.<br>
32+
<span id="points-notice">Earn loyalty points with purchase.</span>
33+
</p>
34+
`;
35+
return column;
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// components/ProductContainer.ts
2+
import { createElement } from '../utils/dom.js';
3+
4+
export function ProductContainer(): HTMLElement {
5+
const container = createElement('div', 'mb-6 pb-6 border-b border-gray-200');
6+
7+
const select = createElement(
8+
'select',
9+
'w-full p-3 border border-gray-300 rounded-lg text-base mb-3',
10+
);
11+
select.id = 'product-select';
12+
13+
const addButton = createElement(
14+
'button',
15+
'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all',
16+
);
17+
addButton.id = 'add-to-cart';
18+
addButton.textContent = 'Add to Cart';
19+
20+
const stockInfo = createElement(
21+
'div',
22+
'text-xs text-red-500 mt-3 whitespace-pre-line',
23+
);
24+
stockInfo.id = 'stock-status';
25+
26+
container.appendChild(select);
27+
container.appendChild(addButton);
28+
container.appendChild(stockInfo);
29+
30+
return container;
31+
}

src/advanced/constants/index.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// constants/index.ts
2+
export const PRODUCT_IDS = {
3+
KEYBOARD: 'p1',
4+
MOUSE: 'p2',
5+
MONITOR_ARM: 'p3',
6+
LAPTOP_POUCH: 'p4',
7+
SPEAKER: 'p5',
8+
} as const;
9+
10+
export const DISCOUNT_RATES = {
11+
KEYBOARD: 0.1,
12+
MOUSE: 0.15,
13+
MONITOR_ARM: 0.2,
14+
LAPTOP_POUCH: 0.05,
15+
SPEAKER: 0.25,
16+
BULK: 0.25,
17+
TUESDAY: 0.1,
18+
LIGHTNING: 0.2,
19+
RECOMMEND: 0.05,
20+
} as const;
21+
22+
export const THRESHOLDS = {
23+
MIN_QUANTITY_FOR_DISCOUNT: 10,
24+
MIN_QUANTITY_FOR_BULK: 30,
25+
LOW_STOCK: 5,
26+
TOTAL_STOCK_WARNING: 50,
27+
} as const;
28+
29+
export const POINTS = {
30+
RATE: 0.001,
31+
BONUS: {
32+
SET: 50,
33+
FULL_SET: 100,
34+
BULK_10: 20,
35+
BULK_20: 50,
36+
BULK_30: 100,
37+
},
38+
} as const;
39+
40+
export const TIMERS = {
41+
LIGHTNING_SALE_INTERVAL: 30000,
42+
RECOMMEND_SALE_INTERVAL: 60000,
43+
LIGHTNING_SALE_MAX_DELAY: 10000,
44+
RECOMMEND_SALE_MAX_DELAY: 20000,
45+
} as const;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// controllers/saleTimers.ts
2+
import { DISCOUNT_RATES, TIMERS } from '../constants/index.js';
3+
import {
4+
getProducts,
5+
getLastSelectedProductId,
6+
updateProduct,
7+
getCartItems,
8+
getElements,
9+
} from '../store/state.js';
10+
import { updateProductOptions } from '../services/product.js';
11+
import { updateCartItemPrice } from '../services/cart.js';
12+
import { updateCart } from '../services/discount.js';
13+
14+
export function setupSaleTimers(): void {
15+
setupLightningSale();
16+
setupRecommendSale();
17+
}
18+
19+
function setupLightningSale(): void {
20+
const delay = Math.random() * TIMERS.LIGHTNING_SALE_MAX_DELAY;
21+
22+
setTimeout(() => {
23+
setInterval(() => {
24+
const products = getProducts();
25+
const availableProducts = products.filter((p) => p.stock > 0 && !p.onSale);
26+
27+
if (availableProducts.length === 0) return;
28+
29+
const randomProduct =
30+
availableProducts[Math.floor(Math.random() * availableProducts.length)];
31+
const newPrice = Math.round(
32+
randomProduct.originalPrice * (1 - DISCOUNT_RATES.LIGHTNING),
33+
);
34+
35+
updateProduct(randomProduct.id, {
36+
price: newPrice,
37+
onSale: true,
38+
});
39+
40+
alert(`⚡번개세일! ${randomProduct.name}이(가) 20% 할인 중입니다!`);
41+
42+
updateProductOptions();
43+
updateCartPrices();
44+
}, TIMERS.LIGHTNING_SALE_INTERVAL);
45+
}, delay);
46+
}
47+
48+
function setupRecommendSale(): void {
49+
const delay = Math.random() * TIMERS.RECOMMEND_SALE_MAX_DELAY;
50+
51+
setTimeout(() => {
52+
setInterval(() => {
53+
const cartItems = getCartItems();
54+
const lastSelectedId = getLastSelectedProductId();
55+
56+
if (Object.keys(cartItems).length === 0 || !lastSelectedId) return;
57+
58+
const products = getProducts();
59+
const recommendableProducts = products.filter(
60+
(p) => p.id !== lastSelectedId && p.stock > 0 && !p.recommendSale,
61+
);
62+
63+
if (recommendableProducts.length === 0) return;
64+
65+
const recommendProduct = recommendableProducts[0];
66+
const newPrice = Math.round(
67+
recommendProduct.price * (1 - DISCOUNT_RATES.RECOMMEND),
68+
);
69+
70+
alert(
71+
`💝 ${recommendProduct.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`,
72+
);
73+
74+
updateProduct(recommendProduct.id, {
75+
price: newPrice,
76+
recommendSale: true,
77+
});
78+
79+
updateProductOptions();
80+
updateCartPrices();
81+
}, TIMERS.RECOMMEND_SALE_INTERVAL);
82+
}, delay);
83+
}
84+
85+
function updateCartPrices(): void {
86+
const elements = getElements();
87+
const cartItemElements = elements.cartItems.children;
88+
89+
for (let i = 0; i < cartItemElements.length; i++) {
90+
const itemElement = cartItemElements[i] as HTMLElement;
91+
const productId = itemElement.id;
92+
const products = getProducts();
93+
const product = products.find((p) => p.id === productId);
94+
95+
if (product) {
96+
updateCartItemPrice(itemElement, product);
97+
}
98+
}
99+
100+
updateCart();
101+
}

0 commit comments

Comments
 (0)