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호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
-
-
-
- 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호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
-
-
- 220원
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
- 230원
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 총 금액
- 670원
-
-
-
-
-
-
-
-
-
-
-
- `;
+ window.__router__ = router;
- const 장바구니_선택있음 = `
-
-
-
-
-
-
- 장바구니
- (2)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

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

-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
- 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호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
-
-
-
-
-
-
-
-
-
-
4.0 (749개 리뷰)
-
-
-
- 220원
-
-
-
- 재고 107개
-
-
-
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
관련 상품
-
같은 카테고리의 다른 상품들
-
-
-
-
-
-
- `;
+ router.register("/404", () => {
+ const root = document.getElementById("root") || document.body;
+ root.innerHTML = notFoundLayout;
+ });
- const _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 || ""}
+
+
+ ${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("")}
+
+
+
+
+
+
+
+ 선택한 상품 (0개)
+ 0원
+
+
+
+ 총 금액
+ ${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호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
+
+
+ 220원
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
+
+
+ 230원
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 총 금액
+ 670원
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+export const cartWithSelectionLayout = `
+
+
+
+
+
+
+ 장바구니
+ (2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

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

+
+
+
+
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
+
+
+ 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 = `
+
+
+
+
+
홈으로
+
+
+ `;
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.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호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
+
+
+
+
+
+
+
+
+
+
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.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호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
+
+
+
+ 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",
+ },
+});