-
Notifications
You must be signed in to change notification settings - Fork 50
[3팀 주민수] Chapter2-1. 프레임워크 없이 SPA 만들기 #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Thomas97-J
wants to merge
43
commits into
hanghae-plus:main
Choose a base branch
from
Thomas97-J:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
caecb93
feat: 기본 코드 추가
JunilHwang efa739e
add: 컴포넌트 일부 추가
ca07329
add: 함수형 라우터 추가
05f5cd3
add: 404 페이지 추가, 공통 레이아웃 추가
b6d2774
mod: 라우터 매칭 방식 변경, 필터 기능 추가 (진행중)
09cf601
Merge commit '9b09aa31aaf07a34b59bf65532f3599cc44994fe'
d13eb93
mod: 페이지네이션 데이터 연결
742e598
add: 리랜더 함수 추가, 라우트 단에서 api 호출하지 않고, 페이지 내부에서 직접 호출하도록 변경
3b8d3c2
mod: 디테일 페이지 내부에서 api 호출하도록 변경
ea0a63f
add: useState, useEffect 모사 훅 추가
8a93a79
remove: 중복된 훅 제거
b3db017
mod: 서치폼 동작 수정, pushWithQuery 에서 빈 쿼리값 자동삭제하도록 변경, E2E 베이직 4번까지 통과
c909ed6
mod: 상품 브랜드 누락 수정 E2E 6번까지 통과
041050e
add: 장바구니 로직 추가
244ee7e
add: 토스트 추가 E2E 7번가지 통과
af23ad6
mod: 하드코딩된 프로덕트 아이디 제거 E2E 8번 통과
b369fa2
mod: 수량 증가 이벤트 명시적으로 추가 E2E 9번 통과
8b09716
mod: 클래스네임 외부에서 주입하지 않도록 변경
7eaa56b
mod: 모든이벤트 #root에다 붙이도록 변경
95f971e
mod: 장바구니 모달 HTML로 페이지에 추가 (dom으로 추가하지 않음)
52c098e
Revert "mod: 모든이벤트 #root에다 붙이도록 변경"
fc32cfd
Merge branch 'fix/cart'
8595e34
mod: 인피니티 스크롤 로직 추가
a5654f1
mod: 토스트 단일 토스트로 수정
bd65791
mod: 파일 경로 오타 수정
b245d10
fix: 파일 경로 오타 수정
7c4a997
mod: 장바구니 잘못된 스타일링 수정
93b8bd1
mod: 상태 관리 로직 useState와 useEffect 사용하도록 변경
dfef953
mod: 상세페이지에서 장바구니에 상품 담을 때 이름 잘못들어가는 현상 수정
f5adb2e
mod: 평점에 따라 별 갯수 변하도록 변경
3d6c932
mod: 관련 상품 노출 기능 추가
9b3ad4a
mod: 항상 관련 상품 섹션 존재하도록 변경
918818d
fix: 상세페이지 경로 products 에서 product로 변경
cee552d
mod: 프로덕트 배포 패스 변경
bbc6675
Remove dist from gitignore for GitHub Pages
2f93eab
mod: 불필요한 html 제거
2da50e5
add: dist 깃 이그노어에 다시 추가
a15551d
mod: 누락된 상품상세 브랜드 명 추가
f6601d0
mod: 디버그 로그 일부 제거
1a3f683
mod: 인피니티 스크롤 시 current 값 추가
b4ba46f
fix: 인피니티 스크롤 오류 수정
aab27d4
mod: 404 처리방식 변경
0c3ba9e
mod: 서비스 워커 로직 누락 확인
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 - 장바구니 아이템 배열 | ||
| * @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); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>`; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>`; | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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. 개선 구조
개선사항:
예) 상위 컴포넌트에서 상태를 계산해 하위 컴포넌트에 props로 넘기기