Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
caecb93
feat: 기본 코드 추가
JunilHwang Sep 26, 2025
efa739e
add: 컴포넌트 일부 추가
Nov 10, 2025
ca07329
add: 함수형 라우터 추가
Nov 11, 2025
05f5cd3
add: 404 페이지 추가, 공통 레이아웃 추가
Nov 11, 2025
b6d2774
mod: 라우터 매칭 방식 변경, 필터 기능 추가 (진행중)
Nov 11, 2025
09cf601
Merge commit '9b09aa31aaf07a34b59bf65532f3599cc44994fe'
Nov 11, 2025
d13eb93
mod: 페이지네이션 데이터 연결
Nov 11, 2025
742e598
add: 리랜더 함수 추가, 라우트 단에서 api 호출하지 않고, 페이지 내부에서 직접 호출하도록 변경
Nov 11, 2025
3b8d3c2
mod: 디테일 페이지 내부에서 api 호출하도록 변경
Nov 11, 2025
ea0a63f
add: useState, useEffect 모사 훅 추가
Nov 11, 2025
8a93a79
remove: 중복된 훅 제거
Nov 12, 2025
b3db017
mod: 서치폼 동작 수정, pushWithQuery 에서 빈 쿼리값 자동삭제하도록 변경, E2E 베이직 4번까지 통과
Nov 12, 2025
c909ed6
mod: 상품 브랜드 누락 수정 E2E 6번까지 통과
Nov 12, 2025
041050e
add: 장바구니 로직 추가
Nov 12, 2025
244ee7e
add: 토스트 추가 E2E 7번가지 통과
Nov 12, 2025
af23ad6
mod: 하드코딩된 프로덕트 아이디 제거 E2E 8번 통과
Nov 12, 2025
b369fa2
mod: 수량 증가 이벤트 명시적으로 추가 E2E 9번 통과
Nov 12, 2025
8b09716
mod: 클래스네임 외부에서 주입하지 않도록 변경
Nov 12, 2025
7eaa56b
mod: 모든이벤트 #root에다 붙이도록 변경
Nov 12, 2025
95f971e
mod: 장바구니 모달 HTML로 페이지에 추가 (dom으로 추가하지 않음)
Nov 12, 2025
52c098e
Revert "mod: 모든이벤트 #root에다 붙이도록 변경"
Nov 12, 2025
fc32cfd
Merge branch 'fix/cart'
Nov 12, 2025
8595e34
mod: 인피니티 스크롤 로직 추가
Nov 12, 2025
a5654f1
mod: 토스트 단일 토스트로 수정
Nov 12, 2025
bd65791
mod: 파일 경로 오타 수정
Nov 12, 2025
b245d10
fix: 파일 경로 오타 수정
Nov 12, 2025
7c4a997
mod: 장바구니 잘못된 스타일링 수정
Nov 13, 2025
93b8bd1
mod: 상태 관리 로직 useState와 useEffect 사용하도록 변경
Nov 13, 2025
dfef953
mod: 상세페이지에서 장바구니에 상품 담을 때 이름 잘못들어가는 현상 수정
Nov 13, 2025
f5adb2e
mod: 평점에 따라 별 갯수 변하도록 변경
Nov 13, 2025
3d6c932
mod: 관련 상품 노출 기능 추가
Nov 13, 2025
9b3ad4a
mod: 항상 관련 상품 섹션 존재하도록 변경
Nov 13, 2025
918818d
fix: 상세페이지 경로 products 에서 product로 변경
Nov 13, 2025
cee552d
mod: 프로덕트 배포 패스 변경
Nov 13, 2025
bbc6675
Remove dist from gitignore for GitHub Pages
Nov 13, 2025
2f93eab
mod: 불필요한 html 제거
Nov 13, 2025
2da50e5
add: dist 깃 이그노어에 다시 추가
Nov 13, 2025
a15551d
mod: 누락된 상품상세 브랜드 명 추가
Nov 13, 2025
f6601d0
mod: 디버그 로그 일부 제거
Nov 13, 2025
1a3f683
mod: 인피니티 스크롤 시 current 값 추가
Nov 13, 2025
b4ba46f
fix: 인피니티 스크롤 오류 수정
Nov 13, 2025
aab27d4
mod: 404 처리방식 변경
Nov 13, 2025
0c3ba9e
mod: 서비스 워커 로직 누락 확인
Nov 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 120
"printWidth": 120,
"embeddedLanguageFormatting": "auto"
}
320 changes: 0 additions & 320 deletions cart-modal.html

This file was deleted.

26 changes: 26 additions & 0 deletions public/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
},
},
},
};
</script>
<script type="module" crossorigin src="/front_7th_chapter2-1/assets/index-B4v_IRqa.js"></script>
<link rel="stylesheet" crossorigin href="/front_7th_chapter2-1/assets/index-CdOd2oIW.css" />
</head>
<body class="bg-gray-50">
<div id="root"></div>
</body>
</html>
1 change: 1 addition & 0 deletions src/api/productApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export async function getProducts(params = {}) {

// 상품 상세 조회
export async function getProduct(productId) {
console.log("productId", productId);
const response = await fetch(`/api/products/${productId}`);
return await response.json();
}
Expand Down
244 changes: 244 additions & 0 deletions src/components/CartModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/**
* 모달 래퍼
*/
const CartModalWrapper = (content) => /* html */ `
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex min-h-full items-end justify-center p-0 sm:items-center sm:p-4 cart-modal-overlay">
<div class="relative bg-white rounded-t-lg sm:rounded-lg shadow-xl w-full max-w-md sm:max-w-lg max-h-[90vh] overflow-hidden cart-modal">
${content}
</div>
</div>
`;

/**
* 헤더
*/
const CartModalHeader = (itemCount = 0) => /* html */ `
<div class="sticky top-0 bg-white border-b border-gray-200 p-4 flex items-center justify-between">
<h2 class="text-lg font-bold text-gray-900 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
장바구니
${itemCount > 0 ? `<span class="text-sm font-normal text-gray-600 ml-1">(${itemCount})</span>` : ""}
</h2>
<button id="cart-modal-close-btn" class="text-gray-400 hover:text-gray-600 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
`;

/**
* 컨텐츠 래퍼
*/
const CartModalContentWrapper = (content) => /* html */ `
<div class="flex flex-col max-h-[calc(90vh-120px)]">
${content}
</div>
`;

/**
* 전체 선택 섹션
*/
const CartModalSelectAll = (itemCount, isAllSelected = false) => /* html */ `
<div class="p-4 border-b border-gray-200 bg-gray-50">
<label class="flex items-center text-sm text-gray-700">
<input
type="checkbox"
id="cart-modal-select-all-checkbox"
${isAllSelected ? "checked" : ""}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mr-2"
>
전체선택 (${itemCount}개)
</label>
</div>
`;

/**
* 아이템 목록 래퍼
*/
const CartModalItemsWrapper = (items) => /* html */ `
<div class="flex-1 overflow-y-auto">
<div class="p-4 space-y-4">
${items}
</div>
</div>
`;

/**
* 개별 아이템 카드
*/
const CartModalItem = (item, isSelected = false) => /* html */ `
<div class="flex items-center py-3 border-b border-gray-100 cart-item" data-product-id="${item.id}">
<!-- 선택 체크박스 -->
<label class="flex items-center mr-3">
<input
type="checkbox"
${isSelected ? "checked" : ""}
class="cart-item-checkbox w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
data-product-id="${item.id}"
>
</label>

<!-- 상품 이미지 -->
<div class="w-16 h-16 bg-gray-100 rounded-lg overflow-hidden mr-3 flex-shrink-0">
<img
src="${item.image}"
alt="${item.title}"
class="w-full h-full object-cover cursor-pointer cart-item-image"
data-product-id="${item.id}"
>
</div>

<!-- 상품 정보 -->
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate cursor-pointer cart-item-title" data-product-id="${item.id}">
${item.title}
</h4>
<p class="text-sm text-gray-600 mt-1">
${item.price.toLocaleString()}원
</p>

<!-- 수량 조절 -->
<div class="flex items-center mt-2">
<button class="quantity-decrease-btn w-7 h-7 flex items-center justify-center border border-gray-300 rounded-l-md bg-gray-50 hover:bg-gray-100" data-product-id="${item.id}">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<input
type="number"
value="${item.quantity}"
min="1"
class="quantity-input w-12 h-7 text-center text-sm border-t border-b border-gray-300 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
disabled
data-product-id="${item.id}"
>
<button class="quantity-increase-btn w-7 h-7 flex items-center justify-center border border-gray-300 rounded-r-md bg-gray-50 hover:bg-gray-100" data-product-id="${item.id}">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
</div>

<!-- 가격 및 삭제 -->
<div class="text-right ml-3">
<p class="text-sm font-medium text-gray-900">
${(item.price * item.quantity).toLocaleString()}원
</p>
<button class="cart-item-remove-btn mt-1 text-xs text-red-600 hover:text-red-800" data-product-id="${item.id}">
삭제
</button>
</div>
</div>
`;

/**
* 빈 장바구니 UI
*/
const CartModalEmptyState = () => /* html */ `
<div class="flex-1 flex items-center justify-center p-8">
<div class="text-center">
<div class="text-gray-400 mb-4">
<svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">장바구니가 비어있습니다</h3>
<p class="text-gray-600">원하는 상품을 담아보세요!</p>
</div>
</div>
`;

/**
* 하단 액션 버튼 영역
*/
const CartModalFooter = ({ selectedCount = 0, selectedAmount = 0, totalAmount = 0 }) => /* html */ `
<div class="sticky bottom-0 bg-white border-t border-gray-200 p-4">
${
selectedCount > 0
? /* html */ `
<!-- 선택된 아이템 정보 -->
<div class="flex justify-between items-center mb-3 text-sm">
<span class="text-gray-600">선택한 상품 (${selectedCount}개)</span>
<span class="font-medium">${selectedAmount.toLocaleString()}원</span>
</div>
`
: ""
}

<!-- 총 금액 -->
<div class="flex justify-between items-center mb-4">
<span class="text-lg font-bold text-gray-900">총 금액</span>
<span class="text-xl font-bold text-blue-600">${totalAmount.toLocaleString()}원</span>
</div>

<!-- 액션 버튼들 -->
<div class="space-y-2">
${
selectedCount > 0
? /* html */ `
<button
id="cart-modal-remove-selected-btn"
class="w-full bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors text-sm"
>
선택한 상품 삭제 (${selectedCount}개)
</button>
`
: ""
}
<div class="flex gap-2">
<button
id="cart-modal-clear-cart-btn"
class="flex-1 bg-gray-600 text-white py-2 px-4 rounded-md hover:bg-gray-700 transition-colors text-sm"
>
전체 비우기
</button>
<button
id="cart-modal-checkout-btn"
class="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors text-sm"
>
구매하기
</button>
</div>
</div>
</div>
`;

/**
* 장바구니 모달 메인 컴포넌트
* @param {Array} cartItems - 장바구니 아이템 배열
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. 문제 상황

CartModal 컴포넌트가 HTML 문자열 생성과 로직이 같이 존재해서 가독성이 약간 떨어집니다. 특히 여러 HTML chunk들을 함수로 나누어 구성했지만, 모두 문자열을 반환하는 방식이라 실제 UI 구조 파악이 어렵고 유지 보수가 번거로워질 수 있습니다.

2. 근본 원인

핵심 문제: UI를 HTML 문자열로 작성하며 로직과 UI 표현이 혼재되어 있음

왜 문제인가: UI 변경 시 문자열 조작이 번거롭고 조건별 UI 변경 표현에 일관성이 떨어지며 JSX나 템플릿 라이브러리 대비 코드 재사용과 유지보수가 어렵습니다.


3. 개선 구조

개선사항:

  • 컴포넌트별로 상태와 UI를 명확히 분리
  • DOM 템플릿 함수에 더 명확한 역할과 props 전달
  • 추후 리액트/뷰 등 프레임워크로 전환을 고려한 구조로 설계

예) 상위 컴포넌트에서 상태를 계산해 하위 컴포넌트에 props로 넘기기

// 현재
const CartModalItem = (item, isSelected = false) => `<div ...>${item.title}</div>`;

// 개선
const CartModalItem = ({ item, isSelected }) => {
  return `<div ...>${item.title}</div>`;
};
  • 또한 실제로는 작은 템플릿 유틸리티를 만들어 HTML 문자열 관련 중복 제거 및 상태 표현을 관리하면 가독성 향상에 도움이 됩니다.

* @param {Array} selectedItems - 선택된 아이템 ID 배열
* @returns {string} HTML 문자열
*/
export const CartModal = ({ cartItems = [], selectedItems = [] }) => {
// 장바구니가 비어있는 경우
if (cartItems.length === 0) {
return CartModalWrapper(CartModalHeader(0) + CartModalContentWrapper(CartModalEmptyState()));
}

// 총 금액 계산
const totalAmount = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);

// 선택된 아이템 계산
const selectedCount = selectedItems.length;
const selectedAmount = cartItems
.filter((item) => selectedItems.includes(item.id))
.reduce((sum, item) => sum + item.price * item.quantity, 0);

// 전체 선택 여부
const isAllSelected = cartItems.length > 0 && selectedItems.length === cartItems.length;

// 아이템 목록 HTML 생성
const itemsHTML = cartItems.map((item) => CartModalItem(item, selectedItems.includes(item.id))).join("");

// 컨텐츠 조립
const content =
CartModalHeader(cartItems.length) +
CartModalContentWrapper(CartModalSelectAll(cartItems.length, isAllSelected) + CartModalItemsWrapper(itemsHTML)) +
CartModalFooter({ selectedCount, selectedAmount, totalAmount });

return CartModalWrapper(content);
};
7 changes: 7 additions & 0 deletions src/components/Footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const Footer = () => {
return /* html */ `<footer class="bg-white shadow-sm sticky top-0 z-40">
<div class="max-w-md mx-auto py-8 text-center text-gray-500">
<p>© ${new Date().getFullYear()} 항해플러스 프론트엔드 쇼핑몰</p>
</div>
</footer>`;
};
62 changes: 62 additions & 0 deletions src/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getCartCount } from "../utils/cartStore.js";

export const Header = ({ isDetailPage = false } = {}) => {
const cartCount = getCartCount();

if (isDetailPage) {
return /* html */ `
<header class="bg-white shadow-sm sticky top-0 z-40">
<div class="max-w-md mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<button onclick="window.history.back()" class="p-2 text-gray-700 hover:text-gray-900 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<h1 class="text-lg font-bold text-gray-900">상품 상세</h1>
</div>
<div class="flex items-center space-x-2">
<!-- 장바구니 아이콘 -->
<button id="cart-icon-btn" class="relative p-2 text-gray-700 hover:text-gray-900 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
${
cartCount > 0
? `<span class="absolute top-0 right-0 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">${cartCount}</span>`
: ""
}
</button>
</div>
</div>
</div>
</header>`;
}

return /* html */ `
<header class="bg-white shadow-sm sticky top-0 z-40">
<div class="max-w-md mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900">
<a href="/" data-link="">쇼핑몰</a>
</h1>
<div class="flex items-center space-x-2">
<!-- 장바구니 아이콘 -->
<button id="cart-icon-btn" class="relative p-2 text-gray-700 hover:text-gray-900 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path>
</svg>
${
cartCount > 0
? `<span class="absolute top-0 right-0 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">${cartCount}</span>`
: ""
}
</button>
</div>
</div>
</div>
</header>`;
};
Loading