diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5189dd5d..6e44a184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,6 @@ jobs: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - uses: pnpm/action-setup@v4 - with: - version: latest - uses: actions/setup-node@v4 with: node-version: 22 @@ -37,8 +35,6 @@ jobs: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - uses: pnpm/action-setup@v4 - with: - version: latest - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..d9b4381d --- /dev/null +++ b/deploy.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# GitHub Pages 배포 스크립트 +# main 브랜치의 빌드 결과를 gh-pages 브랜치에 배포합니다 + +set -e + +echo "🚀 배포를 시작합니다..." + +# 현재 브랜치 저장 +CURRENT_BRANCH=$(git branch --show-current) + +# main 브랜치로 전환 +echo "📦 main 브랜치로 전환 중..." +git checkout main + +# 의존성 설치 (필요한 경우) +if [ ! -d "node_modules" ]; then + echo "📥 의존성 설치 중..." + pnpm install +fi + +# 빌드 +echo "🔨 프로젝트 빌드 중..." +pnpm run build + +# dist 폴더 확인 +if [ ! -d "dist" ]; then + echo "❌ dist 폴더를 찾을 수 없습니다. 빌드가 실패했을 수 있습니다." + exit 1 +fi + +# gh-pages 브랜치로 전환 (없으면 생성) +echo "🌿 gh-pages 브랜치로 전환 중..." +git checkout gh-pages 2>/dev/null || git checkout -b gh-pages + +# dist 폴더의 내용을 루트로 복사 +echo "📋 빌드 파일 복사 중..." +cp -r dist/* . + +# 변경사항 커밋 +echo "💾 변경사항 커밋 중..." +git add . +git commit -m "Deploy: $(date +'%Y-%m-%d %H:%M:%S')" || echo "변경사항이 없습니다." + +# gh-pages 브랜치 푸시 +echo "📤 gh-pages 브랜치 푸시 중..." +git push origin gh-pages + +# 원래 브랜치로 돌아가기 +echo "↩️ 원래 브랜치($CURRENT_BRANCH)로 돌아가는 중..." +git checkout $CURRENT_BRANCH + +echo "✅ 배포가 완료되었습니다!" +echo "🌐 배포 링크: https://taejun0.github.io/front_7th_chapter2-1/" + diff --git a/package.json b/package.json index 5ec7f3f3..262e5225 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "dev": "vite", "dev:hash": "vite --open ./index.hash.html", "build": "vite build", + "deploy": "npm run build && npm run deploy:gh-pages", + "deploy:gh-pages": "git subtree push --prefix dist origin gh-pages", "lint:fix": "eslint --fix", "prettier:write": "prettier --write ./src", "preview": "vite preview", diff --git a/src/app/router/router.js b/src/app/router/router.js new file mode 100644 index 00000000..0de37eca --- /dev/null +++ b/src/app/router/router.js @@ -0,0 +1,66 @@ +export class Router { + constructor() { + this.routes = new Map(); + this.currentPath = window.location.pathname; + this.init(); + } + + init() { + this.handleRoute(); + + window.addEventListener("popstate", () => { + this.currentPath = window.location.pathname; + this.handleRoute(); + }); + + document.addEventListener("click", (e) => { + const link = e.target.closest("a[data-link]"); + if (link) { + e.preventDefault(); + const href = link.getAttribute("href"); + if (href) { + this.navigate(href); + } + } + }); + } + + register(path, handler) { + this.routes.set(path, handler); + } + + navigate(path) { + if (this.currentPath !== path) { + this.currentPath = path; + window.history.pushState({}, "", path); + this.handleRoute(); + } + } + + handleRoute() { + const path = window.location.pathname; + + let handler = this.routes.get(path); + + if (!handler) { + for (const [routePath, routeHandler] of this.routes.entries()) { + if (routePath.includes(":")) { + const pattern = routePath.replace(/:[^/]+/g, "([^/]+)"); + const regex = new RegExp(`^${pattern}$`); + if (regex.test(path)) { + handler = routeHandler; + break; + } + } + } + } + + if (!handler) { + handler = this.routes.get("/"); + } + + if (handler) { + handler(); + } + } +} diff --git a/src/app/services/cartService.js b/src/app/services/cartService.js new file mode 100644 index 00000000..14262268 --- /dev/null +++ b/src/app/services/cartService.js @@ -0,0 +1,93 @@ +const CART_STORAGE_KEY = "shopping_cart"; + +export function getCart() { + try { + const cartData = localStorage.getItem(CART_STORAGE_KEY); + return cartData ? JSON.parse(cartData) : {}; + } catch (error) { + console.error("장바구니 로드 실패:", error); + return {}; + } +} + +function saveCart(cart) { + try { + localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart)); + } catch (error) { + console.error("장바구니 저장 실패:", error); + } +} + +export function addToCart(productId, productData = null, quantity = 1) { + const cart = getCart(); + + if (cart[productId]) { + cart[productId].quantity += quantity; + } else { + cart[productId] = { + productId, + quantity, + ...(productData && { productData }), + }; + } + + saveCart(cart); + updateCartIcon(); + return cart; +} + +export function removeFromCart(productId) { + const cart = getCart(); + delete cart[productId]; + saveCart(cart); + updateCartIcon(); + return cart; +} + +export function updateCartQuantity(productId, quantity) { + const cart = getCart(); + if (cart[productId]) { + if (quantity <= 0) { + delete cart[productId]; + } else { + cart[productId].quantity = quantity; + } + saveCart(cart); + updateCartIcon(); + } + return cart; +} + +export function clearCart() { + localStorage.removeItem(CART_STORAGE_KEY); + updateCartIcon(); +} + +export function getCartItemCount() { + const cart = getCart(); + return Object.values(cart).reduce((total, item) => total + item.quantity, 0); +} + +export function updateCartIcon() { + const cartIconBtn = document.getElementById("cart-icon-btn"); + if (!cartIconBtn) return; + + const count = getCartItemCount(); + const existingBadge = cartIconBtn.querySelector("span"); + + if (count > 0) { + if (existingBadge) { + existingBadge.textContent = count.toString(); + } else { + const badge = document.createElement("span"); + badge.className = + "absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"; + badge.textContent = count.toString(); + cartIconBtn.appendChild(badge); + } + } else { + if (existingBadge) { + existingBadge.remove(); + } + } +} diff --git a/src/components/toast.js b/src/components/toast.js new file mode 100644 index 00000000..947c274d --- /dev/null +++ b/src/components/toast.js @@ -0,0 +1,111 @@ +export const toastTemplates = ` +
+
+
+ + + +
+

장바구니에 추가되었습니다

+ +
+ +
+
+ + + +
+

선택된 상품들이 삭제되었습니다

+ +
+ +
+
+ + + +
+

오류가 발생했습니다.

+ +
+
+ `; + +export function showToast(type = "success", message = null) { + const existingToast = document.querySelector(".toast-container"); + if (existingToast) { + existingToast.remove(); + } + + let toastHTML = ""; + let bgColor = ""; + let icon = ""; + + switch (type) { + case "success": + bgColor = "bg-green-600"; + icon = ` + + `; + message = message || "장바구니에 추가되었습니다"; + break; + case "info": + bgColor = "bg-blue-600"; + icon = ` + + `; + message = message || "선택된 상품들이 삭제되었습니다"; + break; + case "error": + bgColor = "bg-red-600"; + icon = ` + + `; + message = message || "오류가 발생했습니다."; + break; + } + + toastHTML = ` +
+
+ ${icon} +
+

${message}

+ +
+ `; + + const toastElement = document.createElement("div"); + toastElement.innerHTML = toastHTML; + document.body.appendChild(toastElement.firstElementChild); + + const toast = document.querySelector(".toast-container"); + const closeBtn = toast.querySelector(".toast-close-btn"); + + closeBtn.addEventListener("click", () => { + toast.remove(); + }); + + setTimeout(() => { + if (toast && toast.parentElement) { + toast.remove(); + } + }, 3000); +} diff --git a/src/main.js b/src/main.js index 4b055b89..9bb6bf4b 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3 +1,17 @@ +import { Router } from "./app/router/router.js"; +import { renderProductList } from "./pages/productList.js"; +import { renderProductDetail } from "./pages/productDetail.js"; +import { + productListLoadingLayout, + productListLoadedLayout, + productListCategoryDepth1Layout, + productListCategoryDepth2Layout, +} from "./pages/productList.js"; +import { cartEmptyLayout, cartNoSelectionLayout, cartWithSelectionLayout } from "./pages/cart.js"; +import { productDetailLoadingLayout, productDetailLoadedLayout } from "./pages/productDetail.js"; +import { toastTemplates } from "./components/toast.js"; +import { notFoundLayout } from "./pages/notFound.js"; + const enableMocking = () => import("./mocks/browser.js").then(({ worker }) => worker.start({ @@ -5,1148 +19,79 @@ const enableMocking = () => }), ); -function main() { - const 상품목록_레이아웃_로딩 = ` -
-
-
-
-

- 쇼핑몰 -

-
- - -
-
-
-
-
- -
- -
-
- -
- - - -
-
-
- -
- -
-
- - -
- -
-
카테고리 로딩 중...
-
- -
- -
- -
- - -
- -
- - -
-
-
-
- -
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- - - - - 상품을 불러오는 중... -
-
-
-
-
- -
- `; - - const 상품목록_레이아웃_로딩완료 = ` -
-
-
-
-

- 쇼핑몰 -

-
- - -
-
-
-
-
- -
- -
-
- -
- - - -
-
-
- -
- -
-
- - -
- -
- - -
- -
- -
- -
- - -
- -
- - -
-
-
-
- -
-
- -
- 총 340개의 상품 -
- -
-
- -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-
-

- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -

-

-

- 220원 -

-
- - -
-
-
- -
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -
- -
-
-

- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -

-

이지웨이건축자재

-

- 230원 -

-
- - -
-
-
- -
- 모든 상품을 확인했습니다 -
-
-
-
- -
- `; - - const 상품목록_레이아웃_카테고리_1Depth = ` -
- -
- -
-
- -
- - - -
-
-
- - -
- - -
-
- - > -
-
-
- - - -
-
-
- - -
- -
- - -
- -
- - -
-
-
-
-
- `; - - const 상품목록_레이아웃_카테고리_2Depth = ` -
- -
- -
-
- -
- - - -
-
-
- - -
- - -
-
- - >>주방용품 -
-
-
- - - -
-
-
- - -
- -
- - -
- -
- - -
-
-
-
-
- `; - - const 토스트 = ` -
-
-
- - - -
-

장바구니에 추가되었습니다

- -
- -
-
- - - -
-

선택된 상품들이 삭제되었습니다

- -
- -
-
- - - -
-

오류가 발생했습니다.

- -
-
- `; +function getQueryParams() { + const params = new URLSearchParams(window.location.search); + return { + limit: parseInt(params.get("limit")) || 20, + search: params.get("search") || "", + category1: params.get("category1") || "", + category2: params.get("category2") || "", + sort: params.get("sort") || "price_asc", + }; +} - const 장바구니_비어있음 = ` -
-
- -
-

- - - - 장바구니 -

- - -
- - -
- -
-
-
- - - -
-

장바구니가 비어있습니다

-

원하는 상품을 담아보세요!

-
-
-
-
-
- `; +function main() { + const router = new Router(); - const 장바구니_선택없음 = ` -
-
- -
-

- - - - 장바구니 - (2) -

- -
- -
- -
- -
- -
-
-
- - - -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-

- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -

-

- 220원 -

- -
- - - -
-
- -
-

- 440원 -

- -
-
-
- - - -
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -
- -
-

- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -

-

- 230원 -

- -
- - - -
-
- -
-

- 230원 -

- -
-
-
-
-
- -
- - -
- 총 금액 - 670원 -
- -
-
- - -
-
-
-
-
- `; + window.__router__ = router; - const 장바구니_선택있음 = ` -
-
- -
-

- - - - 장바구니 - (2) -

- -
- -
- -
- -
- -
-
-
- - - -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-

- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -

-

- 220원 -

- -
- - - -
-
- -
-

- 440원 -

- -
-
-
- - - -
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -
- -
-

- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -

-

- 230원 -

- -
- - - -
-
- -
-

- 230원 -

- -
-
-
-
-
- -
- -
- 선택한 상품 (1개) - 440원 -
- -
- 총 금액 - 670원 -
- -
- -
- - -
-
-
-
-
- `; + router.register("/", () => { + const params = getQueryParams(); + renderProductList(params); + }); - const 상세페이지_로딩 = ` -
-
-
-
-
- -

상품 상세

-
-
- - -
-
-
-
-
-
-
-
-

상품 정보를 불러오는 중...

-
-
-
- -
- `; + router.register("/product/:id", () => { + const path = window.location.pathname; + const productId = path.split("/product/")[1]; + if (productId) { + renderProductDetail(productId); + } + }); - const 상세페이지_로딩완료 = ` -
-
-
-
-
- -

상품 상세

-
-
- - -
-
-
-
-
- - - -
- -
-
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-

-

PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장

- -
-
- - - - - - - - - - - - - - - -
- 4.0 (749개 리뷰) -
- -
- 220원 -
- -
- 재고 107개 -
- -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다. -
-
-
- -
-
- 수량 -
- - - -
-
- - -
-
- -
- -
- -
-
-

관련 상품

-

같은 카테고리의 다른 상품들

-
-
-
- - -
-
-
-
- -
- `; + router.register("/404", () => { + const root = document.getElementById("root") || document.body; + root.innerHTML = notFoundLayout; + }); - const _404_ = ` -
-
- - - - - - - - - - - - - 404 - - - - - - - - - 페이지를 찾을 수 없습니다 - - - - - - 홈으로 -
-
- `; + router.handleRoute(); +} - document.body.innerHTML = ` - ${상품목록_레이아웃_로딩} +function renderShowcase() { + const showcaseMarkup = ` + ${productListLoadingLayout}
- ${상품목록_레이아웃_로딩완료} + ${productListLoadedLayout}
- ${상품목록_레이아웃_카테고리_1Depth} + ${productListCategoryDepth1Layout}
- ${상품목록_레이아웃_카테고리_2Depth} + ${productListCategoryDepth2Layout}
- ${토스트} + ${toastTemplates}
- ${장바구니_비어있음} + ${cartEmptyLayout}
- ${장바구니_선택없음} + ${cartNoSelectionLayout}
- ${장바구니_선택있음} + ${cartWithSelectionLayout}
- ${상세페이지_로딩} + ${productDetailLoadingLayout}
- ${상세페이지_로딩완료} + ${productDetailLoadedLayout}
- ${_404_} + ${notFoundLayout} `; + const root = document.getElementById("root") || document.body; + root.innerHTML = showcaseMarkup; } -// 애플리케이션 시작 if (import.meta.env.MODE !== "test") { - enableMocking().then(main); + enableMocking().then(() => { + if (new URLSearchParams(window.location.search).get("showcase") === "true") { + renderShowcase(); + } else { + main(); + } + }); } else { main(); } diff --git a/src/pages/cart.js b/src/pages/cart.js new file mode 100644 index 00000000..b8535a83 --- /dev/null +++ b/src/pages/cart.js @@ -0,0 +1,829 @@ +import { getCart, removeFromCart, updateCartQuantity, clearCart } from "../app/services/cartService.js"; +import { updateCartIcon } from "../app/services/cartService.js"; +import { showToast } from "../components/toast.js"; + +function createCartItemCard(productId, item) { + const { quantity, productData } = item; + const price = parseInt(productData?.lprice || 0); + const totalPrice = price * quantity; + + return ` +
+ + + +
+ ${productData?.title || +
+ +
+

+ ${productData?.title || ""} +

+

+ ${price.toLocaleString()}원 +

+ +
+ + + +
+
+ +
+

+ ${totalPrice.toLocaleString()}원 +

+ +
+
+ `; +} + +export function renderCartModal() { + const cart = getCart(); + const cartItems = Object.entries(cart); + const totalCount = cartItems.reduce((sum, [, item]) => sum + item.quantity, 0); + + const root = document.getElementById("root") || document.body; + + const existingModal = root.querySelector(".cart-modal"); + if (existingModal) { + existingModal.remove(); + } + + const modalOverlay = document.createElement("div"); + modalOverlay.className = "fixed inset-0 z-50 overflow-y-auto cart-modal"; + root.appendChild(modalOverlay); + let modalHTML = ` +
+
+
+ +
+

+ + + + 장바구니 + ${totalCount > 0 ? `(${totalCount})` : ""} +

+ +
+ +
+ `; + + if (cartItems.length === 0) { + modalHTML += ` +
+
+
+ + + +
+

장바구니가 비어있습니다

+

원하는 상품을 담아보세요!

+
+
+
+
+
+ `; + } else { + const totalPrice = cartItems.reduce((sum, [, item]) => { + const price = parseInt(item.productData?.lprice || 0); + return sum + price * item.quantity; + }, 0); + + modalHTML += ` + +
+ +
+ +
+
+ ${cartItems.map(([productId, item]) => createCartItemCard(productId, item)).join("")} +
+
+ + +
+ + + +
+ 총 금액 + ${totalPrice.toLocaleString()}원 +
+ +
+ +
+ + +
+
+
+ + + `; + } + + modalOverlay.innerHTML = modalHTML; + + setupCartModalEvents(); +} + +function updateCartItemInModal(productId) { + const modal = document.querySelector(".cart-modal"); + if (!modal) return; + + const cart = getCart(); + const item = cart[productId]; + + if (!item) { + const cartItem = modal.querySelector(`.cart-item[data-product-id="${productId}"]`); + if (cartItem) { + cartItem.remove(); + } + updateCartTotalPrice(); + updateSelectedInfo(); + return; + } + + const cartItem = modal.querySelector(`.cart-item[data-product-id="${productId}"]`); + if (cartItem) { + const { quantity, productData } = item; + const price = parseInt(productData?.lprice || 0); + const totalPrice = price * quantity; + + const quantityInput = cartItem.querySelector(`.quantity-input[data-product-id="${productId}"]`); + if (quantityInput) { + quantityInput.value = quantity; + } + + const priceContainer = cartItem.querySelector(".text-right.ml-3"); + if (priceContainer) { + const totalPriceElement = priceContainer.querySelector("p.text-sm.font-medium.text-gray-900"); + if (totalPriceElement) { + totalPriceElement.textContent = `${totalPrice.toLocaleString()}원`; + } + } + } + + updateCartTotalPrice(); + updateSelectedInfo(); +} + +function updateCartTotalPrice() { + const modal = document.querySelector(".cart-modal"); + if (!modal) return; + + const cart = getCart(); + const cartItems = Object.entries(cart); + const totalPrice = cartItems.reduce((sum, [, item]) => { + const price = parseInt(item.productData?.lprice || 0); + return sum + price * item.quantity; + }, 0); + + const totalPriceElement = modal.querySelector("#cart-total-price"); + if (totalPriceElement) { + totalPriceElement.textContent = `${totalPrice.toLocaleString()}원`; + } + + const totalCount = cartItems.reduce((sum, [, item]) => sum + item.quantity, 0); + const headerCount = modal.querySelector("h2 span"); + if (headerCount) { + headerCount.textContent = `(${totalCount})`; + } else if (totalCount > 0) { + const h2 = modal.querySelector("h2"); + if (h2) { + const span = document.createElement("span"); + span.className = "text-sm font-normal text-gray-600 ml-1"; + span.textContent = `(${totalCount})`; + h2.appendChild(span); + } + } + + const selectAllCheckbox = modal.querySelector("#cart-modal-select-all-checkbox"); + if (selectAllCheckbox) { + const selectAllLabel = selectAllCheckbox.closest("label"); + if (selectAllLabel) { + const labelText = selectAllLabel.textContent.trim(); + if (labelText.includes("전체선택")) { + const textAfterCheckbox = selectAllCheckbox.nextSibling; + if (textAfterCheckbox && textAfterCheckbox.nodeType === Node.TEXT_NODE) { + textAfterCheckbox.textContent = ` 전체선택 (${totalCount}개)`; + } else { + const textNode = document.createTextNode(` 전체선택 (${totalCount}개)`); + selectAllCheckbox.parentNode.insertBefore(textNode, selectAllCheckbox.nextSibling); + } + } + } + } +} + +let cartModalEventsSetup = false; +function setupCartModalEvents() { + if (cartModalEventsSetup) { + return; + } + cartModalEventsSetup = true; + + document.addEventListener("click", (e) => { + const modal = document.querySelector(".cart-modal"); + if (!modal || !modal.contains(e.target)) return; + + const decreaseBtn = e.target.closest(".quantity-decrease-btn"); + const increaseBtn = e.target.closest(".quantity-increase-btn"); + const removeBtn = e.target.closest(".cart-item-remove-btn"); + const itemImage = e.target.closest(".cart-item-image"); + const itemTitle = e.target.closest(".cart-item-title"); + const closeBtn = e.target.closest("#cart-modal-close-btn"); + const removeSelectedBtn = e.target.closest("#cart-modal-remove-selected-btn"); + const clearCartBtn = e.target.closest("#cart-modal-clear-cart-btn"); + + if (closeBtn) { + e.preventDefault(); + closeCartModal(); + return; + } + + if (decreaseBtn) { + e.preventDefault(); + const productId = decreaseBtn.getAttribute("data-product-id"); + const quantityInput = modal.querySelector(`.quantity-input[data-product-id="${productId}"]`); + if (quantityInput) { + const currentQuantity = parseInt(quantityInput.value) || 1; + if (currentQuantity > 1) { + updateCartQuantity(productId, currentQuantity - 1); + updateCartItemInModal(productId); + } + } + return; + } + + if (increaseBtn) { + e.preventDefault(); + const productId = increaseBtn.getAttribute("data-product-id"); + const quantityInput = modal.querySelector(`.quantity-input[data-product-id="${productId}"]`); + if (quantityInput) { + const currentQuantity = parseInt(quantityInput.value) || 1; + updateCartQuantity(productId, currentQuantity + 1); + updateCartItemInModal(productId); + } + return; + } + + if (removeBtn) { + e.preventDefault(); + const productId = removeBtn.getAttribute("data-product-id"); + removeFromCart(productId); + showToast("info", "상품이 장바구니에서 삭제되었습니다"); + updateCartItemInModal(productId); + updateCartIcon(); + + const cart = getCart(); + if (Object.keys(cart).length === 0) { + renderCartModal(); + } + return; + } + + if (removeSelectedBtn) { + e.preventDefault(); + const selectedCheckboxes = modal.querySelectorAll(".cart-item-checkbox:checked"); + if (selectedCheckboxes.length === 0) { + return; + } + + selectedCheckboxes.forEach((checkbox) => { + const productId = checkbox.getAttribute("data-product-id"); + removeFromCart(productId); + updateCartItemInModal(productId); + }); + + showToast("info", "선택된 상품들이 삭제되었습니다"); + updateCartIcon(); + + const cart = getCart(); + if (Object.keys(cart).length === 0) { + renderCartModal(); + } + return; + } + + if (clearCartBtn) { + e.preventDefault(); + clearCart(); + showToast("info", "장바구니가 비워졌습니다"); + renderCartModal(); + updateCartIcon(); + return; + } + + if (itemImage || itemTitle) { + e.preventDefault(); + const productId = (itemImage || itemTitle).getAttribute("data-product-id"); + if (productId) { + const router = window.__router__; + if (router) { + closeCartModal(); + router.navigate(`/product/${productId}`); + } + } + return; + } + }); + + document.addEventListener("click", (e) => { + const overlay = e.target.closest(".cart-modal-overlay"); + if (overlay && e.target === overlay) { + closeCartModal(); + } + }); + + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + const modal = document.querySelector(".cart-modal"); + if (modal) { + closeCartModal(); + } + } + }); + + document.addEventListener("change", (e) => { + const modal = document.querySelector(".cart-modal"); + if (!modal || !modal.contains(e.target)) return; + + if (e.target.classList.contains("cart-item-checkbox")) { + updateSelectAllCheckbox(); + updateSelectedInfo(); + return; + } + + if (e.target.id === "cart-modal-select-all-checkbox") { + const isChecked = e.target.checked; + const itemCheckboxes = modal.querySelectorAll(".cart-item-checkbox"); + itemCheckboxes.forEach((checkbox) => { + checkbox.checked = isChecked; + }); + updateSelectedInfo(); + } + }); +} + +function updateSelectedInfo() { + const modal = document.querySelector(".cart-modal"); + if (!modal) return; + + const selectedCheckboxes = modal.querySelectorAll(".cart-item-checkbox:checked"); + const selectedCount = selectedCheckboxes.length; + const selectedInfo = modal.querySelector("#selected-items-info"); + const removeSelectedBtn = modal.querySelector("#cart-modal-remove-selected-btn"); + + if (selectedCount > 0) { + let selectedPrice = 0; + selectedCheckboxes.forEach((checkbox) => { + const productId = checkbox.getAttribute("data-product-id"); + const cartItem = modal.querySelector(`.cart-item[data-product-id="${productId}"]`); + if (cartItem) { + const priceContainer = cartItem.querySelector(".text-right.ml-3"); + if (priceContainer) { + const totalPriceElement = priceContainer.querySelector("p.text-sm.font-medium.text-gray-900"); + if (totalPriceElement) { + const priceText = totalPriceElement.textContent; + const price = parseInt(priceText.replace(/[^0-9]/g, "")) || 0; + selectedPrice += price; + } + } + } + }); + + if (selectedInfo) { + selectedInfo.style.display = "flex"; + const selectedCountSpan = selectedInfo.querySelector("#selected-count"); + const selectedPriceSpan = selectedInfo.querySelector("#selected-price"); + if (selectedCountSpan) selectedCountSpan.textContent = selectedCount; + if (selectedPriceSpan) selectedPriceSpan.textContent = `${selectedPrice.toLocaleString()}원`; + } + + if (removeSelectedBtn) { + removeSelectedBtn.style.display = "block"; + const removeCountSpan = removeSelectedBtn.querySelector("#remove-selected-count"); + if (removeCountSpan) removeCountSpan.textContent = selectedCount; + } + } else { + if (selectedInfo) selectedInfo.style.display = "none"; + if (removeSelectedBtn) removeSelectedBtn.style.display = "none"; + } +} + +function updateSelectAllCheckbox() { + const modal = document.querySelector(".cart-modal"); + if (!modal) return; + + const selectAllCheckbox = modal.querySelector("#cart-modal-select-all-checkbox"); + const itemCheckboxes = modal.querySelectorAll(".cart-item-checkbox"); + if (selectAllCheckbox && itemCheckboxes.length > 0) { + const allChecked = Array.from(itemCheckboxes).every((cb) => cb.checked); + selectAllCheckbox.checked = allChecked; + } +} + +function closeCartModal() { + const modal = document.querySelector(".cart-modal"); + if (modal) { + modal.remove(); + } +} + +export function openCartModal() { + renderCartModal(); +} + +export const cartEmptyLayout = ` +
+
+ +
+

+ + + + 장바구니 +

+ + +
+ + +
+ +
+
+
+ + + +
+

장바구니가 비어있습니다

+

원하는 상품을 담아보세요!

+
+
+
+
+
+ `; + +export const cartNoSelectionLayout = ` +
+
+ +
+

+ + + + 장바구니 + (2) +

+ +
+ +
+ +
+ +
+ +
+
+
+ + + +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+

+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +

+

+ 220원 +

+ +
+ + + +
+
+ +
+

+ 440원 +

+ +
+
+
+ + + +
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +
+ +
+

+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +

+

+ 230원 +

+ +
+ + + +
+
+ +
+

+ 230원 +

+ +
+
+
+
+
+ +
+ +
+ 총 금액 + 670원 +
+ +
+
+ + +
+
+
+
+
+ `; + +export const cartWithSelectionLayout = ` +
+
+ +
+

+ + + + 장바구니 + (2) +

+ +
+ +
+ +
+ +
+ +
+
+
+ + + +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+

+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +

+

+ 220원 +

+ +
+ + + +
+
+ +
+

+ 440원 +

+ +
+
+
+ + + +
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +
+ +
+

+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +

+

+ 230원 +

+ +
+ + + +
+
+ +
+

+ 230원 +

+ +
+
+
+
+
+ +
+ +
+ 선택한 상품 (1개) + 440원 +
+ +
+ 총 금액 + 670원 +
+ +
+ +
+ + +
+
+
+
+
+ `; diff --git a/src/pages/notFound.js b/src/pages/notFound.js new file mode 100644 index 00000000..61c5a48e --- /dev/null +++ b/src/pages/notFound.js @@ -0,0 +1,34 @@ +export const notFoundLayout = ` +
+
+ + + + + + + + + + + + + 404 + + + + + + + + + 페이지를 찾을 수 없습니다 + + + + + + 홈으로 +
+
+ `; diff --git a/src/pages/productDetail.js b/src/pages/productDetail.js new file mode 100644 index 00000000..d80318d8 --- /dev/null +++ b/src/pages/productDetail.js @@ -0,0 +1,553 @@ +import { getProduct, getProducts } from "../api/productApi.js"; +import { addToCart, updateCartIcon } from "../app/services/cartService.js"; +import { showToast } from "../components/toast.js"; +import { openCartModal } from "./cart.js"; + +function renderStars(rating) { + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 >= 0.5; + let starsHTML = ""; + + for (let i = 0; i < 5; i++) { + if (i < fullStars) { + starsHTML += ` + + `; + } else if (i === fullStars && hasHalfStar) { + starsHTML += ` + + `; + } else { + starsHTML += ` + + `; + } + } + return starsHTML; +} + +function createBreadcrumb(category1, category2) { + let breadcrumb = ``; + + if (category1) { + breadcrumb += ` + + + + + `; + } + + if (category2) { + breadcrumb += ` + + + + + `; + } + + return breadcrumb; +} + +function createRelatedProductCard(product) { + return ` + + `; +} + +export async function renderProductDetail(productId) { + const root = document.getElementById("root") || document.body; + root.innerHTML = productDetailLoadingLayout; + + try { + const product = await getProduct(productId); + + const relatedProductsResponse = await getProducts({ + category2: product.category2, + limit: 4, + }); + + const relatedProducts = relatedProductsResponse.products.filter((p) => p.productId !== productId).slice(0, 4); + + const detailHTML = ` +
+
+
+
+
+ +

상품 상세

+
+
+ + +
+
+
+
+
+ + + +
+ +
+
+ ${product.title} +
+ +
+ ${product.brand ? `

${product.brand}

` : ""} +

${product.title}

+ + ${ + product.rating + ? ` +
+
+ ${renderStars(product.rating)} +
+ ${product.rating} ${product.reviewCount ? `(${product.reviewCount}개 리뷰)` : ""} +
+ ` + : "" + } + +
+ ${parseInt(product.lprice).toLocaleString()}원 +
+ + ${ + product.stock + ? ` +
+ 재고 ${product.stock}개 +
+ ` + : "" + } + + ${ + product.description + ? ` +
+ ${product.description} +
+ ` + : "" + } +
+
+ +
+
+ 수량 +
+ + + +
+
+ + +
+
+ +
+ +
+ + ${ + relatedProducts.length > 0 + ? ` +
+
+

관련 상품

+

같은 카테고리의 다른 상품들

+
+
+
+ ${relatedProducts.map((p) => createRelatedProductCard(p)).join("")} +
+
+
+ ` + : "" + } +
+ +
+ `; + + const root = document.getElementById("root") || document.body; + root.innerHTML = detailHTML; + + setupProductDetailEvents(product); + + updateCartIcon(); + } catch (error) { + console.error("상품 상세 로드 실패:", error); + const router = window.__router__; + if (router) { + router.navigate("/404"); + } + } +} + +function setupProductDetailEvents(product) { + const backBtn = document.getElementById("back-btn"); + if (backBtn) { + backBtn.addEventListener("click", () => { + window.history.back(); + }); + } + + const cartIconBtn = document.getElementById("cart-icon-btn"); + if (cartIconBtn) { + cartIconBtn.addEventListener("click", () => { + openCartModal(); + }); + } + + const goToListBtn = document.getElementById("go-to-product-list"); + if (goToListBtn) { + goToListBtn.addEventListener("click", () => { + const router = window.__router__; + if (router) { + router.navigate("/"); + } + }); + } + + const quantityInput = document.getElementById("quantity-input"); + const decreaseBtn = document.getElementById("quantity-decrease"); + const increaseBtn = document.getElementById("quantity-increase"); + + if (decreaseBtn && quantityInput) { + decreaseBtn.addEventListener("click", () => { + const currentValue = parseInt(quantityInput.value) || 1; + if (currentValue > 1) { + quantityInput.value = currentValue - 1; + } + }); + } + + if (increaseBtn && quantityInput) { + increaseBtn.addEventListener("click", () => { + const currentValue = parseInt(quantityInput.value) || 1; + const maxValue = parseInt(quantityInput.getAttribute("max")) || 999; + if (currentValue < maxValue) { + quantityInput.value = currentValue + 1; + } + }); + } + + const addToCartBtn = document.getElementById("add-to-cart-btn"); + if (addToCartBtn) { + addToCartBtn.addEventListener("click", async () => { + const quantity = parseInt(quantityInput?.value) || 1; + try { + addToCart( + product.productId, + { + title: product.title, + image: product.image, + lprice: product.lprice, + brand: product.brand, + }, + quantity, + ); + + showToast("success", "장바구니에 추가되었습니다"); + updateCartIcon(); + } catch (error) { + console.error("장바구니 추가 실패:", error); + showToast("error", "상품을 장바구니에 추가하는 중 오류가 발생했습니다."); + } + }); + } + + document.addEventListener("click", (e) => { + const relatedProductCard = e.target.closest(".related-product-card"); + if (relatedProductCard) { + const productId = relatedProductCard.getAttribute("data-product-id"); + if (productId) { + const router = window.__router__; + if (router) { + router.navigate(`/product/${productId}`); + } + } + } + }); + + document.addEventListener("click", (e) => { + const breadcrumbLink = e.target.closest(".breadcrumb-link"); + if (breadcrumbLink) { + const category1 = breadcrumbLink.getAttribute("data-category1"); + const category2 = breadcrumbLink.getAttribute("data-category2"); + const router = window.__router__; + if (router) { + let url = "/"; + const params = new URLSearchParams(); + if (category2) { + const productCategory1 = product.category1; + if (productCategory1) { + params.set("category1", productCategory1); + } + params.set("category2", category2); + } else if (category1) { + params.set("category1", category1); + } + if (params.toString()) { + url += `?${params.toString()}`; + } + router.navigate(url); + } + } + }); +} + +export const productDetailLoadingLayout = ` +
+
+
+
+
+ +

상품 상세

+
+
+ + +
+
+
+
+
+
+
+
+

상품 정보를 불러오는 중...

+
+
+
+ +
+ `; + +export const productDetailLoadedLayout = ` +
+
+
+
+
+ +

상품 상세

+
+
+ + +
+
+
+
+
+ + + +
+ +
+
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+

+

PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장

+ +
+
+ + + + + + + + + + + + + + + +
+ 4.0 (749개 리뷰) +
+ +
+ 220원 +
+ +
+ 재고 107개 +
+ +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다. +
+
+
+ +
+
+ 수량 +
+ + + +
+
+ + +
+
+ +
+ +
+ +
+
+

관련 상품

+

같은 카테고리의 다른 상품들

+
+
+
+ + +
+
+
+
+ +
+ `; diff --git a/src/pages/productList.js b/src/pages/productList.js new file mode 100644 index 00000000..c02ee480 --- /dev/null +++ b/src/pages/productList.js @@ -0,0 +1,1085 @@ +import { getProducts, getCategories, getProduct } from "../api/productApi.js"; +import { addToCart, updateCartIcon } from "../app/services/cartService.js"; +import { showToast } from "../components/toast.js"; +import { openCartModal } from "./cart.js"; + +function getQueryParams() { + const params = new URLSearchParams(window.location.search); + return { + limit: parseInt(params.get("limit")) || 20, + search: params.get("search") || "", + category1: params.get("category1") || "", + category2: params.get("category2") || "", + sort: params.get("sort") || "price_asc", + }; +} + +function createProductCard(product) { + return ` +
+ +
+ ${product.title} +
+ +
+
+

+ ${product.title} +

+ ${product.brand ? `

${product.brand}

` : ""} +

+ ${parseInt(product.lprice).toLocaleString()}원 +

+
+ + +
+
+ `; +} + +// function createCategory1Button(category) { +// return ` +// +// `; +// } + +function createCategory2Button(category1, category2, isActive = false) { + const activeClass = isActive + ? "bg-blue-100 border-blue-300 text-blue-800" + : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"; + return ` + + `; +} + +export const productListLoadingLayout = ` +
+
+
+
+

+ 쇼핑몰 +

+
+ + +
+
+
+
+
+ +
+ +
+
+ +
+ + + +
+
+
+ +
+ +
+
+ + +
+ +
+
카테고리 로딩 중...
+
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + + 상품을 불러오는 중... +
+
+
+
+
+ +
+ `; + +function updateURLParams(params) { + const searchParams = new URLSearchParams(); + if (params.limit && params.limit !== 20) { + searchParams.set("limit", params.limit.toString()); + } + if (params.search) { + searchParams.set("search", params.search); + } + if (params.category1) { + searchParams.set("category1", params.category1); + } + if (params.category2) { + searchParams.set("category2", params.category2); + } + if (params.sort && params.sort !== "price_asc") { + searchParams.set("sort", params.sort); + } + + const newURL = searchParams.toString() + ? `${window.location.pathname}?${searchParams.toString()}` + : window.location.pathname; + window.history.pushState({}, "", newURL); +} + +function updateProductList(params) { + updateURLParams(params); + renderProductList(params, true); +} + +let infiniteScrollState = { + currentPage: 1, + isLoading: false, + hasMore: false, + params: null, +}; + +function createSkeletonCards(count = 4) { + return Array(count) + .fill(0) + .map( + () => ` +
+
+
+
+
+
+
+
+
+ `, + ) + .join(""); +} + +function createLoadingIndicator() { + return ` +
+
+ + + + + 상품을 불러오는 중... +
+
+ `; +} + +async function loadNextPage() { + if (infiniteScrollState.isLoading || !infiniteScrollState.hasMore || !infiniteScrollState.params) { + return; + } + + const { params, currentPage } = infiniteScrollState; + const nextPage = currentPage + 1; + + infiniteScrollState.isLoading = true; + + const productsGrid = document.getElementById("products-grid"); + if (!productsGrid) { + infiniteScrollState.isLoading = false; + return; + } + + const existingMessage = productsGrid.parentElement.querySelector(".text-center.py-4.text-sm.text-gray-500"); + if (existingMessage) { + existingMessage.remove(); + } + + const loadingIndicator = document.createElement("div"); + loadingIndicator.innerHTML = createLoadingIndicator(); + productsGrid.parentElement.appendChild(loadingIndicator); + + const skeletonContainer = document.createElement("div"); + skeletonContainer.className = "grid grid-cols-2 gap-4 mb-6"; + skeletonContainer.innerHTML = createSkeletonCards(4); + productsGrid.parentElement.appendChild(skeletonContainer); + + try { + const productsResponse = await getProducts({ + ...params, + page: nextPage, + }); + + const { products = [], pagination = {} } = productsResponse; + + skeletonContainer.remove(); + loadingIndicator.remove(); + + if (products.length > 0) { + productsGrid.innerHTML += products.map((product) => createProductCard(product)).join(""); + infiniteScrollState.currentPage = nextPage; + infiniteScrollState.hasMore = pagination.hasNext || false; + + if (!infiniteScrollState.hasMore) { + const endMessage = document.createElement("div"); + endMessage.className = "text-center py-4 text-sm text-gray-500"; + endMessage.textContent = "모든 상품을 확인했습니다"; + productsGrid.parentElement.appendChild(endMessage); + } + } else { + infiniteScrollState.hasMore = false; + const endMessage = document.createElement("div"); + endMessage.className = "text-center py-4 text-sm text-gray-500"; + endMessage.textContent = "모든 상품을 확인했습니다"; + productsGrid.parentElement.appendChild(endMessage); + } + } catch (error) { + console.error("다음 페이지 로드 실패:", error); + skeletonContainer.remove(); + loadingIndicator.remove(); + } finally { + infiniteScrollState.isLoading = false; + } +} + +function handleScroll() { + const scrollHeight = document.documentElement.scrollHeight; + const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; + const clientHeight = document.documentElement.clientHeight; + + if (scrollHeight - scrollTop - clientHeight < 100) { + loadNextPage(); + } +} + +function removeScrollListener() { + window.removeEventListener("scroll", handleScroll); +} + +function addScrollListener() { + window.addEventListener("scroll", handleScroll, { passive: true }); +} + +let cartIconClickHandlerSetup = false; +function setupCartIconClick() { + if (cartIconClickHandlerSetup) { + return; + } + cartIconClickHandlerSetup = true; + + document.addEventListener("click", (e) => { + const cartIconBtn = e.target.closest("#cart-icon-btn"); + if (cartIconBtn) { + e.preventDefault(); + openCartModal(); + } + }); +} + +let cartButtonHandlerSetup = false; +function setupCartButtons() { + if (cartButtonHandlerSetup) { + return; + } + cartButtonHandlerSetup = true; + + document.addEventListener("click", async (e) => { + const addToCartBtn = e.target.closest(".add-to-cart-btn"); + if (addToCartBtn) { + e.preventDefault(); + e.stopPropagation(); + const productId = addToCartBtn.getAttribute("data-product-id"); + if (productId) { + try { + const product = await getProduct(productId); + + addToCart(productId, { + title: product.title, + image: product.image, + lprice: product.lprice, + brand: product.brand, + }); + + showToast("success", "장바구니에 추가되었습니다"); + } catch (error) { + console.error("장바구니 추가 실패:", error); + showToast("error", "상품을 장바구니에 추가하는 중 오류가 발생했습니다."); + } + } + } + }); +} + +let categoryFilterHandlerSetup = false; +function setupCategoryFilters() { + if (categoryFilterHandlerSetup) { + return; + } + categoryFilterHandlerSetup = true; + + document.addEventListener("click", (e) => { + const category1Btn = e.target.closest(".category1-filter-btn"); + if (category1Btn) { + e.preventDefault(); + const category1 = category1Btn.getAttribute("data-category1"); + if (category1) { + updateProductList({ limit: 20, search: "", category1, category2: "", sort: "price_asc" }); + } + return; + } + + const category2Btn = e.target.closest(".category2-filter-btn"); + if (category2Btn) { + e.preventDefault(); + const category1 = category2Btn.getAttribute("data-category1"); + const category2 = category2Btn.getAttribute("data-category2"); + if (category1 && category2) { + updateProductList({ limit: 20, search: "", category1, category2, sort: "price_asc" }); + } + return; + } + + const breadcrumbCategory1Btn = e.target.closest("[data-breadcrumb='category1']"); + if (breadcrumbCategory1Btn) { + e.preventDefault(); + const category1 = breadcrumbCategory1Btn.getAttribute("data-category1"); + if (category1) { + const currentParams = getQueryParams(); + updateProductList({ + limit: currentParams.limit || 20, + search: currentParams.search || "", + category1, + category2: "", + sort: currentParams.sort || "price_asc", + }); + } + return; + } + + const resetBtn = e.target.closest("[data-breadcrumb='reset']"); + if (resetBtn) { + e.preventDefault(); + updateProductList({ limit: 20, search: "", category1: "", category2: "", sort: "price_asc" }); + return; + } + }); +} + +function setupBreadcrumb(category1, category2) { + const categoryFilterSection = document.querySelector(".space-y-2"); + if (!categoryFilterSection) return; + + const labelContainer = categoryFilterSection.querySelector(".flex.items-center.gap-2"); + if (!labelContainer) return; + + const resetButton = labelContainer.querySelector("[data-breadcrumb='reset']"); + + if (category1 || category2) { + let breadcrumbHTML = ``; + + if (category1) { + breadcrumbHTML += `>`; + breadcrumbHTML += ``; + } + + if (category2) { + breadcrumbHTML += `>`; + breadcrumbHTML += `${category2}`; + } + + if (resetButton) { + resetButton.outerHTML = breadcrumbHTML; + } else { + labelContainer.insertAdjacentHTML("beforeend", breadcrumbHTML); + } + } else { + if (resetButton) { + return; + } else { + labelContainer.insertAdjacentHTML( + "beforeend", + ``, + ); + } + } +} + +let productClickHandlerSetup = false; +function setupProductClickHandlers() { + if (productClickHandlerSetup) { + return; + } + productClickHandlerSetup = true; + + document.addEventListener("click", (e) => { + if (e.target.closest(".add-to-cart-btn")) { + return; + } + + const productImage = e.target.closest(".product-image"); + const productInfo = e.target.closest(".product-info"); + const productCard = e.target.closest(".product-card"); + + if (productImage || productInfo || productCard) { + const card = productCard || e.target.closest("[data-product-id]"); + if (card) { + const productId = card.getAttribute("data-product-id"); + if (productId) { + const router = window.__router__; + if (router) { + router.navigate(`/product/${productId}`); + } else { + window.location.href = `/product/${productId}`; + } + } + } + } + }); +} + +export async function renderProductList(params = {}, isInitialLoad = true) { + const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params; + + if (isInitialLoad) { + removeScrollListener(); + + const root = document.getElementById("root") || document.body; + root.innerHTML = productListLoadingLayout; + + infiniteScrollState = { + currentPage: 1, + isLoading: false, + hasMore: false, + params: { limit, search, category1, category2, sort }, + }; + } + + try { + const [productsResponse, categoriesResponse] = await Promise.all([ + getProducts({ + limit, + search, + category1, + category2, + sort, + page: isInitialLoad ? 1 : infiniteScrollState.currentPage, + }), + getCategories(), + ]); + + const { products = [], pagination = {} } = productsResponse; + const totalCount = pagination.total || 0; + const categories = categoriesResponse || {}; + + infiniteScrollState.hasMore = pagination.hasNext || false; + if (isInitialLoad) { + infiniteScrollState.currentPage = 1; + } + + if (isInitialLoad) { + const category1Container = document.querySelector(".space-y-2 .flex.flex-wrap.gap-2"); + if (category1Container) { + const category1List = Object.keys(categories); + if (category1List.length > 0) { + category1Container.innerHTML = category1List + .map((cat1) => { + const isActive = cat1 === category1; + const activeClass = isActive + ? "bg-blue-100 border-blue-300 text-blue-800" + : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"; + return ` + + `; + }) + .join(""); + } + } + + if (category1 && categories[category1]) { + const category2Container = category1Container?.nextElementSibling; + if ( + category2Container && + category2Container.classList.contains("flex") && + category2Container.classList.contains("flex-wrap") + ) { + const category2List = Object.keys(categories[category1]); + if (category2List.length > 0) { + category2Container.innerHTML = category2List + .map((cat2) => createCategory2Button(category1, cat2, cat2 === category2)) + .join(""); + } + } else { + const category2Container = document.createElement("div"); + category2Container.className = "flex flex-wrap gap-2 mt-2"; + const category2List = Object.keys(categories[category1]); + if (category2List.length > 0) { + category2Container.innerHTML = category2List + .map((cat2) => createCategory2Button(category1, cat2, cat2 === category2)) + .join(""); + if (category1Container && category1Container.parentElement) { + category1Container.parentElement.insertBefore(category2Container, category1Container.nextSibling); + } + } + } + } else { + const category2Container = category1Container?.nextElementSibling; + if ( + category2Container && + category2Container.classList.contains("flex") && + category2Container.classList.contains("flex-wrap") + ) { + category2Container.remove(); + } + } + } + + const productsGrid = document.getElementById("products-grid"); + if (productsGrid) { + if (products.length > 0) { + if (isInitialLoad) { + productsGrid.innerHTML = products.map((product) => createProductCard(product)).join(""); + } else { + productsGrid.innerHTML += products.map((product) => createProductCard(product)).join(""); + } + + const productListContainer = productsGrid.closest(".mb-6 > div"); + if (productListContainer) { + let countInfo = productListContainer.querySelector(".mb-4.text-sm.text-gray-600"); + if (!countInfo) { + countInfo = document.createElement("div"); + countInfo.className = "mb-4 text-sm text-gray-600"; + productListContainer.insertBefore(countInfo, productsGrid); + } + countInfo.innerHTML = `총 ${totalCount}개의 상품`; + + if (isInitialLoad) { + const loadingIndicator = productListContainer.querySelector(".text-center.py-4"); + if (loadingIndicator) { + loadingIndicator.remove(); + } + + addScrollListener(); + } + } + } else { + if (isInitialLoad) { + productsGrid.innerHTML = ` +
+ 상품이 없습니다. +
+ `; + } + } + } + + const searchInput = document.getElementById("search-input"); + if (searchInput) { + searchInput.value = search; + + searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const searchTerm = searchInput.value.trim(); + updateProductList({ ...params, search: searchTerm }); + } + }); + } + + const limitSelect = document.getElementById("limit-select"); + if (limitSelect) { + limitSelect.value = limit.toString(); + limitSelect.addEventListener("change", (e) => { + const newLimit = parseInt(e.target.value); + updateProductList({ ...params, limit: newLimit }); + }); + } + + const sortSelect = document.getElementById("sort-select"); + if (sortSelect) { + sortSelect.value = sort; + sortSelect.addEventListener("change", (e) => { + const newSort = e.target.value; + updateProductList({ ...params, sort: newSort }); + }); + } + + setupCategoryFilters(); + + setupBreadcrumb(category1, category2); + + setupCartButtons(); + + setupCartIconClick(); + + setupProductClickHandlers(); + + updateCartIcon(); + } catch (error) { + console.error("상품 목록 로드 실패:", error); + const productsGrid = document.getElementById("products-grid"); + if (productsGrid) { + productsGrid.innerHTML = ` +
+

상품을 불러오는 중 오류가 발생했습니다.

+ +
+ `; + + const retryBtn = document.getElementById("retry-btn"); + if (retryBtn) { + retryBtn.addEventListener("click", () => { + renderProductList(params); + }); + } + } + } +} + +export const productListLoadedLayout = ` +
+
+
+
+

+ 쇼핑몰 +

+
+ + +
+
+
+
+
+ +
+ +
+
+ +
+ + + +
+
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+ 총 340개의 상품 +
+ +
+
+ +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+
+

+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +

+

+

+ 220원 +

+
+ + +
+
+
+ +
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +
+ +
+
+

+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +

+

이지웨이건축자재

+

+ 230원 +

+
+ + +
+
+
+ +
+ 모든 상품을 확인했습니다 +
+
+
+
+ +
+ `; + +export const productListCategoryDepth1Layout = ` +
+ +
+ +
+
+ +
+ + + +
+
+
+ + +
+ + +
+
+ + > +
+
+
+ + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+ `; + +export const productListCategoryDepth2Layout = ` +
+ +
+ +
+
+ +
+ + + +
+
+
+ + +
+ + +
+
+ + >>주방용품 +
+
+
+ + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+ `; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..58fb6f69 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; + +// GitHub Pages 배포를 위한 base 경로 설정 +// 저장소 이름: front_7th_chapter2-1 +export default defineConfig({ + base: process.env.NODE_ENV === "production" ? "/front_7th_chapter2-1/" : "/", + build: { + outDir: "dist", + }, +});