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.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 ` +
+
+ + + + + + + + + + + + + 404 + + + + + + + + + 페이지를 찾을 수 없습니다 + + + + + + 홈으로 +
+
+ `; +}; + +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 })} + + +
+ +
+
+ ${name} +
+ +
+ ${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} +
+ +
+
+

+ ${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 ` +
+
+ + ${config.icon} + +
+

${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 상품목록_레이아웃_로딩 = ` -
-
-
-
-

- 쇼핑몰 -

-
- - -
-
-
-
-
- -
- -
-
- -
- - - -
-
-
- -
- -
-
- - -
- -
-
카테고리 로딩 중...
-
- -
- -
- -
- - -
- -
- - -
-
-
-
- -
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- - - - - 상품을 불러오는 중... -
-
-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; +async function main() { + // 1. 먼저 로딩 상태 표시 + const loadingHTML = layout({ + children: ProductList, + }); + document.querySelector("#root").innerHTML = loadingHTML; - const 상품목록_레이아웃_로딩완료 = ` -
-
-
-
-

- 쇼핑몰 -

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

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

-

-

- 220원 -

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

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

-

이지웨이건축자재

-

- 230원 -

-
- - -
-
-
- -
- 모든 상품을 확인했습니다 -
-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

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

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

-

- 220원 -

- -
- - - -
-
- -
-

- 440원 -

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

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

-

- 230원 -

- -
- - - -
-
- -
-

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

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

-

- 220원 -

- -
- - - -
-
- -
-

- 440원 -

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

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

-

- 230원 -

- -
- - - -
-
- -
-

- 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 상세페이지_로딩 = ` -
-
-
-
-
- -

상품 상세

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

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

-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; + const 상세페이지_로딩 = layout({ + children: ProductDetailLoading, + }); - const 상세페이지_로딩완료 = ` -
-
-
-
-
- -

상품 상세

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

-

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

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

관련 상품

-

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

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

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; + 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_ = ` -
-
- - - - - - - - - - - - - 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;