[담임매니저 차현빈] Chapter 1-1. 프레임워크 없이 SPA 만들기#75
[담임매니저 차현빈] Chapter 1-1. 프레임워크 없이 SPA 만들기#75chb6734 wants to merge 49 commits intohanghae-plus:mainfrom
Conversation
|
오~~~ |
|
conflicts도 해결해주세요 매니저님 ㅋㅋㅋ |
엌ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ넴.... |
|
대박 코드 훔치러 와야겠다🤩 |
깔짝한거라 가져가실게 없을텐데 ..헣헣 |
해결 완료입니다...ㅎ |
JunilHwang
left a comment
There was a problem hiding this comment.
매니징도 하면서 과제도 하시다니! 대단쓰...
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "18" |
There was a problem hiding this comment.
영서님덕분에 github actions로 배포하는게 퍼졌는데요,
저는 꼭 actions로 할 필요가 없다고 생각해서요 ㅋㅋ 팀원들과 협업할게 아니라면..?
"어떻게 배포가 되는건가?" 자체에 대해 고민하는 과정도 필요하다고 생각합니다! 원리 자체랄까..
There was a problem hiding this comment.
흠.. 원래 원본에 있는 파일인데, 이렇게 추가된게 이상하네요.
컨플릭 해결과정에서 문제가 있었나봐요
| <script type="text/javascript"> | ||
| var pathSegmentsToKeep = 1; | ||
| var l = window.location; | ||
| l.replace( | ||
| l.protocol + | ||
| "//" + | ||
| l.hostname + | ||
| (l.port ? ":" + l.port : "") + | ||
| l.pathname | ||
| .split("/") | ||
| .slice(0, 1 + pathSegmentsToKeep) | ||
| .join("/") + | ||
| "/?/" + | ||
| l.pathname.slice(1).split("/").slice(pathSegmentsToKeep).join("/").replace(/&/g, "~and~") + | ||
| (l.search ? "&" + l.search.slice(1).replace(/&/g, "~and~") : "") + | ||
| l.hash, | ||
| ); | ||
| </script> |
There was a problem hiding this comment.
이 코드 없어도 정상 동작 할 것 같아요 ㅋㅋ
| categories = {}, | ||
| currentCategory1 = "", | ||
| currentCategory2 = "", | ||
| currentLimit = 20, | ||
| currentSort = "price_asc", | ||
| currentSearch = "", |
There was a problem hiding this comment.
current를 꼭 붙였어야할까요!?
없어도 무방해보여요~
| const breadcrumbItems = []; | ||
| breadcrumbItems.push( | ||
| `<button data-breadcrumb="reset" class="text-xs hover:text-blue-800 hover:underline">전체</button>`, | ||
| ); | ||
| if (currentCategory1) { | ||
| breadcrumbItems.push(`<span class="text-xs text-gray-500">></span>`); | ||
| breadcrumbItems.push( | ||
| `<button data-breadcrumb="category1" data-category1="${currentCategory1}" class="text-xs hover:text-blue-800 hover:underline">${currentCategory1}</button>`, | ||
| ); | ||
| } | ||
| if (currentCategory2) { | ||
| breadcrumbItems.push(`<span class="text-xs text-gray-500">></span>`); | ||
| breadcrumbItems.push( | ||
| `<button data-breadcrumb="category2" data-category1="${currentCategory1}" data-category2="${currentCategory2}" class="text-xs hover:text-blue-800 hover:underline">${currentCategory2}</button>`, | ||
| ); | ||
| } | ||
| const breadcrumbHtml = breadcrumbItems.join(""); |
There was a problem hiding this comment.
| const breadcrumbItems = []; | |
| breadcrumbItems.push( | |
| `<button data-breadcrumb="reset" class="text-xs hover:text-blue-800 hover:underline">전체</button>`, | |
| ); | |
| if (currentCategory1) { | |
| breadcrumbItems.push(`<span class="text-xs text-gray-500">></span>`); | |
| breadcrumbItems.push( | |
| `<button data-breadcrumb="category1" data-category1="${currentCategory1}" class="text-xs hover:text-blue-800 hover:underline">${currentCategory1}</button>`, | |
| ); | |
| } | |
| if (currentCategory2) { | |
| breadcrumbItems.push(`<span class="text-xs text-gray-500">></span>`); | |
| breadcrumbItems.push( | |
| `<button data-breadcrumb="category2" data-category1="${currentCategory1}" data-category2="${currentCategory2}" class="text-xs hover:text-blue-800 hover:underline">${currentCategory2}</button>`, | |
| ); | |
| } | |
| const breadcrumbHtml = breadcrumbItems.join(""); | |
| const breadcrumbItems = []; | |
| breadcrumbItems.push( | |
| `<button data-breadcrumb="reset" class="text-xs hover:text-blue-800 hover:underline">전체</button>`, | |
| ); | |
| if (currentCategory1) { | |
| breadcrumbItems.push(...[ | |
| `<span class="text-xs text-gray-500">></span>`, | |
| `<button data-breadcrumb="category1" data-category1="${currentCategory1}" class="text-xs hover:text-blue-800 hover:underline">${currentCategory1}</button>` | |
| ]); | |
| } | |
| if (currentCategory2) { | |
| breadcrumbItems.push(...[ | |
| `<span class="text-xs text-gray-500">></span>`, | |
| `<button data-breadcrumb="category2" data-category1="${currentCategory1}" data-category2="${currentCategory2}" class="text-xs hover:text-blue-800 hover:underline">${currentCategory2}</button>` | |
| ]); | |
| } | |
| const breadcrumbHtml = breadcrumbItems.join(""); |
이렇게 표현할 수도 있답니다 ㅎㅎ
| id: "limit-select", | ||
| label: "개수", | ||
| options: [ | ||
| { value: "10", label: "10개" }, | ||
| { value: "20", label: "20개" }, | ||
| { value: "50", label: "50개" }, | ||
| { value: "100", label: "100개" }, | ||
| ], |
There was a problem hiding this comment.
이렇게만 상수로 분리해도 좋을 것 같아요!
| id: "sort-select", | ||
| label: "정렬", | ||
| options: [ | ||
| { value: "price_asc", label: "가격 낮은순" }, | ||
| { value: "popularity", label: "인기순" }, | ||
| { value: "price_desc", label: "가격 높은순" }, | ||
| { value: "name_asc", label: "이름순" }, | ||
| { value: "name_desc", label: "이름 역순" }, | ||
| ], |
| export const cartManager = { | ||
| getCart() { | ||
| return JSON.parse(storage.getItem("shopping_cart") || "[]"); | ||
| }, | ||
|
|
||
| addToCart(product, cnt = 1) { | ||
| const cart = this.getCart(); | ||
| const existingItem = cart.find((item) => item.productId === product.productId); | ||
| if (existingItem) { | ||
| existingItem.quantity += cnt; | ||
| } else { | ||
| cart.push({ ...product, quantity: cnt, selected: false }); | ||
| } | ||
|
|
||
| storage.setItem("shopping_cart", JSON.stringify(cart)); | ||
| this.updateCartCount(); | ||
| }, | ||
|
|
||
| removeFromCart(productId) { | ||
| const cart = this.getCart().filter((item) => item.productId !== productId); | ||
| storage.setItem("shopping_cart", JSON.stringify(cart)); | ||
| this.updateCartCount(); | ||
| }, | ||
|
|
||
| increaseQuantity(productId) { | ||
| const cart = this.getCart(); | ||
| const existingItem = cart.find((item) => item.productId === productId); | ||
|
|
||
| existingItem.quantity += 1; | ||
| storage.setItem("shopping_cart", JSON.stringify(cart)); | ||
| this.updateCartCount(); | ||
| }, | ||
|
|
||
| decreaseQuantity(productId) { | ||
| const cart = this.getCart(); | ||
| const existingItem = cart.find((item) => item.productId === productId); | ||
|
|
||
| if (existingItem.quantity === 1) { | ||
| this.removeFromCart(productId); | ||
| } else { | ||
| existingItem.quantity -= 1; | ||
| storage.setItem("shopping_cart", JSON.stringify(cart)); | ||
| this.updateCartCount(); | ||
| } | ||
| }, | ||
|
|
||
| getSelectedItems() { | ||
| const cart = this.getCart(); | ||
| return cart.filter((item) => item.selected); | ||
| }, | ||
|
|
||
| toggleSelected(productId) { | ||
| const cart = this.getCart(); | ||
|
|
||
| if (productId) { | ||
| const targetItem = cart.find((item) => item.productId === productId); | ||
| targetItem.selected = !targetItem.selected; | ||
| } else { | ||
| const isAllSelected = cart.every((item) => item.selected); | ||
| cart.forEach((item) => { | ||
| item.selected = !isAllSelected; | ||
| }); | ||
| } | ||
|
|
||
| storage.setItem("shopping_cart", JSON.stringify(cart)); | ||
| }, | ||
|
|
||
| removeSelectedItems() { | ||
| const cart = this.getCart(); | ||
| const updatedCart = cart.filter((item) => !item.selected); | ||
| storage.setItem("shopping_cart", JSON.stringify(updatedCart)); | ||
| this.updateCartCount(); | ||
| }, | ||
|
|
||
| resetCart() { | ||
| storage.removeItem("shopping_cart"); | ||
| this.updateCartCount(); | ||
| }, | ||
|
|
||
| updateCartCount() { | ||
| const cart = this.getCart(); | ||
| const cartButton = document.querySelector("#cart-icon-btn"); | ||
| let badge = cartButton.querySelector(".cart-count-badge"); | ||
|
|
||
| if (cart.length > 0 && cartButton) { | ||
| if (!badge) { | ||
| 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 cart-count-badge"; | ||
| cartButton.appendChild(badge); | ||
| } | ||
| badge.textContent = cart.length; | ||
| } | ||
|
|
||
| if (cart.length === 0) { | ||
| badge.remove(); | ||
| } | ||
| }, | ||
| }; |
There was a problem hiding this comment.
로컬스토리지 사용 부분만 따로 떼어내서 추상화해도 좋을 것 같아요!
| let totalCount = 0; | ||
| let currentPage = 1; | ||
| let currentLimit = 20; | ||
| let currentSort = "price_asc"; | ||
| let hasNext = true; | ||
| let allProducts = []; | ||
| let categories = {}; | ||
| let currentCategory1 = ""; | ||
| let currentCategory2 = ""; | ||
| let currentSearch = ""; | ||
|
|
||
| // 다음 페이지 로딩 중복 방지 플래그 | ||
| let loadingNextPage = false; | ||
|
|
||
| // 현재 상세 페이지 상품 저장용 변수 | ||
| let currentDetailProduct = null; |
There was a problem hiding this comment.
네임스페이스로 묶어서 관리해야 덜 헷갈린답니다!
There was a problem hiding this comment.
메인페이지에 거의 모든 로직이 모여있군요.. ㅎㅎ
이쪽 교통정리를 잘 하는게 관건일 것 같아요.
a90ad7b to
d5eb31b
Compare
과제 체크포인트
배포 링크
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
기술적 성장
자랑하고 싶은 코드
개선이 필요하다고 생각하는 코드
학습 효과 분석
과제 피드백
AI 활용 경험 공유하기
리뷰 받고 싶은 내용
저두... 코드 피드백 해주세여...
와 배포 어렵다고 해서 배포하려고 올린건데 하다가 앉은채로 기절해버렸어요..;;
그래서 배포는 실패...
https://chb6734.github.io/front_6th_chapter1-1/