-
Notifications
You must be signed in to change notification settings - Fork 50
[5팀 오태준] Chapter2-1. 프레임워크 없이 SPA 만들기 #51
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
base: main
Are you sure you want to change the base?
Changes from all commits
7d4b19d
ffda7ef
cc54e46
9986dc7
76de36f
1aa49da
a509bc4
bde3e3a
1f40b50
83e573d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| #!/bin/bash | ||
|
|
||
| # GitHub Pages 배포 스크립트 | ||
| # main 브랜치의 빌드 결과를 gh-pages 브랜치에 배포합니다 | ||
|
|
||
| set -e | ||
|
|
||
| echo "🚀 배포를 시작합니다..." | ||
|
|
||
| # 현재 브랜치 저장 | ||
| CURRENT_BRANCH=$(git branch --show-current) | ||
|
|
||
| # main 브랜치로 전환 | ||
| echo "📦 main 브랜치로 전환 중..." | ||
| git checkout main | ||
|
|
||
| # 의존성 설치 (필요한 경우) | ||
| if [ ! -d "node_modules" ]; then | ||
| echo "📥 의존성 설치 중..." | ||
| pnpm install | ||
| fi | ||
|
|
||
| # 빌드 | ||
| echo "🔨 프로젝트 빌드 중..." | ||
| pnpm run build | ||
|
|
||
| # dist 폴더 확인 | ||
| if [ ! -d "dist" ]; then | ||
| echo "❌ dist 폴더를 찾을 수 없습니다. 빌드가 실패했을 수 있습니다." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # gh-pages 브랜치로 전환 (없으면 생성) | ||
| echo "🌿 gh-pages 브랜치로 전환 중..." | ||
| git checkout gh-pages 2>/dev/null || git checkout -b gh-pages | ||
|
|
||
| # dist 폴더의 내용을 루트로 복사 | ||
| echo "📋 빌드 파일 복사 중..." | ||
| cp -r dist/* . | ||
|
|
||
| # 변경사항 커밋 | ||
| echo "💾 변경사항 커밋 중..." | ||
| git add . | ||
| git commit -m "Deploy: $(date +'%Y-%m-%d %H:%M:%S')" || echo "변경사항이 없습니다." | ||
|
|
||
| # gh-pages 브랜치 푸시 | ||
| echo "📤 gh-pages 브랜치 푸시 중..." | ||
| git push origin gh-pages | ||
|
|
||
| # 원래 브랜치로 돌아가기 | ||
| echo "↩️ 원래 브랜치($CURRENT_BRANCH)로 돌아가는 중..." | ||
| git checkout $CURRENT_BRANCH | ||
|
|
||
| echo "✅ 배포가 완료되었습니다!" | ||
| echo "🌐 배포 링크: https://taejun0.github.io/front_7th_chapter2-1/" | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| export class Router { | ||
| constructor() { | ||
| this.routes = new Map(); | ||
| this.currentPath = window.location.pathname; | ||
| this.init(); | ||
| } | ||
|
|
||
| init() { | ||
| this.handleRoute(); | ||
|
|
||
| window.addEventListener("popstate", () => { | ||
| this.currentPath = window.location.pathname; | ||
| this.handleRoute(); | ||
| }); | ||
|
|
||
| document.addEventListener("click", (e) => { | ||
| const link = e.target.closest("a[data-link]"); | ||
| if (link) { | ||
| e.preventDefault(); | ||
| const href = link.getAttribute("href"); | ||
| if (href) { | ||
| this.navigate(href); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| register(path, handler) { | ||
| this.routes.set(path, handler); | ||
| } | ||
|
|
||
| navigate(path) { | ||
| if (this.currentPath !== path) { | ||
| this.currentPath = path; | ||
| window.history.pushState({}, "", path); | ||
| this.handleRoute(); | ||
| } | ||
| } | ||
|
|
||
| handleRoute() { | ||
| const path = window.location.pathname; | ||
|
|
||
| let handler = this.routes.get(path); | ||
|
|
||
| if (!handler) { | ||
| for (const [routePath, routeHandler] of this.routes.entries()) { | ||
| if (routePath.includes(":")) { | ||
| const pattern = routePath.replace(/:[^/]+/g, "([^/]+)"); | ||
| const regex = new RegExp(`^${pattern}$`); | ||
| if (regex.test(path)) { | ||
| handler = routeHandler; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!handler) { | ||
| handler = this.routes.get("/"); | ||
| } | ||
|
|
||
| if (handler) { | ||
| handler(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| const CART_STORAGE_KEY = "shopping_cart"; | ||
|
|
||
| export function getCart() { | ||
| try { | ||
| const cartData = localStorage.getItem(CART_STORAGE_KEY); | ||
| return cartData ? JSON.parse(cartData) : {}; | ||
| } catch (error) { | ||
| console.error("장바구니 로드 실패:", error); | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| function saveCart(cart) { | ||
| try { | ||
| localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart)); | ||
| } catch (error) { | ||
| console.error("장바구니 저장 실패:", error); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. console.log도 좋지만, 토스트 UI 호출로 사용자에게 장바구니에 상품이 담기지 않았다는 걸 알려 주면 좋을 것 같아요! 그러면 장바구니에 담기지 않은 상품을 고객이 알아서 다시 장바구니에 상품을 담지 않을까~~.. 라는 제 생각! |
||
| } | ||
| } | ||
|
|
||
| export function addToCart(productId, productData = null, quantity = 1) { | ||
| const cart = getCart(); | ||
|
|
||
| if (cart[productId]) { | ||
| cart[productId].quantity += quantity; | ||
| } else { | ||
| cart[productId] = { | ||
| productId, | ||
| quantity, | ||
| ...(productData && { productData }), | ||
| }; | ||
| } | ||
|
|
||
| saveCart(cart); | ||
| updateCartIcon(); | ||
| return cart; | ||
| } | ||
|
|
||
| export function removeFromCart(productId) { | ||
| const cart = getCart(); | ||
| delete cart[productId]; | ||
| saveCart(cart); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 장바구니 서비스의 상태를 다만, cart 상태의 변경 시점마다 개선 제안
|
||
| updateCartIcon(); | ||
| return cart; | ||
| } | ||
|
|
||
| export function updateCartQuantity(productId, quantity) { | ||
| const cart = getCart(); | ||
| if (cart[productId]) { | ||
| if (quantity <= 0) { | ||
| delete cart[productId]; | ||
| } else { | ||
| cart[productId].quantity = quantity; | ||
| } | ||
| saveCart(cart); | ||
| updateCartIcon(); | ||
| } | ||
| return cart; | ||
| } | ||
|
|
||
| export function clearCart() { | ||
| localStorage.removeItem(CART_STORAGE_KEY); | ||
| updateCartIcon(); | ||
| } | ||
|
|
||
| export function getCartItemCount() { | ||
| const cart = getCart(); | ||
| return Object.values(cart).reduce((total, item) => total + item.quantity, 0); | ||
| } | ||
|
|
||
| export function updateCartIcon() { | ||
| const cartIconBtn = document.getElementById("cart-icon-btn"); | ||
| if (!cartIconBtn) return; | ||
|
|
||
| const count = getCartItemCount(); | ||
| const existingBadge = cartIconBtn.querySelector("span"); | ||
|
|
||
| if (count > 0) { | ||
| if (existingBadge) { | ||
| existingBadge.textContent = count.toString(); | ||
| } else { | ||
| const badge = document.createElement("span"); | ||
| badge.className = | ||
| "absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"; | ||
| badge.textContent = count.toString(); | ||
| cartIconBtn.appendChild(badge); | ||
| } | ||
| } else { | ||
| if (existingBadge) { | ||
| existingBadge.remove(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| export const toastTemplates = ` | ||
| <div class="flex flex-col gap-2 items-center justify-center mx-auto" style="width: fit-content;"> | ||
| <div class="bg-green-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center space-x-2 max-w-sm"> | ||
| <div class="flex-shrink-0"> | ||
| <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> | ||
| </svg> | ||
| </div> | ||
| <p class="text-sm font-medium">장바구니에 추가되었습니다</p> | ||
| <button id="toast-close-btn" class="flex-shrink-0 ml-2 text-white hover:text-gray-200"> | ||
| <svg class="w-4 h-4" 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> | ||
|
|
||
| <div class="bg-blue-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center space-x-2 max-w-sm"> | ||
| <div class="flex-shrink-0"> | ||
| <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 토스트 메시지 컴포넌트가 타입별로 적절하게 스타일과 SVG 아이콘을 분리하여 렌더링하는 점이 좋습니다. 다만, DOM 엘리먼트를 동적으로 생성하고 조작하는 부분이 코드 내 여러 영역에 흩어져 있는데, 컴포넌트 패턴 또는 팩토리 함수로 분리하면 좋겠습니다. 개선 제안
|
||
| </svg> | ||
| </div> | ||
| <p class="text-sm font-medium">선택된 상품들이 삭제되었습니다</p> | ||
| <button id="toast-close-btn" class="flex-shrink-0 ml-2 text-white hover:text-gray-200"> | ||
| <svg class="w-4 h-4" 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> | ||
|
|
||
| <div class="bg-red-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center space-x-2 max-w-sm"> | ||
| <div class="flex-shrink-0"> | ||
| <svg class="w-5 h-5" 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"/> | ||
| </svg> | ||
| </div> | ||
| <p class="text-sm font-medium">오류가 발생했습니다.</p> | ||
| <button id="toast-close-btn" class="flex-shrink-0 ml-2 text-white hover:text-gray-200"> | ||
| <svg class="w-4 h-4" 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> | ||
| </div> | ||
| `; | ||
|
|
||
| export function showToast(type = "success", message = null) { | ||
| const existingToast = document.querySelector(".toast-container"); | ||
| if (existingToast) { | ||
| existingToast.remove(); | ||
| } | ||
|
|
||
| let toastHTML = ""; | ||
| let bgColor = ""; | ||
| let icon = ""; | ||
|
|
||
| switch (type) { | ||
| case "success": | ||
| bgColor = "bg-green-600"; | ||
| icon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> | ||
| </svg>`; | ||
| message = message || "장바구니에 추가되었습니다"; | ||
| break; | ||
| case "info": | ||
| bgColor = "bg-blue-600"; | ||
| icon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | ||
| </svg>`; | ||
| message = message || "선택된 상품들이 삭제되었습니다"; | ||
| break; | ||
| case "error": | ||
| bgColor = "bg-red-600"; | ||
| icon = `<svg class="w-5 h-5" 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"/> | ||
| </svg>`; | ||
| message = message || "오류가 발생했습니다."; | ||
| break; | ||
| } | ||
|
|
||
| toastHTML = ` | ||
| <div class="${bgColor} text-white px-4 py-3 rounded-lg shadow-lg flex items-center space-x-2 max-w-sm toast-container" style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;"> | ||
| <div class="flex-shrink-0"> | ||
| ${icon} | ||
| </div> | ||
| <p class="text-sm font-medium">${message}</p> | ||
| <button id="toast-close-btn" class="toast-close-btn flex-shrink-0 ml-2 text-white hover:text-gray-200"> | ||
| <svg class="w-4 h-4" 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 toastElement = document.createElement("div"); | ||
| toastElement.innerHTML = toastHTML; | ||
| document.body.appendChild(toastElement.firstElementChild); | ||
|
|
||
| const toast = document.querySelector(".toast-container"); | ||
| const closeBtn = toast.querySelector(".toast-close-btn"); | ||
|
|
||
| closeBtn.addEventListener("click", () => { | ||
| toast.remove(); | ||
| }); | ||
|
|
||
| setTimeout(() => { | ||
| if (toast && toast.parentElement) { | ||
| toast.remove(); | ||
| } | ||
| }, 3000); | ||
| } | ||
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.
현재 Router 클래스는 경로에 파라미터가 포함된 경우(예:
/product/:id)를 단순히 정규식으로 패턴 매칭하는 방식을 사용하고 있습니다. 그러나 이후 특정 파라미터를 추출하거나 라우팅 핸들러에 파라미터를 전달하는 구조가 없습니다.개선 제안
/product/:id에서id를 추출해 핸들러가 해당 값을 받을 수 있어 동적으로 활용 가능하게 합니다.이렇게 하면 라우터가 더 확장 가능해지고 핸들러가 패스 파라미터를 활용할 수 있습니다.