diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 15a3a274..185416e5 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -32,7 +32,11 @@
**상품 정렬 기능**
+<<<<<<< HEAD
- [ ] 상품을 가격순/이름순으로 오름차순/내림차순 정렬을 할 수 있다.
+=======
+- [ ] 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.
+>>>>>>> caecb93 (feat: 기본 코드 추가)
- [ ] 드롭다운을 통해 정렬 기준을 선택할 수 있다
- [ ] 정렬 변경 시 즉시 목록에 반영된다
@@ -52,6 +56,10 @@
**상품 검색**
- [ ] 상품명 기반 검색을 위한 텍스트 입력 필드가 있다
+<<<<<<< HEAD
+=======
+- [ ] 검색 버튼 클릭으로 검색이 수행된다
+>>>>>>> caecb93 (feat: 기본 코드 추가)
- [ ] Enter 키로 검색이 수행된다
- [ ] 검색어와 일치하는 상품들만 목록에 표시된다
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..85e51b69
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,66 @@
+name: Deploy to GitHub Pages
+
+on:
+ push: # push trigger
+ branches:
+ - main
+ - feature-* # Feature 브랜치도 배포
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build
+ run: pnpm run build
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: "./dist"
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+
+ - name: Output deployment URL
+ run: |
+ REPO_NAME=$(echo ${{ github.repository }} | cut -d'/' -f2)
+ OWNER=$(echo ${{ github.repository }} | cut -d'/' -f1)
+
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "🚀 GitHub Pages Deployment Complete"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "✅ Status: Success"
+ echo "📄 Live URL: https://${OWNER}.github.io/${REPO_NAME}/"
+ echo "📌 Repository: ${{ github.repository }}"
+ echo "📝 Branch: ${{ github.ref_name }}"
+ echo "🔗 Commit: ${{ github.sha }}"
+ echo "👤 Author: ${{ github.actor }}"
+ echo "⏰ Time: $(date)"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
diff --git a/.prettierrc b/.prettierrc
index 1d2699e4..6b3f49f3 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -2,5 +2,6 @@
"tabWidth": 2,
"semi": true,
"singleQuote": false,
- "printWidth": 120
+ "printWidth": 120,
+ "trailingComma": "all"
}
diff --git a/package.json b/package.json
index 5ec7f3f3..766ff157 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"private": true,
"version": "0.0.0",
"type": "module",
+ "packageManager": "pnpm@9.0.0",
"scripts": {
"dev": "vite",
"dev:hash": "vite --open ./index.hash.html",
@@ -40,7 +41,6 @@
"jsdom": "^25.0.1",
"lint-staged": "^15.2.11",
"msw": "^2.10.2",
- "prettier": "^3.4.2",
"vite": "npm:rolldown-vite@latest",
"vitest": "latest"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8137d4c8..6cfec257 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,9 +53,6 @@ importers:
msw:
specifier: ^2.10.2
version: 2.10.2
- prettier:
- specifier: ^3.4.2
- version: 3.5.3
vite:
specifier: npm:rolldown-vite@latest
version: rolldown-vite@6.3.21(esbuild@0.25.1)(yaml@2.7.0)
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
index d2b72964..b1f186b6 100644
--- a/public/mockServiceWorker.js
+++ b/public/mockServiceWorker.js
@@ -100,10 +100,7 @@ addEventListener("fetch", function (event) {
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
- if (
- event.request.cache === "only-if-cached" &&
- event.request.mode !== "same-origin"
- ) {
+ if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
return;
}
@@ -219,9 +216,7 @@ async function getResponse(event, client, requestId) {
const acceptHeader = headers.get("accept");
if (acceptHeader) {
const values = acceptHeader.split(",").map((value) => value.trim());
- const filteredValues = values.filter(
- (value) => value !== "msw/passthrough",
- );
+ const filteredValues = values.filter((value) => value !== "msw/passthrough");
if (filteredValues.length > 0) {
headers.set("accept", filteredValues.join(", "));
@@ -291,10 +286,7 @@ function sendToClient(client, message, transferrables = []) {
resolve(event.data);
};
- client.postMessage(message, [
- channel.port2,
- ...transferrables.filter(Boolean),
- ]);
+ client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
});
}
diff --git a/requirement.md b/requirement.md
index d450ef17..61855590 100644
--- a/requirement.md
+++ b/requirement.md
@@ -20,7 +20,11 @@
### 상품 정렬 기능
+<<<<<<< HEAD
- 상품을 가격순/이름순으로 오름차순/내림차순 정렬을 할 수 있다.
+=======
+- 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.
+>>>>>>> caecb93 (feat: 기본 코드 추가)
- 드롭다운을 통해 정렬 기준을 선택할 수 있다
- 정렬 변경 시 즉시 목록에 반영된다
diff --git a/src/components/Cart.js b/src/components/Cart.js
new file mode 100644
index 00000000..55b2bdee
--- /dev/null
+++ b/src/components/Cart.js
@@ -0,0 +1,241 @@
+import { getCartItemsFromLocalStorage, getCartSelectAllStatus } from "../utils/storage";
+
+export const Cart = () => {
+ const cartItems = getCartItemsFromLocalStorage();
+ const selectAllStatus = getCartSelectAllStatus();
+ if (cartItems.length > 0) {
+ return /* HTML */ `
+
+
+
+
+
+
+
+
+
+
+
+
+ 장바구니
+ (${cartItems.length})
+
+
+
+
+
+
+
+
+
+
+
+
+ ${cartItems
+ .map((item) => {
+ return /* HTML */ `
+
+
+
+
+
+

+
+
+
+
+ ${item.title}
+
+
${item.price}원
+
+
+
+
+
+
+ ${Number(item.price * item.quantity).toLocaleString()}원
+
+
+
+
+ `;
+ })
+ .join("")}
+
+
+
+
+
+
+
+ 총 금액
+ 670원
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ } else {
+ return /* HTML */ `
+
+
+
+
+
+
+
+
+
+
+ 장바구니
+
+
+
+
+
+
+
+
+
+
장바구니가 비어있습니다
+
원하는 상품을 담아보세요!
+
+
+
+
+
+
+ `;
+ }
+};
diff --git a/src/components/Footer.js b/src/components/Footer.js
new file mode 100644
index 00000000..70ad6b23
--- /dev/null
+++ b/src/components/Footer.js
@@ -0,0 +1,9 @@
+export const Footer = () => {
+ return /* HTML */ `
+
+ `;
+};
diff --git a/src/components/Header.js b/src/components/Header.js
new file mode 100644
index 00000000..8126fe4d
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,67 @@
+import { getCartCountFromLocalStorage } from "../utils/storage";
+
+export const renderCartBadge = () => {
+ const button = document.querySelector("#cart-icon-btn");
+ if (!button) return;
+
+ const count = getCartCountFromLocalStorage();
+
+ let badge = button.querySelector(".cart-count-badge");
+
+ if (!badge) {
+ badge = document.createElement("span");
+ badge.className =
+ "cart-count-badge absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center";
+ button.appendChild(badge);
+ }
+
+ badge.textContent = count;
+ badge.style.display = count > 0 ? "flex" : "none";
+};
+
+export const Header = ({ title = "쇼핑몰" }) => {
+ return /* HTML */ `
+
+
+
+ ${title === "쇼핑몰"
+ ? /* HTML */ `
+
+ `
+ : ""}
+ ${title === "상품 상세"
+ ? /* HTML */ `
+
+ `
+ : ""}
+
+
+
+
+
+ `;
+};
diff --git a/src/components/ProductItem.js b/src/components/ProductItem.js
new file mode 100644
index 00000000..a0a665d9
--- /dev/null
+++ b/src/components/ProductItem.js
@@ -0,0 +1,34 @@
+export const ProductItem = ({ title, image, lprice, productId, brand }) => {
+ return /* HTML */ `
+
+
+
+

+
+
+
+
+
${title}
+
${brand || ""}
+
${Number(lprice).toLocaleString()}원
+
+
+
+
+
+ `;
+};
diff --git a/src/components/ProductList.js b/src/components/ProductList.js
new file mode 100644
index 00000000..b28845b4
--- /dev/null
+++ b/src/components/ProductList.js
@@ -0,0 +1,60 @@
+import { ProductItem } from "./ProductItem";
+
+const Skeleton = /* HTML */ `
+
+`;
+
+const Loading = /* HTML */ `
+
+
+
+
상품을 불러오는 중...
+
+
+`;
+
+export const ProductList = ({ isLoading, pagination, products }) => {
+ return /* HTML */ `
+
+
+
+ ${isLoading
+ ? /* HTML */ `
+
+
+
+ ${Skeleton.repeat(4)}
+
+ ${Loading}
+ `
+ : /* HTML */ `
+
+
+
+ 총 ${pagination.total}개의 상품
+
+
+
${products.map(ProductItem).join("")}
+
+
모든 상품을 확인했습니다
+
+ `}
+
+
+ `;
+};
diff --git a/src/components/SearchForm.js b/src/components/SearchForm.js
new file mode 100644
index 00000000..29dc13ae
--- /dev/null
+++ b/src/components/SearchForm.js
@@ -0,0 +1,159 @@
+import { filters } from "../store/filters";
+
+const CategoryButton = (category) => {
+ return /*HTML*/ `
+
+`;
+};
+
+export const Category2Button = (category1, category2, isSelected = false) => {
+ return /* HTML */ `
+
+ `;
+};
+
+export const CategoryBreadcrumb = (category1, category2) => {
+ return /* HTML */ `
+ ${category2
+ ? /* HTML */ `
+ >
+ ${category2}
+ `
+ : /* HTML */ `
+ >
+ `}
+ `;
+};
+
+const getSortLabel = (sort) => {
+ switch (sort) {
+ case "price_asc":
+ return "가격 낮은순";
+ case "price_desc":
+ return "가격 높은순";
+ case "name_asc":
+ return "이름순";
+ case "name_desc":
+ return "이름 역순";
+ default:
+ return "가격 낮은순";
+ }
+};
+
+export const SearchForm = ({ categories, isLoading }) => {
+ const filtersState = filters.getState();
+
+ return /* HTML */ `
+
+
+
+
+
+
+
+
+
+
+
+ ${filtersState.category1 ? CategoryBreadcrumb(filtersState.category1) : ""}
+ ${filtersState.category2 ? CategoryBreadcrumb(filtersState.category1, filtersState.category2) : ""}
+
+
+ ${(isLoading &&
+ /* HTML */ `
`) ||
+ ""}
+
+ ${(!isLoading &&
+ !filtersState.category1 &&
+ /* HTML */ `
+ ${Object.keys(categories).map(CategoryButton).join("")}
+
`) ||
+ ""}
+
+ ${(!isLoading &&
+ filtersState.category1 &&
+ /* HTML */ `
+ ${Object.keys(categories[filtersState.category1])
+ .map((category2) => Category2Button(filtersState.category1, category2))
+ .join("")}
+
`) ||
+ ""}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+};
diff --git a/src/components/Toast.js b/src/components/Toast.js
new file mode 100644
index 00000000..37c06cfd
--- /dev/null
+++ b/src/components/Toast.js
@@ -0,0 +1,35 @@
+export const AddToCartToast = () => {
+ return /* HTML */ `
+
+
+
장바구니에 추가되었습니다
+
+
+ `;
+};
+
+// 토스트 보여주고, 3초 후 사라지게
+export const showToast = (children) => {
+ const $root = document.getElementById("root");
+ const $toast = document.createElement("div");
+ $toast.className = "fixed bottom-0 left-0 right-0 p-4 flex justify-center";
+ $toast.innerHTML = children;
+ $root.appendChild($toast);
+
+ const $toastCloseBtn = $toast.querySelector("#toast-close-btn");
+ $toastCloseBtn.addEventListener("click", () => {
+ $toast.remove();
+ });
+
+ setTimeout(() => {
+ $toast.remove();
+ }, 3000);
+};
diff --git a/src/components/index.js b/src/components/index.js
new file mode 100644
index 00000000..431721c6
--- /dev/null
+++ b/src/components/index.js
@@ -0,0 +1,6 @@
+export * from "./Header";
+export * from "./Footer";
+export * from "./SearchForm";
+export * from "./ProductList";
+export * from "./Cart";
+export * from "./Toast";
diff --git a/src/main.js b/src/main.js
index 4b055b89..ef16030c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,1147 +1,212 @@
+import { getCategories, getProduct, getProducts } from "./api/productApi.js";
+import { Cart } from "./components/Cart.js";
+import { renderCartBadge } from "./components/Header.js";
+import { AddToCartToast, showToast } from "./components/Toast.js";
+import { DetailPage } from "./pages/DetailPage.js";
+import { HomePage } from "./pages/HomePage.js";
+import { filters } from "./store/filters.js";
+import {
+ addCartItemToLocalStorage,
+ getCartItemsFromLocalStorage,
+ updateCartItemSelectedStatus,
+ updateCartSelectAllStatus,
+} from "./utils/storage.js";
+
+let categories = [];
+
const enableMocking = () =>
import("./mocks/browser.js").then(({ worker }) =>
worker.start({
+ serviceWorker: {
+ url: `${import.meta.env.BASE_URL}mockServiceWorker.js`,
+ },
onUnhandledRequest: "bypass",
}),
);
-function main() {
- const 상품목록_레이아웃_로딩 = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
상품을 불러오는 중...
-
-
-
-
-
-
-
- `;
+const init = async () => {
+ const $root = document.getElementById("root");
+ $root.innerHTML = HomePage({ isLoading: true });
+ const data = await getProducts();
+
+ // 카테고리 담기
+ const newCategories = await getCategories();
+ categories = newCategories;
+
+ $root.innerHTML = HomePage({ ...data, categories, isLoading: false });
+
+ filters.subscribe(render);
+ eventHandlers();
+ renderCartBadge();
+ renderCart();
+};
+
+// Cart를 렌더링하는 함수
+const renderCart = () => {
+ const cartContainer = document.querySelector(".cart-modal-container");
+ if (!cartContainer) return;
+
+ // 현재 모달이 보이는 상태인지 확인
+ const cartModal = cartContainer.querySelector(".cart-modal");
+ const isVisible = cartModal && !cartModal.classList.contains("hidden");
+
+ // Cart 다시 렌더링
+ cartContainer.innerHTML = Cart();
+
+ // 이전 상태 유지
+ if (isVisible) {
+ cartContainer.querySelector(".cart-modal").classList.remove("hidden");
+ }
+};
+
+const render = async () => {
+ const basePath = import.meta.env.BASE_URL; // vite 제공
+ const pathName = window.location.pathname;
+ const relativePath = pathName.replace(basePath, "/").replace(/\/$/, "") || "/";
+
+ const $root = document.getElementById("root");
+
+ if (relativePath === "/") {
+ const data = await getProducts(filters.getState());
+ $root.innerHTML = HomePage({ ...data, categories, isLoading: false });
+ } else {
+ const productId = window.location.pathname.split("/").pop();
+ $root.innerHTML = DetailPage({ isLoading: true });
+ const data = await getProduct(productId);
+ $root.innerHTML = DetailPage({ ...data, isLoading: false });
+ }
+
+ eventHandlers();
+ renderCartBadge();
+ renderCart();
+
+ window.addEventListener("popstate", render);
+};
+
+const eventHandlers = () => {
+ document.addEventListener("click", async (event) => {
+ // 상품 카드 클릭 이벤트 핸들러 -> 상세 페이지로 이동
+ // if (document.querySelector(".product-card") && !event.target.closest(".add-to-cart-btn")) {
+ // event.stopPropagation();
+ // const productId = event.target.closest(".product-card").dataset.productId;
+ // history.pushState("", "", `/product/${productId}`);
+ // render();
+ // }
+
+ // breadcrumb 전체 버튼 클릭 시 필터 초기화
+ if (event.target.closest("button[data-breadcrumb='reset']")) {
+ filters.setState({ category1: "", category2: "" });
+ }
+
+ // breadcrumb 카테고리 1 버튼 클릭 시 필터 적용
+ if (event.target.closest("button[data-breadcrumb='category1']")) {
+ const category1 = event.target.closest("button[data-breadcrumb='category1']").dataset.category1;
+ filters.setState({ category1, category2: "" });
+ }
+
+ // 카테고리 1 필터 버튼 클릭 이벤트 핸들러
+ if (event.target.closest(".category1-filter-btn")) {
+ const category1 = event.target.dataset.category1;
+
+ filters.setState({ category1 });
+ }
+ // 카테고리 2 버튼 클릭 이벤트 핸들러
+ if (event.target.closest(".category2-filter-btn")) {
+ const category1 = event.target.closest(".category2-filter-btn").dataset.category1;
+ const category2 = event.target.closest(".category2-filter-btn").dataset.category2;
- const 상품목록_레이아웃_로딩완료 = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 총 340개의 상품
-
-
-
-
-
-
-

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

-
-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
이지웨이건축자재
-
- 230원
-
-
-
-
-
-
-
-
-
- 모든 상품을 확인했습니다
-
-
-
-
-
-
- `;
+ filters.setState({ category1, category2 });
+ }
- const 상품목록_레이아웃_카테고리_1Depth = `
-
-
-
-
-
-
-
-
+ // 장바구니 버튼 클릭 이벤트 핸들러
+ if (event.target.closest("#cart-icon-btn")) {
+ const cartModal = document.querySelector(".cart-modal");
+ if (!cartModal) {
+ console.error("❌ .cart-modal을 찾을 수 없습니다!");
+ return;
+ }
+ cartModal.classList.remove("hidden");
+ }
-
-
-
-
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ // 장바구니 모달 닫기 버튼 클릭 이벤트 핸들러
+ if (event.target.closest("#cart-modal-close-btn")) {
+ const cartModal = document.querySelector(".cart-modal");
+ cartModal.classList.add("hidden");
+ }
- const 상품목록_레이아웃_카테고리_2Depth = `
-
-
-
-
-
-
-
-
+ // 장바구니 모달 배경 클릭 시 닫기
+ if (event.target.closest(".cart-modal-overlay")) {
+ const cartModal = document.querySelector(".cart-modal");
+ cartModal.classList.add("hidden");
+ }
-
-
-
-
- >>주방용품
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ // 아이템 중 장바구니 담기 눌렀을 때 로컬 스토리지에 추가
+ if (event.target.closest(".add-to-cart-btn")) {
+ event.preventDefault();
+ event.stopPropagation();
+ const productId = event.target.closest(".add-to-cart-btn").dataset.productId;
+ const product = await getProduct(productId);
+ addCartItemToLocalStorage(productId, product);
+ renderCart();
+ renderCartBadge();
+ showToast(AddToCartToast());
+ }
- const 토스트 = `
-
-
-
-
장바구니에 추가되었습니다
-
-
-
-
-
-
선택된 상품들이 삭제되었습니다
-
-
-
-
-
-
오류가 발생했습니다.
-
-
-
- `;
+ // 장바구니 전체선택 체크박스 클릭
+ if (event.target.id === "cart-modal-select-all-checkbox") {
+ event.preventDefault();
+ event.stopPropagation();
- const 장바구니_비어있음 = `
-
-
-
-
-
-
- 장바구니
-
-
-
-
-
-
-
-
-
-
-
-
장바구니가 비어있습니다
-
원하는 상품을 담아보세요!
-
-
-
-
-
- `;
+ const selectAll = event.target.checked;
+ updateCartSelectAllStatus(selectAll);
+ renderCart();
+ }
- const 장바구니_선택없음 = `
-
-
-
-
-
-
- 장바구니
- (2)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

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

-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
- 230원
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 총 금액
- 670원
-
-
-
-
-
-
-
-
-
-
-
- `;
+ // 장바구니 체크박스 클릭
+ if (event.target.classList.contains("cart-item-checkbox")) {
+ event.preventDefault();
+ event.stopPropagation();
- const 장바구니_선택있음 = `
-
-
-
-
-
-
- 장바구니
- (2)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

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

-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
- 230원
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 선택한 상품 (1개)
- 440원
-
-
-
- 총 금액
- 670원
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ const cartItems = getCartItemsFromLocalStorage();
+ const cartItem = cartItems.find((item) => item.id === event.target.dataset.productId);
+ cartItem.selected = !cartItem.selected;
+ updateCartItemSelectedStatus(cartItem.id, cartItem.selected);
+ renderCart();
+ }
+ });
- const 상세페이지_로딩 = `
-
-
-
-
-
-
-
- `;
+ // input 검색 이벤트 핸들러
+ document.addEventListener("keyup", (event) => {
+ const target = document.getElementById("search-input");
+ if (target && event.key === "Enter") {
+ const search = target.value;
- const 상세페이지_로딩완료 = `
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
-
PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
-
-
-
-
-
-
-
-
-
-
4.0 (749개 리뷰)
-
-
-
- 220원
-
-
-
- 재고 107개
-
-
-
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
관련 상품
-
같은 카테고리의 다른 상품들
-
-
-
-
-
-
- `;
+ if (search.length > 0) {
+ filters.setState({ search });
+ } else {
+ filters.setState({ search: "" });
+ }
+ }
+ });
- const _404_ = `
-
-
-
-
-
홈으로
-
-
- `;
+ document.addEventListener("change", () => {
+ // 개수 선택 이벤트 핸들러
+ const limitSelect = document.getElementById("limit-select");
+ if (limitSelect) {
+ const limit = limitSelect.value;
+ filters.setState({ limit });
+ }
+ // 정렬 선택 이벤트 핸들러
+ const sortSelect = document.getElementById("sort-select");
+ if (sortSelect) {
+ const sort = sortSelect.value;
+ filters.setState({ sort });
+ }
+ });
+};
- document.body.innerHTML = `
- ${상품목록_레이아웃_로딩}
-
- ${상품목록_레이아웃_로딩완료}
-
- ${상품목록_레이아웃_카테고리_1Depth}
-
- ${상품목록_레이아웃_카테고리_2Depth}
-
- ${토스트}
-
- ${장바구니_비어있음}
-
- ${장바구니_선택없음}
-
- ${장바구니_선택있음}
-
- ${상세페이지_로딩}
-
- ${상세페이지_로딩완료}
-
- ${_404_}
- `;
+async function main() {
+ init();
}
// 애플리케이션 시작
diff --git a/src/pages/DetailPage.js b/src/pages/DetailPage.js
new file mode 100644
index 00000000..03e1c0d3
--- /dev/null
+++ b/src/pages/DetailPage.js
@@ -0,0 +1,215 @@
+import { PageLayout } from "./PageLayout";
+
+export const DetailPage = ({
+ isLoading,
+ title,
+ image,
+ lprice,
+ mallName,
+ productId,
+ category1,
+ category2,
+ description,
+ rating,
+ reviewCount,
+ stock,
+}) => {
+ // {
+ // "title": "샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이",
+ // "link": "https://smartstore.naver.com/main/products/9396357056",
+ // "image": "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg",
+ // "lprice": "230",
+ // "hprice": "",
+ // "mallName": "EASYWAY",
+ // "productId": "86940857379",
+ // "productType": "2",
+ // "brand": "이지웨이건축자재",
+ // "maker": "",
+ // "category1": "생활/건강",
+ // "category2": "생활용품",
+ // "category3": "생활잡화",
+ // "category4": "문풍지",
+ // "description": "샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이에 대한 상세 설명입니다. 이지웨이건축자재 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.",
+ // "rating": 5,
+ // "reviewCount": 422,
+ // "stock": 92,
+ // "images": [
+ // "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg",
+ // "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1_2.jpg",
+ // "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1_3.jpg"
+ // ],
+ // }
+
+ return PageLayout({
+ title: "상품 상세",
+ children: isLoading
+ ? /* HTML */ ``
+ : /* HTML */ `
+
+
+
+
+
+
+
+
+

+
+
+
+
${mallName}
+
${title}
+
+
+
+
+
+
+
+
+
+
${rating} (${reviewCount}개 리뷰)
+
+
+
+ ${Number(lprice).toLocaleString()}원
+
+
+
재고 ${stock}개
+
+
${description}
+
+
+
+
+
+
+
+
+
+
+
+
+
관련 상품
+
같은 카테고리의 다른 상품들
+
+
+
+
+ `,
+ });
+};
diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js
new file mode 100644
index 00000000..9eb8128d
--- /dev/null
+++ b/src/pages/HomePage.js
@@ -0,0 +1,13 @@
+import { PageLayout } from "./PageLayout";
+import { SearchForm, ProductList } from "../components";
+
+export const HomePage = ({ filters, pagination, products, categories, isLoading = false }) => {
+ return /* HTML */ `
+ ${PageLayout({
+ children: `
+ ${SearchForm({ filters, categories, isLoading })}
+ ${ProductList({ pagination, products, isLoading })}
+ `,
+ })}
+ `;
+};
diff --git a/src/pages/PageLayout.js b/src/pages/PageLayout.js
new file mode 100644
index 00000000..379f796b
--- /dev/null
+++ b/src/pages/PageLayout.js
@@ -0,0 +1,13 @@
+import { Footer } from "../components/Footer";
+import { Header } from "../components/Header";
+
+export const PageLayout = ({ children, title = "쇼핑몰" }) => {
+ return /* HTML */ `
+
+ ${Header({ title })}
+
${children}
+
+ ${Footer()}
+
+ `;
+};
diff --git a/src/setupTests.js b/src/setupTests.js
new file mode 100644
index 00000000..d72b8905
--- /dev/null
+++ b/src/setupTests.js
@@ -0,0 +1,16 @@
+import "@testing-library/jest-dom";
+import { configure } from "@testing-library/dom";
+import { afterAll, beforeAll } from "vitest";
+import { server } from "./__tests__/mockServerHandler.js";
+
+configure({
+ asyncUtilTimeout: 5000,
+});
+
+beforeAll(() => {
+ server.listen({ onUnhandledRequest: "error" });
+});
+
+afterAll(() => {
+ server.close();
+});
diff --git a/src/store/filters.js b/src/store/filters.js
new file mode 100644
index 00000000..6de53e39
--- /dev/null
+++ b/src/store/filters.js
@@ -0,0 +1,9 @@
+import { createStore } from "./store";
+
+export const filters = createStore({
+ limit: "20",
+ search: "",
+ category1: "",
+ category2: "",
+ sort: "price_asc",
+});
diff --git a/src/store/store.js b/src/store/store.js
new file mode 100644
index 00000000..d1e0d462
--- /dev/null
+++ b/src/store/store.js
@@ -0,0 +1,18 @@
+export function createStore(initialState) {
+ let state = initialState;
+ const observers = new Set();
+
+ const getState = () => state;
+
+ const setState = (rest) => {
+ state = { ...state, ...rest };
+ observers.forEach((observer) => observer(state));
+ };
+
+ const subscribe = (render) => {
+ observers.add(render);
+ return () => observers.delete(render);
+ };
+
+ return { getState, setState, subscribe };
+}
diff --git a/src/template.js b/src/template.js
new file mode 100644
index 00000000..214e63b7
--- /dev/null
+++ b/src/template.js
@@ -0,0 +1,1114 @@
+const 상품목록_레이아웃_로딩 = /*HTML*/ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
상품을 불러오는 중...
+
+
+
+
+
+
+
+
+ `;
+
+const 상품목록_레이아웃_로딩완료 = /*HTML*/ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 총 340개의 상품
+
+
+
+
+
+
+

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

+
+
+
+
+
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
+
+
이지웨이건축자재
+
+ 230원
+
+
+
+
+
+
+
+
+
+ 모든 상품을 확인했습니다
+
+
+
+
+
+
+ `;
+
+const 상품목록_레이아웃_카테고리_1Depth = /*HTML*/ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+const 상품목록_레이아웃_카테고리_2Depth = /*HTML*/ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >>주방용품
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+const 토스트 = /*HTML*/ `
+
+
+
+
장바구니에 추가되었습니다
+
+
+
+
+
+
선택된 상품들이 삭제되었습니다
+
+
+
+
+
+
오류가 발생했습니다.
+
+
+
+ `;
+
+const 장바구니_비어있음 = /*HTML*/ `
+
+
+
+
+
+
+ 장바구니
+
+
+
+
+
+
+
+
+
+
+
+
장바구니가 비어있습니다
+
원하는 상품을 담아보세요!
+
+
+
+
+
+ `;
+
+const 장바구니_선택없음 = /*HTML*/ `
+
+
+
+
+
+
+ 장바구니
+ (2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

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

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

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

+
+
+
+
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
+
+
+ 230원
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 선택한 상품 (1개)
+ 440원
+
+
+
+ 총 금액
+ 670원
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+const 상세페이지_로딩 = /*HTML*/ `
+
+
+
+
+
+
+
+ `;
+
+const 상세페이지_로딩완료 = /*HTML*/ `
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
+
+
+
+
+
+
+
+
+
+
4.0 (749개 리뷰)
+
+
+
+ 220원
+
+
+
+ 재고 107개
+
+
+
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
관련 상품
+
같은 카테고리의 다른 상품들
+
+
+
+
+
+
+ `;
+
+const _404_ = /*HTML*/ `
+
+
+
+
+
홈으로
+
+
+ `;
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 00000000..d1d8893e
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,72 @@
+// items라는 배열 안에 장바구니 상품 추가
+export const addCartItemToLocalStorage = (productId, product) => {
+ if (!localStorage.getItem("shopping_cart")) {
+ localStorage.setItem("shopping_cart", JSON.stringify({ selectedAll: false, items: [] }));
+ }
+
+ const productsInStorage = JSON.parse(localStorage.getItem("shopping_cart"));
+ const findProduct = productsInStorage.items.find((item) => item.id === productId);
+
+ if (findProduct) {
+ productsInStorage.items.find((item) => item.id === productId).quantity += 1;
+ localStorage.setItem("shopping_cart", JSON.stringify(productsInStorage));
+ } else {
+ const productInfo = {
+ id: productId,
+ image: product.image,
+ price: Number(product.lprice),
+ quantity: 1,
+ selected: false,
+ title: product.title,
+ };
+ // shopping_cart 객체의 items 배열에 추가
+ productsInStorage.items.push(productInfo);
+ localStorage.setItem("shopping_cart", JSON.stringify(productsInStorage));
+ }
+};
+
+// 장바구니 상품 제거
+export const removeCartItemFromLocalStorage = (productId) => {
+ const productsInStorage = JSON.parse(localStorage.getItem("shopping_cart"));
+ productsInStorage.items = productsInStorage.items.filter((item) => item.id !== productId);
+ localStorage.setItem("shopping_cart", JSON.stringify(productsInStorage));
+};
+
+// 장바구니 조회
+export const getCartItemsFromLocalStorage = () => {
+ return JSON.parse(localStorage.getItem("shopping_cart"))?.items || [];
+};
+
+// 장바구니 카운트
+export const getCartCountFromLocalStorage = () => {
+ return JSON.parse(localStorage.getItem("shopping_cart"))?.items?.length || 0;
+};
+
+// 장바구니 체크박스 상태 업데이트
+export const updateCartItemSelectedStatus = (productId, selected) => {
+ const productsInStorage = JSON.parse(localStorage.getItem("shopping_cart"));
+ const cartItem = productsInStorage.items.find((item) => item.id === productId);
+ cartItem.selected = selected;
+
+ // 모든 아이템이 선택되었으면 selectedAll도 true로, 하나라도 해제되면 false로
+ const allSelected = productsInStorage.items.length > 0 && productsInStorage.items.every((item) => item.selected);
+ productsInStorage.selectedAll = allSelected;
+
+ localStorage.setItem("shopping_cart", JSON.stringify(productsInStorage));
+};
+
+// 전체선택 상태 업데이트
+export const updateCartSelectAllStatus = (selectAll) => {
+ const productsInStorage = JSON.parse(localStorage.getItem("shopping_cart"));
+ productsInStorage.selectedAll = selectAll;
+ // 모든 아이템의 selected 상태를 selectAll 값으로 변경
+ productsInStorage.items.forEach((item) => {
+ item.selected = selectAll;
+ });
+ localStorage.setItem("shopping_cart", JSON.stringify(productsInStorage));
+};
+
+// 전체선택 상태 조회
+export const getCartSelectAllStatus = () => {
+ return JSON.parse(localStorage.getItem("shopping_cart"))?.selectedAll || false;
+};
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 00000000..586cb802
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,19 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ base: "/front_7th_chapter2-1/",
+ build: {
+ outDir: "dist",
+ },
+ test: {
+ globals: true,
+ environment: "jsdom",
+ setupFiles: "./src/setupTests.js",
+ exclude: ["**/e2e/**", "**/*.e2e.spec.js", "**/node_modules/**"],
+ poolOptions: {
+ threads: {
+ singleThread: true,
+ },
+ },
+ },
+});