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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@
"workerDirectory": [
"public"
]
},
"dependencies": {
"playwright": "^1.56.1"
}
}
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 162 additions & 0 deletions src/components/Cart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const CartItem = ({ product, isSelected = false }) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

장바구니 모달 컴포넌트가 기능별로 CartItem과 CartModal로 나뉘어져 있어 책임이 잘 분리되어 있습니다. 👍 UI 요소들을 템플릿 리터럴로 깔끔하게 관리한 점도 가독성에 도움이 됩니다.

다만 내부 상태 관리와 이벤트 핸들링이 의존하는 부분이 따로 보여야 유지보수에 유리합니다. 현재의 HTML 생성 코드와 상태나 이벤트 관리를 분리해서 구현할 수 있다면 테스트 및 확장성이 더 좋아질 수 있어요.

return `
<div class="flex items-center py-3 border-b border-gray-100 cart-item" data-product-id="${product.id}">
<!-- 선택 체크박스 -->
<label class="flex items-center mr-3">
<input type="checkbox" ${isSelected ? "checked" : ""} class="cart-item-checkbox w-4 h-4 text-blue-600 border-gray-300 rounded
focus:ring-blue-500" data-product-id="${product.id}">
</label>
<!-- 상품 이미지 -->
<div class="w-16 h-16 bg-gray-100 rounded-lg overflow-hidden mr-3 flex-shrink-0">
<img src="${product.imageUrl}" alt="${product.name}" class="w-full h-full object-cover cursor-pointer cart-item-image" data-product-id="${product.id}">
</div>
<!-- 상품 정보 -->
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate cursor-pointer cart-item-title" data-product-id="${product.id}">
${product.name}
</h4>
<p class="text-sm text-gray-600 mt-1">
${product.price.toLocaleString()}원
</p>
<!-- 수량 조절 -->
<div class="flex items-center mt-2">
<button class="quantity-decrease-btn w-7 h-7 flex items-center justify-center
border border-gray-300 rounded-l-md bg-gray-50 hover:bg-gray-100" data-product-id="${product.id}">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

장바구니 아이템에서 수량 입력(line 25)이 disabled로 막혀 있고 수량 버튼에도 이벤트 연결이 없어, 요구사항인 '수량 증가/감소' 기능이 아예 동작하지 않습니다. 수량 변경 후 전체 금액도 갱신되어야 하므로, input을 활성화하거나 버튼 클릭 시 수량 상태를 변경하고 총합을 다시 계산하는 로직을 추가해 주세요.

<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<input type="number" value="${product.quantity}" min="1" class="quantity-input w-12 h-7 text-center text-sm border-t border-b
border-gray-300 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" disabled data-product-id="${product.id}">
<button class="quantity-increase-btn w-7 h-7 flex items-center justify-center
border border-gray-300 rounded-r-md bg-gray-50 hover:bg-gray-100" data-product-id="${product.id}">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
</div>
<!-- 가격 및 삭제 -->
<div class="text-right ml-3">
<p class="text-sm font-medium text-gray-900">
${(product.price * product.quantity).toLocaleString()}원
</p>
<button class="cart-item-remove-btn mt-1 text-xs text-red-600 hover:text-red-800" data-product-id="${product.id}">
삭제
</button>
</div>
</div>
`;
};

const CartModal = ({ items = [], selectedCount = 0, totalPrice = 0, hasSelection = false }) => {
const isEmpty = items.length === 0;

return `
<div class="flex min-h-full items-end justify-center p-0 sm:items-center sm:p-4">
<div class="relative bg-white rounded-t-lg sm:rounded-lg shadow-xl w-full max-w-md sm:max-w-lg max-h-[90vh] overflow-hidden">
<!-- 헤더 -->
<div class="sticky top-0 bg-white border-b border-gray-200 p-4 flex items-center justify-between">
<h2 class="text-lg font-bold text-gray-900 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
장바구니${!isEmpty ? ` <span class="text-sm font-normal text-gray-600 ml-1">(${items.length})</span>` : ""}
</h2>
<button id="cart-modal-close-btn" class="text-gray-400 hover:text-gray-600 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>

<!-- 컨텐츠 -->
<div class="flex flex-col max-h-[calc(90vh-120px)]">
${
isEmpty
? `
<!-- 빈 장바구니 -->
<div class="flex-1 flex items-center justify-center p-8">
<div class="text-center">
<div class="text-gray-400 mb-4">
<svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">장바구니가 비어있습니다</h3>
<p class="text-gray-600">원하는 상품을 담아보세요!</p>
</div>
</div>
`
: `
<!-- 전체 선택 섹션 -->
<div class="p-4 border-b border-gray-200 bg-gray-50">
<label class="flex items-center text-sm text-gray-700">
<input type="checkbox" id="cart-modal-select-all-checkbox" class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mr-2">
전체선택 (${items.length}개)
</label>
</div>
<!-- 아이템 목록 -->
<div class="flex-1 overflow-y-auto">
<div class="p-4 space-y-4">
${items.map((item) => CartItem(item)).join("")}
</div>
</div>
`
}
</div>

${
!isEmpty
? `
<!-- 하단 액션 -->
<div class="sticky bottom-0 bg-white border-t border-gray-200 p-4">
${
hasSelection
? `
<!-- 선택된 아이템 정보 -->
<div class="flex justify-between items-center mb-3 text-sm">
<span class="text-gray-600">선택한 상품 (${selectedCount}개)</span>
<span class="font-medium">${totalPrice.toLocaleString()}원</span>
</div>
`
: ""
}
<!-- 총 금액 -->
<div class="flex justify-between items-center mb-4">
<span class="text-lg font-bold text-gray-900">총 금액</span>
<span class="text-xl font-bold text-blue-600">${totalPrice.toLocaleString()}원</span>
</div>
<!-- 액션 버튼들 -->
<div class="space-y-2">
${
hasSelection
? `
<button id="cart-modal-remove-selected-btn" class="w-full bg-red-600 text-white py-2 px-4 rounded-md
hover:bg-red-700 transition-colors text-sm">
선택한 상품 삭제 (${selectedCount}개)
</button>
`
: ""
}
<div class="flex gap-2">
<button id="cart-modal-clear-cart-btn" class="flex-1 bg-gray-600 text-white py-2 px-4 rounded-md
hover:bg-gray-700 transition-colors text-sm">
전체 비우기
</button>
<button id="cart-modal-checkout-btn" class="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md
hover:bg-blue-700 transition-colors text-sm">
구매하기
</button>
</div>
</div>
</div>
`
: ""
}
</div>
</div>
`;
};

export { CartModal, CartItem };
55 changes: 55 additions & 0 deletions src/components/CategoryFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const CategoryFilter1Depth = ({ category1, subCategories = [] }) => {
return `
<div class="space-y-2">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">카테고리:</label>
<button data-breadcrumb="reset" class="text-xs hover:text-blue-800 hover:underline">전체</button>
<span class="text-xs text-gray-500">&gt;</span>
<button data-breadcrumb="category1" data-category1="${category1}" class="text-xs hover:text-blue-800 hover:underline">${category1}</button>
</div>
<div class="space-y-2">
<div class="flex flex-wrap gap-2">
${subCategories
.map(
(cat) => `
<button data-category1="${category1}" data-category2="${cat}" class="category2-filter-btn text-left px-3 py-2 text-sm rounded-md border transition-colors bg-white border-gray-300 text-gray-700 hover:bg-gray-50">
${cat}
</button>
`,
)
.join("")}
</div>
</div>
</div>
`;
};

const CategoryFilter2Depth = ({ category1, category2, subCategories = [] }) => {
return `
<div class="space-y-2">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">카테고리:</label>
<button data-breadcrumb="reset" class="text-xs hover:text-blue-800 hover:underline">전체</button>
<span class="text-xs text-gray-500">&gt;</span>
<button data-breadcrumb="category1" data-category1="${category1}" class="text-xs hover:text-blue-800 hover:underline">${category1}</button>
<span class="text-xs text-gray-500">&gt;</span>
<span class="text-xs text-gray-600 cursor-default">${category2}</span>
</div>
<div class="space-y-2">
<div class="flex flex-wrap gap-2">
${subCategories
.map(
(cat) => `
<button data-category1="${category1}" data-category2="${cat}" class="category2-filter-btn text-left px-3 py-2 text-sm rounded-md border transition-colors ${cat === category2 ? "bg-blue-100 border-blue-300 text-blue-800" : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"}">
${cat}
</button>
`,
)
.join("")}
</div>
</div>
</div>
`;
};

export { CategoryFilter1Depth, CategoryFilter2Depth };
11 changes: 11 additions & 0 deletions src/components/Footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const Footer = () => {
return `
<footer class="bg-white shadow-sm sticky top-0 z-40">
<div class="max-w-md mx-auto py-8 text-center text-gray-500">
<p>© 2025 항해플러스 프론트엔드 쇼핑몰</p>
</div>
</footer>
`;
};

export default Footer;
22 changes: 22 additions & 0 deletions src/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const Header = () => {
return `
<header class="bg-white shadow-sm sticky top-0 z-40">
<div class="max-w-md mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900">
<a href="/" data-link="">쇼핑몰</a>
</h1>
<div class="flex items-center space-x-2">
<button id="cart-icon-btn" class="relative p-2 text-gray-700 hover:text-gray-900 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
</button>
</div>
</div>
</div>
</header>`;
};

export default Header;
38 changes: 38 additions & 0 deletions src/components/NotFound.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const NotFound = () => {
return `
<main class="max-w-md mx-auto px-4 py-4">
<div class="text-center my-4 py-20 shadow-md p-6 bg-white rounded-lg">
<svg viewBox="0 0 320 180" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="blueGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4285f4;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1a73e8;stop-opacity:1" />
</linearGradient>
<filter id="softShadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="2" stdDeviation="8" flood-color="#000000" flood-opacity="0.1"/>
</filter>
</defs>

<!-- 404 Numbers -->
<text x="160" y="85" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="48" font-weight="600" fill="url(#blueGradient)" text-anchor="middle">404</text>

<!-- Icon decoration -->
<circle cx="80" cy="60" r="3" fill="#e8f0fe" opacity="0.8"/>
<circle cx="240" cy="60" r="3" fill="#e8f0fe" opacity="0.8"/>
<circle cx="90" cy="45" r="2" fill="#4285f4" opacity="0.5"/>
<circle cx="230" cy="45" r="2" fill="#4285f4" opacity="0.5"/>

<!-- Message -->
<text x="160" y="110" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="14" font-weight="400" fill="#5f6368" text-anchor="middle">페이지를 찾을 수 없습니다</text>

<!-- Subtle bottom accent -->
<rect x="130" y="130" width="60" height="2" rx="1" fill="url(#blueGradient)" opacity="0.3"/>
</svg>

<a href="/" data-link class="inline-block px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">홈으로</a>
</div>
</main>
`;
};

export default NotFound;
Loading
Loading