diff --git a/package.json b/package.json
index 5ec7f3f3..7e6c884b 100644
--- a/package.json
+++ b/package.json
@@ -48,5 +48,8 @@
"workerDirectory": [
"public"
]
+ },
+ "dependencies": {
+ "playwright": "^1.56.1"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8137d4c8..0db03a34 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,6 +7,10 @@ settings:
importers:
.:
+ dependencies:
+ playwright:
+ specifier: ^1.56.1
+ version: 1.56.1
devDependencies:
'@eslint/js':
specifier: ^9.16.0
@@ -1736,11 +1740,21 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ playwright-core@1.56.1:
+ resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
playwright@1.53.2:
resolution: {integrity: sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==}
engines: {node: '>=18'}
hasBin: true
+ playwright@1.56.1:
+ resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
@@ -3687,12 +3701,20 @@ snapshots:
playwright-core@1.53.2: {}
+ playwright-core@1.56.1: {}
+
playwright@1.53.2:
dependencies:
playwright-core: 1.53.2
optionalDependencies:
fsevents: 2.3.2
+ playwright@1.56.1:
+ dependencies:
+ playwright-core: 1.56.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
diff --git a/src/components/Cart.js b/src/components/Cart.js
new file mode 100644
index 00000000..15e94d5b
--- /dev/null
+++ b/src/components/Cart.js
@@ -0,0 +1,162 @@
+const CartItem = ({ product, isSelected = false }) => {
+ return `
+
+
+
+
+
+

+
+
+
+
+ ${product.name}
+
+
+ ${product.price.toLocaleString()}원
+
+
+
+
+
+
+
+ ${(product.price * product.quantity).toLocaleString()}원
+
+
+
+
+ `;
+};
+
+const CartModal = ({ items = [], selectedCount = 0, totalPrice = 0, hasSelection = false }) => {
+ const isEmpty = items.length === 0;
+
+ return `
+
+
+
+
+
+
+ 장바구니${!isEmpty ? ` (${items.length})` : ""}
+
+
+
+
+
+
+ ${
+ isEmpty
+ ? `
+
+
+
+
+
장바구니가 비어있습니다
+
원하는 상품을 담아보세요!
+
+
+ `
+ : `
+
+
+
+
+
+
+
+ ${items.map((item) => CartItem(item)).join("")}
+
+
+ `
+ }
+
+
+ ${
+ !isEmpty
+ ? `
+
+
+ ${
+ hasSelection
+ ? `
+
+
+ 선택한 상품 (${selectedCount}개)
+ ${totalPrice.toLocaleString()}원
+
+ `
+ : ""
+ }
+
+
+ 총 금액
+ ${totalPrice.toLocaleString()}원
+
+
+
+ ${
+ hasSelection
+ ? `
+
+ `
+ : ""
+ }
+
+
+
+
+
+
+ `
+ : ""
+ }
+
+
+ `;
+};
+
+export { CartModal, CartItem };
diff --git a/src/components/CategoryFilter.js b/src/components/CategoryFilter.js
new file mode 100644
index 00000000..5bec0470
--- /dev/null
+++ b/src/components/CategoryFilter.js
@@ -0,0 +1,55 @@
+const CategoryFilter1Depth = ({ category1, subCategories = [] }) => {
+ return `
+
+
+
+
+ >
+
+
+
+
+ ${subCategories
+ .map(
+ (cat) => `
+
+ `,
+ )
+ .join("")}
+
+
+
+ `;
+};
+
+const CategoryFilter2Depth = ({ category1, category2, subCategories = [] }) => {
+ return `
+
+
+
+
+ >
+
+ >
+ ${category2}
+
+
+
+ ${subCategories
+ .map(
+ (cat) => `
+
+ `,
+ )
+ .join("")}
+
+
+
+ `;
+};
+
+export { CategoryFilter1Depth, CategoryFilter2Depth };
diff --git a/src/components/Footer.js b/src/components/Footer.js
new file mode 100644
index 00000000..904223dd
--- /dev/null
+++ b/src/components/Footer.js
@@ -0,0 +1,11 @@
+const Footer = () => {
+ return `
+
+ `;
+};
+
+export default Footer;
diff --git a/src/components/Header.js b/src/components/Header.js
new file mode 100644
index 00000000..c84e7269
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,22 @@
+const Header = () => {
+ return `
+ `;
+};
+
+export default Header;
diff --git a/src/components/NotFound.js b/src/components/NotFound.js
new file mode 100644
index 00000000..72f76dcc
--- /dev/null
+++ b/src/components/NotFound.js
@@ -0,0 +1,38 @@
+const NotFound = () => {
+ return `
+
+
+
+
+
홈으로
+
+
+ `;
+};
+
+export default NotFound;
diff --git a/src/components/ProductDetail.js b/src/components/ProductDetail.js
new file mode 100644
index 00000000..f38d5b5f
--- /dev/null
+++ b/src/components/ProductDetail.js
@@ -0,0 +1,195 @@
+import { Loading } from "./ProductList.js";
+
+const ProductDetailLoading = () => Loading();
+
+const StarRating = ({ rating = 4 }) => {
+ const stars = [];
+ for (let i = 1; i <= 5; i++) {
+ const isFilled = i <= rating;
+ stars.push(`
+
+ `);
+ }
+ return stars.join("");
+};
+
+const Breadcrumb = ({ category1, category2 }) => `
+
+`;
+
+const RelatedProductCard = ({ id, name, price, imageUrl }) => `
+
+`;
+
+const ProductDetailHeader = ({ cartCount = 0 }) => `
+
+
+
+
+
+
+
+
+
+
+`;
+
+const ProductDetailLoaded = ({ product, relatedProducts = [] }) => {
+ const {
+ id,
+ name,
+ price,
+ imageUrl,
+ mallName = "",
+ rating = 4,
+ reviewCount = 749,
+ stock = 107,
+ description = "",
+ category1 = "생활/건강",
+ category2 = "생활용품",
+ cartCount = 0,
+ } = product;
+
+ return () => `
+ ${ProductDetailHeader({ cartCount })}
+ ${Breadcrumb({ category1, category2 })}
+
+
+
+
+
+
+

+
+
+
+ ${mallName ? `
${mallName}
` : ""}
+
${name}
+
+
+
+ ${StarRating({ rating })}
+
+
${rating}.0 (${reviewCount}개 리뷰)
+
+
+
+ ${price}원
+
+
+
+ 재고 ${stock}개
+
+
+
+ ${description || `${name}에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.`}
+
+
+
+
+
+
+
+
+
+
+
+ ${
+ relatedProducts.length > 0
+ ? `
+
+
+
관련 상품
+
같은 카테고리의 다른 상품들
+
+
+
+ ${relatedProducts.map((p) => RelatedProductCard(p)).join("")}
+
+
+
+ `
+ : ""
+ }
+ `;
+};
+
+export { ProductDetailLoading, ProductDetailLoaded };
diff --git a/src/components/ProductList.js b/src/components/ProductList.js
new file mode 100644
index 00000000..f7306390
--- /dev/null
+++ b/src/components/ProductList.js
@@ -0,0 +1,172 @@
+const Skeleton = () => {
+ return `
+
+ `;
+};
+
+const Loading = () => {
+ return `
+
+
+
+
상품을 불러오는 중...
+
+
+ `;
+};
+
+const ProductItem = ({ id, name, price, imageUrl, mallName = "" }) => {
+ return `
+
+
+
+

+
+
+
+
+
+ ${name}
+
+
${mallName}
+
+ ${price.toLocaleString()}원
+
+
+
+
+
+
+ `;
+};
+
+const ProductList = ({ isLoading = true, products = [], totalCount = 0, categories = {} } = {}) => {
+ // 1depth 카테고리 버튼들 생성
+ const category1Buttons = Object.keys(categories)
+ .map(
+ (cat1) => `
+
+ `,
+ )
+ .join("");
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${category1Buttons || '
카테고리 로딩 중...
'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${
+ !isLoading && totalCount > 0
+ ? `
+
+
+ 총 ${totalCount}개의 상품
+
+ `
+ : ""
+ }
+
+
+ ${
+ isLoading
+ ? `${Skeleton().repeat(4)}`
+ : products.map((product) => ProductItem(product)).join("")
+ }
+
+ ${
+ isLoading
+ ? Loading()
+ : `
+ 모든 상품을 확인했습니다
+
`
+ }
+
+
+ `;
+};
+
+export { ProductList, ProductItem, Skeleton, Loading };
diff --git a/src/components/Toast.js b/src/components/Toast.js
new file mode 100644
index 00000000..7324845b
--- /dev/null
+++ b/src/components/Toast.js
@@ -0,0 +1,46 @@
+const Toast = ({ type = "success", message }) => {
+ const configs = {
+ success: {
+ bgColor: "bg-green-600",
+ icon: ``,
+ },
+ info: {
+ bgColor: "bg-blue-600",
+ icon: ``,
+ },
+ error: {
+ bgColor: "bg-red-600",
+ icon: ``,
+ },
+ };
+
+ const config = configs[type] || configs.success;
+
+ return `
+
+
+
+
+
${message}
+
+
+ `;
+};
+
+const ToastDemo = () => {
+ return `
+
+ ${Toast({ type: "success", message: "장바구니에 추가되었습니다" })}
+ ${Toast({ type: "info", message: "선택된 상품들이 삭제되었습니다" })}
+ ${Toast({ type: "error", message: "오류가 발생했습니다." })}
+
+ `;
+};
+
+export { Toast, ToastDemo };
diff --git a/src/main.js b/src/main.js
index 4b055b89..3b488a16 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,3 +1,7 @@
+import { ProductList } from "./components/ProductList.js";
+import layout from "./page/PageLayout.js";
+import { getProducts, getCategories } from "./api/productApi.js";
+
const enableMocking = () =>
import("./mocks/browser.js").then(({ worker }) =>
worker.start({
@@ -5,1119 +9,160 @@ const enableMocking = () =>
}),
);
-function main() {
- const 상품목록_레이아웃_로딩 = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
상품을 불러오는 중...
-
-
-
-
-
-
-
- `;
+async function main() {
+ // 1. 먼저 로딩 상태 표시
+ const loadingHTML = layout({
+ children: ProductList,
+ });
+ document.querySelector("#root").innerHTML = loadingHTML;
- const 상품목록_레이아웃_로딩완료 = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 총 340개의 상품
-
-
-
-
-
-
-

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

-
-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
이지웨이건축자재
-
- 230원
-
-
-
-
-
-
-
-
-
- 모든 상품을 확인했습니다
-
-
-
-
-
-
- `;
+ // 2. 데이터 로드
+ try {
+ // 상품과 카테고리를 동시에 로드
+ const [productsData, categoriesData] = await Promise.all([getProducts({ limit: 20, page: 1 }), getCategories()]);
- const 상품목록_레이아웃_카테고리_1Depth = `
-
-
-
-
-
-
-
-
+ // API 응답 데이터를 ProductList가 기대하는 형식으로 변환
+ const products = productsData.products.map((item) => ({
+ id: item.productId,
+ name: item.title,
+ price: parseInt(item.lprice),
+ imageUrl: item.image,
+ mallName: item.mallName,
+ }));
-
-
-
-
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ // 3. 데이터 로드 완료 후 상품 목록 렌더링
+ const productsHTML = layout({
+ children: () =>
+ ProductList({
+ isLoading: false,
+ products: products,
+ totalCount: productsData.pagination.total,
+ categories: categoriesData,
+ }),
+ });
+ document.querySelector("#root").innerHTML = productsHTML;
+ } catch (error) {
+ console.error("상품 목록을 불러오는데 실패했습니다:", error);
+ // 에러 처리 로직 추가 가능
+ }
- const 상품목록_레이아웃_카테고리_2Depth = `
-
-
-
-
-
-
-
-
+ // ============================================
+ // 아래는 컴포넌트 데모 코드 (개발 참고용)
+ // ============================================
+ /*
+ const 상품목록_레이아웃_카테고리_1Depth = CategoryFilter1Depth({
+ category1: "생활/건강",
+ subCategories: ["생활용품", "주방용품", "문구/사무용품"],
+ });
-
-
-
-
- >>주방용품
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ const 상품목록_레이아웃_카테고리_2Depth = CategoryFilter2Depth({
+ category1: "생활/건강",
+ category2: "주방용품",
+ subCategories: ["생활용품", "주방용품", "문구/사무용품"],
+ });
- const 토스트 = `
-
-
-
-
장바구니에 추가되었습니다
-
-
-
-
-
-
선택된 상품들이 삭제되었습니다
-
-
-
-
-
-
오류가 발생했습니다.
-
-
-
- `;
+ const 토스트 = ToastDemo();
- const 장바구니_비어있음 = `
-
-
-
-
-
-
- 장바구니
-
-
-
-
-
-
-
-
-
-
-
-
장바구니가 비어있습니다
-
원하는 상품을 담아보세요!
-
-
-
-
-
- `;
+ const 장바구니_비어있음 = CartModal({
+ items: [],
+ selectedCount: 0,
+ totalPrice: 0,
+ hasSelection: false,
+ });
- const 장바구니_선택없음 = `
-
-
-
-
-
-
- 장바구니
- (2)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

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

-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
- 230원
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 총 금액
- 670원
-
-
-
-
-
-
-
-
-
-
-
- `;
+ const 장바구니_선택없음 = CartModal({
+ items: [
+ {
+ product: {
+ id: "85067212996",
+ name: "PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장",
+ price: 220,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg",
+ quantity: 2,
+ },
+ isSelected: false,
+ },
+ {
+ product: {
+ id: "86940857379",
+ name: "샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이",
+ price: 230,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg",
+ quantity: 1,
+ },
+ isSelected: false,
+ },
+ ],
+ selectedCount: 0,
+ totalPrice: 670,
+ hasSelection: false,
+ });
- const 장바구니_선택있음 = `
-
-
-
-
-
-
- 장바구니
- (2)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

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

-
-
-
-
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이
-
-
- 230원
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 선택한 상품 (1개)
- 440원
-
-
-
- 총 금액
- 670원
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ const 장바구니_선택있음 = CartModal({
+ items: [
+ {
+ product: {
+ id: "85067212996",
+ name: "PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장",
+ price: 220,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg",
+ quantity: 2,
+ },
+ isSelected: true,
+ },
+ {
+ product: {
+ id: "86940857379",
+ name: "샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이",
+ price: 230,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg",
+ quantity: 1,
+ },
+ isSelected: false,
+ },
+ ],
+ selectedCount: 1,
+ totalPrice: 670,
+ hasSelection: true,
+ });
- const 상세페이지_로딩 = `
-
-
-
-
-
-
-
- `;
+ const 상세페이지_로딩 = layout({
+ children: ProductDetailLoading,
+ });
- const 상세페이지_로딩완료 = `
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
-
PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장
-
-
-
-
-
-
-
-
-
-
4.0 (749개 리뷰)
-
-
-
- 220원
-
-
-
- 재고 107개
-
-
-
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
관련 상품
-
같은 카테고리의 다른 상품들
-
-
-
-
-
-
- `;
+ const 상세페이지_로딩완료 = layout({
+ children: ProductDetailLoaded({
+ product: {
+ id: "85067212996",
+ name: "PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장",
+ price: 220,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg",
+ mallName: "",
+ rating: 4,
+ reviewCount: 749,
+ stock: 107,
+ category1: "생활/건강",
+ category2: "생활용품",
+ cartCount: 1,
+ },
+ relatedProducts: [
+ {
+ id: "86940857379",
+ name: "샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이",
+ price: 230,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg",
+ },
+ {
+ id: "82094468339",
+ name: "실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제",
+ price: 280,
+ imageUrl: "https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg",
+ },
+ ],
+ }),
+ });
- const _404_ = `
-
-
-
-
-
홈으로
-
-
- `;
+ const _404_ = NotFound();
document.body.innerHTML = `
${상품목록_레이아웃_로딩}
@@ -1142,6 +187,7 @@ function main() {
${_404_}
`;
+ */
}
// 애플리케이션 시작
diff --git a/src/page/PageLayout.js b/src/page/PageLayout.js
new file mode 100644
index 00000000..2a8beed6
--- /dev/null
+++ b/src/page/PageLayout.js
@@ -0,0 +1,14 @@
+import Footer from "../components/Footer";
+import Header from "../components/Header";
+
+const layout = ({ children }) => `
+
+ ${Header()}
+
+ ${children()}
+
+ ${Footer()}
+
+ `;
+
+export default layout;