-
Notifications
You must be signed in to change notification settings - Fork 50
[7팀 박희정] Chapter2-1. 프레임워크 없이 SPA 만들기 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,5 +48,8 @@ | |
| "workerDirectory": [ | ||
| "public" | ||
| ] | ||
| }, | ||
| "dependencies": { | ||
| "playwright": "^1.56.1" | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| const CartItem = ({ product, isSelected = false }) => { | ||
| 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"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 장바구니 아이템에서 수량 입력(line 25)이 |
||
| <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 }; | ||
| 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">></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">></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">></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 }; |
| 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; |
| 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; |
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 모달 컴포넌트가 기능별로 CartItem과 CartModal로 나뉘어져 있어 책임이 잘 분리되어 있습니다. 👍 UI 요소들을 템플릿 리터럴로 깔끔하게 관리한 점도 가독성에 도움이 됩니다.
다만 내부 상태 관리와 이벤트 핸들링이 의존하는 부분이 따로 보여야 유지보수에 유리합니다. 현재의 HTML 생성 코드와 상태나 이벤트 관리를 분리해서 구현할 수 있다면 테스트 및 확장성이 더 좋아질 수 있어요.