From f87212719645f4f4ccbb768f034043e511f1c0b7 Mon Sep 17 00:00:00 2001 From: angie <148928168+angielee0304@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:49:07 +0900 Subject: [PATCH 01/55] =?UTF-8?q?feat:=20=EC=BB=A4=EC=84=9C=EB=A3=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/clean-code.mdc | 96 +++++++ .cursorrules | 96 +++++++ src/main.original.js | 477 ++++++++++++++++++++++------------- 3 files changed, 498 insertions(+), 171 deletions(-) create mode 100644 .cursor/rules/clean-code.mdc create mode 100644 .cursorrules diff --git a/.cursor/rules/clean-code.mdc b/.cursor/rules/clean-code.mdc new file mode 100644 index 000000000..b70a21c2f --- /dev/null +++ b/.cursor/rules/clean-code.mdc @@ -0,0 +1,96 @@ +--- +alwaysApply: false +--- + +# Clean Code Writing Rules + +## MANDATORY CODE WRITING RULES + +You MUST follow these rules when writing any code: + +### CORE DESIGN PRINCIPLES + +- **DRY**: NEVER repeat the same code +- **KISS**: Write code as simply as possible +- **YAGNI**: Do NOT write unnecessary code +- **Single Responsibility**: Functions MUST be under 20 lines and have ONE clear responsibility + +### CODE ORGANIZATION RULES + +Apply these 4 organization principles: + +- **Proximity**: Group related elements with blank lines +- **Commonality**: Group related functionality into functions +- **Similarity**: Use similar names and positions for similar roles +- **Continuity**: Arrange code in dependency order + +### NAMING REQUIREMENTS + +#### Naming Principles (ALL MUST BE FOLLOWED) + +1. **Predictable**: Name must allow prediction of value, type, and return value +2. **Contextual**: Add descriptive adjectives or nouns for context +3. **Clear**: Remove unnecessary words while maintaining clear meaning +4. **Concise**: Brief yet clearly convey role and purpose +5. **Consistent**: Use identical terms for identical intentions across entire codebase + +#### REQUIRED Naming Patterns + +**Action Functions - USE THESE PATTERNS:** + +``` +// Creation: create~(), add~(), push~(), insert~(), new~(), append~(), spawn~(), make~(), build~(), generate~() +// Retrieval: get~(), fetch~(), query~() +// Transformation: parse~(), split~(), transform~(), serialize~() +// Modification: update~(), mutation~() +// Deletion: delete~(), remove~() +// Communication: put~(), send~(), dispatch~(), receive~() +// Validation: validate~(), check~() +// Calculation: calc~(), compute~() +// Control: init~(), configure~(), start~(), stop~() +// Storage: save~(), store~() +// Logging: log~(), record~() +``` + +**Data Variables - USE THESE PATTERNS:** + +``` +// Quantities: count~, sum~, num~, min~, max~, total +// State: is~, has~, current~, selected~ +// Progressive/Past: ~ing, ~ed +// Information: ~name, ~title, ~desc, ~text, ~data +// Identifiers: ~ID, ~code, ~index, ~key +// Time: ~at, ~date +// Type: ~type +// Collections: ~s +// Others: item, temp, params, error +// Conversion: from(), of() +``` + +### ABSTRACTION RULES + +- **Data Abstraction**: Simplify data structure and processing methods +- **Process Abstraction**: Encapsulate complex logic into simple interfaces +- **Appropriate Level**: Do NOT over-abstract or under-abstract + +### MANDATORY CHECKLIST + +Before finalizing ANY code, you MUST verify: + +1. ✅ **Applied standard naming patterns** from above +2. ✅ **Organized code using 4 organization principles** +3. ✅ **Split complex logic into small functions** +4. ✅ **Code expresses intent without comments** (comments only when absolutely necessary) +5. ✅ **Maintained consistent formatting** + +### FORBIDDEN PRACTICES + +- ❌ Do NOT mix similar terms (`display` vs `show`) +- ❌ Do NOT write functions longer than 20 lines +- ❌ Do NOT repeat code patterns +- ❌ Do NOT use unclear or ambiguous names +- ❌ Do NOT violate naming consistency across codebase + +## COMPLIANCE REQUIREMENT + +ALL code output MUST comply with these rules. No exceptions. diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..7f2645968 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,96 @@ +--- +alwaysApply: true +--- + +# Clean Code Writing Rules + +## MANDATORY CODE WRITING RULES + +You MUST follow these rules when writing any code: + +### CORE DESIGN PRINCIPLES + +- **DRY**: NEVER repeat the same code +- **KISS**: Write code as simply as possible +- **YAGNI**: Do NOT write unnecessary code +- **Single Responsibility**: Functions MUST be under 20 lines and have ONE clear responsibility + +### CODE ORGANIZATION RULES + +Apply these 4 organization principles: + +- **Proximity**: Group related elements with blank lines +- **Commonality**: Group related functionality into functions +- **Similarity**: Use similar names and positions for similar roles +- **Continuity**: Arrange code in dependency order + +### NAMING REQUIREMENTS + +#### Naming Principles (ALL MUST BE FOLLOWED) + +1. **Predictable**: Name must allow prediction of value, type, and return value +2. **Contextual**: Add descriptive adjectives or nouns for context +3. **Clear**: Remove unnecessary words while maintaining clear meaning +4. **Concise**: Brief yet clearly convey role and purpose +5. **Consistent**: Use identical terms for identical intentions across entire codebase + +#### REQUIRED Naming Patterns + +**Action Functions - USE THESE PATTERNS:** + +``` +// Creation: create~(), add~(), push~(), insert~(), new~(), append~(), spawn~(), make~(), build~(), generate~() +// Retrieval: get~(), fetch~(), query~() +// Transformation: parse~(), split~(), transform~(), serialize~() +// Modification: update~(), mutation~() +// Deletion: delete~(), remove~() +// Communication: put~(), send~(), dispatch~(), receive~() +// Validation: validate~(), check~() +// Calculation: calc~(), compute~() +// Control: init~(), configure~(), start~(), stop~() +// Storage: save~(), store~() +// Logging: log~(), record~() +``` + +**Data Variables - USE THESE PATTERNS:** + +``` +// Quantities: count~, sum~, num~, min~, max~, total +// State: is~, has~, current~, selected~ +// Progressive/Past: ~ing, ~ed +// Information: ~name, ~title, ~desc, ~text, ~data +// Identifiers: ~ID, ~code, ~index, ~key +// Time: ~at, ~date +// Type: ~type +// Collections: ~s +// Others: item, temp, params, error +// Conversion: from(), of() +``` + +### ABSTRACTION RULES + +- **Data Abstraction**: Simplify data structure and processing methods +- **Process Abstraction**: Encapsulate complex logic into simple interfaces +- **Appropriate Level**: Do NOT over-abstract or under-abstract + +### MANDATORY CHECKLIST + +Before finalizing ANY code, you MUST verify: + +1. ✅ **Applied standard naming patterns** from above +2. ✅ **Organized code using 4 organization principles** +3. ✅ **Split complex logic into small functions** +4. ✅ **Code expresses intent without comments** (comments only when absolutely necessary) +5. ✅ **Maintained consistent formatting** + +### FORBIDDEN PRACTICES + +- ❌ Do NOT mix similar terms (`display` vs `show`) +- ❌ Do NOT write functions longer than 20 lines +- ❌ Do NOT repeat code patterns +- ❌ Do NOT use unclear or ambiguous names +- ❌ Do NOT violate naming consistency across codebase + +## COMPLIANCE REQUIREMENT + +ALL code output MUST comply with these rules. No exceptions. diff --git a/src/main.original.js b/src/main.original.js index 0fd12e7c9..95741dc31 100644 --- a/src/main.original.js +++ b/src/main.original.js @@ -1,17 +1,17 @@ -var prodList -var bonusPts = 0 -var stockInfo -var itemCnt -var lastSel -var sel -var addBtn -var totalAmt = 0 -var PRODUCT_ONE = 'p1' -var p2 = 'p2' -var product_3 = 'p3' -var p4 = "p4" -var PRODUCT_5 = `p5` -var cartDisp +var prodList; +var bonusPts = 0; +var stockInfo; +var itemCnt; +var lastSel; +var sel; +var addBtn; +var totalAmt = 0; +var PRODUCT_ONE = "p1"; +var p2 = "p2"; +var product_3 = "p3"; +var p4 = "p4"; +var PRODUCT_5 = `p5`; +var cartDisp; function main() { var root; var header; @@ -27,10 +27,42 @@ function main() { itemCnt = 0; lastSel = null; prodList = [ - {id: PRODUCT_ONE, name: '버그 없애는 키보드', val: 10000, originalVal: 10000, q: 50, onSale: false, suggestSale: false}, - {id: p2, name: '생산성 폭발 마우스', val: 20000, originalVal: 20000, q: 30, onSale: false, suggestSale: false}, - {id: product_3, name: "거북목 탈출 모니터암", val: 30000, originalVal: 30000, q: 20, onSale: false, suggestSale: false}, - {id: p4, name: "에러 방지 노트북 파우치", val: 15000, originalVal: 15000, q: 0, onSale: false, suggestSale: false}, + { + id: PRODUCT_ONE, + name: "버그 없애는 키보드", + val: 10000, + originalVal: 10000, + q: 50, + onSale: false, + suggestSale: false, + }, + { + id: p2, + name: "생산성 폭발 마우스", + val: 20000, + originalVal: 20000, + q: 30, + onSale: false, + suggestSale: false, + }, + { + id: product_3, + name: "거북목 탈출 모니터암", + val: 30000, + originalVal: 30000, + q: 20, + onSale: false, + suggestSale: false, + }, + { + id: p4, + name: "에러 방지 노트북 파우치", + val: 15000, + originalVal: 15000, + q: 0, + onSale: false, + suggestSale: false, + }, { id: PRODUCT_5, name: `코딩할 때 듣는 Lo-Fi 스피커`, @@ -38,42 +70,45 @@ function main() { originalVal: 25000, q: 10, onSale: false, - suggestSale: false - } - ] - var root = document.getElementById('app') - header = document.createElement('div'); - header.className = 'mb-8' + suggestSale: false, + }, + ]; + var root = document.getElementById("app"); + header = document.createElement("div"); + header.className = "mb-8"; header.innerHTML = `

🛒 Hanghae Online Store

Shopping Cart

🛍️ 0 items in cart

`; - sel = document.createElement('select'); - sel.id = 'product-select'; - gridContainer = document.createElement('div'); + sel = document.createElement("select"); + sel.id = "product-select"; + gridContainer = document.createElement("div"); leftColumn = document.createElement("div"); - leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto' - selectorContainer = document.createElement('div'); - selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200'; - sel.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3'; - gridContainer.className = 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden'; - addBtn = document.createElement('button'); - stockInfo = document.createElement('div'); - addBtn.id = 'add-to-cart'; - stockInfo.id = 'stock-status'; - stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line'; - addBtn.innerHTML = 'Add to Cart'; - addBtn.className = 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all'; + leftColumn["className"] = + "bg-white border border-gray-200 p-8 overflow-y-auto"; + selectorContainer = document.createElement("div"); + selectorContainer.className = "mb-6 pb-6 border-b border-gray-200"; + sel.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; + gridContainer.className = + "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; + addBtn = document.createElement("button"); + stockInfo = document.createElement("div"); + addBtn.id = "add-to-cart"; + stockInfo.id = "stock-status"; + stockInfo.className = "text-xs text-red-500 mt-3 whitespace-pre-line"; + addBtn.innerHTML = "Add to Cart"; + addBtn.className = + "w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all"; selectorContainer.appendChild(sel); selectorContainer.appendChild(addBtn); selectorContainer.appendChild(stockInfo); leftColumn.appendChild(selectorContainer); - cartDisp = document.createElement('div'); + cartDisp = document.createElement("div"); leftColumn.appendChild(cartDisp); - cartDisp.id = 'cart-items'; - rightColumn = document.createElement('div'); - rightColumn.className = 'bg-black text-white p-8 flex flex-col'; + cartDisp.id = "cart-items"; + rightColumn = document.createElement("div"); + rightColumn.className = "bg-black text-white p-8 flex flex-col"; rightColumn.innerHTML = `

Order Summary

@@ -103,28 +138,31 @@ function main() { Earn loyalty points with purchase.

`; - sum = rightColumn.querySelector('#cart-total'); - manualToggle = document.createElement('button'); + sum = rightColumn.querySelector("#cart-total"); + manualToggle = document.createElement("button"); manualToggle.onclick = function () { - manualOverlay.classList.toggle('hidden'); - manualColumn.classList.toggle('translate-x-full'); + manualOverlay.classList.toggle("hidden"); + manualColumn.classList.toggle("translate-x-full"); }; - manualToggle.className = 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50'; + manualToggle.className = + "fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50"; manualToggle.innerHTML = ` `; - manualOverlay = document.createElement('div'); - manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300'; + manualOverlay = document.createElement("div"); + manualOverlay.className = + "fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300"; manualOverlay.onclick = function (e) { if (e.target === manualOverlay) { - manualOverlay.classList.add('hidden'); - manualColumn.classList.add('translate-x-full'); + manualOverlay.classList.add("hidden"); + manualColumn.classList.add("translate-x-full"); } }; - manualColumn = document.createElement('div'); - manualColumn.className = 'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300'; + manualColumn = document.createElement("div"); + manualColumn.className = + "fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300"; manualColumn.innerHTML = ` + 1 - +
-
${itemToAdd.onSale || itemToAdd.suggestSale ? '₩' + itemToAdd.originalVal.toLocaleString() + ' ₩' + itemToAdd.val.toLocaleString() + '' : '₩' + itemToAdd.val.toLocaleString()}
- Remove +
${ + itemToAdd.onSale || itemToAdd.suggestSale + ? '₩' + + itemToAdd.originalVal.toLocaleString() + + ' ₩' + + itemToAdd.val.toLocaleString() + + "" + : "₩" + itemToAdd.val.toLocaleString() + }
+ Remove
`; cartDisp.appendChild(newItem); @@ -748,9 +880,12 @@ addBtn.addEventListener("click", function () { }); cartDisp.addEventListener("click", function (event) { var tgt = event.target; - if (tgt.classList.contains('quantity-change') || tgt.classList.contains("remove-item")) { + if ( + tgt.classList.contains("quantity-change") || + tgt.classList.contains("remove-item") + ) { var prodId = tgt.dataset.productId; - var itemElem = document.getElementById(prodId) + var itemElem = document.getElementById(prodId); var prod = null; for (var prdIdx = 0; prdIdx < prodList.length; prdIdx++) { @@ -759,9 +894,9 @@ cartDisp.addEventListener("click", function (event) { break; } } - if (tgt.classList.contains('quantity-change')) { + if (tgt.classList.contains("quantity-change")) { var qtyChange = parseInt(tgt.dataset.change); - var qtyElem = itemElem.querySelector('.quantity-number'); + var qtyElem = itemElem.querySelector(".quantity-number"); var currentQty = parseInt(qtyElem.textContent); var newQty = currentQty + qtyChange; if (newQty > 0 && newQty <= prod.q + currentQty) { @@ -771,10 +906,10 @@ cartDisp.addEventListener("click", function (event) { prod.q += currentQty; itemElem.remove(); } else { - alert('재고가 부족합니다.'); + alert("재고가 부족합니다."); } - } else if (tgt.classList.contains('remove-item')) { - var qtyElem = itemElem.querySelector('.quantity-number'); + } else if (tgt.classList.contains("remove-item")) { + var qtyElem = itemElem.querySelector(".quantity-number"); var remQty = parseInt(qtyElem.textContent); prod.q += remQty; itemElem.remove(); @@ -784,4 +919,4 @@ cartDisp.addEventListener("click", function (event) { handleCalculateCartStuff(); onUpdateSelectOptions(); } -}); \ No newline at end of file +}); From 4acd5bd56ef67294d10767fc7d12fbee209bc906 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 22:40:20 +0900 Subject: [PATCH 02/55] =?UTF-8?q?chore:=20prettier,=20eslint=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorrules | 96 --- .github/pull_request_template.md | 1 - .husky/pre-commit | 4 + .prettierignore | 29 + .prettierrc | 13 + README.md | 6 +- docs/README.md | 9 +- eslint.config.js | 95 +++ index.advanced.html | 68 +- index.basic.html | 68 +- index.html | 68 +- package.json | 26 +- pnpm-lock.yaml | 1002 +++++++++++++++++++++++++++++- src/setupTests.js | 2 +- vite.config.js | 8 +- 15 files changed, 1287 insertions(+), 208 deletions(-) delete mode 100644 .cursorrules create mode 100644 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 7f2645968..000000000 --- a/.cursorrules +++ /dev/null @@ -1,96 +0,0 @@ ---- -alwaysApply: true ---- - -# Clean Code Writing Rules - -## MANDATORY CODE WRITING RULES - -You MUST follow these rules when writing any code: - -### CORE DESIGN PRINCIPLES - -- **DRY**: NEVER repeat the same code -- **KISS**: Write code as simply as possible -- **YAGNI**: Do NOT write unnecessary code -- **Single Responsibility**: Functions MUST be under 20 lines and have ONE clear responsibility - -### CODE ORGANIZATION RULES - -Apply these 4 organization principles: - -- **Proximity**: Group related elements with blank lines -- **Commonality**: Group related functionality into functions -- **Similarity**: Use similar names and positions for similar roles -- **Continuity**: Arrange code in dependency order - -### NAMING REQUIREMENTS - -#### Naming Principles (ALL MUST BE FOLLOWED) - -1. **Predictable**: Name must allow prediction of value, type, and return value -2. **Contextual**: Add descriptive adjectives or nouns for context -3. **Clear**: Remove unnecessary words while maintaining clear meaning -4. **Concise**: Brief yet clearly convey role and purpose -5. **Consistent**: Use identical terms for identical intentions across entire codebase - -#### REQUIRED Naming Patterns - -**Action Functions - USE THESE PATTERNS:** - -``` -// Creation: create~(), add~(), push~(), insert~(), new~(), append~(), spawn~(), make~(), build~(), generate~() -// Retrieval: get~(), fetch~(), query~() -// Transformation: parse~(), split~(), transform~(), serialize~() -// Modification: update~(), mutation~() -// Deletion: delete~(), remove~() -// Communication: put~(), send~(), dispatch~(), receive~() -// Validation: validate~(), check~() -// Calculation: calc~(), compute~() -// Control: init~(), configure~(), start~(), stop~() -// Storage: save~(), store~() -// Logging: log~(), record~() -``` - -**Data Variables - USE THESE PATTERNS:** - -``` -// Quantities: count~, sum~, num~, min~, max~, total -// State: is~, has~, current~, selected~ -// Progressive/Past: ~ing, ~ed -// Information: ~name, ~title, ~desc, ~text, ~data -// Identifiers: ~ID, ~code, ~index, ~key -// Time: ~at, ~date -// Type: ~type -// Collections: ~s -// Others: item, temp, params, error -// Conversion: from(), of() -``` - -### ABSTRACTION RULES - -- **Data Abstraction**: Simplify data structure and processing methods -- **Process Abstraction**: Encapsulate complex logic into simple interfaces -- **Appropriate Level**: Do NOT over-abstract or under-abstract - -### MANDATORY CHECKLIST - -Before finalizing ANY code, you MUST verify: - -1. ✅ **Applied standard naming patterns** from above -2. ✅ **Organized code using 4 organization principles** -3. ✅ **Split complex logic into small functions** -4. ✅ **Code expresses intent without comments** (comments only when absolutely necessary) -5. ✅ **Maintained consistent formatting** - -### FORBIDDEN PRACTICES - -- ❌ Do NOT mix similar terms (`display` vs `show`) -- ❌ Do NOT write functions longer than 20 lines -- ❌ Do NOT repeat code patterns -- ❌ Do NOT use unclear or ambiguous names -- ❌ Do NOT violate naming consistency across codebase - -## COMPLIANCE REQUIREMENT - -ALL code output MUST comply with these rules. No exceptions. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 002f074f7..fe88b88f5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,7 +30,6 @@ - [ ] 변경한 구조와 코드가 기존의 모든 기능은 그대로 유지했는가? - [ ] (핵심!) 변경한 구조와 코드를 새로운 한번에 새로만들지 않고 점진적으로 개선했는가? - ## 과제 셀프회고 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..58b1861cc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm exec lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..89d23d5f4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ +pnpm-lock.yaml + +# Build outputs +dist/ +build/ + +# Cache +.cache/ +.vite/ + +# Logs +*.log + +# OS generated files +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ + +# Test coverage +coverage/ + +# Config files that should maintain their formatting +package-lock.json +yarn.lock \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..5fd80f367 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "quoteProps": "as-needed", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/README.md b/README.md index 66d05f176..74c430a6b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ 이번 과제는 더티코드를 클린코드의 형태로 개선을 하는 과제입니다. 주어진 테스트를 통과하면서 원래 기능과 동일한 동작을 하는 코드를 만들어주세요. basic과제는 제공되는 더티코드를 클린코드와 리팩토링 원칙에 입각해서 더 나은 코드로 만들어보세요. 주어진 테스트를 참고삼아 좋은 이름, 좋은 모양, 좋은 구조를 가지는 코드로 만들어 보세요. [목표] + - /basic 디렉토리에 있는 더티코드를 클린코드로 리팩토링 - 바닐라스크립트를 사용해서 작성된 코드를 클린코드와 리팩토링 원칙에 입각해서 더 나은 코드로 개선 - 추후 React + Typescript로 고도화 리팩토링을 할 예정이니 이를 꼭 염두에 두고 React로 바꿔도 코드가 많이 변경되지 않도록 리팩토링 @@ -17,27 +18,28 @@ - = 어플리케이션 요구사항을 모두 만족할 것 (/docs/PRD.md 참고) [과제의 취지] + - 나쁜 코드를 몸으로 충분히 느껴보고, 왜 나쁜 코드를 쓰는 것이 나쁜지 이해하기 - 나쁘지 않은 코드만으로 충분하지 않다는 것을 이해하기 - 더 좋은 코드란 무엇인지를 고민하며 나의 코드 취향과 코드 감각 이해하기 - ## 심확과제: 유지보수 하기 좋은 코드만들기 심화과제는 **기본과제에서 작성한 코드를 기술고도화를 하는 것입니다.** 바닐라 자바스크립트로 되어 있는 코드를 유지보수하기에 유리한 기술스택(React + Typescript)으로 고도화 리팩토링을 진행해주세요. 우리의 목표는 앞으로 유지보수를 더 잘할 수 있도록 하기 위함입니다. 최소 React와 Typescript를 이용한 코드로 개선해주세요. 그 밖의 기술선택과 폴더/파일 구조, 테스트 코드등은 자유입니다. [목표] + - /advanced 디렉토리에 있는 기본과제 코드를 React + Typescript로 고도화 리팩토링 - /basic 기본과제에서 작성한 코드를 복사해서 /advanced 디렉토리에서 고도화 리팩토링 진행 - 기본과제에서 작성한 코드를 React + Typescript로 고도화 리팩토링 [과제의 취지] + - React가 어떻게 유지보수하기 좋은 코드로 만들어주는지 이해하기 - Typescript가 어떻게 유지보수하기 좋은 코드로 만들어주는지 이해하기 - 내가 추구하는 좋은 코드와 React가 추구하는 코드와 어떻게 다른지 이해하기 - ## 요청사항 과제를 진행할 떄 AI를 쓰는 것은 자유입니다. 오히려 AI를 활용하는 연습을 해야하는게 시대의 흐름이겠지요. diff --git a/docs/README.md b/docs/README.md index b9cfd2b6b..516c3b057 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,26 +5,31 @@ ## 📄 문서 목록 ### 1. [01-PRD.md](./01-PRD.md) - 🎯 **필수** + - **장바구니 기능 상세 요구사항 명세서** - 상품 정보, 할인 정책, 포인트 시스템, UI/UX 요구사항 등 전체 비즈니스 규칙 정의 - 과제 수행 시 반드시 참고해야 할 핵심 문서 ### 2. [02-dirty-code-analysis.md](./02-dirty-code-analysis.md) - 🎯 **필수** + - **현재 더티코드의 문제점 상세 분석** - 10가지 카테고리별로 코드의 문제점을 구체적으로 파악 - 리팩토링 전 현재 상태를 이해하는데 중요 ### 3. [03-cart-tailwind-evaluation.md](./03-cart-tailwind-evaluation.md) - ✅ **권장** + - **더티코드 종합 평가 보고서** - 변수 인접성, 코드 응집도, 중복 등 10가지 평가 항목으로 분석 - 리팩토링 우선순위 결정에 도움 ### 4. [04-practical-clean-code-guide.md](./04-practical-clean-code-guide.md) - ✅ **권장** + - **실전 클린코드 작성 가이드** - 10가지 실전 원칙과 구체적인 예시 제공 - 리팩토링 시 실제로 적용할 수 있는 행동 지침 ### 5. [05-clean-code-theory-lesson.md](./05-clean-code-theory-lesson.md) - 📖 **참고** + - **클린코드 이론과 실습 가이드** - 더티코드 징후, 클린코드 원칙, 리팩토링 전략 - 이론적 배경을 이해하고 싶을 때 참고 @@ -32,12 +37,14 @@ ## 🗺️ 문서 활용 가이드 ### 과제 진행 순서 + 1. **01-PRD.md**를 먼저 읽어 요구사항을 완전히 이해 2. **02-dirty-code-analysis.md**로 현재 코드의 문제점 파악 3. **04-practical-clean-code-guide.md**를 참고하여 리팩토링 계획 수립 4. 필요 시 **03-cart-tailwind-evaluation.md**와 **05-clean-code-theory-lesson.md** 참고 ### 우선순위 + - 🎯 **필수**: 과제 수행에 반드시 필요한 문서 - ✅ **권장**: 리팩토링 품질 향상에 도움이 되는 문서 -- 📖 **참고**: 추가적인 학습이나 이해를 위한 문서 \ No newline at end of file +- 📖 **참고**: 추가적인 학습이나 이해를 위한 문서 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..df6f569a0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,95 @@ +import js from "@eslint/js"; +import prettier from "eslint-plugin-prettier"; +import prettierConfig from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + prettierConfig, + { + ignores: [ + "node_modules/**", + "dist/**", + "build/**", + ".cache/**", + ".vite/**", + "coverage/**", + "pnpm-lock.yaml", + ], + }, + { + files: ["**/*.js"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + console: "readonly", + document: "readonly", + window: "readonly", + alert: "readonly", + setTimeout: "readonly", + setInterval: "readonly", + Math: "readonly", + Date: "readonly", + parseInt: "readonly", + location: "readonly", + }, + }, + plugins: { + prettier, + }, + rules: { + // Prettier 규칙 + "prettier/prettier": "error", + + // 변수 관련 (일부 완화) + "no-var": "warn", + "prefer-const": "warn", + "no-unused-vars": "warn", + "no-redeclare": "warn", + + // 함수 관련 + "prefer-arrow-callback": "warn", + + // 코드 품질 + eqeqeq: ["error", "always"], + "no-console": "warn", + "no-alert": "warn", + "no-empty": "warn", + + // 코딩 스타일 + "consistent-return": "error", + curly: ["error", "all"], + "no-else-return": "warn", + + // 복잡도 제한 (완화) + "max-len": [ + "warn", + { + code: 120, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + }, + ], + "max-lines-per-function": ["warn", { max: 100 }], + complexity: ["warn", { max: 15 }], + }, + }, + { + files: ["**/*.test.js"], + languageOptions: { + globals: { + describe: "readonly", + it: "readonly", + expect: "readonly", + beforeEach: "readonly", + afterEach: "readonly", + vi: "readonly", + }, + }, + rules: { + "no-console": "off", + "max-lines-per-function": "off", + }, + }, +]; diff --git a/index.advanced.html b/index.advanced.html index a070c3355..4af4b87d6 100644 --- a/index.advanced.html +++ b/index.advanced.html @@ -1,32 +1,40 @@ - + - - - - Hanghae Shopping Cart - - - + - - -
- -
- - - \ No newline at end of file + }; + + + +
+ +
+ + + diff --git a/index.basic.html b/index.basic.html index 1c8cd3b16..ac4e2954b 100644 --- a/index.basic.html +++ b/index.basic.html @@ -1,32 +1,40 @@ - + - - - - Hanghae Shopping Cart - - - + - - -
- -
- - - \ No newline at end of file + }; + + + +
+ +
+ + + diff --git a/index.html b/index.html index da107a22e..157dc1360 100644 --- a/index.html +++ b/index.html @@ -1,32 +1,40 @@ - + - - - - Hanghae Shopping Cart - - - + - - -
- -
- - - \ No newline at end of file + }; + + + +
+ +
+ + + diff --git a/package.json b/package.json index 121aab60d..350615187 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,38 @@ "test": "vitest", "test:basic": "vitest basic.test.js", "test:advanced": "vitest advanced.test.js", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "lint": "eslint . --ext .js", + "lint:fix": "eslint . --ext .js --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check": "pnpm run lint && pnpm run format:check", + "fix": "pnpm run lint:fix && pnpm run format", + "prepare": "husky" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.3", + "globals": "^16.3.0", + "husky": "^9.1.7", "jsdom": "^26.1.0", + "lint-staged": "^16.1.2", + "prettier": "^3.6.2", "vite": "^7.0.5", "vitest": "^3.2.4" + }, + "lint-staged": { + "*.js": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md}": [ + "prettier --write" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49a2140ea..06dd62a61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^9.32.0 + version: 9.32.0 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -17,15 +20,36 @@ importers: '@vitest/ui': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) + eslint: + specifier: ^9.32.0 + version: 9.32.0 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.32.0) + eslint-plugin-prettier: + specifier: ^5.5.3 + version: 5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2) + globals: + specifier: ^16.3.0 + version: 16.3.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 jsdom: specifier: ^26.1.0 version: 26.1.0 + lint-staged: + specifier: ^16.1.2 + version: 16.1.2 + prettier: + specifier: ^3.6.2 + version: 3.6.2 vite: specifier: ^7.0.5 - version: 7.0.5 + version: 7.0.5(yaml@2.8.0) vitest: specifier: ^3.2.4 - version: 3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0) + version: 3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0)(yaml@2.8.0) packages: @@ -231,9 +255,71 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.32.0': + resolution: {integrity: sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.4': + resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -363,6 +449,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -397,14 +486,35 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -413,6 +523,13 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -424,10 +541,24 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} @@ -440,10 +571,22 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -451,6 +594,20 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -478,6 +635,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -488,10 +648,17 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -500,13 +667,94 @@ packages: engines: {node: '>=18'} hasBin: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.3: + resolution: {integrity: sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.32.0: + resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fdir@6.4.6: resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: @@ -518,6 +766,22 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -526,6 +790,22 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -542,23 +822,67 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -568,9 +892,49 @@ packages: canvas: optional: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lint-staged@16.1.2: + resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} @@ -584,10 +948,21 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -595,17 +970,52 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nano-spawn@1.0.2: + resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==} + engines: {node: '>=20.17'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -616,14 +1026,36 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -639,6 +1071,17 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.45.1: resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -654,13 +1097,33 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sirv@3.0.1: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -671,10 +1134,26 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} @@ -685,6 +1164,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -714,6 +1197,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -726,6 +1213,13 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -819,11 +1313,24 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -843,6 +1350,15 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: '@adobe/css-tools@4.4.3': {} @@ -963,8 +1479,67 @@ snapshots: '@esbuild/win32-x64@0.25.8': optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0)': + dependencies: + eslint: 9.32.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.32.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.4': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@jridgewell/sourcemap-codec@1.5.4': {} + '@pkgr/core@0.2.9': {} + '@polka/url@1.0.0-next.29': {} '@rollup/rollup-android-arm-eabi@4.45.1': @@ -1062,6 +1637,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/json-schema@7.0.15': {} + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -1070,13 +1647,13 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.5)': + '@vitest/mocker@3.2.4(vite@7.0.5(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.5 + vite: 7.0.5(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -1107,7 +1684,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0) + vitest: 3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -1115,16 +1692,39 @@ snapshots: loupe: 3.2.0 tinyrainbow: 2.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + agent-base@7.1.4: {} + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} + ansi-regex@6.1.0: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} + + argparse@2.0.1: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -1133,8 +1733,21 @@ snapshots: assertion-error@2.0.1: {} + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + cac@6.7.14: {} + callsites@3.1.0: {} + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -1153,14 +1766,37 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.4.1: {} + check-error@2.1.1: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + colorette@2.0.20: {} + + commander@14.0.0: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css.escape@1.5.1: {} cssstyle@4.6.0: @@ -1181,14 +1817,20 @@ snapshots: deep-eql@5.0.2: {} + deep-is@0.1.4: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + emoji-regex@10.4.0: {} + entities@6.0.1: {} + environment@1.1.0: {} + es-module-lexer@1.7.0: {} esbuild@0.25.8: @@ -1220,23 +1862,143 @@ snapshots: '@esbuild/win32-ia32': 0.25.8 '@esbuild/win32-x64': 0.25.8 + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.32.0): + dependencies: + eslint: 9.32.0 + + eslint-plugin-prettier@5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2): + dependencies: + eslint: 9.32.0 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.32.0) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.32.0: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.15.1 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.32.0 + '@eslint/plugin-kit': 0.3.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + expect-type@1.2.2: {} + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 fflate@0.8.2: {} + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + flatted@3.3.3: {} fsevents@2.3.3: optional: true + get-east-asian-width@1.3.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.3.0: {} + has-flag@4.0.0: {} html-encoding-sniffer@4.0.0: @@ -1257,18 +2019,49 @@ snapshots: transitivePeerDependencies: - supports-color + husky@9.1.7: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + is-extglob@2.1.1: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isexe@2.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0: dependencies: cssstyle: 4.6.0 @@ -1296,8 +2089,63 @@ snapshots: - supports-color - utf-8-validate + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lint-staged@16.1.2: + dependencies: + chalk: 5.4.1 + commander: 14.0.0 + debug: 4.4.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + nano-spawn: 1.0.2 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.0 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + lodash@4.17.21: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + loupe@3.2.0: {} lru-cache@10.4.3: {} @@ -1308,34 +2156,90 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + mrmime@2.0.1: {} ms@2.1.3: {} + nano-spawn@1.0.2: {} + nanoid@3.3.11: {} + natural-compare@1.4.0: {} + nwsapi@2.2.20: {} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse5@7.3.0: dependencies: entities: 6.0.1 + path-exists@4.0.0: {} + + path-key@3.1.1: {} + pathe@2.0.3: {} pathval@2.0.1: {} picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.3: {} + pidtree@0.6.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -1351,6 +2255,15 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + resolve-from@4.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + rollup@4.45.1: dependencies: '@types/estree': 1.0.8 @@ -1385,24 +2298,56 @@ snapshots: dependencies: xmlchars: 2.2.0 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + source-map-js@1.2.1: {} stackback@0.0.2: {} std-env@3.9.0: {} + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + strip-json-comments@3.1.1: {} + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 @@ -1413,6 +2358,10 @@ snapshots: symbol-tree@3.2.4: {} + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1434,6 +2383,10 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + totalist@3.0.1: {} tough-cookie@5.1.2: @@ -1444,13 +2397,21 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.2.4: + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.5 + vite: 7.0.5(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -1465,7 +2426,7 @@ snapshots: - tsx - yaml - vite@7.0.5: + vite@7.0.5(yaml@2.8.0): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -1475,12 +2436,13 @@ snapshots: tinyglobby: 0.2.14 optionalDependencies: fsevents: 2.3.3 + yaml: 2.8.0 - vitest@3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0): + vitest@3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.5) + '@vitest/mocker': 3.2.4(vite@7.0.5(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -1498,8 +2460,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.5 - vite-node: 3.2.4 + vite: 7.0.5(yaml@2.8.0) + vite-node: 3.2.4(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@vitest/ui': 3.2.4(vitest@3.2.4) @@ -1535,13 +2497,29 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + ws@8.18.3: {} xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} + + yaml@2.8.0: {} + + yocto-queue@0.1.0: {} diff --git a/src/setupTests.js b/src/setupTests.js index 7b0828bfa..d0de870dc 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1 +1 @@ -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; diff --git a/vite.config.js b/vite.config.js index dc9c66995..496383c53 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,9 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, - environment: 'jsdom', - setupFiles: 'src/setupTests.js' + environment: "jsdom", + setupFiles: "src/setupTests.js", }, -}) +}); From 86a8ba2cb3e0a63047ee19e7c60eb3ae59f4adde Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 22:40:40 +0900 Subject: [PATCH 03/55] =?UTF-8?q?docs:=20docs=EC=97=90=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EA=B7=9C=EC=B9=99=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C,=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/assignment.mdc | 13 + .cursor/rules/clean-code.mdc | 2 +- .cursor/rules/toss-frontend-rules.mdc | 1381 +++++++++++++++++ docs/01-PRD.md | 48 +- docs/02-dirty-code-analysis.md | 144 +- docs/03-cart-tailwind-evaluation.md | 105 +- docs/04-practical-clean-code-guide.md | 125 +- docs/05-clean-code-theory-lesson.md | 75 +- docs/06-clean-code.md | 489 ++++++ ...40\353\247\201\352\263\204\355\232\215.md" | 306 ++++ 10 files changed, 2525 insertions(+), 163 deletions(-) create mode 100644 .cursor/rules/assignment.mdc create mode 100644 .cursor/rules/toss-frontend-rules.mdc create mode 100644 docs/06-clean-code.md create mode 100644 "docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" diff --git a/.cursor/rules/assignment.mdc b/.cursor/rules/assignment.mdc new file mode 100644 index 000000000..b3a97308c --- /dev/null +++ b/.cursor/rules/assignment.mdc @@ -0,0 +1,13 @@ +--- +alwaysApply: true +--- + +# 절대 반드시 지켜야만 하는 절대적 원칙 (안지키면 다시해야함) + +- 코드의 동작이나 구현이 바뀌면 안되고 반드시 구조 변경(리팩토링)만 해야해야만해 +- 공통으로 쓰이는 파일만 공통 폴더에 넣어두고, 비즈니스 로직이 담긴 경우, 관심사끼리 묶어 폴더로 관리해야해 +- src/basic/tests/basic.test.js, src/advanced/tests/advanced.test.js 테스트 코드가 모두 하나도 빠짐없이 통과해야해 (테스트는 npx vitest run 으로 watch 가 발생하지 않도록 해) +- 테스트 코드 검증 여부는 main.basic.js 를 기준으로 검사해야만해. origin 파일은 의미없어. + +-> 결과파일은 main.basic.js 에 적용해줘 +-> 작업 후 마지막으로 절대 원칙이 지켜졌는지 한번 더 컴토 후 올바르게 고치고 알려줘 \ No newline at end of file diff --git a/.cursor/rules/clean-code.mdc b/.cursor/rules/clean-code.mdc index b70a21c2f..7f2645968 100644 --- a/.cursor/rules/clean-code.mdc +++ b/.cursor/rules/clean-code.mdc @@ -1,5 +1,5 @@ --- -alwaysApply: false +alwaysApply: true --- # Clean Code Writing Rules diff --git a/.cursor/rules/toss-frontend-rules.mdc b/.cursor/rules/toss-frontend-rules.mdc new file mode 100644 index 000000000..fc9cbd06c --- /dev/null +++ b/.cursor/rules/toss-frontend-rules.mdc @@ -0,0 +1,1381 @@ +--- +alwaysApply: true +--- + + +# Frontend Design Guideline + +This document summarizes key frontend design principles and rules, showcasing +recommended patterns. Follow these guidelines when writing frontend code. + +# Readability + +Improving the clarity and ease of understanding code. + +## Naming Magic Numbers + +**Rule:** Replace magic numbers with named constants for clarity. + +**Reasoning:** + +- Improves clarity by giving semantic meaning to unexplained values. +- Enhances maintainability. + +#### Recommended Pattern: + +```typescript +const ANIMATION_DELAY_MS = 300; + +async function onLikeClick() { + await postLike(url); + await delay(ANIMATION_DELAY_MS); // Clearly indicates waiting for animation + await refetchPostLike(); +} +``` + +## Abstracting Implementation Details + +**Rule:** Abstract complex logic/interactions into dedicated components/HOCs. + +**Reasoning:** + +- Reduces cognitive load by separating concerns. +- Improves readability, testability, and maintainability of components. + +#### Recommended Pattern 1: Auth Guard + +(Login check abstracted to a wrapper/guard component) + +```tsx +// App structure +function App() { + return ( + + {" "} + {/* Wrapper handles auth check */} + + + ); +} + +// AuthGuard component encapsulates the check/redirect logic +function AuthGuard({ children }) { + const status = useCheckLoginStatus(); + useEffect(() => { + if (status === "LOGGED_IN") { + location.href = "/home"; + } + }, [status]); + + // Render children only if not logged in, otherwise render null (or loading) + return status !== "LOGGED_IN" ? children : null; +} + +// LoginStartPage is now simpler, focused only on login UI/logic +function LoginStartPage() { + // ... login related logic ONLY ... + return <>{/* ... login related components ... */}; +} +``` + +#### Recommended Pattern 2: Dedicated Interaction Component + +(Dialog logic abstracted into a dedicated `InviteButton` component) + +```tsx +export function FriendInvitation() { + const { data } = useQuery(/* ... */); + + return ( + <> + {/* Use the dedicated button component */} + + {/* ... other UI ... */} + + ); +} + +// InviteButton handles the confirmation flow internally +function InviteButton({ name }) { + const handleClick = async () => { + const canInvite = await overlay.openAsync(({ isOpen, close }) => ( + + )); + + if (canInvite) { + await sendPush(); + } + }; + + return ; +} +``` + +## Separating Code Paths for Conditional Rendering + +**Rule:** Separate significantly different conditional UI/logic into distinct +components. + +**Reasoning:** + +- Improves readability by avoiding complex conditionals within one component. +- Ensures each specialized component has a clear, single responsibility. + +#### Recommended Pattern: + +(Separate components for each role) + +```tsx +function SubmitButton() { + const isViewer = useRole() === "viewer"; + + // Delegate rendering to specialized components + return isViewer ? : ; +} + +// Component specifically for the 'viewer' role +function ViewerSubmitButton() { + return Submit; +} + +// Component specifically for the 'admin' (or non-viewer) role +function AdminSubmitButton() { + useEffect(() => { + showAnimation(); // Animation logic isolated here + }, []); + + return ; +} +``` + +## Simplifying Complex Ternary Operators + +**Rule:** Replace complex/nested ternaries with `if`/`else` or IIFEs for +readability. + +**Reasoning:** + +- Makes conditional logic easier to follow quickly. +- Improves overall code maintainability. + +#### Recommended Pattern: + +(Using an IIFE with `if` statements) + +```typescript +const status = (() => { + if (ACondition && BCondition) return "BOTH"; + if (ACondition) return "A"; + if (BCondition) return "B"; + return "NONE"; +})(); +``` + +## Reducing Eye Movement (Colocating Simple Logic) + +**Rule:** Colocate simple, localized logic or use inline definitions to reduce +context switching. + +**Reasoning:** + +- Allows top-to-bottom reading and faster comprehension. +- Reduces cognitive load from context switching (eye movement). + +#### Recommended Pattern A: Inline `switch` + +```tsx +function Page() { + const user = useUser(); + + // Logic is directly visible here + switch (user.role) { + case "admin": + return ( +
+ + +
+ ); + case "viewer": + return ( +
+ {/* Example for viewer */} + +
+ ); + default: + return null; + } +} +``` + +#### Recommended Pattern B: Colocated simple policy object + +```tsx +function Page() { + const user = useUser(); + // Simple policy defined right here, easy to see + const policy = { + admin: { canInvite: true, canView: true }, + viewer: { canInvite: false, canView: true }, + }[user.role]; + + // Ensure policy exists before accessing properties if role might not match + if (!policy) return null; + + return ( +
+ + +
+ ); +} +``` + +## Naming Complex Conditions + +**Rule:** Assign complex boolean conditions to named variables. + +**Reasoning:** + +- Makes the _meaning_ of the condition explicit. +- Improves readability and self-documentation by reducing cognitive load. + +#### Recommended Pattern: + +(Conditions assigned to named variables) + +```typescript +const matchedProducts = products.filter((product) => { + // Check if product belongs to the target category + const isSameCategory = product.categories.some( + (category) => category.id === targetCategory.id + ); + + // Check if any product price falls within the desired range + const isPriceInRange = product.prices.some( + (price) => price >= minPrice && price <= maxPrice + ); + + // The overall condition is now much clearer + return isSameCategory && isPriceInRange; +}); +``` + +**Guidance:** Name conditions when the logic is complex, reused, or needs unit +testing. Avoid naming very simple, single-use conditions. + +# Predictability + +Ensuring code behaves as expected based on its name, parameters, and context. + +## Standardizing Return Types + +**Rule:** Use consistent return types for similar functions/hooks. + +**Reasoning:** + +- Improves code predictability; developers can anticipate return value shapes. +- Reduces confusion and potential errors from inconsistent types. + +#### Recommended Pattern 1: API Hooks (React Query) + +```typescript +// Always return the Query object +import { useQuery, UseQueryResult } from "@tanstack/react-query"; + +// Assuming fetchUser returns Promise +function useUser(): UseQueryResult { + const query = useQuery({ queryKey: ["user"], queryFn: fetchUser }); + return query; +} + +// Assuming fetchServerTime returns Promise +function useServerTime(): UseQueryResult { + const query = useQuery({ + queryKey: ["serverTime"], + queryFn: fetchServerTime, + }); + return query; +} +``` + +#### Recommended Pattern 2: Validation Functions + +(Using a consistent type, ideally a Discriminated Union) + +```typescript +type ValidationResult = { ok: true } | { ok: false; reason: string }; + +function checkIsNameValid(name: string): ValidationResult { + if (name.length === 0) return { ok: false, reason: "Name cannot be empty." }; + if (name.length >= 20) + return { ok: false, reason: "Name cannot be longer than 20 characters." }; + return { ok: true }; +} + +function checkIsAgeValid(age: number): ValidationResult { + if (!Number.isInteger(age)) + return { ok: false, reason: "Age must be an integer." }; + if (age < 18) return { ok: false, reason: "Age must be 18 or older." }; + if (age > 99) return { ok: false, reason: "Age must be 99 or younger." }; + return { ok: true }; +} + +// Usage allows safe access to 'reason' only when ok is false +const nameValidation = checkIsNameValid(name); +if (!nameValidation.ok) { + console.error(nameValidation.reason); +} +``` + +## Revealing Hidden Logic (Single Responsibility) + +**Rule:** Avoid hidden side effects; functions should only perform actions +implied by their signature (SRP). + +**Reasoning:** + +- Leads to predictable behavior without unintended side effects. +- Creates more robust, testable code through separation of concerns (SRP). + +#### Recommended Pattern: + +```typescript +// Function *only* fetches balance +async function fetchBalance(): Promise { + const balance = await http.get("..."); + return balance; +} + +// Caller explicitly performs logging where needed +async function handleUpdateClick() { + const balance = await fetchBalance(); // Fetch + logging.log("balance_fetched"); // Log (explicit action) + await syncBalance(balance); // Another action +} +``` + +## Using Unique and Descriptive Names (Avoiding Ambiguity) + +**Rule:** Use unique, descriptive names for custom wrappers/functions to avoid +ambiguity. + +**Reasoning:** + +- Avoids ambiguity and enhances predictability. +- Allows developers to understand specific actions (e.g., adding auth) directly + from the name. + +#### Recommended Pattern: + +```typescript +// In httpService.ts - Clearer module name +import { http as httpLibrary } from "@some-library/http"; + +export const httpService = { + // Unique module name + async getWithAuth(url: string) { + // Descriptive function name + const token = await fetchToken(); + return httpLibrary.get(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + }, +}; + +// In fetchUser.ts - Usage clearly indicates auth +import { httpService } from "./httpService"; +export async function fetchUser() { + // Name 'getWithAuth' makes the behavior explicit + return await httpService.getWithAuth("..."); +} +``` + +# Cohesion + +Keeping related code together and ensuring modules have a well-defined, single +purpose. + +## Considering Form Cohesion + +**Rule:** Choose field-level or form-level cohesion based on form requirements. + +**Reasoning:** + +- Balances field independence (field-level) vs. form unity (form-level). +- Ensures related form logic is appropriately grouped based on requirements. + +#### Recommended Pattern (Field-Level Example): + +```tsx +// Each field uses its own `validate` function +import { useForm } from "react-hook-form"; + +export function Form() { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + /* defaultValues etc. */ + }); + + const onSubmit = handleSubmit((formData) => { + console.log("Form submitted:", formData); + }); + + return ( +
+
+ + value.trim() === "" ? "Please enter your name." : true, // Example validation + })} + placeholder="Name" + /> + {errors.name &&

{errors.name.message}

} +
+
+ + /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value) + ? true + : "Invalid email address.", // Example validation + })} + placeholder="Email" + /> + {errors.email &&

{errors.email.message}

} +
+ +
+ ); +} +``` + +#### Recommended Pattern (Form-Level Example): + +```tsx +// A single schema defines validation for the whole form +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +const schema = z.object({ + name: z.string().min(1, "Please enter your name."), + email: z.string().min(1, "Please enter your email.").email("Invalid email."), +}); + +export function Form() { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { name: "", email: "" }, + }); + + const onSubmit = handleSubmit((formData) => { + console.log("Form submitted:", formData); + }); + + return ( +
+
+ + {errors.name &&

{errors.name.message}

} +
+
+ + {errors.email &&

{errors.email.message}

} +
+ +
+ ); +} +``` + +**Guidance:** Choose **field-level** for independent validation, async checks, +or reusable fields. Choose **form-level** for related fields, wizard forms, or +interdependent validation. + +## Organizing Code by Feature/Domain + +**Rule:** Organize directories by feature/domain, not just by code type. + +**Reasoning:** + +- Increases cohesion by keeping related files together. +- Simplifies feature understanding, development, maintenance, and deletion. + +#### Recommended Pattern: + +(Organized by feature/domain) + +``` +src/ +├── components/ # Shared/common components +├── hooks/ # Shared/common hooks +├── utils/ # Shared/common utils +├── domains/ +│ ├── user/ +│ │ ├── components/ +│ │ │ └── UserProfileCard.tsx +│ │ ├── hooks/ +│ │ │ └── useUser.ts +│ │ └── index.ts # Optional barrel file +│ ├── product/ +│ │ ├── components/ +│ │ │ └── ProductList.tsx +│ │ ├── hooks/ +│ │ │ └── useProducts.ts +│ │ └── ... +│ └── order/ +│ ├── components/ +│ │ └── OrderSummary.tsx +│ ├── hooks/ +│ │ └── useOrder.ts +│ └── ... +└── App.tsx +``` + +## Relating Magic Numbers to Logic + +**Rule:** Define constants near related logic or ensure names link them clearly. + +**Reasoning:** + +- Improves cohesion by linking constants to the logic they represent. +- Prevents silent failures caused by updating logic without updating related + constants. + +#### Recommended Pattern: + +```typescript +// Constant clearly named and potentially defined near animation logic +const ANIMATION_DELAY_MS = 300; + +async function onLikeClick() { + await postLike(url); + // Delay uses the constant, maintaining the link to the animation + await delay(ANIMATION_DELAY_MS); + await refetchPostLike(); +} +``` + +_Ensure constants are maintained alongside the logic they depend on or clearly +named to show the relationship._ + +# Coupling + +Minimizing dependencies between different parts of the codebase. + +## Balancing Abstraction and Coupling (Avoiding Premature Abstraction) + +**Rule:** Avoid premature abstraction of duplicates if use cases might diverge; +prefer lower coupling. + +**Reasoning:** + +- Avoids tight coupling from forcing potentially diverging logic into one + abstraction. +- Allowing some duplication can improve decoupling and maintainability when + future needs are uncertain. + +#### Guidance: + +Before abstracting, consider if the logic is truly identical and likely to +_stay_ identical across all use cases. If divergence is possible (e.g., +different pages needing slightly different behavior from a shared hook like +`useOpenMaintenanceBottomSheet`), keeping the logic separate initially (allowing +duplication) can lead to more maintainable, decoupled code. Discuss trade-offs +with the team. _[No specific 'good' code example here, as the recommendation is +situational awareness rather than a single pattern]._ + +## Scoping State Management (Avoiding Overly Broad Hooks) + +**Rule:** Break down broad state management into smaller, focused +hooks/contexts. + +**Reasoning:** + +- Reduces coupling by ensuring components only depend on necessary state slices. +- Improves performance by preventing unnecessary re-renders from unrelated state + changes. + +#### Recommended Pattern: + +(Focused hooks, low coupling) + +```typescript +// Hook specifically for cardId query param +import { useQueryParam, NumberParam } from "use-query-params"; +import { useCallback } from "react"; + +export function useCardIdQueryParam() { + // Assuming 'query' provides the raw param value + const [cardIdParam, setCardIdParam] = useQueryParam("cardId", NumberParam); + + const setCardId = useCallback( + (newCardId: number | undefined) => { + setCardIdParam(newCardId, "replaceIn"); // Or 'push' depending on desired history behavior + }, + [setCardIdParam] + ); + + // Provide a stable return tuple + return [cardIdParam ?? undefined, setCardId] as const; +} + +// Separate hook for date range, etc. +// export function useDateRangeQueryParam() { /* ... */ } +``` + +Components now only import and use `useCardIdQueryParam` if they need `cardId`, +decoupling them from date range state, etc. + +## Eliminating Props Drilling with Composition + +**Rule:** Use Component Composition instead of Props Drilling. + +**Reasoning:** + +- Significantly reduces coupling by eliminating unnecessary intermediate + dependencies. +- Makes refactoring easier and clarifies data flow in flatter component trees. + +#### Recommended Pattern: + +```tsx +import React, { useState } from "react"; + +// Assume Modal, Input, Button, ItemEditList components exist + +function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) { + const [keyword, setKeyword] = useState(""); + + // Render children directly within Modal, passing props only where needed + return ( + + {/* Input and Button rendered directly */} +
+ setKeyword(e.target.value)} // State managed here + placeholder="Search items..." + /> + +
+ {/* ItemEditList rendered directly, gets props it needs */} + +
+ ); +} + +// The intermediate ItemEditBody component is eliminated, reducing coupling. +``` +# Frontend Design Guideline + +This document summarizes key frontend design principles and rules, showcasing +recommended patterns. Follow these guidelines when writing frontend code. + +# Readability + +Improving the clarity and ease of understanding code. + +## Naming Magic Numbers + +**Rule:** Replace magic numbers with named constants for clarity. + +**Reasoning:** + +- Improves clarity by giving semantic meaning to unexplained values. +- Enhances maintainability. + +#### Recommended Pattern: + +```typescript +const ANIMATION_DELAY_MS = 300; + +async function onLikeClick() { + await postLike(url); + await delay(ANIMATION_DELAY_MS); // Clearly indicates waiting for animation + await refetchPostLike(); +} +``` + +## Abstracting Implementation Details + +**Rule:** Abstract complex logic/interactions into dedicated components/HOCs. + +**Reasoning:** + +- Reduces cognitive load by separating concerns. +- Improves readability, testability, and maintainability of components. + +#### Recommended Pattern 1: Auth Guard + +(Login check abstracted to a wrapper/guard component) + +```tsx +// App structure +function App() { + return ( + + {" "} + {/* Wrapper handles auth check */} + + + ); +} + +// AuthGuard component encapsulates the check/redirect logic +function AuthGuard({ children }) { + const status = useCheckLoginStatus(); + useEffect(() => { + if (status === "LOGGED_IN") { + location.href = "/home"; + } + }, [status]); + + // Render children only if not logged in, otherwise render null (or loading) + return status !== "LOGGED_IN" ? children : null; +} + +// LoginStartPage is now simpler, focused only on login UI/logic +function LoginStartPage() { + // ... login related logic ONLY ... + return <>{/* ... login related components ... */}; +} +``` + +#### Recommended Pattern 2: Dedicated Interaction Component + +(Dialog logic abstracted into a dedicated `InviteButton` component) + +```tsx +export function FriendInvitation() { + const { data } = useQuery(/* ... */); + + return ( + <> + {/* Use the dedicated button component */} + + {/* ... other UI ... */} + + ); +} + +// InviteButton handles the confirmation flow internally +function InviteButton({ name }) { + const handleClick = async () => { + const canInvite = await overlay.openAsync(({ isOpen, close }) => ( + + )); + + if (canInvite) { + await sendPush(); + } + }; + + return ; +} +``` + +## Separating Code Paths for Conditional Rendering + +**Rule:** Separate significantly different conditional UI/logic into distinct +components. + +**Reasoning:** + +- Improves readability by avoiding complex conditionals within one component. +- Ensures each specialized component has a clear, single responsibility. + +#### Recommended Pattern: + +(Separate components for each role) + +```tsx +function SubmitButton() { + const isViewer = useRole() === "viewer"; + + // Delegate rendering to specialized components + return isViewer ? : ; +} + +// Component specifically for the 'viewer' role +function ViewerSubmitButton() { + return Submit; +} + +// Component specifically for the 'admin' (or non-viewer) role +function AdminSubmitButton() { + useEffect(() => { + showAnimation(); // Animation logic isolated here + }, []); + + return ; +} +``` + +## Simplifying Complex Ternary Operators + +**Rule:** Replace complex/nested ternaries with `if`/`else` or IIFEs for +readability. + +**Reasoning:** + +- Makes conditional logic easier to follow quickly. +- Improves overall code maintainability. + +#### Recommended Pattern: + +(Using an IIFE with `if` statements) + +```typescript +const status = (() => { + if (ACondition && BCondition) return "BOTH"; + if (ACondition) return "A"; + if (BCondition) return "B"; + return "NONE"; +})(); +``` + +## Reducing Eye Movement (Colocating Simple Logic) + +**Rule:** Colocate simple, localized logic or use inline definitions to reduce +context switching. + +**Reasoning:** + +- Allows top-to-bottom reading and faster comprehension. +- Reduces cognitive load from context switching (eye movement). + +#### Recommended Pattern A: Inline `switch` + +```tsx +function Page() { + const user = useUser(); + + // Logic is directly visible here + switch (user.role) { + case "admin": + return ( +
+ + +
+ ); + case "viewer": + return ( +
+ {/* Example for viewer */} + +
+ ); + default: + return null; + } +} +``` + +#### Recommended Pattern B: Colocated simple policy object + +```tsx +function Page() { + const user = useUser(); + // Simple policy defined right here, easy to see + const policy = { + admin: { canInvite: true, canView: true }, + viewer: { canInvite: false, canView: true }, + }[user.role]; + + // Ensure policy exists before accessing properties if role might not match + if (!policy) return null; + + return ( +
+ + +
+ ); +} +``` + +## Naming Complex Conditions + +**Rule:** Assign complex boolean conditions to named variables. + +**Reasoning:** + +- Makes the _meaning_ of the condition explicit. +- Improves readability and self-documentation by reducing cognitive load. + +#### Recommended Pattern: + +(Conditions assigned to named variables) + +```typescript +const matchedProducts = products.filter((product) => { + // Check if product belongs to the target category + const isSameCategory = product.categories.some( + (category) => category.id === targetCategory.id + ); + + // Check if any product price falls within the desired range + const isPriceInRange = product.prices.some( + (price) => price >= minPrice && price <= maxPrice + ); + + // The overall condition is now much clearer + return isSameCategory && isPriceInRange; +}); +``` + +**Guidance:** Name conditions when the logic is complex, reused, or needs unit +testing. Avoid naming very simple, single-use conditions. + +# Predictability + +Ensuring code behaves as expected based on its name, parameters, and context. + +## Standardizing Return Types + +**Rule:** Use consistent return types for similar functions/hooks. + +**Reasoning:** + +- Improves code predictability; developers can anticipate return value shapes. +- Reduces confusion and potential errors from inconsistent types. + +#### Recommended Pattern 1: API Hooks (React Query) + +```typescript +// Always return the Query object +import { useQuery, UseQueryResult } from "@tanstack/react-query"; + +// Assuming fetchUser returns Promise +function useUser(): UseQueryResult { + const query = useQuery({ queryKey: ["user"], queryFn: fetchUser }); + return query; +} + +// Assuming fetchServerTime returns Promise +function useServerTime(): UseQueryResult { + const query = useQuery({ + queryKey: ["serverTime"], + queryFn: fetchServerTime, + }); + return query; +} +``` + +#### Recommended Pattern 2: Validation Functions + +(Using a consistent type, ideally a Discriminated Union) + +```typescript +type ValidationResult = { ok: true } | { ok: false; reason: string }; + +function checkIsNameValid(name: string): ValidationResult { + if (name.length === 0) return { ok: false, reason: "Name cannot be empty." }; + if (name.length >= 20) + return { ok: false, reason: "Name cannot be longer than 20 characters." }; + return { ok: true }; +} + +function checkIsAgeValid(age: number): ValidationResult { + if (!Number.isInteger(age)) + return { ok: false, reason: "Age must be an integer." }; + if (age < 18) return { ok: false, reason: "Age must be 18 or older." }; + if (age > 99) return { ok: false, reason: "Age must be 99 or younger." }; + return { ok: true }; +} + +// Usage allows safe access to 'reason' only when ok is false +const nameValidation = checkIsNameValid(name); +if (!nameValidation.ok) { + console.error(nameValidation.reason); +} +``` + +## Revealing Hidden Logic (Single Responsibility) + +**Rule:** Avoid hidden side effects; functions should only perform actions +implied by their signature (SRP). + +**Reasoning:** + +- Leads to predictable behavior without unintended side effects. +- Creates more robust, testable code through separation of concerns (SRP). + +#### Recommended Pattern: + +```typescript +// Function *only* fetches balance +async function fetchBalance(): Promise { + const balance = await http.get("..."); + return balance; +} + +// Caller explicitly performs logging where needed +async function handleUpdateClick() { + const balance = await fetchBalance(); // Fetch + logging.log("balance_fetched"); // Log (explicit action) + await syncBalance(balance); // Another action +} +``` + +## Using Unique and Descriptive Names (Avoiding Ambiguity) + +**Rule:** Use unique, descriptive names for custom wrappers/functions to avoid +ambiguity. + +**Reasoning:** + +- Avoids ambiguity and enhances predictability. +- Allows developers to understand specific actions (e.g., adding auth) directly + from the name. + +#### Recommended Pattern: + +```typescript +// In httpService.ts - Clearer module name +import { http as httpLibrary } from "@some-library/http"; + +export const httpService = { + // Unique module name + async getWithAuth(url: string) { + // Descriptive function name + const token = await fetchToken(); + return httpLibrary.get(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + }, +}; + +// In fetchUser.ts - Usage clearly indicates auth +import { httpService } from "./httpService"; +export async function fetchUser() { + // Name 'getWithAuth' makes the behavior explicit + return await httpService.getWithAuth("..."); +} +``` + +# Cohesion + +Keeping related code together and ensuring modules have a well-defined, single +purpose. + +## Considering Form Cohesion + +**Rule:** Choose field-level or form-level cohesion based on form requirements. + +**Reasoning:** + +- Balances field independence (field-level) vs. form unity (form-level). +- Ensures related form logic is appropriately grouped based on requirements. + +#### Recommended Pattern (Field-Level Example): + +```tsx +// Each field uses its own `validate` function +import { useForm } from "react-hook-form"; + +export function Form() { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + /* defaultValues etc. */ + }); + + const onSubmit = handleSubmit((formData) => { + console.log("Form submitted:", formData); + }); + + return ( +
+
+ + value.trim() === "" ? "Please enter your name." : true, // Example validation + })} + placeholder="Name" + /> + {errors.name &&

{errors.name.message}

} +
+
+ + /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value) + ? true + : "Invalid email address.", // Example validation + })} + placeholder="Email" + /> + {errors.email &&

{errors.email.message}

} +
+ +
+ ); +} +``` + +#### Recommended Pattern (Form-Level Example): + +```tsx +// A single schema defines validation for the whole form +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +const schema = z.object({ + name: z.string().min(1, "Please enter your name."), + email: z.string().min(1, "Please enter your email.").email("Invalid email."), +}); + +export function Form() { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { name: "", email: "" }, + }); + + const onSubmit = handleSubmit((formData) => { + console.log("Form submitted:", formData); + }); + + return ( +
+
+ + {errors.name &&

{errors.name.message}

} +
+
+ + {errors.email &&

{errors.email.message}

} +
+ +
+ ); +} +``` + +**Guidance:** Choose **field-level** for independent validation, async checks, +or reusable fields. Choose **form-level** for related fields, wizard forms, or +interdependent validation. + +## Organizing Code by Feature/Domain + +**Rule:** Organize directories by feature/domain, not just by code type. + +**Reasoning:** + +- Increases cohesion by keeping related files together. +- Simplifies feature understanding, development, maintenance, and deletion. + +#### Recommended Pattern: + +(Organized by feature/domain) + +``` +src/ +├── components/ # Shared/common components +├── hooks/ # Shared/common hooks +├── utils/ # Shared/common utils +├── domains/ +│ ├── user/ +│ │ ├── components/ +│ │ │ └── UserProfileCard.tsx +│ │ ├── hooks/ +│ │ │ └── useUser.ts +│ │ └── index.ts # Optional barrel file +│ ├── product/ +│ │ ├── components/ +│ │ │ └── ProductList.tsx +│ │ ├── hooks/ +│ │ │ └── useProducts.ts +│ │ └── ... +│ └── order/ +│ ├── components/ +│ │ └── OrderSummary.tsx +│ ├── hooks/ +│ │ └── useOrder.ts +│ └── ... +└── App.tsx +``` + +## Relating Magic Numbers to Logic + +**Rule:** Define constants near related logic or ensure names link them clearly. + +**Reasoning:** + +- Improves cohesion by linking constants to the logic they represent. +- Prevents silent failures caused by updating logic without updating related + constants. + +#### Recommended Pattern: + +```typescript +// Constant clearly named and potentially defined near animation logic +const ANIMATION_DELAY_MS = 300; + +async function onLikeClick() { + await postLike(url); + // Delay uses the constant, maintaining the link to the animation + await delay(ANIMATION_DELAY_MS); + await refetchPostLike(); +} +``` + +_Ensure constants are maintained alongside the logic they depend on or clearly +named to show the relationship._ + +# Coupling + +Minimizing dependencies between different parts of the codebase. + +## Balancing Abstraction and Coupling (Avoiding Premature Abstraction) + +**Rule:** Avoid premature abstraction of duplicates if use cases might diverge; +prefer lower coupling. + +**Reasoning:** + +- Avoids tight coupling from forcing potentially diverging logic into one + abstraction. +- Allowing some duplication can improve decoupling and maintainability when + future needs are uncertain. + +#### Guidance: + +Before abstracting, consider if the logic is truly identical and likely to +_stay_ identical across all use cases. If divergence is possible (e.g., +different pages needing slightly different behavior from a shared hook like +`useOpenMaintenanceBottomSheet`), keeping the logic separate initially (allowing +duplication) can lead to more maintainable, decoupled code. Discuss trade-offs +with the team. _[No specific 'good' code example here, as the recommendation is +situational awareness rather than a single pattern]._ + +## Scoping State Management (Avoiding Overly Broad Hooks) + +**Rule:** Break down broad state management into smaller, focused +hooks/contexts. + +**Reasoning:** + +- Reduces coupling by ensuring components only depend on necessary state slices. +- Improves performance by preventing unnecessary re-renders from unrelated state + changes. + +#### Recommended Pattern: + +(Focused hooks, low coupling) + +```typescript +// Hook specifically for cardId query param +import { useQueryParam, NumberParam } from "use-query-params"; +import { useCallback } from "react"; + +export function useCardIdQueryParam() { + // Assuming 'query' provides the raw param value + const [cardIdParam, setCardIdParam] = useQueryParam("cardId", NumberParam); + + const setCardId = useCallback( + (newCardId: number | undefined) => { + setCardIdParam(newCardId, "replaceIn"); // Or 'push' depending on desired history behavior + }, + [setCardIdParam] + ); + + // Provide a stable return tuple + return [cardIdParam ?? undefined, setCardId] as const; +} + +// Separate hook for date range, etc. +// export function useDateRangeQueryParam() { /* ... */ } +``` + +Components now only import and use `useCardIdQueryParam` if they need `cardId`, +decoupling them from date range state, etc. + +## Eliminating Props Drilling with Composition + +**Rule:** Use Component Composition instead of Props Drilling. + +**Reasoning:** + +- Significantly reduces coupling by eliminating unnecessary intermediate + dependencies. +- Makes refactoring easier and clarifies data flow in flatter component trees. + +#### Recommended Pattern: + +```tsx +import React, { useState } from "react"; + +// Assume Modal, Input, Button, ItemEditList components exist + +function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) { + const [keyword, setKeyword] = useState(""); + + // Render children directly within Modal, passing props only where needed + return ( + + {/* Input and Button rendered directly */} +
+ setKeyword(e.target.value)} // State managed here + placeholder="Search items..." + /> + +
+ {/* ItemEditList rendered directly, gets props it needs */} + +
+ ); +} + +// The intermediate ItemEditBody component is eliminated, reducing coupling. +``` \ No newline at end of file diff --git a/docs/01-PRD.md b/docs/01-PRD.md index 34ca75605..445765964 100644 --- a/docs/01-PRD.md +++ b/docs/01-PRD.md @@ -3,12 +3,15 @@ ## 1. 프로젝트 개요 ### 1.1 제품명 + 🛒 Hanghae Online Store - Shopping Cart ### 1.2 목적 + 개발자를 위한 전문 용품 온라인 쇼핑몰의 장바구니 기능 구현 ### 1.3 핵심 가치 + - 실시간 할인 정책 적용으로 최적의 구매 혜택 제공 - 포인트 적립 시스템을 통한 고객 로열티 강화 - 직관적인 UI/UX로 편리한 쇼핑 경험 제공 @@ -16,15 +19,17 @@ ## 2. 상품 정보 ### 2.1 상품 목록 -| ID | 상품명 | 기본 가격 | 초기 재고 | 개별 할인율 | -|---|---|---|---|---| -| p1 | 버그 없애는 키보드 | 10,000원 | 50개 | 10개 이상 시 10% | -| p2 | 생산성 폭발 마우스 | 20,000원 | 30개 | 10개 이상 시 15% | -| p3 | 거북목 탈출 모니터암 | 30,000원 | 20개 | 10개 이상 시 20% | -| p4 | 에러 방지 노트북 파우치 | 15,000원 | 0개 (품절) | 10개 이상 시 5% | -| p5 | 코딩할 때 듣는 Lo-Fi 스피커 | 25,000원 | 10개 | 10개 이상 시 25% | + +| ID | 상품명 | 기본 가격 | 초기 재고 | 개별 할인율 | +| --- | --------------------------- | --------- | ---------- | ---------------- | +| p1 | 버그 없애는 키보드 | 10,000원 | 50개 | 10개 이상 시 10% | +| p2 | 생산성 폭발 마우스 | 20,000원 | 30개 | 10개 이상 시 15% | +| p3 | 거북목 탈출 모니터암 | 30,000원 | 20개 | 10개 이상 시 20% | +| p4 | 에러 방지 노트북 파우치 | 15,000원 | 0개 (품절) | 10개 이상 시 5% | +| p5 | 코딩할 때 듣는 Lo-Fi 스피커 | 25,000원 | 10개 | 10개 이상 시 25% | ### 2.2 재고 관리 + - 재고가 5개 미만인 상품은 "재고 부족" 표시 - 재고가 0개인 상품은 "품절" 표시 및 선택 불가 - 전체 재고가 50개 미만일 경우 상품 선택 드롭다운 테두리 색상 변경 (orange) @@ -32,21 +37,25 @@ ## 3. 할인 정책 ### 3.1 개별 상품 할인 + - 각 상품별로 10개 이상 구매 시 지정된 할인율 적용 - 할인된 상품은 굵은 글씨로 표시 ### 3.2 전체 수량 할인 + - 장바구니 내 전체 상품 수량이 30개 이상일 경우 25% 할인 - 개별 상품 할인과 중복 적용 불가 (더 큰 할인율 적용) ### 3.3 특별 할인 #### 3.3.1 화요일 할인 + - 매주 화요일 10% 추가 할인 - 다른 할인과 중복 적용 가능 - 화요일에만 특별 할인 배너 표시 #### 3.3.2 번개세일 (⚡) + - 무작위 시간(0~10초 사이)에 시작 - 30초마다 무작위 상품 선택하여 20% 할인 - 재고가 있는 상품에만 적용 @@ -54,6 +63,7 @@ - 선택 드롭다운에 ⚡ 아이콘 표시 #### 3.3.3 추천할인 (💝) + - 무작위 시간(0~20초 사이)에 시작 - 60초마다 마지막 선택한 상품과 다른 상품 추천 - 5% 추가 할인 제공 @@ -61,15 +71,18 @@ - 선택 드롭다운에 💝 아이콘 표시 #### 3.3.4 할인 중복 + - ⚡번개세일 + 💝추천할인 = 25% SUPER SALE - 화요일 할인은 모든 할인과 중복 가능 ## 4. 포인트 적립 시스템 ### 4.1 기본 적립 + - 최종 결제 금액의 0.1% (1,000원당 1포인트) ### 4.2 추가 적립 + - 화요일 구매: 기본 포인트 2배 - 키보드+마우스 세트 구매: +50p - 키보드+마우스+모니터암 풀세트 구매: +100p @@ -79,18 +92,21 @@ - 30개 이상: +100p ### 4.3 포인트 표시 + - 적립 예정 포인트 실시간 계산 및 표시 - 포인트 적립 내역 상세 표시 ## 5. UI/UX 요구사항 ### 5.1 레이아웃 + - 반응형 디자인 (모바일/데스크톱) - 좌측: 상품 선택 및 장바구니 - 우측: 주문 요약 정보 - 우측 상단: 도움말 버튼 (고정 위치) ### 5.2 상품 선택 영역 + - 드롭다운 메뉴로 상품 선택 - 할인 중인 상품 강조 표시: - ⚡번개세일: 빨간색 굵은 글씨 @@ -100,6 +116,7 @@ - 재고 현황 실시간 표시 ### 5.3 장바구니 영역 + - 상품별 카드 형식 표시 - 각 상품 정보: - 상품 이미지 (그라디언트 배경) @@ -112,6 +129,7 @@ - 마지막 상품은 하단 테두리 없음 ### 5.4 주문 요약 영역 + - 검은색 배경, 흰색 텍스트 - 표시 항목: - 소계 (Subtotal) @@ -124,12 +142,14 @@ - 하단 안내 문구 ### 5.5 도움말 모달 + - 우측 상단 고정 버튼 - 클릭 시 슬라이드 형식으로 표시 - 할인 정책 및 포인트 적립 안내 - 배경 클릭 또는 X 버튼으로 닫기 ### 5.6 애니메이션 및 전환 효과 + - 버튼 호버 효과 - 모달 슬라이드 애니메이션 - 수량 변경 시 부드러운 전환 @@ -137,26 +157,31 @@ ## 6. 기능 요구사항 ### 6.1 상품 추가 + - 선택한 상품을 장바구니에 추가 - 이미 있는 상품은 수량 증가 - 재고 초과 시 알림 표시 - 품절 상품은 선택 불가 ### 6.2 수량 변경 + - +/- 버튼으로 수량 조절 - 재고 한도 내에서만 증가 가능 - 수량 0이 되면 자동 제거 ### 6.3 상품 제거 + - Remove 버튼 클릭 시 즉시 제거 - 제거된 수량만큼 재고 복구 ### 6.4 실시간 계산 + - 수량 변경 시 즉시 재계산 - 할인 정책 자동 적용 - 포인트 실시간 업데이트 ### 6.5 상태 관리 + - 장바구니 상품 수 표시 (헤더) - 재고 부족/품절 상태 표시 - 마지막 선택 상품 기억 (추천할인용) @@ -164,14 +189,17 @@ ## 7. 기술적 요구사항 ### 7.1 성능 + - 모든 계산은 클라이언트 사이드에서 처리 - 실시간 업데이트 시 깜빡임 없이 부드럽게 전환 ### 7.2 브라우저 호환성 + - 모던 브라우저 지원 (Chrome, Firefox, Safari, Edge) - ES6+ 문법 사용 가능 ### 7.3 스타일링 + - Tailwind CSS 사용 (CDN) - 커스텀 유틸리티 클래스: - tracking-extra-wide @@ -180,6 +208,7 @@ - bg-gradient-black ### 7.4 접근성 + - 시맨틱 HTML 사용 - 버튼에 적절한 aria-label - 키보드 네비게이션 지원 @@ -187,14 +216,17 @@ ## 8. 예외 처리 ### 8.1 재고 부족 + - 장바구니 추가/수량 증가 시 재고 확인 - 부족 시 "재고가 부족합니다." 알림 ### 8.2 빈 장바구니 + - 장바구니가 비어있을 때 포인트 섹션 숨김 - 주문 요약에 기본값 표시 ### 8.3 동시성 이슈 + - 번개세일과 추천할인이 같은 상품에 적용될 수 있음 - 할인율은 누적되어 최대 25% 적용 @@ -212,4 +244,4 @@ - 장바구니 저장 기능 - 결제 시스템 연동 - 상품 리뷰 및 평점 -- 위시리스트 기능 \ No newline at end of file +- 위시리스트 기능 diff --git a/docs/02-dirty-code-analysis.md b/docs/02-dirty-code-analysis.md index 149cfecca..c8c8c901d 100644 --- a/docs/02-dirty-code-analysis.md +++ b/docs/02-dirty-code-analysis.md @@ -1,51 +1,64 @@ # cart-tailwind.html 더티코드 상세 분석 ## 개요 + 이 문서는 cart-tailwind.html 파일의 더티코드 패턴을 카테고리별로 분석하여, 학생들이 실제 코드에서 문제점을 찾고 개선하는 능력을 기르는 데 도움을 주기 위해 작성되었습니다. ## 1. 변수 선언과 네이밍의 문제점 ### 1.1 일관성 없는 변수 선언 + ```javascript // 33-34번 줄: var 사용 -var prodList,sel,addBtn,cartDisp,sum,stockInfo -var lastSel,bonusPts=0,totalAmt=0,itemCnt=0 +var prodList, sel, addBtn, cartDisp, sum, stockInfo; +var lastSel, + bonusPts = 0, + totalAmt = 0, + itemCnt = 0; // 47-48번 줄: const, let 혼용 -const PRODUCT_ONE = 'p1', PRODUCT_TWO = 'p2', p3_id = 'p3' -let p4 = "p4", productFive = `p5` +const PRODUCT_ONE = "p1", + PRODUCT_TWO = "p2", + p3_id = "p3"; +let p4 = "p4", + productFive = `p5`; ``` **문제점:** + - var, let, const가 일관성 없이 사용됨 - 전역 스코프 오염 - 호이스팅으로 인한 예측 불가능한 동작 ### 1.2 의미 없는 변수명 + ```javascript // 의미를 알 수 없는 축약어 -var p, q, amt, sel, tgt -var curItem, qtyElem, itemTot, disc +var p, q, amt, sel, tgt; +var curItem, qtyElem, itemTot, disc; ``` **개선 방향:** + - `p` → `product` - `q` → `quantity` - `amt` → `amount` - `sel` → `selector` 또는 `productSelect` ### 1.3 네이밍 컨벤션 불일치 + ```javascript // 다양한 네이밍 스타일 혼재 -const PRODUCT_ONE = 'p1' // UPPER_SNAKE_CASE -let productFive = 'p5' // camelCase -var p3_id = 'p3' // snake_case -var totalAmt = 0 // 축약어 사용 +const PRODUCT_ONE = "p1"; // UPPER_SNAKE_CASE +let productFive = "p5"; // camelCase +var p3_id = "p3"; // snake_case +var totalAmt = 0; // 축약어 사용 ``` ## 2. 함수 설계의 문제점 ### 2.1 과도하게 긴 함수 + ```javascript // calcCart 함수: 351-560번 줄 (209줄!) function calcCart() { @@ -61,47 +74,52 @@ function calcCart() { ``` **문제점:** + - 단일 책임 원칙 위반 - 테스트하기 어려움 - 재사용 불가능 - 디버깅 어려움 ### 2.2 함수 정의 위치의 혼란 + ```javascript // 36번 줄: main 함수 전에 갑자기 등장 -function getDiscountRate(productId, quantity) { } +function getDiscountRate(productId, quantity) {} // 50번 줄: main 함수 정의 -function main() { } +function main() {} // 644번 줄: 중간에 갑자기 등장 -function getTotalStock() { } +function getTotalStock() {} ``` ## 3. 중복 코드 패턴 ### 3.1 포인트 계산 중복 + ```javascript // calcCart 함수 내부 (541-542번 줄) -var pts = Math.floor(totalAmt/1000); -if(new Date().getDay() === 2) pts *= 2; +var pts = Math.floor(totalAmt / 1000); +if (new Date().getDay() === 2) pts *= 2; // renderBonusPts 함수 내부 (570-584번 줄) -var basePoints = Math.floor(totalAmt/1000) -if(new Date().getDay() === 2) finalPoints *= 2; +var basePoints = Math.floor(totalAmt / 1000); +if (new Date().getDay() === 2) finalPoints *= 2; // 여러 곳에서 반복되는 화요일 체크 -if(new Date().getDay() === 2) { } +if (new Date().getDay() === 2) { +} ``` ### 3.2 재고 체크 중복 + ```javascript // updateSelOpts (310번 줄) -prodList.forEach(p => totalStock += p.q); +prodList.forEach(p => (totalStock += p.q)); // calcCart (364-368번 줄) -for(var idx=0; idx 0) { +for (var idx = 0; idx < prodList.length; idx++) { + if (prodList[idx].q < 5 && prodList[idx].q > 0) { lowStockItems.push(prodList[idx].name); } } @@ -109,41 +127,47 @@ for(var idx=0; idx sum += p.q); + prodList.forEach(p => (sum += p.q)); return sum; } ``` ### 3.3 DOM 조작 중복 + ```javascript // 여러 곳에서 반복되는 패턴 -document.getElementById('loyalty-points').textContent = '...'; -document.getElementById('loyalty-points').style.display = 'block'; -document.getElementById('loyalty-points').innerHTML = '...'; +document.getElementById("loyalty-points").textContent = "..."; +document.getElementById("loyalty-points").style.display = "block"; +document.getElementById("loyalty-points").innerHTML = "..."; ``` ## 4. 매직 넘버와 하드코딩 ### 4.1 설명 없는 숫자들 + ```javascript -if(quantity < 10) return 0; // 10의 의미? -if(itemCnt >= 30) { } // 30의 의미? -totalAmt *= (1 - 0.1); // 0.1의 의미? -if(product.q < 5) { } // 5의 의미? -setInterval(function() { }, 30000); // 30000의 의미? +if (quantity < 10) return 0; // 10의 의미? +if (itemCnt >= 30) { +} // 30의 의미? +totalAmt *= 1 - 0.1; // 0.1의 의미? +if (product.q < 5) { +} // 5의 의미? +setInterval(function () {}, 30000); // 30000의 의미? ``` ### 4.2 하드코딩된 할인율 + ```javascript -if(productId === 'p1') return 0.1; // 10% -if(productId === 'p2') return 0.15; // 15% -if(productId === 'p3') return 0.2; // 20% +if (productId === "p1") return 0.1; // 10% +if (productId === "p2") return 0.15; // 15% +if (productId === "p3") return 0.2; // 20% luckyItem.val = Math.round(luckyItem.originalVal * 0.8); // 20% 할인 ``` ## 5. 전역 상태 관리의 문제 ### 5.1 전역 변수 직접 조작 + ```javascript // main 함수 내부에서 전역 변수 초기화 (52-54번 줄) totalAmt = 0; @@ -156,45 +180,49 @@ bonusPts = finalPoints; ``` ### 5.2 상태의 일관성 부재 + ```javascript // 같은 정보를 여러 곳에서 중복 관리 // DOM의 수량과 prodList의 재고가 따로 관리됨 qtyElem.textContent = newQty; -itemToAdd['q']--; +itemToAdd["q"]--; ``` ## 6. 비즈니스 로직과 UI의 혼재 ### 6.1 calcCart 함수의 다중 책임 + ```javascript function calcCart() { // 비즈니스 로직 totalAmt += itemTot * (1 - disc); - + // DOM 조작 - elem.style.fontWeight = q >= 10 ? 'bold' : 'normal'; - + elem.style.fontWeight = q >= 10 ? "bold" : "normal"; + // 로깅 - console.log('할인 적용: ' + curItem.name); - + console.log("할인 적용: " + curItem.name); + // UI 업데이트 - document.getElementById('item-count').textContent = '...'; + document.getElementById("item-count").textContent = "..."; } ``` ## 7. 에러 처리 부재 ### 7.1 null 체크 누락 + ```javascript // DOM 요소가 없을 수 있음 -var totalDiv = sum.querySelector('.text-2xl'); -if(totalDiv) { - totalDiv.textContent = '₩' + Math.round(totalAmt).toLocaleString(); +var totalDiv = sum.querySelector(".text-2xl"); +if (totalDiv) { + totalDiv.textContent = "₩" + Math.round(totalAmt).toLocaleString(); } // 하지만 다른 곳에서는 체크하지 않음 ``` ### 7.2 예외 상황 미처리 + ```javascript // 배열이 비어있을 때 처리 없음 var luckyIdx = Math.floor(Math.random() * prodList.length); @@ -204,13 +232,17 @@ var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? ## 8. 성능 문제 ### 8.1 불필요한 반복 계산 + ```javascript // 매번 새로운 Date 객체 생성 -if(new Date().getDay() === 2) { } -if(new Date().getDay() === 2) { } +if (new Date().getDay() === 2) { +} +if (new Date().getDay() === 2) { +} ``` ### 8.2 비효율적인 DOM 조작 + ```javascript // innerHTML 반복 사용 summaryDetails.innerHTML += `...`; @@ -221,13 +253,15 @@ summaryDetails.innerHTML += `...`; ## 9. 일관성 없는 코딩 스타일 ### 9.1 공백과 들여쓰기 + ```javascript // 일관성 없는 공백 -var opt = document.createElement("option") -let leftColumn = document.createElement("div") +var opt = document.createElement("option"); +let leftColumn = document.createElement("div"); ``` ### 9.2 괄호 사용 + ```javascript // 때로는 괄호 사용 leftColumn['className'] = '...' @@ -236,25 +270,29 @@ leftColumn.appendChild(...) ``` ### 9.3 문자열 표현 + ```javascript // 따옴표 혼용 -const PRODUCT_ONE = 'p1' -let p4 = "p4" -productFive = `p5` +const PRODUCT_ONE = "p1"; +let p4 = "p4"; +productFive = `p5`; ``` ## 10. 주석과 문서화 ### 10.1 의미 없는 주석 + ```javascript // Header -let header = document.createElement('div'); +let header = document.createElement("div"); // 여기서도 카트 체크 (중복) -if(cartDisp.children.length === 0) { } +if (cartDisp.children.length === 0) { +} ``` ### 10.2 복잡한 로직에 대한 설명 부재 + ```javascript // 이 계산이 왜 필요한지 설명 없음 discRate = discRate + 0.1 * (1 - discRate); @@ -301,4 +339,4 @@ discRate = discRate + 0.1 * (1 - discRate); 4. **Level 4: 재설계하기** - 클린 아키텍처로 전체 재설계 - - 모던 JavaScript 패턴 적용 \ No newline at end of file + - 모던 JavaScript 패턴 적용 diff --git a/docs/03-cart-tailwind-evaluation.md b/docs/03-cart-tailwind-evaluation.md index e575262a5..1bfb56d24 100644 --- a/docs/03-cart-tailwind-evaluation.md +++ b/docs/03-cart-tailwind-evaluation.md @@ -5,6 +5,7 @@ 본 문서는 작성된 클린코드 가이드라인을 기준으로 `cart-tailwind.html` 파일의 더티코드를 종합적으로 평가합니다. ### 평가 기준 + 1. 실전 클린코드 가이드 (practical-clean-code-guide.md) 2. 더티코드 분석 기준 (dirty-code-analysis.md) 3. 코드 삭제의 기술 (the-art-of-code-deletion.md) @@ -17,6 +18,7 @@ ### 🔴 심각도: 매우 높음 #### 대표적인 안티패턴 (calcCart 함수) + ```javascript // 409-433번 줄: 24개의 변수를 상단에 선언 var cartItems, subTot, itemDiscounts, calculationStartTime, isCalculating... @@ -28,19 +30,21 @@ cartItems = cartDisp.children; calculationStartTime = performance.now ? performance.now() : Date.now(); ``` -**문제점**: +**문제점**: + - 변수 선언과 사용 사이 평균 거리: **50줄 이상** - 코드 읽기 시 계속 위아래로 스크롤 필요 - 변수의 용도를 즉시 파악 불가능 #### renderBonusPts 함수의 극단적 사례 + ```javascript // 740-741번 줄: 혼란스러운 중복 선언 -var hasProducts = {keyboard: false, mouse: false, arm: false}; +var hasProducts = { keyboard: false, mouse: false, arm: false }; // 그런데 이미 hasKeyboard, hasMouse 변수도 따로 선언됨 // 765-768번 줄: 불필요한 검증 -if(!hasKeyboard) hasKeyboard = hasProducts.keyboard; +if (!hasKeyboard) hasKeyboard = hasProducts.keyboard; ``` **평가**: 변수 관리가 전혀 체계적이지 않음 @@ -52,6 +56,7 @@ if(!hasKeyboard) hasKeyboard = hasProducts.keyboard; ### 🔴 심각도: 매우 높음 #### 포인트 계산 중복 (총 5곳) + 1. calcCart 함수 (668-671번 줄) 2. renderBonusPts 함수 (719-733번 줄) 3. calcCart 내부 다시 (684번 줄) @@ -71,7 +76,8 @@ if(new Date().getDay() === 2) finalPoints *= 2; var tempTotal = Array.from(cartDisp.children).reduce(...); ``` -**문제점**: +**문제점**: + - 동일 계산이 최소 5번 반복 - 각각 미묘하게 다른 구현 (일관성 없음) - 버그 수정 시 모든 곳을 찾아 수정해야 함 @@ -83,6 +89,7 @@ var tempTotal = Array.from(cartDisp.children).reduce(...); ### 🟡 심각도: 높음 #### 발견된 매직 넘버 + - `10`: 할인 시작 수량 (8곳) - `30`: 대량구매 기준 (6곳) - `0.1, 0.15, 0.2, 0.25`: 할인율 (각 3곳 이상) @@ -90,9 +97,10 @@ var tempTotal = Array.from(cartDisp.children).reduce(...); - `5`: 재고 부족 기준 (4곳) ```javascript -if(quantity < 10) return 0; // 10의 의미? -if(itemCnt >= 30) { } // 30의 의미? -setInterval(function() { }, 30000); // 30초? 의미 불명 +if (quantity < 10) return 0; // 10의 의미? +if (itemCnt >= 30) { +} // 30의 의미? +setInterval(function () {}, 30000); // 30초? 의미 불명 ``` **평가**: 비즈니스 규칙이 코드 전체에 흩어져 있음 @@ -104,22 +112,28 @@ setInterval(function() { }, 30000); // 30초? 의미 불명 ### 🔴 심각도: 매우 높음 #### 전역 변수 목록 + ```javascript -var prodList, sel, addBtn, cartDisp, sum, stockInfo -var lastSel, bonusPts=0, totalAmt=0, itemCnt=0 +var prodList, sel, addBtn, cartDisp, sum, stockInfo; +var lastSel, + bonusPts = 0, + totalAmt = 0, + itemCnt = 0; ``` **문제점**: + - 10개 이상의 전역 변수 - 여러 함수에서 직접 수정 - 상태 변경 추적 불가능 - 동시성 문제 발생 가능 #### 상태 불일치 예시 + ```javascript // DOM의 수량과 prodList의 재고가 따로 관리 qtyElem.textContent = newQty; -itemToAdd['q']--; // 동기화 안 됨 +itemToAdd["q"]--; // 동기화 안 됨 ``` --- @@ -129,6 +143,7 @@ itemToAdd['q']--; // 동기화 안 됨 ### 🔴 심각도: 매우 높음 #### calcCart 함수 분석 + - **총 길이**: 288줄 (408-696번) - **책임 개수**: 최소 8개 1. 총액 계산 @@ -149,21 +164,24 @@ itemToAdd['q']--; // 동기화 안 됨 ### 🟡 심각도: 높음 #### 변수 선언 방식 + ```javascript -var prodList,sel,addBtn // var, 콤마 구분 -const PRODUCT_ONE = 'p1' // const, 대문자 -let p4 = "p4" // let, 쌍따옴표 -productFive = `p5` // 템플릿 리터럴 +var prodList, sel, addBtn; // var, 콤마 구분 +const PRODUCT_ONE = "p1"; // const, 대문자 +let p4 = "p4"; // let, 쌍따옴표 +productFive = `p5`; // 템플릿 리터럴 ``` #### 반복문 사용 + ```javascript // 총 5가지 다른 방식 -for(var i=0; i { }) -while(idx < nodes.length) { } -for(const node of nodes) { } -Array.from().reduce() +for (var i = 0; i < length; i++) {} +prodList.forEach(p => {}); +while (idx < nodes.length) {} +for (const node of nodes) { +} +Array.from().reduce(); ``` **평가**: 마치 여러 사람이 작성한 것처럼 일관성 없음 @@ -175,12 +193,14 @@ Array.from().reduce() ### 🔴 삭제 난이도: 매우 높음 #### 삭제하기 어려운 이유 + 1. **전역 의존성**: 모든 함수가 전역 변수에 의존 2. **숨겨진 부작용**: DOM 조작이 곳곳에 숨어있음 3. **불명확한 경계**: 어디서 시작하고 끝나는지 불명확 4. **중복 코드**: 삭제하면 다른 곳도 수정 필요 #### 예시: updateSelOpts 함수 + ```javascript function updateSelOpts() { sel.innerHTML = ''; // 전역 변수 직접 조작 @@ -198,10 +218,11 @@ function updateSelOpts() { ### 🟡 심각도: 중간 #### 발견된 문제 + ```javascript // null 체크 누락 -var totalDiv = sum.querySelector('.text-2xl'); -totalDiv.textContent = '₩' + Math.round(totalAmt); // totalDiv가 null이면? +var totalDiv = sum.querySelector(".text-2xl"); +totalDiv.textContent = "₩" + Math.round(totalAmt); // totalDiv가 null이면? // 배열 접근 시 경계 체크 없음 var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? @@ -216,14 +237,17 @@ var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? ### 🟡 심각도: 중간 #### 비효율적인 패턴 + 1. **반복적인 DOM 조작** + ```javascript - summaryDetails.innerHTML += `...`; // 매번 전체 재렌더링 + summaryDetails.innerHTML += `...`; // 매번 전체 재렌더링 ``` 2. **불필요한 재계산** + ```javascript - new Date().getDay() === 2 // 여러 번 호출 + new Date().getDay() === 2; // 여러 번 호출 ``` 3. **메모리 누수 가능성** @@ -238,6 +262,7 @@ var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? ### 🔴 테스트 난이도: 불가능 #### 테스트를 방해하는 요소 + 1. **전역 상태 의존**: 격리된 테스트 불가능 2. **DOM 직접 조작**: 모킹 어려움 3. **비순수 함수**: 부작용으로 가득 @@ -250,28 +275,31 @@ var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? ### 점수표 (100점 만점) -| 평가 항목 | 점수 | 설명 | -|----------|------|------| -| 가독성 | 15/100 | 변수명, 구조, 흐름 모두 불명확 | -| 유지보수성 | 10/100 | 한 곳 수정 시 전체 영향 | -| 테스트 가능성 | 5/100 | 사실상 테스트 불가능 | -| 확장성 | 10/100 | 새 기능 추가 매우 어려움 | -| 성능 | 30/100 | 기능은 하지만 비효율적 | -| **총점** | **14/100** | **F등급** | +| 평가 항목 | 점수 | 설명 | +| ------------- | ---------- | ------------------------------ | +| 가독성 | 15/100 | 변수명, 구조, 흐름 모두 불명확 | +| 유지보수성 | 10/100 | 한 곳 수정 시 전체 영향 | +| 테스트 가능성 | 5/100 | 사실상 테스트 불가능 | +| 확장성 | 10/100 | 새 기능 추가 매우 어려움 | +| 성능 | 30/100 | 기능은 하지만 비효율적 | +| **총점** | **14/100** | **F등급** | ### 주요 개선 필요 사항 #### 즉시 개선 (Critical) + 1. 전역 변수 제거 → 모듈 패턴 적용 2. calcCart 함수 분리 → 10개 이상의 작은 함수로 3. 중복 코드 통합 → DRY 원칙 적용 #### 단기 개선 (High) + 1. 매직 넘버 → 명명된 상수로 2. 일관된 코딩 스타일 적용 3. 에러 처리 추가 #### 장기 개선 (Medium) + 1. 테스트 코드 작성 2. TypeScript 마이그레이션 3. 컴포넌트 기반 아키텍처 @@ -301,6 +329,7 @@ var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? ### AI 시대의 시사점 이런 더티코드는 AI가 생성하기 쉬운 패턴입니다: + - 동작은 하지만 구조가 엉망 - 복사-붙여넣기 스타일 - 전체적인 설계 없이 부분만 해결 @@ -312,28 +341,34 @@ var luckyItem = prodList[luckyIdx]; // prodList가 비어있으면? ## 리팩토링 로드맵 ### Phase 1: 긴급 수술 (1주) + ```javascript // Before -var prodList, sel, addBtn, cartDisp, sum, stockInfo +var prodList, sel, addBtn, cartDisp, sum, stockInfo; // After const ShoppingCart = (() => { let state = { products: [], cart: [] }; - return { /* public API */ }; + return { + /* public API */ + }; })(); ``` ### Phase 2: 구조 개선 (2주) + - 비즈니스 로직 분리 - UI 렌더링 분리 - 이벤트 핸들링 정리 ### Phase 3: 현대화 (3주) + - ES6+ 문법 적용 - 모듈 시스템 도입 - 테스트 코드 작성 ### 예상 결과 + - 코드 라인 수: 1,168줄 → 약 600줄 (50% 감소) - 함수 개수: 8개 → 약 30개 (적절한 크기로 분할) - 테스트 커버리지: 0% → 80% 이상 @@ -342,7 +377,7 @@ const ShoppingCart = (() => { ## 최종 평가 -이 코드는 **"클린코드가 왜 중요한가"를 가르치기 위한 완벽한 반면교사**입니다. +이 코드는 **"클린코드가 왜 중요한가"를 가르치기 위한 완벽한 반면교사**입니다. 거의 모든 안티패턴을 포함하고 있어, 학습 자료로서의 가치는 매우 높습니다. -> "이 코드를 개선할 수 있다면, 어떤 레거시 코드도 두렵지 않을 것입니다." \ No newline at end of file +> "이 코드를 개선할 수 있다면, 어떤 레거시 코드도 두렵지 않을 것입니다." diff --git a/docs/04-practical-clean-code-guide.md b/docs/04-practical-clean-code-guide.md index f02549608..e8634c67a 100644 --- a/docs/04-practical-clean-code-guide.md +++ b/docs/04-practical-clean-code-guide.md @@ -5,6 +5,7 @@ ### 1. 변수와 함수의 인접성 원칙 (Proximity Principle) #### ❌ 나쁜 예: 선언과 사용이 멀리 떨어진 코드 + ```javascript // cart-tailwind.html의 실제 예 function calcCart() { @@ -17,28 +18,29 @@ function calcCart() { var lowStockItems; var idx; // ... 25개 이상의 변수 선언 - + // 100줄 떨어진 곳에서 첫 사용 cartItems = cartDisp.children; - + // 또 50줄 후에 subTot 사용 subTot = 0; } ``` #### ✅ 좋은 예: 선언과 사용을 가깝게 + ```javascript function calcCart() { const cartItems = cartDisp.children; let subtotal = 0; - + // cartItems를 바로 사용 for (const item of cartItems) { - const quantity = parseInt(item.querySelector('.quantity-number').textContent); + const quantity = parseInt(item.querySelector(".quantity-number").textContent); const price = getProductPrice(item.id); subtotal += quantity * price; } - + // 필요한 시점에 선언 const discounts = calculateDiscounts(subtotal, cartItems); const total = subtotal - discounts; @@ -46,6 +48,7 @@ function calcCart() { ``` ### 행동 지침 + 1. **변수는 첫 사용 직전에 선언하라** 2. **함수 상단에 모든 변수를 선언하지 마라** 3. **관련된 코드는 물리적으로 가깝게 배치하라** @@ -57,40 +60,42 @@ function calcCart() { ### 멘탈 모델: "관련 있는 것끼리 모으고, 관련 없는 것은 분리하라" #### ❌ 나쁜 예: 섞여있는 관심사 + ```javascript function processCart() { // 할인 계산 var discount = 0; if (quantity > 10) discount = 0.1; - + // 갑자기 DOM 조작 - document.getElementById('loading').style.display = 'none'; - + document.getElementById("loading").style.display = "none"; + // 다시 할인 계산 if (isTuesday()) discount += 0.1; - + // 또 DOM 조작 - document.getElementById('total').textContent = total; - + document.getElementById("total").textContent = total; + // 다시 비즈니스 로직 var points = calculatePoints(total); } ``` #### ✅ 좋은 예: 관련 코드끼리 그룹화 + ```javascript function processCart() { // 1. 할인 계산 블록 const discount = calculateTotalDiscount(); - + // 2. 포인트 계산 블록 const points = calculatePoints(total); - + // 3. UI 업데이트 블록 updateUI({ total, discount, - points + points, }); } @@ -103,6 +108,7 @@ function calculateTotalDiscount() { ``` ### 행동 지침 + 1. **같은 목적의 코드는 연속된 블록으로 작성** 2. **다른 관심사가 섞이면 즉시 함수로 분리** 3. **빈 줄로 논리적 블록을 구분** @@ -114,14 +120,20 @@ function calculateTotalDiscount() { ### 멘탈 모델: "영리한 코드보다 명확한 코드" #### ❌ 나쁜 예: 복잡한 조건문 + ```javascript // 무엇을 하는지 파악하기 어려움 -if (product.q > 0 && (!product.onSale || (product.onSale && product.suggestSale)) && (itemCount < 30 || (itemCount >= 30 && totalAmt > 100000))) { +if ( + product.q > 0 && + (!product.onSale || (product.onSale && product.suggestSale)) && + (itemCount < 30 || (itemCount >= 30 && totalAmt > 100000)) +) { // 처리 } ``` #### ✅ 좋은 예: 의도가 명확한 조건문 + ```javascript const isInStock = product.quantity > 0; const hasSpecialDiscount = product.onSale && product.suggestSale; @@ -133,6 +145,7 @@ if (isInStock && (hasSpecialDiscount || isEligibleForPurchase)) { ``` ### 행동 지침 + 1. **복잡한 조건은 변수로 추출하여 이름을 붙여라** 2. **부정 조건보다 긍정 조건을 사용하라** 3. **조기 반환(early return)으로 중첩을 줄여라** @@ -144,40 +157,42 @@ if (isInStock && (hasSpecialDiscount || isEligibleForPurchase)) { ### 멘탈 모델: "함수 안에서는 한 가지 추상화 수준만 다뤄라" #### ❌ 나쁜 예: 여러 추상화 수준이 혼재 + ```javascript function addToCart(productId) { // 고수준: 비즈니스 로직 const product = findProduct(productId); - + // 저수준: DOM 조작 디테일 - const div = document.createElement('div'); - div.className = 'cart-item'; + const div = document.createElement("div"); + div.className = "cart-item"; div.innerHTML = `${product.name}`; - + // 중간 수준: 계산 const price = product.price * (1 - discount); - + // 초저수준: 스타일 조작 - div.style.backgroundColor = '#f0f0f0'; - div.style.padding = '10px'; + div.style.backgroundColor = "#f0f0f0"; + div.style.padding = "10px"; } ``` #### ✅ 좋은 예: 동일한 추상화 수준 + ```javascript function addToCart(productId) { const product = findProduct(productId); const cartItem = createCartItem(product); const finalPrice = calculateFinalPrice(product); - + appendToCart(cartItem); updateTotalPrice(finalPrice); } // 세부 구현은 각자의 함수에서 function createCartItem(product) { - const element = document.createElement('div'); - element.className = 'cart-item'; + const element = document.createElement("div"); + element.className = "cart-item"; element.innerHTML = renderCartItemTemplate(product); applyCartItemStyles(element); return element; @@ -185,6 +200,7 @@ function createCartItem(product) { ``` ### 행동 지침 + 1. **함수를 읽을 때 줌 레벨이 바뀌면 안 됨** 2. **고수준 함수는 '무엇'을, 저수준 함수는 '어떻게'를** 3. **TO 문단으로 함수를 설명할 수 있어야 함** @@ -200,28 +216,30 @@ function createCartItem(product) { ### 멘탈 모델: "데이터는 한 방향으로 흐른다" #### ❌ 나쁜 예: 예측 불가능한 데이터 흐름 + ```javascript let total = 0; function calculatePrice() { - total = 100; // 전역 변수 수정 + total = 100; // 전역 변수 수정 applyDiscount(); return total; } function applyDiscount() { if (isVIP) { - total *= 0.8; // 또 전역 변수 수정 + total *= 0.8; // 또 전역 변수 수정 } addTax(); } function addTax() { - total *= 1.1; // 또또 전역 변수 수정 + total *= 1.1; // 또또 전역 변수 수정 } ``` #### ✅ 좋은 예: 명확한 데이터 흐름 + ```javascript function calculatePrice(basePrice) { const discountedPrice = applyDiscount(basePrice); @@ -239,6 +257,7 @@ function addTax(price) { ``` ### 행동 지침 + 1. **함수는 입력을 받아 출력을 반환** 2. **side effect는 함수명에 명시** 3. **전역 변수 수정 대신 새 값을 반환** @@ -250,22 +269,25 @@ function addTax(price) { ### 멘탈 모델: "한 가지 방법을 정하면 끝까지 지켜라" #### ❌ 나쁜 예: 일관성 없는 스타일 + ```javascript // 배열 순회 방법이 제각각 -prodList.forEach(p => totalStock += p.q); +prodList.forEach(p => (totalStock += p.q)); -for(var i=0; i { @@ -280,6 +302,7 @@ products.forEach(product => { ``` ### 행동 지침 + 1. **팀 컨벤션을 정하고 자동화 도구 사용** 2. **기존 코드의 스타일을 따르기** 3. **한 파일 안에서는 반드시 일관성 유지** @@ -291,22 +314,24 @@ products.forEach(product => { ### 멘탈 모델: "실패할 수 있는 모든 곳에서 실패를 처리하라" #### ❌ 나쁜 예: 숨겨진 에러 + ```javascript function getProduct(id) { const product = products.find(p => p.id === id); - return product.name; // product가 undefined면? + return product.name; // product가 undefined면? } ``` #### ✅ 좋은 예: 명시적 에러 처리 + ```javascript function getProduct(id) { const product = products.find(p => p.id === id); - + if (!product) { throw new Error(`Product not found: ${id}`); } - + return product; } @@ -318,6 +343,7 @@ function getProductSafe(id) { ``` ### 행동 지침 + 1. **null/undefined 체크를 명시적으로** 2. **에러는 빨리 발생시키고 명확하게 전달** 3. **복구 가능한 에러와 불가능한 에러 구분** @@ -329,6 +355,7 @@ function getProductSafe(id) { ### 멘탈 모델: "주석이 필요하다면 코드를 개선할 여지가 있다" #### ❌ 나쁜 예: 주석으로 설명 + ```javascript // 화요일이면 10% 추가 할인 if (new Date().getDay() === 2) { @@ -342,6 +369,7 @@ if (q >= 10) { ``` #### ✅ 좋은 예: 코드 자체가 설명 + ```javascript const TUESDAY = 2; const TUESDAY_DISCOUNT_RATE = 0.1; @@ -357,6 +385,7 @@ if (quantity >= BULK_DISCOUNT_THRESHOLD) { ``` ### 행동 지침 + 1. **함수와 변수 이름으로 의도 표현** 2. **매직 넘버는 상수로 추출** 3. **복잡한 조건은 함수로 추출** @@ -368,22 +397,24 @@ if (quantity >= BULK_DISCOUNT_THRESHOLD) { ### 멘탈 모델: "테스트하기 어렵다면 설계가 잘못된 것" #### ❌ 나쁜 예: 테스트 불가능한 코드 + ```javascript function processOrder() { const today = new Date(); - const total = document.getElementById('total').textContent; - + const total = document.getElementById("total").textContent; + if (today.getDay() === 2) { - fetch('/api/discount') + fetch("/api/discount") .then(res => res.json()) .then(discount => { - document.getElementById('final').textContent = total * discount; + document.getElementById("final").textContent = total * discount; }); } } ``` #### ✅ 좋은 예: 테스트 가능한 코드 + ```javascript function calculateOrderTotal(total, date, discountService) { if (isTuesday(date)) { @@ -394,14 +425,11 @@ function calculateOrderTotal(total, date, discountService) { } // 의존성 주입으로 테스트 가능 -const total = calculateOrderTotal( - 100, - new Date(), - discountService -); +const total = calculateOrderTotal(100, new Date(), discountService); ``` ### 행동 지침 + 1. **순수 함수로 비즈니스 로직 분리** 2. **외부 의존성은 주입받기** 3. **시간, 랜덤같은 비결정적 요소 격리** @@ -413,9 +441,11 @@ const total = calculateOrderTotal( ### 멘탈 모델: "완벽한 코드는 없다. 조금씩 개선하라" ### 리팩토링 우선순위 + 1. **가독성** → 2. **중복 제거** → 3. **구조 개선** → 4. **성능 최적화** ### 실전 체크리스트 + - [ ] 이 함수는 한 가지 일만 하는가? - [ ] 변수명이 그 역할을 설명하는가? - [ ] 조건문을 읽고 즉시 이해되는가? @@ -426,6 +456,7 @@ const total = calculateOrderTotal( - [ ] 테스트를 작성할 수 있는가? ### 행동 지침 + 1. **보이스카우트 규칙: 처음 왔을 때보다 깨끗하게** 2. **작은 개선을 자주, 큰 개선은 신중하게** 3. **팀과 함께 개선 (코드 리뷰 활용)** @@ -435,18 +466,22 @@ const total = calculateOrderTotal( ## 실전 연습 방법 ### 1단계: 인식 (Recognition) + - 더티코드 패턴을 보면 즉시 알아차리기 - "이상한 냄새"를 감지하는 능력 기르기 ### 2단계: 분석 (Analysis) + - 왜 이 코드가 문제인지 설명할 수 있기 - 어떤 상황에서 버그가 발생할지 예측하기 ### 3단계: 개선 (Improvement) + - 구체적인 개선 방법 제시하기 - 트레이드오프 고려하기 ### 4단계: 예방 (Prevention) + - 처음부터 클린하게 작성하기 - 코드 리뷰에서 조기 발견하기 @@ -454,4 +489,4 @@ const total = calculateOrderTotal( > "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Martin Fowler -코드는 한 번 쓰고 여러 번 읽힌다. 미래의 자신과 동료를 위해 읽기 쉬운 코드를 작성하자. \ No newline at end of file +코드는 한 번 쓰고 여러 번 읽힌다. 미래의 자신과 동료를 위해 읽기 쉬운 코드를 작성하자. diff --git a/docs/05-clean-code-theory-lesson.md b/docs/05-clean-code-theory-lesson.md index a0aacfdf2..d4ca0c98a 100644 --- a/docs/05-clean-code-theory-lesson.md +++ b/docs/05-clean-code-theory-lesson.md @@ -3,11 +3,13 @@ ## 서론: AI가 코딩하는 시대, 왜 클린코드를 알아야 하는가? ### 1. AI 시대의 개발자 역할 변화 + - **코드 작성자 → 코드 큐레이터**: AI가 생성한 코드의 품질을 판단하고 개선 - **문제 해결자 → 아키텍트**: 전체적인 구조와 설계를 이해하고 지시 - **디버거 → 코드 리뷰어**: 더 많은 코드를 빠르게 검토하고 평가 ### 2. 클린코드 분별 능력의 중요성 + - AI는 동작하는 코드를 만들 수 있지만, 유지보수하기 좋은 코드인지는 별개 - 기술 부채를 조기에 발견하고 예방하는 능력 필요 - 팀 협업과 장기적인 프로젝트 성공을 위한 필수 역량 @@ -17,24 +19,30 @@ ### 1.1 전역 상태 관리의 문제점 #### 안티패턴 예시 + ```javascript // ❌ 나쁜 예: cart-tailwind.html에서 -var prodList,sel,addBtn,cartDisp,sum,stockInfo -var lastSel,bonusPts=0,totalAmt=0,itemCnt=0 +var prodList, sel, addBtn, cartDisp, sum, stockInfo; +var lastSel, + bonusPts = 0, + totalAmt = 0, + itemCnt = 0; ``` #### 문제점 + - **예측 불가능성**: 어디서든 값이 변경될 수 있음 - **테스트 어려움**: 전역 상태 때문에 격리된 테스트 불가능 - **동시성 문제**: 여러 함수가 동시에 접근하면 예상치 못한 결과 #### 클린코드 원칙 + ```javascript // ✅ 좋은 예: 상태를 캡슐화 class ShoppingCart { #items = []; #totalAmount = 0; - + addItem(product, quantity) { // 명확한 인터페이스를 통한 상태 변경 } @@ -44,14 +52,19 @@ class ShoppingCart { ### 1.2 네이밍의 중요성 #### 안티패턴 예시 + ```javascript // ❌ 나쁜 예 -var p, q, amt, sel, tgt -const PRODUCT_ONE = 'p1', PRODUCT_TWO = 'p2', p3_id = 'p3' -let p4 = "p4", productFive = `p5` // 일관성 없는 선언과 네이밍 +var p, q, amt, sel, tgt; +const PRODUCT_ONE = "p1", + PRODUCT_TWO = "p2", + p3_id = "p3"; +let p4 = "p4", + productFive = `p5`; // 일관성 없는 선언과 네이밍 ``` #### 클린코드 원칙 + - **의미 있는 이름 사용**: `p` → `product`, `q` → `quantity` - **일관된 네이밍 컨벤션**: camelCase 또는 snake_case 중 하나만 - **검색 가능한 이름**: 약어보다는 전체 단어 사용 @@ -59,6 +72,7 @@ let p4 = "p4", productFive = `p5` // 일관성 없는 선언과 네이밍 ### 1.3 함수의 단일 책임 원칙 #### 안티패턴 예시 + ```javascript // ❌ calcCart 함수가 너무 많은 일을 함 function calcCart() { @@ -73,12 +87,13 @@ function calcCart() { ``` #### 클린코드 원칙 + ```javascript // ✅ 각 함수는 하나의 일만 -function calculateSubtotal(items) { } -function applyDiscounts(subtotal, discountRules) { } -function calculatePoints(amount, bonusRules) { } -function updateUI(cartState) { } +function calculateSubtotal(items) {} +function applyDiscounts(subtotal, discountRules) {} +function calculatePoints(amount, bonusRules) {} +function updateUI(cartState) {} ``` ## Part 2: 코드 중복과 DRY 원칙 @@ -86,24 +101,26 @@ function updateUI(cartState) { } ### 2.1 중복 코드의 문제점 #### 안티패턴 예시 + ```javascript // ❌ 포인트 계산이 여러 곳에 중복 // calcCart() 함수 내부 -var pts = Math.floor(totalAmt/1000); -if(new Date().getDay() === 2) pts *= 2; +var pts = Math.floor(totalAmt / 1000); +if (new Date().getDay() === 2) pts *= 2; // renderBonusPts() 함수 내부 -var basePoints = Math.floor(totalAmt/1000) -if(new Date().getDay() === 2) finalPoints *= 2; +var basePoints = Math.floor(totalAmt / 1000); +if (new Date().getDay() === 2) finalPoints *= 2; // addBtn 이벤트 핸들러 내부 var tempTotal = 0; -for(var i=0; i= 30) { // 30은 왜? ``` #### 클린코드 원칙 + ```javascript // ✅ 상수로 의미 부여 const DISCOUNT_THRESHOLD = 10; @@ -128,7 +147,7 @@ const TUESDAY_DISCOUNT_RATE = 0.1; const PRODUCT_DISCOUNTS = { KEYBOARD: 0.1, MOUSE: 0.15, - MONITOR_ARM: 0.2 + MONITOR_ARM: 0.2, }; ``` @@ -137,21 +156,23 @@ const PRODUCT_DISCOUNTS = { ### 3.1 관심사의 분리 (Separation of Concerns) #### 안티패턴 예시 + ```javascript // ❌ 비즈니스 로직과 UI 로직이 혼재 function calcCart() { // 계산 로직 totalAmt += itemTot * (1 - disc); - + // DOM 직접 조작 - elem.style.fontWeight = q >= 10 ? 'bold' : 'normal'; - + elem.style.fontWeight = q >= 10 ? "bold" : "normal"; + // 콘솔 로깅 - console.log('할인 적용: ' + curItem.name); + console.log("할인 적용: " + curItem.name); } ``` #### 클린코드 원칙 + - **Model**: 데이터와 비즈니스 로직 - **View**: UI 렌더링 - **Controller**: 사용자 입력 처리와 조정 @@ -159,10 +180,12 @@ function calcCart() { ### 3.2 의존성 역전 원칙 #### 문제점 + - 고수준 모듈이 저수준 모듈에 직접 의존 - DOM 요소에 직접 접근하여 테스트 어려움 #### 해결 방법 + ```javascript // ✅ 인터페이스를 통한 의존성 주입 class CartService { @@ -202,6 +225,7 @@ class CartService { ## Part 5: AI 시대의 코드 리뷰 체크리스트 ### 5.1 즉시 거부해야 할 코드 + - [ ] 전역 변수 남용 - [ ] 하드코딩된 값 - [ ] 300줄 이상의 함수 @@ -209,6 +233,7 @@ class CartService { - [ ] 테스트 불가능한 구조 ### 5.2 개선 요청할 코드 + - [ ] 불명확한 변수명 - [ ] 주석 없는 복잡한 로직 - [ ] 일관성 없는 코딩 스타일 @@ -216,6 +241,7 @@ class CartService { - [ ] 에러 처리 부재 ### 5.3 AI 생성 코드 평가 기준 + 1. **정확성**: 요구사항을 충족하는가? 2. **가독성**: 다른 개발자가 이해할 수 있는가? 3. **유지보수성**: 변경이 쉬운가? @@ -225,20 +251,24 @@ class CartService { ## 실습 과제 가이드 ### Phase 1: 문제 인식 + - cart-tailwind.html의 안티패턴 10개 찾기 - 각 문제가 야기할 수 있는 실제 버그 시나리오 작성 ### Phase 2: 설계 + - 클린 아키텍처로 재설계 - 모듈 다이어그램 작성 - 인터페이스 정의 ### Phase 3: 구현 + - 테스트 먼저 작성 (TDD) - 점진적 리팩토링 - 코드 리뷰와 개선 ### Phase 4: 검증 + - 성능 비교 - 가독성 평가 - 확장성 테스트 @@ -246,17 +276,20 @@ class CartService { ## 결론: 클린코드는 팀워크다 ### AI와의 협업에서 클린코드의 역할 + - AI는 빠르게 코드를 생성하지만, 품질 판단은 인간의 몫 - 클린코드 원칙을 알면 AI에게 더 나은 지시 가능 - 코드 리뷰 능력이 곧 AI 활용 능력 ### 지속적인 학습 + - 클린코드는 한 번에 완성되지 않음 - 팀의 합의와 지속적인 개선이 필요 - 실무 경험을 통한 감각 습득이 중요 ## 참고 자료 + - Clean Code by Robert C. Martin - Refactoring by Martin Fowler - The Pragmatic Programmer by David Thomas & Andrew Hunt -- Working Effectively with Legacy Code by Michael Feathers \ No newline at end of file +- Working Effectively with Legacy Code by Michael Feathers diff --git a/docs/06-clean-code.md b/docs/06-clean-code.md new file mode 100644 index 000000000..887254cf5 --- /dev/null +++ b/docs/06-clean-code.md @@ -0,0 +1,489 @@ +[GitHub - Youngerjesus/refactoring-2nd-edition: 마틴 파울러 님의 리팩토링을 읽고 정리한 레파지토리입니다.](https://github.com/Youngerjesus/refactoring-2nd-edition?tab=readme-ov-file) + +[Refactoring and Design Patterns](https://refactoring.guru/) + +[Catalog of Refactorings](https://refactoring.com/catalog/) + +# 클린코드 개요 + +클린코드란 가독성이 높고, 이해하기 쉬우며, 유지보수가 용이한 코드를 의미합니다. 마틴 파울러의 "리팩토링" 책에서는 이를 "냄새 나는 코드"를 제거하고 코드의 품질을 향상시키는 과정으로 설명합니다. + +# 왜 해야 하는가 + + + +## 유지보수성 향상 + +- 코드 이해 시간 단축: 클린 코드는 직관적이고 자체 설명적이어서, 새로운 개발자가 프로젝트에 참여하거나 기존 개발자가 오래된 코드를 revisit할 때 빠르게 이해할 수 있습니다. +- 변경 용이성: 잘 구조화된 코드는 새로운 기능 추가나 기존 기능 수정 시 영향 범위를 최소화하며, 예측 가능한 방식으로 변경할 수 있습니다. +- 기술 부채 감소: 클린 코드 작성은 시간이 지나도 코드의 품질을 유지하여, 향후 대규모 리팩토링의 필요성을 줄입니다. + +## 버그 감소 + +- 가독성 향상: 명확한 코드는 논리적 오류를 쉽게 발견할 수 있게 합니다. +- 부작용 최소화: 함수의 책임을 명확히 하고 부수 효과를 줄임으로써, 예기치 않은 버그 발생을 방지합니다. +- 테스트 용이성: 클린 코드는 단위 테스트 작성을 쉽게 만들어, 버그를 사전에 발견하고 수정할 수 있습니다. +- 디버깅 효율성: 문제 발생 시, 명확한 구조와 네이밍으로 인해 버그의 원인을 빠르게 추적할 수 있습니다. + +## 개발 속도 향상 + +- 초기 개발 속도 vs 장기적 이익: 클린 코드 작성에 초기에는 시간이 더 걸릴 수 있지만, 장기적으로는 유지보수와 기능 추가가 쉬워져 전체적인 개발 속도가 향상됩니다. +- 재사용성 증가: 잘 설계된 모듈과 함수는 다른 부분에서 쉽게 재사용될 수 있어, 중복 작업을 줄이고 개발 속도를 높입니다. +- 리팩토링 시간 감소: 지속적으로 클린 코드를 유지하면, 대규모 리팩토링에 들이는 시간을 줄일 수 있습니다. +- 문서화 필요성 감소: 자체 설명적인 코드는 별도의 문서화 필요성을 줄여, 문서 작성과 유지보수에 드는 시간을 절약합니다. + +## 협업 개선 + +- 코드 리뷰 효율성: 클린 코드는 리뷰 과정을 더 효율적으로 만들어, 팀원 간의 피드백과 지식 공유를 촉진합니다. +- 일관성 유지: 팀 내에서 합의된 코딩 표준을 따르면, 여러 개발자가 작성한 코드도 일관성을 유지할 수 있습니다. +- 의사소통 개선: 명확한 네이밍과 구조는 코드를 통한 의사소통을 원활하게 하여, 팀원 간의 이해도를 높입니다. +- 온보딩 시간 단축: 새로운 팀원이 프로젝트에 참여할 때, 클린 코드는 빠른 적응을 도와 생산성을 높입니다. +- 책임 소재 명확화: 잘 구조화된 코드는 각 부분의 책임이 명확해져, 문제 발생 시 해당 부분을 담당하는 팀원을 쉽게 식별할 수 있습니다. + +이러한 세부적인 이점들이 모여 전체적인 소프트웨어 개발 프로세스의 효율성과 품질을 크게 향상시킵니다. 클린 코드는 단순히 "좋은 습관"이 아니라, **프로젝트의 성공과 팀의 생산성에 직접적인 영향을 미치는 중요한 실천 사항**입니다. + +# 무엇을 해야 하는가 + + + +## 의미 있는 이름 사용 + +- 변수, 함수, 클래스에 그 목적과 역할을 명확히 나타내는 이름을 부여합니다. +- 약어나 모호한 단어 사용을 피하고, 구체적이고 서술적인 이름을 선택합니다. +- 네이밍 컨벤션을 일관성 있게 유지합니다 (예: camelCase, PascalCase). +- 컨텍스트를 고려하여 불필요한 정보는 제거합니다. + 예: `getUserInfo()` 대신 `fetchUserProfile()` + +## 함수를 작고 단일 책임을 가지도록 만들기 + +- 각 함수는 한 가지 작업만 수행하도록 합니다. +- 함수의 길이를 20-30줄 이내로 유지합니다. +- 함수의 매개변수 수를 3개 이하로 제한합니다. +- 부수 효과(side effects)를 최소화하고, 있다면 명확히 표현합니다. +- 추상화 수준을 일관되게 유지합니다. + 예: 큰 함수를 여러 개의 작은 함수로 분리하여 각각의 역할을 명확히 합니다. + +## 중복 코드 제거 + +- DRY (Don't Repeat Yourself) 원칙을 적용합니다. +- 반복되는 코드 패턴을 식별하고 공통 함수나 모듈로 추출합니다. +- 상속, 컴포지션, 고차 함수 등을 활용하여 코드 재사용성을 높입니다. +- 유틸리티 함수나 헬퍼 클래스를 만들어 공통 기능을 모듈화합니다. + 예: 여러 곳에서 사용되는 날짜 포맷팅 로직을 하나의 유틸리티 함수로 만듭니다. + +## 주석 대신 자체 설명적인 코드 작성 + +- 코드 자체로 의도와 동작을 명확히 표현합니다. +- 복잡한 알고리즘이나 비즈니스 로직의 경우에만 주석을 사용합니다. +- 주석 대신 함수 이름이나 변수 이름으로 의도를 표현합니다. +- 주석이 필요한 경우, 'why'에 초점을 맞춥니다. + 예: `// 사용자 나이 계산` 주석 대신 `calculateUserAge()` 함수 사용 + +## 일관된 포맷팅 유지 + +- 팀 내에서 합의된 코딩 스타일 가이드를 따릅니다. +- 들여쓰기, 중괄호 위치, 공백 사용 등을 일관되게 유지합니다. +- 자동 포맷터(예: Prettier)를 사용하여 일관성을 강제합니다. +- 파일, 클래스, 함수의 구조를 일관되게 유지합니다. + 예: 모든 import 문을 파일 상단에 그룹화하고, 알파벳 순으로 정렬합니다. + +## 복잡한 조건문 단순화 + +- 깊은 중첩을 피하고, early return을 활용합니다. +- 복잡한 불리언 표현식을 명명된 함수로 추출합니다. +- 스위치 문 대신 객체 리터럴이나 Map을 사용합니다. +- 다형성을 활용하여 조건부 로직을 대체합니다. + 예: `if (isValidUser && hasPermission && !isLocked)` 대신 `canAccessResource()` 함수 사용 + +## 적절한 추상화 수준 유지 + +- 비즈니스 로직과 저수준 구현 세부사항을 분리합니다. +- 인터페이스와 구현을 명확히 구분합니다. +- 과도한 추상화를 피하고, 필요한 만큼만 추상화합니다. +- 동일한 수준의 추상화를 한 곳에서 다룹니다. +- SOLID 원칙을 적용하여 모듈 간 결합도를 낮추고 응집도를 높입니다. + 예: 데이터베이스 쿼리 로직을 Repository 패턴을 사용하여 추상화합니다. + +이러한 실천 사항들을 일관되게 적용하면, 코드의 가독성, 유지보수성, 확장성이 크게 향상됩니다. 각 항목은 서로 연관되어 있으며, 종합적으로 적용될 때 가장 효과적인 결과를 얻을 수 있습니다. + +# 어떻게 해야 하는가 + +- 코드 리뷰 실시 +- 지속적인 리팩토링 수행 +- 단위 테스트 작성 +- 정적 코드 분석 도구 사용 (예: ESLint) +- 코드 포맷터 사용 (예: Prettier) +- 디자인 패턴 및 SOLID 원칙 적용 +- 페어 프로그래밍 또는 모브 프로그래밍 실천 + +# 예제 (10가지) + +## 의미 있는 변수명 사용: + +```jsx +// Bad +const d = new Date(); +// Good +const currentDate = new Date(); +``` + +## 함수 추출: + +```jsx +// Bad +function calculateTotalPrice(items) { + let total = 0; + for (let item of items) { + total += item.price * item.quantity; + } + return total * 1.1; // 10% tax +} + +// Good +function calculateSubtotal(items) { + return items.reduce((total, item) => total + item.price * item.quantity, 0); +} + +function applyTax(amount, taxRate = 0.1) { + return amount * (1 + taxRate); +} + +function calculateTotalPrice(items) { + const subtotal = calculateSubtotal(items); + return applyTax(subtotal); +} +``` + +## 매직 넘버 제거: + +```jsx +// Bad +if (employee.age > 65) { + // retire +} + +// Good +const RETIREMENT_AGE = 65; +if (employee.age > RETIREMENT_AGE) { + // retire +} +``` + +## 조건문 단순화: + +```jsx +// Bad +if (isActive === true) { + // do something +} + +// Good +if (isActive) { + // do something +} +``` + +## 중복 제거: + +```jsx +// Bad +function validateName(name) { + if (name.length < 2) { + throw new Error("Name is too short"); + } +} + +function validateEmail(email) { + if (email.length < 2) { + throw new Error("Email is too short"); + } + // other validations... +} + +// Good +function validateMinLength(value, minLength, fieldName) { + if (value.length < minLength) { + throw new Error(`${fieldName} is too short`); + } +} + +function validateName(name) { + validateMinLength(name, 2, "Name"); +} + +function validateEmail(email) { + validateMinLength(email, 2, "Email"); + // other validations... +} +``` + +## 함수 인자 개수 줄이기: + +```jsx +// Bad +function createUser(name, age, city, country, email) { + // create user +} + +// Good +function createUser(userInfo) { + const { name, age, city, country, email } = userInfo; + // create user +} +``` + +## 부수 효과 제거: + +```jsx +// Bad +let total = 0; +function addToTotal(value) { + total += value; +} + +// Good +function addToTotal(currentTotal, value) { + return currentTotal + value; +} +``` + +## 널 객체 패턴 사용: + +```jsx +// Bad +function getUser(id) { + const user = findUser(id); + if (user == null) { + return { name: "Guest", permissions: [] }; + } + return user; +} + +// Good +const NULL_USER = { name: "Guest", permissions: [] }; +function getUser(id) { + return findUser(id) || NULL_USER; +} +``` + +## 조건부 로직을 다형성으로 대체: + +```jsx +// Bad +function calculateArea(shape) { + if (shape.type === "circle") { + return Math.PI * shape.radius ** 2; + } else if (shape.type === "rectangle") { + return shape.width * shape.height; + } +} + +// Good +class Circle { + constructor(radius) { + this.radius = radius; + } + + calculateArea() { + return Math.PI * this.radius ** 2; + } +} + +class Rectangle { + constructor(width, height) { + this.width = width; + this.height = height; + } + + calculateArea() { + return this.width * this.height; + } +} +``` + +## 예외 처리: + +```jsx +// Bad +function divide(a, b) { + if (b !== 0) { + return a / b; + } +} + +// Good +function divide(a, b) { + if (b === 0) { + throw new Error("Division by zero"); + } + return a / b; +} +``` + +# ESLint와 Prettier 소개 및 사용 방법 + +## ESLint + +ESLint는 JavaScript 코드의 품질을 향상시키는 강력한 정적 코드 분석 도구입니다. 코드 스타일 문제, 잠재적 버그, 안티 패턴 등을 식별하고 수정할 수 있습니다. + +## Prettier + +Prettier는 코드 포맷터로, 일관된 코드 스타일을 자동으로 적용합니다. 들여쓰기, 줄 바꿈, 공백 등의 스타일 문제를 자동으로 해결해줍니다. + +## 설치 및 초기 설정 + +```bash +# ESLint와 Prettier 설치 +npm install eslint prettier --save-dev + +# ESLint 설정 초기화 +npx eslint --init + +# ESLint와 Prettier 통합을 위한 추가 패키지 설치 +npm install --save-dev eslint-config-prettier eslint-plugin-prettier + +``` + +## 사용 방법 + +ESLint: + +```bash +# 특정 파일 린팅 +npx eslint yourfile.js + +# 프로젝트 전체 npx eslint yourfile.js린팅 +npx eslint . + +# 자동 수정 가능한 문제 수정 +npx eslint . --fix + +``` + +Prettier: + +```bash +# 특정 파일 포맷팅 +npx prettier --write yourfile.js + +# 프로젝트 전체 포맷팅 +npx prettier --write . + +# 포맷팅이 필요한 파일 확인 +npx prettier --check . + +``` + +## 설정 파일 + +ESLint: `.eslintrc.js` 또는 `.eslintrc.json` + +```jsx +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: ["eslint:recommended", "plugin:react/recommended", "prettier"], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 12, + sourceType: "module", + }, + plugins: ["react"], + rules: { + // 여기에 custom 규칙 추가 + }, +}; +``` + +Prettier: `.prettierrc` + +```json +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} +``` + +## 유용한 ESLint 플러그인 + +- eslint-plugin-jest: Jest 테스트 관련 린팅 규칙 +- eslint-plugin-node: Node.js 관련 린팅 규칙 +- eslint-plugin-security: 보안 관련 린팅 규칙 +- eslint-plugin-sonarjs: SonarJS에서 제공하는 버그 및 코드 스멜 탐지 규칙 +- eslint-plugin-unicorn: 추가적인 코드 품질 향상을 위한 규칙 모음 +- eslint-plugin-compat: 브라우저 호환성 체크 +- eslint-plugin-prettier: ESLint와 Prettier 통합 + +## 유용한 Prettier 플러그인 + +- @prettier/plugin-php: PHP 코드 포맷팅 +- prettier-plugin-solidity: Solidity 코드 포맷팅 +- prettier-plugin-packagejson: package.json 파일 포맷팅 +- prettier-plugin-java: Java 코드 포맷팅 + +## 유명한 ESLint 규칙 및 설정 + +- airbnb 스타일 가이드: `eslint-config-airbnb` +- Google 스타일 가이드: `eslint-config-google` +- Standard 스타일 가이드: `eslint-config-standard` + +## 주요 ESLint 규칙: + +- `no-unused-vars`: 사용되지 않는 변수 검출 +- `no-console`: console.log() 사용 제한 +- `eqeqeq`: === 및 !== 연산자 사용 강제 +- `prefer-const`: 재할당되지 않는 변수에 const 사용 강제 +- `no-var`: var 대신 let 또는 const 사용 강제 +- `curly`: 모든 제어문에 중괄호 사용 강제 +- `max-len`: 최대 줄 길이 제한 +- `no-multiple-empty-lines`: 연속된 빈 줄 제한 +- `camelcase`: 카멜케이스 네이밍 강제 +- `no-shadow`: 변수 섀도잉 방지 + +## ESLint와 Prettier 통합 + +`.eslintrc.js`에 다음 설정 추가: + +```jsx +{ + "extends": [ + "plugin:prettier/recommended" + ], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error" + } +} + +``` + +이렇게 설정하면 ESLint가 Prettier 규칙을 적용하고, 스타일 관련 문제를 자동으로 수정할 수 있습니다. + +## IDE 통합 + +대부분의 현대적인 IDE와 텍스트 에디터(VS Code, WebStorm, Sublime Text 등)는 ESLint와 Prettier를 플러그인 형태로 지원합니다. 이를 통해 코드 작성 시 실시간으로 문제를 확인하고 수정할 수 있습니다. + +이러한 도구와 설정을 활용하면 코드 품질을 크게 향상시키고, 팀 내에서 일관된 코딩 스타일을 유지할 수 있습니다. diff --git "a/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" "b/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" new file mode 100644 index 000000000..432c4d7ec --- /dev/null +++ "b/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" @@ -0,0 +1,306 @@ +# 장바구니 시스템 리팩토링 계획서 + +## 📋 개요 + +### 현재 상태 + +- **파일**: `src/basic/main.basic.js` (763줄) +- **주요 문제**: 과도하게 긴 함수, 전역 상태, 중복 코드, 매직 넘버 +- **테스트**: `src/basic/__tests__/basic.test.js` (모든 테스트 통과 필수) + +### 목표 + +- 코드 동작 유지 (기능 변경 금지) +- 구조 변경을 통한 가독성 및 유지보수성 향상 +- Clean Code 원칙 적용 +- 모든 테스트 통과 보장 + +--- + +## 🎯 리팩토링 우선순위 + +### 1단계 (Critical) - 안정성 확보 + +테스트 안정성을 보장하면서 기본적인 구조 정리 + +### 2단계 (High) - 가독성 개선 + +함수 분리와 네이밍을 통한 코드 이해도 향상 + +### 3단계 (Medium) - 중복 제거 + +반복되는 패턴과 코드 블록 추상화 + +### 4단계 (Low) - 최적화 + +성능 개선 및 추가적인 구조 정리 + +--- + +## 📝 상세 태스크 목록 + +### 🚨 1단계: 안정성 확보 (Critical) + +#### 1.1 테스트 환경 검증 + +- **Priority**: P0 +- **Size**: Small +- **Tasks**: + - [ ] 현재 테스트 실행하여 기준선 확보 + - [ ] 테스트 중 스킵된 항목 파악 + - [ ] 리팩토링 전 스냅샷 생성 + +#### 1.2 전역 변수 정리 + +- **Priority**: P0 +- **Size**: Medium +- **Problems**: + ```javascript + var prodList, + bonusPts = 0, + stockInfo, + itemCnt, + lastSel, + sel, + addBtn, + totalAmt = 0; + ``` +- **Tasks**: + - [ ] 전역 변수 목록 정리 (prodList, bonusPts, stockInfo 등) + - [ ] const/let으로 변경 가능한 변수 식별 + - [ ] 변수 스코프 최소화 계획 수립 + +#### 1.3 매직 넘버 상수화 + +- **Priority**: P0 +- **Size**: Small +- **Problems**: `10`, `30`, `0.1`, `5` 등 의미 불명확한 숫자들 +- **Tasks**: + - [ ] 할인 임계값 상수화 (`BULK_DISCOUNT_THRESHOLD = 10`) + - [ ] 할인율 상수화 (`TUESDAY_DISCOUNT_RATE = 0.1`) + - [ ] 재고 부족 기준 상수화 (`LOW_STOCK_THRESHOLD = 5`) + - [ ] 포인트 적립 기준 상수화 (`POINTS_PER_1000 = 1`) + +### 🔧 2단계: 가독성 개선 (High) + +#### 2.1 거대한 함수 분해 - calcCart + +- **Priority**: P1 +- **Size**: Large +- **Problems**: 209줄의 `calcCart` 함수가 모든 책임을 가짐 +- **Tasks**: + - [ ] 장바구니 아이템 계산 분리 (`calculateCartItems`) + - [ ] 할인 계산 분리 (`calculateDiscounts`) + - [ ] 포인트 계산 분리 (`calculatePoints`) + - [ ] UI 업데이트 분리 (`updateUI`) + - [ ] 재고 상태 업데이트 분리 (`updateStockStatus`) + +#### 2.2 함수명 및 변수명 개선 + +- **Priority**: P1 +- **Size**: Medium +- **Problems**: `p`, `q`, `amt`, `sel`, `tgt` 등 의미 불분명한 이름들 +- **Tasks**: + - [ ] 상품 변수명 정규화 (`p1` → `PRODUCT_KEYBOARD`) + - [ ] 수량 변수명 명확화 (`q` → `quantity`) + - [ ] 금액 변수명 명확화 (`amt` → `amount`) + - [ ] 선택자 변수명 명확화 (`sel` → `productSelect`) + +#### 2.3 조건문 가독성 개선 + +- **Priority**: P1 +- **Size**: Medium +- **Problems**: 복잡한 중첩 조건문과 긴 조건식 +- **Tasks**: + - [ ] 화요일 체크 함수화 (`isTuesday()`) + - [ ] 할인 조건 함수화 (`isEligibleForBulkDiscount()`) + - [ ] 재고 상태 체크 함수화 (`isLowStock()`, `isOutOfStock()`) + - [ ] 조기 반환 패턴 적용 + +### 🔄 3단계: 중복 제거 (Medium) + +#### 3.1 포인트 계산 로직 통합 + +- **Priority**: P2 +- **Size**: Medium +- **Problems**: `calcCart`와 `doRenderBonusPoints`에서 중복 계산 +- **Tasks**: + - [ ] 기본 포인트 계산 함수 생성 (`calculateBasePoints`) + - [ ] 화요일 포인트 배수 함수 생성 (`applyTuesdayPointsMultiplier`) + - [ ] 세트 구매 보너스 함수 생성 (`calculateSetBonus`) + - [ ] 수량별 보너스 함수 생성 (`calculateQuantityBonus`) + +#### 3.2 재고 체크 로직 통합 + +- **Priority**: P2 +- **Size**: Small +- **Problems**: 여러 곳에서 재고 계산 중복 +- **Tasks**: + - [ ] 총 재고 계산 함수 정리 (`getTotalStock`) + - [ ] 재고 부족 상품 목록 함수 생성 (`getLowStockItems`) + - [ ] 재고 상태 메시지 생성 함수 생성 (`generateStockMessage`) + +#### 3.3 DOM 조작 패턴 추상화 + +- **Priority**: P2 +- **Size**: Medium +- **Problems**: 반복되는 DOM 조작 코드 +- **Tasks**: + - [ ] 요소 텍스트 업데이트 헬퍼 (`updateElementText`) + - [ ] 장바구니 아이템 생성 템플릿화 (`createCartItemElement`) + - [ ] 할인 표시 로직 통합 (`updateDiscountDisplay`) + +### ⚡ 4단계: 최적화 (Low) + +#### 4.1 성능 최적화 + +- **Priority**: P3 +- **Size**: Small +- **Problems**: 불필요한 재계산과 DOM 조작 +- **Tasks**: + - [ ] Date 객체 생성 최소화 + - [ ] innerHTML 대신 textContent 사용 최적화 + - [ ] 이벤트 리스너 최적화 + +#### 4.2 코드 조직화 + +- **Priority**: P3 +- **Size**: Medium +- **Tasks**: + - [ ] 상수 영역 정리 + - [ ] 함수 선언 순서 정리 (호출 순서대로) + - [ ] 관련 함수들 그룹화 + - [ ] 주석 정리 및 JSDoc 적용 + +--- + +## 🧪 각 단계별 검증 기준 + +### 1단계 완료 기준 + +- [ ] 모든 테스트 통과 +- [ ] 전역 변수 const/let으로 전환 +- [ ] 매직 넘버 상수화 완료 +- [ ] 코드 동작 100% 일치 + +### 2단계 완료 기준 + +- [ ] `calcCart` 함수 200줄 → 50줄 이하 +- [ ] 함수별 단일 책임 원칙 적용 +- [ ] 의미 있는 함수/변수명 적용 +- [ ] 모든 테스트 통과 + +### 3단계 완료 기준 + +- [ ] 중복 코드 90% 이상 제거 +- [ ] 포인트/재고 계산 로직 통합 +- [ ] DOM 조작 헬퍼 함수 적용 +- [ ] 모든 테스트 통과 + +### 4단계 완료 기준 + +- [ ] 불필요한 재계산 제거 +- [ ] 코드 구조 최종 정리 +- [ ] JSDoc 문서화 완료 +- [ ] 모든 테스트 통과 + +--- + +## 🔧 리팩토링 실행 가이드 + +### 안전한 리팩토링을 위한 규칙 + +1. **테스트 우선**: 모든 변경 후 즉시 테스트 실행 +2. **작은 단위**: 한 번에 하나의 변경만 수행 +3. **기능 보존**: 동작 변경 절대 금지 +4. **점진적 개선**: 단계별 검증 후 다음 단계 진행 + +### 각 태스크 실행 순서 + +```bash +# 1. 현재 상태 검증 +npm test + +# 2. 하나의 태스크 수행 +# 예: 매직 넘버 상수화 + +# 3. 즉시 테스트 검증 +npm test + +# 4. 통과하면 다음 태스크, 실패하면 롤백 +``` + +### 리팩토링 체크리스트 + +#### 각 변경 후 확인사항 + +- [ ] `npm test` 실행하여 모든 테스트 통과 +- [ ] 브라우저에서 기능 동작 확인 +- [ ] 콘솔 에러 없음 확인 +- [ ] 메모리 누수 없음 확인 + +#### 완료 후 최종 검증 + +- [ ] 전체 테스트 통과 (스킵된 테스트 제외) +- [ ] 기능 시나리오 수동 테스트 +- [ ] 코드 리뷰 준비 (변경점 정리) +- [ ] 성능 비교 (변경 전/후) + +--- + +## 📊 예상 결과 + +### 리팩토링 전 + +- **파일 수**: 1개 (main.basic.js) +- **총 라인 수**: 763줄 +- **최대 함수 길이**: 209줄 (`calcCart`) +- **전역 변수**: 12개 +- **매직 넘버**: 15개 이상 + +### 리팩토링 후 (목표) + +- **파일 수**: 1개 (구조만 정리, 파일 분리는 하지 않음) +- **총 라인 수**: 700줄 이하 (주석 포함) +- **최대 함수 길이**: 20줄 이하 +- **전역 변수**: 5개 이하 +- **매직 넘버**: 0개 + +### 품질 개선 지표 + +- **가독성**: 함수명으로 의도 파악 가능 +- **유지보수성**: 단일 책임 함수로 변경 영향 최소화 +- **테스트 안정성**: 100% 테스트 통과 유지 +- **성능**: 기존 성능 유지 또는 개선 + +--- + +## 🚨 주의사항 + +### 절대 금지 사항 + +- ❌ 기능 동작 변경 +- ❌ 테스트 코드 수정 +- ❌ 새로운 파일 생성 (단일 파일 유지) +- ❌ 외부 라이브러리 추가 + +### 필수 준수 사항 + +- ✅ 모든 테스트 통과 유지 +- ✅ `main.basic.js` 파일 기준으로 작업 +- ✅ 단계별 검증 수행 +- ✅ Clean Code 원칙 적용 + +--- + +## 📅 예상 일정 + +| 단계 | 소요 시간 | 완료 기준 | +| -------- | ------------- | ------------------- | +| 1단계 | 2-3시간 | 안정성 확보 | +| 2단계 | 4-5시간 | 가독성 개선 | +| 3단계 | 3-4시간 | 중복 제거 | +| 4단계 | 2-3시간 | 최적화 | +| **총계** | **11-15시간** | **완전한 리팩토링** | + +이 계획서를 기반으로 체계적이고 안전한 리팩토링을 수행하여 깨끗하고 유지보수 가능한 코드로 개선할 수 있습니다. From eceb74dfe2f9cd921fcdc4366a9cda63e685442a Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 22:42:04 +0900 Subject: [PATCH 04/55] =?UTF-8?q?refactor:=20var=20=EB=B3=80=EC=88=98=20le?= =?UTF-8?q?t,=20const=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/main.basic.js | 587 ++++++++++++++++++++++------------------ 1 file changed, 330 insertions(+), 257 deletions(-) diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 825eae3a5..542927a5a 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,36 +1,68 @@ -var prodList -var bonusPts = 0 -var stockInfo -var itemCnt -var lastSel -var sel -var addBtn -var totalAmt = 0 -var PRODUCT_ONE = 'p1' -var p2 = 'p2' -var product_3 = 'p3' -var p4 = "p4" -var PRODUCT_5 = `p5` -var cartDisp +let prodList; +let bonusPts = 0; +let stockInfo; +let itemCnt; +let lastSel; +let sel; +let addBtn; +let totalAmt = 0; +const PRODUCT_ONE = "p1"; +const p2 = "p2"; +const product_3 = "p3"; +const p4 = "p4"; +const PRODUCT_5 = `p5`; +let cartDisp; function main() { var root; - var header; - var gridContainer; - var leftColumn; - var selectorContainer; - var rightColumn; - var manualToggle; - var manualOverlay; - var manualColumn; - var lightningDelay; + let header; + let gridContainer; + let leftColumn; + let selectorContainer; + let rightColumn; + let manualToggle; + let manualOverlay; + let manualColumn; + let lightningDelay; totalAmt = 0; itemCnt = 0; lastSel = null; prodList = [ - {id: PRODUCT_ONE, name: '버그 없애는 키보드', val: 10000, originalVal: 10000, q: 50, onSale: false, suggestSale: false}, - {id: p2, name: '생산성 폭발 마우스', val: 20000, originalVal: 20000, q: 30, onSale: false, suggestSale: false}, - {id: product_3, name: "거북목 탈출 모니터암", val: 30000, originalVal: 30000, q: 20, onSale: false, suggestSale: false}, - {id: p4, name: "에러 방지 노트북 파우치", val: 15000, originalVal: 15000, q: 0, onSale: false, suggestSale: false}, + { + id: PRODUCT_ONE, + name: "버그 없애는 키보드", + val: 10000, + originalVal: 10000, + q: 50, + onSale: false, + suggestSale: false, + }, + { + id: p2, + name: "생산성 폭발 마우스", + val: 20000, + originalVal: 20000, + q: 30, + onSale: false, + suggestSale: false, + }, + { + id: product_3, + name: "거북목 탈출 모니터암", + val: 30000, + originalVal: 30000, + q: 20, + onSale: false, + suggestSale: false, + }, + { + id: p4, + name: "에러 방지 노트북 파우치", + val: 15000, + originalVal: 15000, + q: 0, + onSale: false, + suggestSale: false, + }, { id: PRODUCT_5, name: `코딩할 때 듣는 Lo-Fi 스피커`, @@ -38,42 +70,44 @@ function main() { originalVal: 25000, q: 10, onSale: false, - suggestSale: false - } - ] - var root = document.getElementById('app') - header = document.createElement('div'); - header.className = 'mb-8' + suggestSale: false, + }, + ]; + var root = document.getElementById("app"); + header = document.createElement("div"); + header.className = "mb-8"; header.innerHTML = `

🛒 Hanghae Online Store

Shopping Cart

🛍️ 0 items in cart

`; - sel = document.createElement('select'); - sel.id = 'product-select'; - gridContainer = document.createElement('div'); + sel = document.createElement("select"); + sel.id = "product-select"; + gridContainer = document.createElement("div"); leftColumn = document.createElement("div"); - leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto' - selectorContainer = document.createElement('div'); - selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200'; - sel.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3'; - gridContainer.className = 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden'; - addBtn = document.createElement('button'); - stockInfo = document.createElement('div'); - addBtn.id = 'add-to-cart'; - stockInfo.id = 'stock-status'; - stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line'; - addBtn.innerHTML = 'Add to Cart'; - addBtn.className = 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all'; + leftColumn["className"] = "bg-white border border-gray-200 p-8 overflow-y-auto"; + selectorContainer = document.createElement("div"); + selectorContainer.className = "mb-6 pb-6 border-b border-gray-200"; + sel.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; + gridContainer.className = + "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; + addBtn = document.createElement("button"); + stockInfo = document.createElement("div"); + addBtn.id = "add-to-cart"; + stockInfo.id = "stock-status"; + stockInfo.className = "text-xs text-red-500 mt-3 whitespace-pre-line"; + addBtn.innerHTML = "Add to Cart"; + addBtn.className = + "w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all"; selectorContainer.appendChild(sel); selectorContainer.appendChild(addBtn); selectorContainer.appendChild(stockInfo); leftColumn.appendChild(selectorContainer); - cartDisp = document.createElement('div'); + cartDisp = document.createElement("div"); leftColumn.appendChild(cartDisp); - cartDisp.id = 'cart-items'; - rightColumn = document.createElement('div'); - rightColumn.className = 'bg-black text-white p-8 flex flex-col'; + cartDisp.id = "cart-items"; + rightColumn = document.createElement("div"); + rightColumn.className = "bg-black text-white p-8 flex flex-col"; rightColumn.innerHTML = `

Order Summary

@@ -103,28 +137,30 @@ function main() { Earn loyalty points with purchase.

`; - sum = rightColumn.querySelector('#cart-total'); - manualToggle = document.createElement('button'); + sum = rightColumn.querySelector("#cart-total"); + manualToggle = document.createElement("button"); manualToggle.onclick = function () { - manualOverlay.classList.toggle('hidden'); - manualColumn.classList.toggle('translate-x-full'); + manualOverlay.classList.toggle("hidden"); + manualColumn.classList.toggle("translate-x-full"); }; - manualToggle.className = 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50'; + manualToggle.className = + "fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50"; manualToggle.innerHTML = ` `; - manualOverlay = document.createElement('div'); - manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300'; + manualOverlay = document.createElement("div"); + manualOverlay.className = "fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300"; manualOverlay.onclick = function (e) { if (e.target === manualOverlay) { - manualOverlay.classList.add('hidden'); - manualColumn.classList.add('translate-x-full'); + manualOverlay.classList.add("hidden"); + manualColumn.classList.add("translate-x-full"); } }; - manualColumn = document.createElement('div'); - manualColumn.className = 'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300'; + manualColumn = document.createElement("div"); + manualColumn.className = + "fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300"; manualColumn.innerHTML = `
`; } - itemCountElement = document.getElementById('item-count'); + itemCountElement = document.getElementById("item-count"); if (itemCountElement) { previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0); - itemCountElement.textContent = '🛍️ ' + itemCnt + ' items in cart'; + itemCountElement.textContent = "🛍️ " + itemCnt + " items in cart"; if (previousCount !== itemCnt) { - itemCountElement.setAttribute('data-changed', 'true'); + itemCountElement.setAttribute("data-changed", "true"); } } - stockMsg = ''; - for (var stockIdx = 0; stockIdx < prodList.length; stockIdx++) { - var item = prodList[stockIdx]; + stockMsg = ""; + for (let stockIdx = 0; stockIdx < prodList.length; stockIdx++) { + const item = prodList[stockIdx]; if (item.q < 5) { if (item.q > 0) { - stockMsg = stockMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n'; + stockMsg = stockMsg + item.name + ": 재고 부족 (" + item.q + "개 남음)\n"; } else { - stockMsg = stockMsg + item.name + ': 품절\n'; + stockMsg = stockMsg + item.name + ": 품절\n"; } } } @@ -509,29 +558,29 @@ function handleCalculateCartStuff() { handleStockInfoUpdate(); doRenderBonusPoints(); } -var doRenderBonusPoints = function() { - var basePoints; - var finalPoints; - var pointsDetail; - var hasKeyboard; - var hasMouse; - var hasMonitorArm; - var nodes; +var doRenderBonusPoints = function () { + let basePoints; + let finalPoints; + let pointsDetail; + let hasKeyboard; + let hasMouse; + let hasMonitorArm; + let nodes; if (cartDisp.children.length === 0) { - document.getElementById('loyalty-points').style.display = 'none'; + document.getElementById("loyalty-points").style.display = "none"; return; } - basePoints = Math.floor(totalAmt / 1000) + basePoints = Math.floor(totalAmt / 1000); finalPoints = 0; pointsDetail = []; if (basePoints > 0) { finalPoints = basePoints; - pointsDetail.push('기본: ' + basePoints + 'p'); + pointsDetail.push("기본: " + basePoints + "p"); } if (new Date().getDay() === 2) { if (basePoints > 0) { finalPoints = basePoints * 2; - pointsDetail.push('화요일 2배'); + pointsDetail.push("화요일 2배"); } } hasKeyboard = false; @@ -539,14 +588,16 @@ var doRenderBonusPoints = function() { hasMonitorArm = false; nodes = cartDisp.children; for (const node of nodes) { - var product = null; - for (var pIdx = 0; pIdx < prodList.length; pIdx++) { + let product = null; + for (let pIdx = 0; pIdx < prodList.length; pIdx++) { if (prodList[pIdx].id === node.id) { product = prodList[pIdx]; break; } } - if (!product) continue; + if (!product) { + continue; + } if (product.id === PRODUCT_ONE) { hasKeyboard = true; } else if (product.id === p2) { @@ -557,43 +608,48 @@ var doRenderBonusPoints = function() { } if (hasKeyboard && hasMouse) { finalPoints = finalPoints + 50; - pointsDetail.push('키보드+마우스 세트 +50p'); + pointsDetail.push("키보드+마우스 세트 +50p"); } if (hasKeyboard && hasMouse && hasMonitorArm) { finalPoints = finalPoints + 100; - pointsDetail.push('풀세트 구매 +100p'); + pointsDetail.push("풀세트 구매 +100p"); } if (itemCnt >= 30) { finalPoints = finalPoints + 100; - pointsDetail.push('대량구매(30개+) +100p'); + pointsDetail.push("대량구매(30개+) +100p"); } else { if (itemCnt >= 20) { finalPoints = finalPoints + 50; - pointsDetail.push('대량구매(20개+) +50p'); + pointsDetail.push("대량구매(20개+) +50p"); } else { if (itemCnt >= 10) { finalPoints = finalPoints + 20; - pointsDetail.push('대량구매(10개+) +20p'); + pointsDetail.push("대량구매(10개+) +20p"); } } } bonusPts = finalPoints; - var ptsTag = document.getElementById('loyalty-points'); + const ptsTag = document.getElementById("loyalty-points"); if (ptsTag) { if (bonusPts > 0) { - ptsTag.innerHTML = '
적립 포인트: ' + bonusPts + 'p
' + - '
' + pointsDetail.join(', ') + '
'; - ptsTag.style.display = 'block'; + ptsTag.innerHTML = + '
적립 포인트: ' + + bonusPts + + "p
" + + '
' + + pointsDetail.join(", ") + + "
"; + ptsTag.style.display = "block"; } else { - ptsTag.textContent = '적립 포인트: 0p'; - ptsTag.style.display = 'block' + ptsTag.textContent = "적립 포인트: 0p"; + ptsTag.style.display = "block"; } } -} +}; function onGetStockTotal() { - var sum; - var i; - var currentProduct; + let sum; + let i; + let currentProduct; sum = 0; for (i = 0; i < prodList.length; i++) { currentProduct = prodList[i]; @@ -601,61 +657,77 @@ function onGetStockTotal() { } return sum; } -var handleStockInfoUpdate = function() { - var infoMsg; - var totalStock; - var messageOptimizer; - infoMsg = ''; +var handleStockInfoUpdate = function () { + let infoMsg; + let totalStock; + let messageOptimizer; + infoMsg = ""; totalStock = onGetStockTotal(); if (totalStock < 30) { } - prodList.forEach(function (item) { + prodList.forEach(item => { if (item.q < 5) { if (item.q > 0) { - infoMsg = infoMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n'; + infoMsg = infoMsg + item.name + ": 재고 부족 (" + item.q + "개 남음)\n"; } else { - infoMsg = infoMsg + item.name + ': 품절\n'; + infoMsg = infoMsg + item.name + ": 품절\n"; } } }); stockInfo.textContent = infoMsg; -} +}; function doUpdatePricesInCart() { - var totalCount = 0, j = 0; - var cartItems; + let totalCount = 0, + j = 0; + let cartItems; while (cartDisp.children[j]) { - var qty = cartDisp.children[j].querySelector('.quantity-number'); + const qty = cartDisp.children[j].querySelector(".quantity-number"); totalCount += qty ? parseInt(qty.textContent) : 0; j++; } totalCount = 0; for (j = 0; j < cartDisp.children.length; j++) { - totalCount += parseInt(cartDisp.children[j].querySelector('.quantity-number').textContent); + totalCount += parseInt(cartDisp.children[j].querySelector(".quantity-number").textContent); } cartItems = cartDisp.children; - for (var i = 0; i < cartItems.length; i++) { - var itemId = cartItems[i].id; - var product = null; - for (var productIdx = 0; productIdx < prodList.length; productIdx++) { + for (let i = 0; i < cartItems.length; i++) { + const itemId = cartItems[i].id; + let product = null; + for (let productIdx = 0; productIdx < prodList.length; productIdx++) { if (prodList[productIdx].id === itemId) { product = prodList[productIdx]; break; } } if (product) { - var priceDiv = cartItems[i].querySelector('.text-lg'); - var nameDiv = cartItems[i].querySelector('h3'); + const priceDiv = cartItems[i].querySelector(".text-lg"); + const nameDiv = cartItems[i].querySelector("h3"); if (product.onSale && product.suggestSale) { - priceDiv.innerHTML = '₩' + product.originalVal.toLocaleString() + ' ₩' + product.val.toLocaleString() + ''; - nameDiv.textContent = '⚡💝' + product.name; + priceDiv.innerHTML = + '₩' + + product.originalVal.toLocaleString() + + ' ₩' + + product.val.toLocaleString() + + ""; + nameDiv.textContent = "⚡💝" + product.name; } else if (product.onSale) { - priceDiv.innerHTML = '₩' + product.originalVal.toLocaleString() + ' ₩' + product.val.toLocaleString() + ''; - nameDiv.textContent = '⚡' + product.name; + priceDiv.innerHTML = + '₩' + + product.originalVal.toLocaleString() + + ' ₩' + + product.val.toLocaleString() + + ""; + nameDiv.textContent = "⚡" + product.name; } else if (product.suggestSale) { - priceDiv.innerHTML = '₩' + product.originalVal.toLocaleString() + ' ₩' + product.val.toLocaleString() + ''; - nameDiv.textContent = '💝' + product.name; + priceDiv.innerHTML = + '₩' + + product.originalVal.toLocaleString() + + ' ₩' + + product.val.toLocaleString() + + ""; + nameDiv.textContent = "💝" + product.name; } else { - priceDiv.textContent = '₩' + product.val.toLocaleString(); + priceDiv.textContent = "₩" + product.val.toLocaleString(); nameDiv.textContent = product.name; } } @@ -663,10 +735,10 @@ function doUpdatePricesInCart() { handleCalculateCartStuff(); } main(); -addBtn.addEventListener("click", function () { - var selItem = sel.value - var hasItem = false; - for (var idx = 0; idx < prodList.length; idx++) { +addBtn.addEventListener("click", () => { + const selItem = sel.value; + let hasItem = false; + for (let idx = 0; idx < prodList.length; idx++) { if (prodList[idx].id === selItem) { hasItem = true; break; @@ -675,36 +747,37 @@ addBtn.addEventListener("click", function () { if (!selItem || !hasItem) { return; } - var itemToAdd = null; - for (var j = 0; j < prodList.length; j++) { + let itemToAdd = null; + for (let j = 0; j < prodList.length; j++) { if (prodList[j].id === selItem) { itemToAdd = prodList[j]; break; } } if (itemToAdd && itemToAdd.q > 0) { - var item = document.getElementById(itemToAdd['id']); + const item = document.getElementById(itemToAdd["id"]); if (item) { - var qtyElem = item.querySelector('.quantity-number') - var newQty = parseInt(qtyElem['textContent']) + 1 + const qtyElem = item.querySelector(".quantity-number"); + const newQty = parseInt(qtyElem["textContent"]) + 1; if (newQty <= itemToAdd.q + parseInt(qtyElem.textContent)) { qtyElem.textContent = newQty; - itemToAdd['q']-- + itemToAdd["q"]--; } else { - alert('재고가 부족합니다.'); + alert("재고가 부족합니다."); } } else { - var newItem = document.createElement('div'); + const newItem = document.createElement("div"); newItem.id = itemToAdd.id; - newItem.className = 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0'; + newItem.className = + "grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0"; newItem.innerHTML = `
-

${itemToAdd.onSale && itemToAdd.suggestSale ? '⚡💝' : itemToAdd.onSale ? '⚡' : itemToAdd.suggestSale ? '💝' : ''}${itemToAdd.name}

+

${itemToAdd.onSale && itemToAdd.suggestSale ? "⚡💝" : itemToAdd.onSale ? "⚡" : itemToAdd.suggestSale ? "💝" : ""}${itemToAdd.name}

PRODUCT

-

${itemToAdd.onSale || itemToAdd.suggestSale ? '₩' + itemToAdd.originalVal.toLocaleString() + ' ₩' + itemToAdd.val.toLocaleString() + '' : '₩' + itemToAdd.val.toLocaleString()}

+

${itemToAdd.onSale || itemToAdd.suggestSale ? '₩' + itemToAdd.originalVal.toLocaleString() + ' ₩' + itemToAdd.val.toLocaleString() + "" : "₩" + itemToAdd.val.toLocaleString()}

1 @@ -712,7 +785,7 @@ addBtn.addEventListener("click", function () {
-
${itemToAdd.onSale || itemToAdd.suggestSale ? '₩' + itemToAdd.originalVal.toLocaleString() + ' ₩' + itemToAdd.val.toLocaleString() + '' : '₩' + itemToAdd.val.toLocaleString()}
+
${itemToAdd.onSale || itemToAdd.suggestSale ? '₩' + itemToAdd.originalVal.toLocaleString() + ' ₩' + itemToAdd.val.toLocaleString() + "" : "₩" + itemToAdd.val.toLocaleString()}
Remove
`; @@ -723,23 +796,23 @@ addBtn.addEventListener("click", function () { lastSel = selItem; } }); -cartDisp.addEventListener("click", function (event) { - var tgt = event.target; - if (tgt.classList.contains('quantity-change') || tgt.classList.contains("remove-item")) { - var prodId = tgt.dataset.productId; - var itemElem = document.getElementById(prodId) - var prod = null; - for (var prdIdx = 0; prdIdx < prodList.length; prdIdx++) { +cartDisp.addEventListener("click", event => { + const tgt = event.target; + if (tgt.classList.contains("quantity-change") || tgt.classList.contains("remove-item")) { + const prodId = tgt.dataset.productId; + const itemElem = document.getElementById(prodId); + let prod = null; + for (let prdIdx = 0; prdIdx < prodList.length; prdIdx++) { if (prodList[prdIdx].id === prodId) { prod = prodList[prdIdx]; break; } } - if (tgt.classList.contains('quantity-change')) { - var qtyChange = parseInt(tgt.dataset.change); - var qtyElem = itemElem.querySelector('.quantity-number'); - var currentQty = parseInt(qtyElem.textContent); - var newQty = currentQty + qtyChange; + if (tgt.classList.contains("quantity-change")) { + const qtyChange = parseInt(tgt.dataset.change); + var qtyElem = itemElem.querySelector(".quantity-number"); + const currentQty = parseInt(qtyElem.textContent); + const newQty = currentQty + qtyChange; if (newQty > 0 && newQty <= prod.q + currentQty) { qtyElem.textContent = newQty; prod.q -= qtyChange; @@ -747,11 +820,11 @@ cartDisp.addEventListener("click", function (event) { prod.q += currentQty; itemElem.remove(); } else { - alert('재고가 부족합니다.'); + alert("재고가 부족합니다."); } - } else if (tgt.classList.contains('remove-item')) { - var qtyElem = itemElem.querySelector('.quantity-number'); - var remQty = parseInt(qtyElem.textContent); + } else if (tgt.classList.contains("remove-item")) { + var qtyElem = itemElem.querySelector(".quantity-number"); + const remQty = parseInt(qtyElem.textContent); prod.q += remQty; itemElem.remove(); } @@ -760,4 +833,4 @@ cartDisp.addEventListener("click", function (event) { handleCalculateCartStuff(); onUpdateSelectOptions(); } -}); \ No newline at end of file +}); From f8a0f2b212b9432378369e38325eb474633ffcf7 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 23:10:26 +0900 Subject: [PATCH 05/55] =?UTF-8?q?refactor:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/data/product.data.js | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/basic/data/product.data.js diff --git a/src/basic/data/product.data.js b/src/basic/data/product.data.js new file mode 100644 index 000000000..d1244cdfc --- /dev/null +++ b/src/basic/data/product.data.js @@ -0,0 +1,47 @@ +export const PRODUCT_LIST = [ + { + id: "p1", + name: "버그 없애는 키보드", + val: 10000, + originalVal: 10000, + q: 50, + onSale: false, + suggestSale: false, + }, + { + id: "p2", + name: "생산성 폭발 마우스", + val: 20000, + originalVal: 20000, + q: 30, + onSale: false, + suggestSale: false, + }, + { + id: "p3", + name: "거북목 탈출 모니터암", + val: 30000, + originalVal: 30000, + q: 20, + onSale: false, + suggestSale: false, + }, + { + id: "p4", + name: "에러 방지 노트북 파우치", + val: 15000, + originalVal: 15000, + q: 0, + onSale: false, + suggestSale: false, + }, + { + id: "p5", + name: `코딩할 때 듣는 Lo-Fi 스피커`, + val: 25000, + originalVal: 25000, + q: 10, + onSale: false, + suggestSale: false, + }, +]; From 1b4c7c220d2b0129dda41d20c7f8a31ca40f872d Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 23:10:52 +0900 Subject: [PATCH 06/55] =?UTF-8?q?chore:=20eslint=20airbnb=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.js | 135 +++++- package.json | 2 + pnpm-lock.yaml | 1194 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1314 insertions(+), 17 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index df6f569a0..a1f2e2c4b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,7 @@ import js from "@eslint/js"; import prettier from "eslint-plugin-prettier"; import prettierConfig from "eslint-config-prettier"; +import importPlugin from "eslint-plugin-import"; export default [ js.configs.recommended, @@ -36,32 +37,104 @@ export default [ }, plugins: { prettier, + import: importPlugin, }, rules: { // Prettier 규칙 "prettier/prettier": "error", - // 변수 관련 (일부 완화) - "no-var": "warn", - "prefer-const": "warn", - "no-unused-vars": "warn", - "no-redeclare": "warn", + // Airbnb 기반 변수 및 함수 규칙 + "no-var": "error", + "prefer-const": "error", + "no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + ignoreRestSiblings: true, + argsIgnorePattern: "^_", + }, + ], + "no-redeclare": "error", + "no-shadow": "error", + "no-undef": "error", - // 함수 관련 - "prefer-arrow-callback": "warn", + // 함수 관련 (Airbnb 스타일) + "prefer-arrow-callback": ["error", { allowNamedFunctions: false }], + "func-style": ["error", "declaration", { allowArrowFunctions: true }], + "arrow-body-style": ["error", "as-needed"], + "arrow-parens": ["error", "as-needed"], + "arrow-spacing": ["error", { before: true, after: true }], - // 코드 품질 - eqeqeq: ["error", "always"], + // 코드 품질 (Airbnb 기준) + eqeqeq: ["error", "always", { null: "ignore" }], "no-console": "warn", - "no-alert": "warn", - "no-empty": "warn", + "no-alert": "error", + "no-debugger": "error", + "no-empty": "error", + "no-eval": "error", + "no-implied-eval": "error", + "no-new-func": "error", + "no-script-url": "error", - // 코딩 스타일 - "consistent-return": "error", + // 객체 및 배열 (Airbnb 스타일) + "object-shorthand": ["error", "always"], + "prefer-destructuring": [ + "error", + { + array: true, + object: true, + }, + { + enforceForRenamedProperties: false, + }, + ], + "quote-props": ["error", "as-needed"], + "no-array-constructor": "error", + "no-new-object": "error", + + // 문자열 (Airbnb 스타일) + "prefer-template": "error", + "template-curly-spacing": "error", + "no-useless-concat": "error", + + // 조건문 및 루프 curly: ["error", "all"], - "no-else-return": "warn", + "no-else-return": ["error", { allowElseIf: false }], + "no-lonely-if": "error", + "no-unneeded-ternary": ["error", { defaultAssignment: false }], + "no-nested-ternary": "error", - // 복잡도 제한 (완화) + // 공백 및 스타일 (Prettier와 충돌하지 않는 것들) + "space-before-function-paren": [ + "error", + { + anonymous: "always", + named: "never", + asyncArrow: "always", + }, + ], + "keyword-spacing": [ + "error", + { + before: true, + after: true, + }, + ], + + // Import/Export 관련 + "import/no-unresolved": "off", // 프로젝트 특성상 비활성화 + "import/prefer-default-export": "off", + "import/no-default-export": "off", + "import/order": [ + "error", + { + groups: ["builtin", "external", "internal", "parent", "sibling", "index"], + "newlines-between": "never", + }, + ], + + // 복잡도 제한 (리팩토링 프로젝트에 맞게 완화) "max-len": [ "warn", { @@ -69,10 +142,36 @@ export default [ ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true, + ignoreComments: true, + }, + ], + "max-lines-per-function": ["warn", { max: 80 }], + complexity: ["warn", { max: 12 }], + "max-depth": ["warn", { max: 4 }], + "max-params": ["warn", { max: 4 }], + + // 기타 품질 규칙 + "consistent-return": "error", + "default-case": "error", + "no-case-declarations": "error", + "no-fallthrough": "error", + "no-param-reassign": ["error", { props: false }], + "no-return-assign": ["error", "always"], + "no-sequences": "error", + "no-throw-literal": "error", + "no-unused-expressions": [ + "error", + { + allowShortCircuit: false, + allowTernary: false, + allowTaggedTemplates: false, }, ], - "max-lines-per-function": ["warn", { max: 100 }], - complexity: ["warn", { max: 15 }], + "no-useless-call": "error", + "no-useless-return": "error", + "prefer-promise-reject-errors": ["error", { allowEmptyReject: true }], + radix: "error", + yoda: "error", }, }, { @@ -90,6 +189,8 @@ export default [ rules: { "no-console": "off", "max-lines-per-function": "off", + "prefer-arrow-callback": "off", + "func-style": "off", }, }, ]; diff --git a/package.json b/package.json index 350615187..e790b7ad3 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "@testing-library/user-event": "^14.6.1", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", + "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.3", "globals": "^16.3.0", "husky": "^9.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06dd62a61..60abef0cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,15 @@ importers: eslint: specifier: ^9.32.0 version: 9.32.0 + eslint-config-airbnb-base: + specifier: ^15.0.0 + version: 15.0.0(eslint-plugin-import@2.32.0(eslint@9.32.0))(eslint@9.32.0) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.32.0) + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(eslint@9.32.0) eslint-plugin-prettier: specifier: ^5.5.3 version: 5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2) @@ -423,6 +429,9 @@ packages: cpu: [x64] os: [win32] + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -452,6 +461,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -537,10 +549,42 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -555,6 +599,18 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -604,6 +660,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -619,6 +678,26 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -638,16 +717,32 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -659,9 +754,37 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + esbuild@0.25.8: resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} @@ -671,12 +794,53 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-airbnb-base@15.0.0: + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-plugin-prettier@5.5.3: resolution: {integrity: sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -785,15 +949,41 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -806,10 +996,41 @@ packages: resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -847,10 +1068,50 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} @@ -859,10 +1120,26 @@ packages: resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} engines: {node: '>=18'} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -870,6 +1147,45 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -901,6 +1217,10 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -948,6 +1268,10 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -963,6 +1287,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -985,6 +1312,34 @@ packages: nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -993,6 +1348,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1016,6 +1375,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1039,6 +1401,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1071,10 +1437,23 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1090,6 +1469,18 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1097,6 +1488,22 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1105,6 +1512,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1134,6 +1557,10 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -1142,10 +1569,26 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + strip-ansi@7.1.0: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1161,6 +1604,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1213,10 +1660,33 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1313,6 +1783,22 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1602,6 +2088,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.45.1': optional: true + '@rtsao/scc@1.1.0': {} + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 @@ -1639,6 +2127,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -1731,8 +2221,64 @@ snapshots: aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + balanced-match@1.0.2: {} brace-expansion@1.1.12: @@ -1746,6 +2292,23 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} chai@5.2.1: @@ -1791,6 +2354,8 @@ snapshots: concat-map@0.0.1: {} + confusing-browser-globals@1.0.11: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1809,6 +2374,28 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -1819,20 +2406,124 @@ snapshots: deep-is@0.1.4: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + dequal@2.0.3: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + emoji-regex@10.4.0: {} entities@6.0.1: {} environment@1.1.0: {} + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild@0.25.8: optionalDependencies: '@esbuild/aix-ppc64': 0.25.8 @@ -1864,10 +2555,63 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(eslint@9.32.0))(eslint@9.32.0): + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 9.32.0 + eslint-plugin-import: 2.32.0(eslint@9.32.0) + object.assign: 4.1.7 + object.entries: 1.1.9 + semver: 6.3.1 + eslint-config-prettier@10.1.8(eslint@9.32.0): dependencies: eslint: 9.32.0 + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.32.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + eslint: 9.32.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(eslint@9.32.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.32.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.32.0) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-prettier@5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2): dependencies: eslint: 9.32.0 @@ -1986,11 +2730,52 @@ snapshots: flatted@3.3.3: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + get-east-asian-width@1.3.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -1999,8 +2784,35 @@ snapshots: globals@16.3.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -2036,22 +2848,129 @@ snapshots: indent-string@4.0.0: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@4.0.0: {} is-fullwidth-code-point@5.0.0: dependencies: get-east-asian-width: 1.3.0 + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} js-tokens@4.0.0: {} @@ -2095,6 +3014,10 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@1.0.2: + dependencies: + minimist: 1.2.8 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2156,6 +3079,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 + math-intrinsics@1.1.0: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -2169,6 +3094,8 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimist@1.2.8: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -2181,6 +3108,46 @@ snapshots: nwsapi@2.2.20: {} + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -2194,6 +3161,12 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -2214,6 +3187,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -2226,6 +3201,8 @@ snapshots: pidtree@0.6.0: {} + possible-typed-array-names@1.1.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -2255,8 +3232,34 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + resolve-from@4.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -2292,18 +3295,89 @@ snapshots: rrweb-cssom@0.8.0: {} + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} saxes@6.0.0: dependencies: xmlchars: 2.2.0 + semver@6.3.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -2330,6 +3404,11 @@ snapshots: std-env@3.9.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + string-argv@0.3.2: {} string-width@7.2.0: @@ -2338,10 +3417,35 @@ snapshots: get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + strip-ansi@7.1.0: dependencies: ansi-regex: 6.1.0 + strip-bom@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -2356,6 +3460,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} synckit@0.11.11: @@ -2397,10 +3503,57 @@ snapshots: dependencies: punycode: 2.3.1 + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -2497,6 +3650,47 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 From f98e2c7cc1f9a02d8745f27448d6a04bca693455 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 23:13:49 +0900 Subject: [PATCH 07/55] =?UTF-8?q?feat:=20ESLint,=20Prettier=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20Airbnb=20=EB=A3=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/main.basic.js | 313 ++++++++++++++++++---------------------- 1 file changed, 141 insertions(+), 172 deletions(-) diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 542927a5a..3ca1c1a77 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,17 +1,24 @@ -let prodList; -let bonusPts = 0; +import { PRODUCT_LIST } from "./data/product.data"; + +// 상태 변수 +let bonusPoints = 0; +let itemCounts; +let lastSelect; +let totalAmount = 0; + +// DOM 요소 let stockInfo; -let itemCnt; -let lastSel; -let sel; -let addBtn; -let totalAmt = 0; -const PRODUCT_ONE = "p1"; -const p2 = "p2"; -const product_3 = "p3"; -const p4 = "p4"; -const PRODUCT_5 = `p5`; -let cartDisp; +let select; +let addButton; +let cartDisplay; + +// 상수 +const PRODUCT_1 = "p1"; +const PRODUCT_2 = "p2"; +const PRODUCT_3 = "p3"; +const PRODUCT_4 = "p4"; +const PRODUCT_5 = "p5"; + function main() { var root; let header; @@ -23,57 +30,13 @@ function main() { let manualOverlay; let manualColumn; let lightningDelay; - totalAmt = 0; - itemCnt = 0; - lastSel = null; - prodList = [ - { - id: PRODUCT_ONE, - name: "버그 없애는 키보드", - val: 10000, - originalVal: 10000, - q: 50, - onSale: false, - suggestSale: false, - }, - { - id: p2, - name: "생산성 폭발 마우스", - val: 20000, - originalVal: 20000, - q: 30, - onSale: false, - suggestSale: false, - }, - { - id: product_3, - name: "거북목 탈출 모니터암", - val: 30000, - originalVal: 30000, - q: 20, - onSale: false, - suggestSale: false, - }, - { - id: p4, - name: "에러 방지 노트북 파우치", - val: 15000, - originalVal: 15000, - q: 0, - onSale: false, - suggestSale: false, - }, - { - id: PRODUCT_5, - name: `코딩할 때 듣는 Lo-Fi 스피커`, - val: 25000, - originalVal: 25000, - q: 10, - onSale: false, - suggestSale: false, - }, - ]; + + totalAmount = 0; + itemCounts = 0; + lastSelect = null; + var root = document.getElementById("app"); + header = document.createElement("div"); header.className = "mb-8"; header.innerHTML = ` @@ -81,31 +44,36 @@ function main() {
Shopping Cart

🛍️ 0 items in cart

`; - sel = document.createElement("select"); - sel.id = "product-select"; + + select = document.createElement("select"); + select.id = "product-select"; + select.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; + gridContainer = document.createElement("div"); + gridContainer.className = + "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; + leftColumn = document.createElement("div"); leftColumn["className"] = "bg-white border border-gray-200 p-8 overflow-y-auto"; + selectorContainer = document.createElement("div"); selectorContainer.className = "mb-6 pb-6 border-b border-gray-200"; - sel.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; - gridContainer.className = - "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; - addBtn = document.createElement("button"); + + addButton = document.createElement("button"); stockInfo = document.createElement("div"); - addBtn.id = "add-to-cart"; + addButton.id = "add-to-cart"; stockInfo.id = "stock-status"; stockInfo.className = "text-xs text-red-500 mt-3 whitespace-pre-line"; - addBtn.innerHTML = "Add to Cart"; - addBtn.className = + addButton.innerHTML = "Add to Cart"; + addButton.className = "w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all"; - selectorContainer.appendChild(sel); - selectorContainer.appendChild(addBtn); + selectorContainer.appendChild(select); + selectorContainer.appendChild(addButton); selectorContainer.appendChild(stockInfo); leftColumn.appendChild(selectorContainer); - cartDisp = document.createElement("div"); - leftColumn.appendChild(cartDisp); - cartDisp.id = "cart-items"; + cartDisplay = document.createElement("div"); + leftColumn.appendChild(cartDisplay); + cartDisplay.id = "cart-items"; rightColumn = document.createElement("div"); rightColumn.className = "bg-black text-white p-8 flex flex-col"; rightColumn.innerHTML = ` @@ -229,16 +197,16 @@ function main() { root.appendChild(manualToggle); root.appendChild(manualOverlay); let initStock = 0; - for (let i = 0; i < prodList.length; i++) { - initStock += prodList[i].q; + for (let i = 0; i < PRODUCT_LIST.length; i++) { + initStock += PRODUCT_LIST[i].q; } onUpdateSelectOptions(); handleCalculateCartStuff(); lightningDelay = Math.random() * 10000; setTimeout(() => { setInterval(() => { - const luckyIdx = Math.floor(Math.random() * prodList.length); - const luckyItem = prodList[luckyIdx]; + const luckyIdx = Math.floor(Math.random() * PRODUCT_LIST.length); + const luckyItem = PRODUCT_LIST[luckyIdx]; if (luckyItem.q > 0 && !luckyItem.onSale) { luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100); luckyItem.onSale = true; @@ -250,15 +218,15 @@ function main() { }, lightningDelay); setTimeout(() => { setInterval(() => { - if (cartDisp.children.length === 0) { + if (cartDisplay.children.length === 0) { } - if (lastSel) { + if (lastSelect) { let suggest = null; - for (let k = 0; k < prodList.length; k++) { - if (prodList[k].id !== lastSel) { - if (prodList[k].q > 0) { - if (!prodList[k].suggestSale) { - suggest = prodList[k]; + for (let k = 0; k < PRODUCT_LIST.length; k++) { + if (PRODUCT_LIST[k].id !== lastSelect) { + if (PRODUCT_LIST[k].q > 0) { + if (!PRODUCT_LIST[k].suggestSale) { + suggest = PRODUCT_LIST[k]; break; } } @@ -275,20 +243,21 @@ function main() { }, 60000); }, Math.random() * 20000); } + let sum; function onUpdateSelectOptions() { let totalStock; let opt; let discountText; - sel.innerHTML = ""; + select.innerHTML = ""; totalStock = 0; - for (let idx = 0; idx < prodList.length; idx++) { - const _p = prodList[idx]; + for (let idx = 0; idx < PRODUCT_LIST.length; idx++) { + const _p = PRODUCT_LIST[idx]; totalStock = totalStock + _p.q; } - for (var i = 0; i < prodList.length; i++) { + for (var i = 0; i < PRODUCT_LIST.length; i++) { (function () { - const item = prodList[i]; + const item = PRODUCT_LIST[i]; opt = document.createElement("option"); opt.value = item.id; discountText = ""; @@ -325,13 +294,13 @@ function onUpdateSelectOptions() { opt.textContent = item.name + " - " + item.val + "원" + discountText; } } - sel.appendChild(opt); + select.appendChild(opt); })(); } if (totalStock < 50) { - sel.style.borderColor = "orange"; + select.style.borderColor = "orange"; } else { - sel.style.borderColor = ""; + select.style.borderColor = ""; } } function handleCalculateCartStuff() { @@ -354,27 +323,27 @@ function handleCalculateCartStuff() { let stockMsg; let pts; let hasP1; - let hasP2; + let hasPRODUCT_2; let loyaltyDiv; - totalAmt = 0; - itemCnt = 0; - originalTotal = totalAmt; - cartItems = cartDisp.children; + totalAmount = 0; + itemCounts = 0; + originalTotal = totalAmount; + cartItems = cartDisplay.children; subTot = 0; bulkDisc = subTot; itemDiscounts = []; lowStockItems = []; - for (idx = 0; idx < prodList.length; idx++) { - if (prodList[idx].q < 5 && prodList[idx].q > 0) { - lowStockItems.push(prodList[idx].name); + for (idx = 0; idx < PRODUCT_LIST.length; idx++) { + if (PRODUCT_LIST[idx].q < 5 && PRODUCT_LIST[idx].q > 0) { + lowStockItems.push(PRODUCT_LIST[idx].name); } } for (let i = 0; i < cartItems.length; i++) { (function () { let curItem; - for (let j = 0; j < prodList.length; j++) { - if (prodList[j].id === cartItems[i].id) { - curItem = prodList[j]; + for (let j = 0; j < PRODUCT_LIST.length; j++) { + if (PRODUCT_LIST[j].id === cartItems[i].id) { + curItem = PRODUCT_LIST[j]; break; } } @@ -385,7 +354,7 @@ function handleCalculateCartStuff() { q = parseInt(qtyElem.textContent); itemTot = curItem.val * q; disc = 0; - itemCnt += q; + itemCounts += q; subTot += itemTot; const itemDiv = cartItems[i]; const priceElems = itemDiv.querySelectorAll(".text-lg, .text-xs"); @@ -395,16 +364,16 @@ function handleCalculateCartStuff() { } }); if (q >= 10) { - if (curItem.id === PRODUCT_ONE) { + if (curItem.id === PRODUCT_1) { disc = 10 / 100; } else { - if (curItem.id === p2) { + if (curItem.id === PRODUCT_2) { disc = 15 / 100; } else { - if (curItem.id === product_3) { + if (curItem.id === PRODUCT_3) { disc = 20 / 100; } else { - if (curItem.id === p4) { + if (curItem.id === PRODUCT_4) { disc = 5 / 100; } else { if (curItem.id === PRODUCT_5) { @@ -418,24 +387,24 @@ function handleCalculateCartStuff() { itemDiscounts.push({ name: curItem.name, discount: disc * 100 }); } } - totalAmt += itemTot * (1 - disc); + totalAmount += itemTot * (1 - disc); })(); } let discRate = 0; var originalTotal = subTot; - if (itemCnt >= 30) { - totalAmt = (subTot * 75) / 100; + if (itemCounts >= 30) { + totalAmount = (subTot * 75) / 100; discRate = 25 / 100; } else { - discRate = (subTot - totalAmt) / subTot; + discRate = (subTot - totalAmount) / subTot; } const today = new Date(); const isTuesday = today.getDay() === 2; const tuesdaySpecial = document.getElementById("tuesday-special"); if (isTuesday) { - if (totalAmt > 0) { - totalAmt = (totalAmt * 90) / 100; - discRate = 1 - totalAmt / originalTotal; + if (totalAmount > 0) { + totalAmount = (totalAmount * 90) / 100; + discRate = 1 - totalAmount / originalTotal; tuesdaySpecial.classList.remove("hidden"); } else { tuesdaySpecial.classList.add("hidden"); @@ -443,15 +412,15 @@ function handleCalculateCartStuff() { } else { tuesdaySpecial.classList.add("hidden"); } - document.getElementById("item-count").textContent = "🛍️ " + itemCnt + " items in cart"; + document.getElementById("item-count").textContent = "🛍️ " + itemCounts + " items in cart"; summaryDetails = document.getElementById("summary-details"); summaryDetails.innerHTML = ""; if (subTot > 0) { for (let i = 0; i < cartItems.length; i++) { var curItem; - for (let j = 0; j < prodList.length; j++) { - if (prodList[j].id === cartItems[i].id) { - curItem = prodList[j]; + for (let j = 0; j < PRODUCT_LIST.length; j++) { + if (PRODUCT_LIST[j].id === cartItems[i].id) { + curItem = PRODUCT_LIST[j]; break; } } @@ -472,7 +441,7 @@ function handleCalculateCartStuff() { ₩${subTot.toLocaleString()} `; - if (itemCnt >= 30) { + if (itemCounts >= 30) { summaryDetails.innerHTML += `
🎉 대량구매 할인 (30개 이상) @@ -490,7 +459,7 @@ function handleCalculateCartStuff() { }); } if (isTuesday) { - if (totalAmt > 0) { + if (totalAmount > 0) { summaryDetails.innerHTML += `
🌟 화요일 추가 할인 @@ -508,11 +477,11 @@ function handleCalculateCartStuff() { } totalDiv = sum.querySelector(".text-2xl"); if (totalDiv) { - totalDiv.textContent = "₩" + Math.round(totalAmt).toLocaleString(); + totalDiv.textContent = "₩" + Math.round(totalAmount).toLocaleString(); } loyaltyPointsDiv = document.getElementById("loyalty-points"); if (loyaltyPointsDiv) { - points = Math.floor(totalAmt / 1000); + points = Math.floor(totalAmount / 1000); if (points > 0) { loyaltyPointsDiv.textContent = "적립 포인트: " + points + "p"; loyaltyPointsDiv.style.display = "block"; @@ -523,8 +492,8 @@ function handleCalculateCartStuff() { } discountInfoDiv = document.getElementById("discount-info"); discountInfoDiv.innerHTML = ""; - if (discRate > 0 && totalAmt > 0) { - savedAmount = originalTotal - totalAmt; + if (discRate > 0 && totalAmount > 0) { + savedAmount = originalTotal - totalAmount; discountInfoDiv.innerHTML = `
@@ -538,14 +507,14 @@ function handleCalculateCartStuff() { itemCountElement = document.getElementById("item-count"); if (itemCountElement) { previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0); - itemCountElement.textContent = "🛍️ " + itemCnt + " items in cart"; - if (previousCount !== itemCnt) { + itemCountElement.textContent = "🛍️ " + itemCounts + " items in cart"; + if (previousCount !== itemCounts) { itemCountElement.setAttribute("data-changed", "true"); } } stockMsg = ""; - for (let stockIdx = 0; stockIdx < prodList.length; stockIdx++) { - const item = prodList[stockIdx]; + for (let stockIdx = 0; stockIdx < PRODUCT_LIST.length; stockIdx++) { + const item = PRODUCT_LIST[stockIdx]; if (item.q < 5) { if (item.q > 0) { stockMsg = stockMsg + item.name + ": 재고 부족 (" + item.q + "개 남음)\n"; @@ -566,11 +535,11 @@ var doRenderBonusPoints = function () { let hasMouse; let hasMonitorArm; let nodes; - if (cartDisp.children.length === 0) { + if (cartDisplay.children.length === 0) { document.getElementById("loyalty-points").style.display = "none"; return; } - basePoints = Math.floor(totalAmt / 1000); + basePoints = Math.floor(totalAmount / 1000); finalPoints = 0; pointsDetail = []; if (basePoints > 0) { @@ -586,23 +555,23 @@ var doRenderBonusPoints = function () { hasKeyboard = false; hasMouse = false; hasMonitorArm = false; - nodes = cartDisp.children; + nodes = cartDisplay.children; for (const node of nodes) { let product = null; - for (let pIdx = 0; pIdx < prodList.length; pIdx++) { - if (prodList[pIdx].id === node.id) { - product = prodList[pIdx]; + for (let pIdx = 0; pIdx < PRODUCT_LIST.length; pIdx++) { + if (PRODUCT_LIST[pIdx].id === node.id) { + product = PRODUCT_LIST[pIdx]; break; } } if (!product) { continue; } - if (product.id === PRODUCT_ONE) { + if (product.id === PRODUCT_1) { hasKeyboard = true; - } else if (product.id === p2) { + } else if (product.id === PRODUCT_2) { hasMouse = true; - } else if (product.id === product_3) { + } else if (product.id === PRODUCT_3) { hasMonitorArm = true; } } @@ -614,27 +583,27 @@ var doRenderBonusPoints = function () { finalPoints = finalPoints + 100; pointsDetail.push("풀세트 구매 +100p"); } - if (itemCnt >= 30) { + if (itemCounts >= 30) { finalPoints = finalPoints + 100; pointsDetail.push("대량구매(30개+) +100p"); } else { - if (itemCnt >= 20) { + if (itemCounts >= 20) { finalPoints = finalPoints + 50; pointsDetail.push("대량구매(20개+) +50p"); } else { - if (itemCnt >= 10) { + if (itemCounts >= 10) { finalPoints = finalPoints + 20; pointsDetail.push("대량구매(10개+) +20p"); } } } - bonusPts = finalPoints; + bonusPoints = finalPoints; const ptsTag = document.getElementById("loyalty-points"); if (ptsTag) { - if (bonusPts > 0) { + if (bonusPoints > 0) { ptsTag.innerHTML = '
적립 포인트: ' + - bonusPts + + bonusPoints + "p
" + '
' + pointsDetail.join(", ") + @@ -651,8 +620,8 @@ function onGetStockTotal() { let i; let currentProduct; sum = 0; - for (i = 0; i < prodList.length; i++) { - currentProduct = prodList[i]; + for (i = 0; i < PRODUCT_LIST.length; i++) { + currentProduct = PRODUCT_LIST[i]; sum += currentProduct.q; } return sum; @@ -665,7 +634,7 @@ var handleStockInfoUpdate = function () { totalStock = onGetStockTotal(); if (totalStock < 30) { } - prodList.forEach(item => { + PRODUCT_LIST.forEach(item => { if (item.q < 5) { if (item.q > 0) { infoMsg = infoMsg + item.name + ": 재고 부족 (" + item.q + "개 남음)\n"; @@ -680,22 +649,22 @@ function doUpdatePricesInCart() { let totalCount = 0, j = 0; let cartItems; - while (cartDisp.children[j]) { - const qty = cartDisp.children[j].querySelector(".quantity-number"); + while (cartDisplay.children[j]) { + const qty = cartDisplay.children[j].querySelector(".quantity-number"); totalCount += qty ? parseInt(qty.textContent) : 0; j++; } totalCount = 0; - for (j = 0; j < cartDisp.children.length; j++) { - totalCount += parseInt(cartDisp.children[j].querySelector(".quantity-number").textContent); + for (j = 0; j < cartDisplay.children.length; j++) { + totalCount += parseInt(cartDisplay.children[j].querySelector(".quantity-number").textContent); } - cartItems = cartDisp.children; + cartItems = cartDisplay.children; for (let i = 0; i < cartItems.length; i++) { const itemId = cartItems[i].id; let product = null; - for (let productIdx = 0; productIdx < prodList.length; productIdx++) { - if (prodList[productIdx].id === itemId) { - product = prodList[productIdx]; + for (let productIdx = 0; productIdx < PRODUCT_LIST.length; productIdx++) { + if (PRODUCT_LIST[productIdx].id === itemId) { + product = PRODUCT_LIST[productIdx]; break; } } @@ -735,11 +704,11 @@ function doUpdatePricesInCart() { handleCalculateCartStuff(); } main(); -addBtn.addEventListener("click", () => { - const selItem = sel.value; +addButton.addEventListener("click", () => { + const selItem = select.value; let hasItem = false; - for (let idx = 0; idx < prodList.length; idx++) { - if (prodList[idx].id === selItem) { + for (let idx = 0; idx < PRODUCT_LIST.length; idx++) { + if (PRODUCT_LIST[idx].id === selItem) { hasItem = true; break; } @@ -748,9 +717,9 @@ addBtn.addEventListener("click", () => { return; } let itemToAdd = null; - for (let j = 0; j < prodList.length; j++) { - if (prodList[j].id === selItem) { - itemToAdd = prodList[j]; + for (let j = 0; j < PRODUCT_LIST.length; j++) { + if (PRODUCT_LIST[j].id === selItem) { + itemToAdd = PRODUCT_LIST[j]; break; } } @@ -789,22 +758,22 @@ addBtn.addEventListener("click", () => { Remove
`; - cartDisp.appendChild(newItem); + cartDisplay.appendChild(newItem); itemToAdd.q--; } handleCalculateCartStuff(); - lastSel = selItem; + lastSelect = selItem; } }); -cartDisp.addEventListener("click", event => { +cartDisplay.addEventListener("click", event => { const tgt = event.target; if (tgt.classList.contains("quantity-change") || tgt.classList.contains("remove-item")) { const prodId = tgt.dataset.productId; const itemElem = document.getElementById(prodId); let prod = null; - for (let prdIdx = 0; prdIdx < prodList.length; prdIdx++) { - if (prodList[prdIdx].id === prodId) { - prod = prodList[prdIdx]; + for (let prdIdx = 0; prdIdx < PRODUCT_LIST.length; prdIdx++) { + if (PRODUCT_LIST[prdIdx].id === prodId) { + prod = PRODUCT_LIST[prdIdx]; break; } } From 4cbe040571c3c2340628783719056c62951c82d8 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 23:15:52 +0900 Subject: [PATCH 08/55] =?UTF-8?q?refactor:=20var=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/main.basic.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 3ca1c1a77..d29183c35 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -20,7 +20,7 @@ const PRODUCT_4 = "p4"; const PRODUCT_5 = "p5"; function main() { - var root; + let root; let header; let gridContainer; let leftColumn; @@ -35,7 +35,7 @@ function main() { itemCounts = 0; lastSelect = null; - var root = document.getElementById("app"); + root = document.getElementById("app"); header = document.createElement("div"); header.className = "mb-8"; @@ -255,7 +255,7 @@ function onUpdateSelectOptions() { const _p = PRODUCT_LIST[idx]; totalStock = totalStock + _p.q; } - for (var i = 0; i < PRODUCT_LIST.length; i++) { + for (let i = 0; i < PRODUCT_LIST.length; i++) { (function () { const item = PRODUCT_LIST[i]; opt = document.createElement("option"); @@ -309,7 +309,7 @@ function handleCalculateCartStuff() { let itemDiscounts; let lowStockItems; let idx; - var originalTotal; + let originalTotal; let bulkDisc; let itemDisc; let savedAmount; @@ -391,7 +391,7 @@ function handleCalculateCartStuff() { })(); } let discRate = 0; - var originalTotal = subTot; + originalTotal = subTot; if (itemCounts >= 30) { totalAmount = (subTot * 75) / 100; discRate = 25 / 100; @@ -417,7 +417,7 @@ function handleCalculateCartStuff() { summaryDetails.innerHTML = ""; if (subTot > 0) { for (let i = 0; i < cartItems.length; i++) { - var curItem; + let curItem; for (let j = 0; j < PRODUCT_LIST.length; j++) { if (PRODUCT_LIST[j].id === cartItems[i].id) { curItem = PRODUCT_LIST[j]; @@ -527,7 +527,7 @@ function handleCalculateCartStuff() { handleStockInfoUpdate(); doRenderBonusPoints(); } -var doRenderBonusPoints = function () { +const doRenderBonusPoints = function () { let basePoints; let finalPoints; let pointsDetail; @@ -626,7 +626,7 @@ function onGetStockTotal() { } return sum; } -var handleStockInfoUpdate = function () { +const handleStockInfoUpdate = function () { let infoMsg; let totalStock; let messageOptimizer; @@ -779,7 +779,7 @@ cartDisplay.addEventListener("click", event => { } if (tgt.classList.contains("quantity-change")) { const qtyChange = parseInt(tgt.dataset.change); - var qtyElem = itemElem.querySelector(".quantity-number"); + const qtyElem = itemElem.querySelector(".quantity-number"); const currentQty = parseInt(qtyElem.textContent); const newQty = currentQty + qtyChange; if (newQty > 0 && newQty <= prod.q + currentQty) { @@ -792,7 +792,7 @@ cartDisplay.addEventListener("click", event => { alert("재고가 부족합니다."); } } else if (tgt.classList.contains("remove-item")) { - var qtyElem = itemElem.querySelector(".quantity-number"); + const qtyElem = itemElem.querySelector(".quantity-number"); const remQty = parseInt(qtyElem.textContent); prod.q += remQty; itemElem.remove(); From 3a4dd7b6d18d8d039a4c3a191dd1e75da1a1b58a Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Mon, 28 Jul 2025 23:36:55 +0900 Subject: [PATCH 09/55] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=8A=A4=EC=BD=94=ED=94=84=20=EC=B5=9C=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/main.basic.js | 95 ++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index d29183c35..faadf316d 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,7 +1,6 @@ import { PRODUCT_LIST } from "./data/product.data"; // 상태 변수 -let bonusPoints = 0; let itemCounts; let lastSelect; let totalAmount = 0; @@ -105,7 +104,6 @@ function main() { Earn loyalty points with purchase.

`; - sum = rightColumn.querySelector("#cart-total"); manualToggle = document.createElement("button"); manualToggle.onclick = function () { manualOverlay.classList.toggle("hidden"); @@ -244,23 +242,23 @@ function main() { }, Math.random() * 20000); } -let sum; function onUpdateSelectOptions() { - let totalStock; - let opt; - let discountText; select.innerHTML = ""; - totalStock = 0; + + // 총 재고 계산 + let totalStock = 0; for (let idx = 0; idx < PRODUCT_LIST.length; idx++) { const _p = PRODUCT_LIST[idx]; totalStock = totalStock + _p.q; } + + // 옵션 생성 for (let i = 0; i < PRODUCT_LIST.length; i++) { (function () { const item = PRODUCT_LIST[i]; - opt = document.createElement("option"); + const opt = document.createElement("option"); opt.value = item.id; - discountText = ""; + let discountText = ""; if (item.onSale) { discountText += " ⚡SALE"; } @@ -303,37 +301,18 @@ function onUpdateSelectOptions() { select.style.borderColor = ""; } } + function handleCalculateCartStuff() { - let cartItems; - let subTot; - let itemDiscounts; - let lowStockItems; - let idx; - let originalTotal; - let bulkDisc; - let itemDisc; - let savedAmount; - let summaryDetails; - let totalDiv; - let loyaltyPointsDiv; - let points; - let discountInfoDiv; - let itemCountElement; - let previousCount; - let stockMsg; - let pts; - let hasP1; - let hasPRODUCT_2; - let loyaltyDiv; + // 초기화 totalAmount = 0; itemCounts = 0; - originalTotal = totalAmount; - cartItems = cartDisplay.children; - subTot = 0; - bulkDisc = subTot; - itemDiscounts = []; - lowStockItems = []; - for (idx = 0; idx < PRODUCT_LIST.length; idx++) { + let originalTotal = totalAmount; + const cartItems = cartDisplay.children; + let subTot = 0; + const itemDiscounts = []; + const lowStockItems = []; + + for (let idx = 0; idx < PRODUCT_LIST.length; idx++) { if (PRODUCT_LIST[idx].q < 5 && PRODUCT_LIST[idx].q > 0) { lowStockItems.push(PRODUCT_LIST[idx].name); } @@ -412,8 +391,10 @@ function handleCalculateCartStuff() { } else { tuesdaySpecial.classList.add("hidden"); } + + // DOM 업데이트 document.getElementById("item-count").textContent = "🛍️ " + itemCounts + " items in cart"; - summaryDetails = document.getElementById("summary-details"); + const summaryDetails = document.getElementById("summary-details"); summaryDetails.innerHTML = ""; if (subTot > 0) { for (let i = 0; i < cartItems.length; i++) { @@ -475,13 +456,17 @@ function handleCalculateCartStuff() {
`; } - totalDiv = sum.querySelector(".text-2xl"); + + // 총액 및 포인트 표시 + const cartTotalElement = document.getElementById("cart-total"); + const totalDiv = cartTotalElement.querySelector(".text-2xl"); if (totalDiv) { totalDiv.textContent = "₩" + Math.round(totalAmount).toLocaleString(); } - loyaltyPointsDiv = document.getElementById("loyalty-points"); + + const loyaltyPointsDiv = document.getElementById("loyalty-points"); if (loyaltyPointsDiv) { - points = Math.floor(totalAmount / 1000); + const points = Math.floor(totalAmount / 1000); if (points > 0) { loyaltyPointsDiv.textContent = "적립 포인트: " + points + "p"; loyaltyPointsDiv.style.display = "block"; @@ -490,10 +475,12 @@ function handleCalculateCartStuff() { loyaltyPointsDiv.style.display = "block"; } } - discountInfoDiv = document.getElementById("discount-info"); + + // 할인 정보 표시 + const discountInfoDiv = document.getElementById("discount-info"); discountInfoDiv.innerHTML = ""; if (discRate > 0 && totalAmount > 0) { - savedAmount = originalTotal - totalAmount; + const savedAmount = originalTotal - totalAmount; discountInfoDiv.innerHTML = `
@@ -504,15 +491,19 @@ function handleCalculateCartStuff() {
`; } - itemCountElement = document.getElementById("item-count"); + + // 아이템 카운트 업데이트 + const itemCountElement = document.getElementById("item-count"); if (itemCountElement) { - previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0); + const previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0); itemCountElement.textContent = "🛍️ " + itemCounts + " items in cart"; if (previousCount !== itemCounts) { itemCountElement.setAttribute("data-changed", "true"); } } - stockMsg = ""; + + // 재고 메시지 생성 + let stockMsg = ""; for (let stockIdx = 0; stockIdx < PRODUCT_LIST.length; stockIdx++) { const item = PRODUCT_LIST[stockIdx]; if (item.q < 5) { @@ -527,6 +518,7 @@ function handleCalculateCartStuff() { handleStockInfoUpdate(); doRenderBonusPoints(); } + const doRenderBonusPoints = function () { let basePoints; let finalPoints; @@ -597,13 +589,12 @@ const doRenderBonusPoints = function () { } } } - bonusPoints = finalPoints; const ptsTag = document.getElementById("loyalty-points"); if (ptsTag) { - if (bonusPoints > 0) { + if (finalPoints > 0) { ptsTag.innerHTML = '
적립 포인트: ' + - bonusPoints + + finalPoints + "p
" + '
' + pointsDetail.join(", ") + @@ -615,6 +606,7 @@ const doRenderBonusPoints = function () { } } }; + function onGetStockTotal() { let sum; let i; @@ -626,6 +618,7 @@ function onGetStockTotal() { } return sum; } + const handleStockInfoUpdate = function () { let infoMsg; let totalStock; @@ -645,6 +638,7 @@ const handleStockInfoUpdate = function () { }); stockInfo.textContent = infoMsg; }; + function doUpdatePricesInCart() { let totalCount = 0, j = 0; @@ -703,7 +697,9 @@ function doUpdatePricesInCart() { } handleCalculateCartStuff(); } + main(); + addButton.addEventListener("click", () => { const selItem = select.value; let hasItem = false; @@ -765,6 +761,7 @@ addButton.addEventListener("click", () => { lastSelect = selItem; } }); + cartDisplay.addEventListener("click", event => { const tgt = event.target; if (tgt.classList.contains("quantity-change") || tgt.classList.contains("remove-item")) { From 624228c45156cedee601c1edc0dbc57ef1673510 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 00:00:23 +0900 Subject: [PATCH 10/55] =?UTF-8?q?refactor:=20=EB=A7=A4=EC=A7=81=20?= =?UTF-8?q?=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...40\353\247\201\352\263\204\355\232\215.md" | 6 +- src/basic/data/date.data.js | 3 + src/basic/data/discount.data.js | 21 ++ src/basic/data/point.data.js | 17 ++ src/basic/data/product.data.js | 10 +- src/basic/data/quantity.data.js | 17 ++ src/basic/data/time.data.js | 12 ++ src/basic/main.basic.js | 185 +++++++++++++----- 8 files changed, 213 insertions(+), 58 deletions(-) create mode 100644 src/basic/data/date.data.js create mode 100644 src/basic/data/discount.data.js create mode 100644 src/basic/data/point.data.js create mode 100644 src/basic/data/quantity.data.js create mode 100644 src/basic/data/time.data.js diff --git "a/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" "b/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" index 432c4d7ec..36541838b 100644 --- "a/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" +++ "b/docs/\353\246\254\355\214\251\355\206\240\353\247\201\352\263\204\355\232\215.md" @@ -66,9 +66,9 @@ totalAmt = 0; ``` - **Tasks**: - - [ ] 전역 변수 목록 정리 (prodList, bonusPts, stockInfo 등) - - [ ] const/let으로 변경 가능한 변수 식별 - - [ ] 변수 스코프 최소화 계획 수립 + - [x] 전역 변수 목록 정리 (prodList, bonusPts, stockInfo 등) + - [x] const/let으로 변경 가능한 변수 식별 + - [x] 변수 스코프 최소화 계획 수립 #### 1.3 매직 넘버 상수화 diff --git a/src/basic/data/date.data.js b/src/basic/data/date.data.js new file mode 100644 index 000000000..2af335498 --- /dev/null +++ b/src/basic/data/date.data.js @@ -0,0 +1,3 @@ +const TUESDAY_DAY_OF_WEEK = 2; + +export { TUESDAY_DAY_OF_WEEK }; diff --git a/src/basic/data/discount.data.js b/src/basic/data/discount.data.js new file mode 100644 index 000000000..257438002 --- /dev/null +++ b/src/basic/data/discount.data.js @@ -0,0 +1,21 @@ +const DISCOUNT_RATE_PRODUCT_1 = 10; +const DISCOUNT_RATE_PRODUCT_2 = 15; +const DISCOUNT_RATE_PRODUCT_3 = 20; +const DISCOUNT_RATE_PRODUCT_4 = 5; +const DISCOUNT_RATE_PRODUCT_5 = 25; +const DISCOUNT_RATE_BULK = 25; +const DISCOUNT_RATE_TUESDAY = 10; +const DISCOUNT_RATE_LIGHTNING = 20; +const DISCOUNT_RATE_SUGGESTION = 5; + +export { + DISCOUNT_RATE_PRODUCT_1, + DISCOUNT_RATE_PRODUCT_2, + DISCOUNT_RATE_PRODUCT_3, + DISCOUNT_RATE_PRODUCT_4, + DISCOUNT_RATE_PRODUCT_5, + DISCOUNT_RATE_BULK, + DISCOUNT_RATE_TUESDAY, + DISCOUNT_RATE_LIGHTNING, + DISCOUNT_RATE_SUGGESTION, +}; diff --git a/src/basic/data/point.data.js b/src/basic/data/point.data.js new file mode 100644 index 000000000..ce834fdea --- /dev/null +++ b/src/basic/data/point.data.js @@ -0,0 +1,17 @@ +const POINT_RATE_BASE = 1000; // 1000원당 1포인트 +const POINT_BONUS_KEYBOARD_MOUSE_SET = 50; +const POINT_BONUS_FULL_SET = 100; +const POINT_BONUS_QUANTITY_TIER1 = 20; // 10개 이상 +const POINT_BONUS_QUANTITY_TIER2 = 50; // 20개 이상 +const POINT_BONUS_QUANTITY_TIER3 = 100; // 30개 이상 +const POINT_MULTIPLIER_TUESDAY = 2; + +export { + POINT_RATE_BASE, + POINT_BONUS_KEYBOARD_MOUSE_SET, + POINT_BONUS_FULL_SET, + POINT_BONUS_QUANTITY_TIER1, + POINT_BONUS_QUANTITY_TIER2, + POINT_BONUS_QUANTITY_TIER3, + POINT_MULTIPLIER_TUESDAY, +}; diff --git a/src/basic/data/product.data.js b/src/basic/data/product.data.js index d1244cdfc..24ce7bd81 100644 --- a/src/basic/data/product.data.js +++ b/src/basic/data/product.data.js @@ -1,4 +1,10 @@ -export const PRODUCT_LIST = [ +const PRODUCT_1 = "p1"; +const PRODUCT_2 = "p2"; +const PRODUCT_3 = "p3"; +const PRODUCT_4 = "p4"; +const PRODUCT_5 = "p5"; + +const PRODUCT_LIST = [ { id: "p1", name: "버그 없애는 키보드", @@ -45,3 +51,5 @@ export const PRODUCT_LIST = [ suggestSale: false, }, ]; + +export { PRODUCT_1, PRODUCT_2, PRODUCT_3, PRODUCT_4, PRODUCT_5, PRODUCT_LIST }; diff --git a/src/basic/data/quantity.data.js b/src/basic/data/quantity.data.js new file mode 100644 index 000000000..93cc39e0d --- /dev/null +++ b/src/basic/data/quantity.data.js @@ -0,0 +1,17 @@ +const MIN_QUANTITY_FOR_DISCOUNT = 10; +const MIN_QUANTITY_FOR_BULK_DISCOUNT = 30; +const MIN_QUANTITY_FOR_POINT_BONUS_TIER1 = 10; +const MIN_QUANTITY_FOR_POINT_BONUS_TIER2 = 20; +const MIN_QUANTITY_FOR_POINT_BONUS_TIER3 = 30; +const LOW_STOCK_THRESHOLD = 5; +const TOTAL_STOCK_WARNING_THRESHOLD = 50; + +export { + MIN_QUANTITY_FOR_DISCOUNT, + MIN_QUANTITY_FOR_BULK_DISCOUNT, + MIN_QUANTITY_FOR_POINT_BONUS_TIER1, + MIN_QUANTITY_FOR_POINT_BONUS_TIER2, + MIN_QUANTITY_FOR_POINT_BONUS_TIER3, + LOW_STOCK_THRESHOLD, + TOTAL_STOCK_WARNING_THRESHOLD, +}; diff --git a/src/basic/data/time.data.js b/src/basic/data/time.data.js new file mode 100644 index 000000000..2ccc57385 --- /dev/null +++ b/src/basic/data/time.data.js @@ -0,0 +1,12 @@ +// 시간 관련 상수 (milliseconds) +const LIGHTNING_SALE_MAX_DELAY = 10000; +const LIGHTNING_SALE_INTERVAL = 30000; +const SUGGESTION_SALE_INTERVAL = 60000; +const SUGGESTION_SALE_MAX_DELAY = 20000; + +export { + LIGHTNING_SALE_MAX_DELAY, + LIGHTNING_SALE_INTERVAL, + SUGGESTION_SALE_INTERVAL, + SUGGESTION_SALE_MAX_DELAY, +}; diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index faadf316d..543230f4d 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,4 +1,47 @@ -import { PRODUCT_LIST } from "./data/product.data"; +import { + DISCOUNT_RATE_PRODUCT_1, + DISCOUNT_RATE_PRODUCT_2, + DISCOUNT_RATE_PRODUCT_3, + DISCOUNT_RATE_PRODUCT_4, + DISCOUNT_RATE_PRODUCT_5, + DISCOUNT_RATE_BULK, + DISCOUNT_RATE_TUESDAY, + DISCOUNT_RATE_LIGHTNING, + DISCOUNT_RATE_SUGGESTION, +} from "./data/discount.data.js"; +import { + PRODUCT_1, + PRODUCT_2, + PRODUCT_3, + PRODUCT_4, + PRODUCT_5, + PRODUCT_LIST, +} from "./data/product.data.js"; +import { + MIN_QUANTITY_FOR_DISCOUNT, + MIN_QUANTITY_FOR_BULK_DISCOUNT, + MIN_QUANTITY_FOR_POINT_BONUS_TIER1, + MIN_QUANTITY_FOR_POINT_BONUS_TIER2, + MIN_QUANTITY_FOR_POINT_BONUS_TIER3, + LOW_STOCK_THRESHOLD, + TOTAL_STOCK_WARNING_THRESHOLD, +} from "./data/quantity.data.js"; +import { + POINT_RATE_BASE, + POINT_BONUS_KEYBOARD_MOUSE_SET, + POINT_BONUS_FULL_SET, + POINT_BONUS_QUANTITY_TIER1, + POINT_BONUS_QUANTITY_TIER2, + POINT_BONUS_QUANTITY_TIER3, + POINT_MULTIPLIER_TUESDAY, +} from "./data/point.data.js"; +import { + LIGHTNING_SALE_MAX_DELAY, + LIGHTNING_SALE_INTERVAL, + SUGGESTION_SALE_INTERVAL, + SUGGESTION_SALE_MAX_DELAY, +} from "./data/time.data.js"; +import { TUESDAY_DAY_OF_WEEK } from "./data/date.data.js"; // 상태 변수 let itemCounts; @@ -11,13 +54,6 @@ let select; let addButton; let cartDisplay; -// 상수 -const PRODUCT_1 = "p1"; -const PRODUCT_2 = "p2"; -const PRODUCT_3 = "p3"; -const PRODUCT_4 = "p4"; -const PRODUCT_5 = "p5"; - function main() { let root; let header; @@ -200,19 +236,21 @@ function main() { } onUpdateSelectOptions(); handleCalculateCartStuff(); - lightningDelay = Math.random() * 10000; + lightningDelay = Math.random() * LIGHTNING_SALE_MAX_DELAY; setTimeout(() => { setInterval(() => { const luckyIdx = Math.floor(Math.random() * PRODUCT_LIST.length); const luckyItem = PRODUCT_LIST[luckyIdx]; if (luckyItem.q > 0 && !luckyItem.onSale) { - luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100); + luckyItem.val = Math.round((luckyItem.originalVal * (100 - DISCOUNT_RATE_LIGHTNING)) / 100); luckyItem.onSale = true; - alert("⚡번개세일! " + luckyItem.name + "이(가) 20% 할인 중입니다!"); + alert( + "⚡번개세일! " + luckyItem.name + "이(가) " + DISCOUNT_RATE_LIGHTNING + "% 할인 중입니다!" + ); onUpdateSelectOptions(); doUpdatePricesInCart(); } - }, 30000); + }, LIGHTNING_SALE_INTERVAL); }, lightningDelay); setTimeout(() => { setInterval(() => { @@ -231,15 +269,21 @@ function main() { } } if (suggest) { - alert("💝 " + suggest.name + "은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!"); - suggest.val = Math.round((suggest.val * (100 - 5)) / 100); + alert( + "💝 " + + suggest.name + + "은(는) 어떠세요? 지금 구매하시면 " + + DISCOUNT_RATE_SUGGESTION + + "% 추가 할인!" + ); + suggest.val = Math.round((suggest.val * (100 - DISCOUNT_RATE_SUGGESTION)) / 100); suggest.suggestSale = true; onUpdateSelectOptions(); doUpdatePricesInCart(); } } - }, 60000); - }, Math.random() * 20000); + }, SUGGESTION_SALE_INTERVAL); + }, Math.random() * SUGGESTION_SALE_MAX_DELAY); } function onUpdateSelectOptions() { @@ -271,6 +315,7 @@ function onUpdateSelectOptions() { opt.className = "text-gray-400"; } else { if (item.onSale && item.suggestSale) { + const superSaleRate = DISCOUNT_RATE_LIGHTNING + DISCOUNT_RATE_SUGGESTION; opt.textContent = "⚡💝" + item.name + @@ -278,15 +323,33 @@ function onUpdateSelectOptions() { item.originalVal + "원 → " + item.val + - "원 (25% SUPER SALE!)"; + "원 (" + + superSaleRate + + "% SUPER SALE!)"; opt.className = "text-purple-600 font-bold"; } else if (item.onSale) { opt.textContent = - "⚡" + item.name + " - " + item.originalVal + "원 → " + item.val + "원 (20% SALE!)"; + "⚡" + + item.name + + " - " + + item.originalVal + + "원 → " + + item.val + + "원 (" + + DISCOUNT_RATE_LIGHTNING + + "% SALE!)"; opt.className = "text-red-500 font-bold"; } else if (item.suggestSale) { opt.textContent = - "💝" + item.name + " - " + item.originalVal + "원 → " + item.val + "원 (5% 추천할인!)"; + "💝" + + item.name + + " - " + + item.originalVal + + "원 → " + + item.val + + "원 (" + + DISCOUNT_RATE_SUGGESTION + + "% 추천할인!)"; opt.className = "text-blue-500 font-bold"; } else { opt.textContent = item.name + " - " + item.val + "원" + discountText; @@ -295,7 +358,7 @@ function onUpdateSelectOptions() { select.appendChild(opt); })(); } - if (totalStock < 50) { + if (totalStock < TOTAL_STOCK_WARNING_THRESHOLD) { select.style.borderColor = "orange"; } else { select.style.borderColor = ""; @@ -313,7 +376,7 @@ function handleCalculateCartStuff() { const lowStockItems = []; for (let idx = 0; idx < PRODUCT_LIST.length; idx++) { - if (PRODUCT_LIST[idx].q < 5 && PRODUCT_LIST[idx].q > 0) { + if (PRODUCT_LIST[idx].q < LOW_STOCK_THRESHOLD && PRODUCT_LIST[idx].q > 0) { lowStockItems.push(PRODUCT_LIST[idx].name); } } @@ -339,24 +402,24 @@ function handleCalculateCartStuff() { const priceElems = itemDiv.querySelectorAll(".text-lg, .text-xs"); priceElems.forEach(elem => { if (elem.classList.contains("text-lg")) { - elem.style.fontWeight = q >= 10 ? "bold" : "normal"; + elem.style.fontWeight = q >= MIN_QUANTITY_FOR_DISCOUNT ? "bold" : "normal"; } }); - if (q >= 10) { + if (q >= MIN_QUANTITY_FOR_DISCOUNT) { if (curItem.id === PRODUCT_1) { - disc = 10 / 100; + disc = DISCOUNT_RATE_PRODUCT_1 / 100; } else { if (curItem.id === PRODUCT_2) { - disc = 15 / 100; + disc = DISCOUNT_RATE_PRODUCT_2 / 100; } else { if (curItem.id === PRODUCT_3) { - disc = 20 / 100; + disc = DISCOUNT_RATE_PRODUCT_3 / 100; } else { if (curItem.id === PRODUCT_4) { - disc = 5 / 100; + disc = DISCOUNT_RATE_PRODUCT_4 / 100; } else { if (curItem.id === PRODUCT_5) { - disc = 25 / 100; + disc = DISCOUNT_RATE_PRODUCT_5 / 100; } } } @@ -371,18 +434,18 @@ function handleCalculateCartStuff() { } let discRate = 0; originalTotal = subTot; - if (itemCounts >= 30) { - totalAmount = (subTot * 75) / 100; - discRate = 25 / 100; + if (itemCounts >= MIN_QUANTITY_FOR_BULK_DISCOUNT) { + totalAmount = (subTot * (100 - DISCOUNT_RATE_BULK)) / 100; + discRate = DISCOUNT_RATE_BULK / 100; } else { discRate = (subTot - totalAmount) / subTot; } const today = new Date(); - const isTuesday = today.getDay() === 2; + const isTuesday = today.getDay() === TUESDAY_DAY_OF_WEEK; const tuesdaySpecial = document.getElementById("tuesday-special"); if (isTuesday) { if (totalAmount > 0) { - totalAmount = (totalAmount * 90) / 100; + totalAmount = (totalAmount * (100 - DISCOUNT_RATE_TUESDAY)) / 100; discRate = 1 - totalAmount / originalTotal; tuesdaySpecial.classList.remove("hidden"); } else { @@ -466,7 +529,7 @@ function handleCalculateCartStuff() { const loyaltyPointsDiv = document.getElementById("loyalty-points"); if (loyaltyPointsDiv) { - const points = Math.floor(totalAmount / 1000); + const points = Math.floor(totalAmount / POINT_RATE_BASE); if (points > 0) { loyaltyPointsDiv.textContent = "적립 포인트: " + points + "p"; loyaltyPointsDiv.style.display = "block"; @@ -506,7 +569,7 @@ function handleCalculateCartStuff() { let stockMsg = ""; for (let stockIdx = 0; stockIdx < PRODUCT_LIST.length; stockIdx++) { const item = PRODUCT_LIST[stockIdx]; - if (item.q < 5) { + if (item.q < LOW_STOCK_THRESHOLD) { if (item.q > 0) { stockMsg = stockMsg + item.name + ": 재고 부족 (" + item.q + "개 남음)\n"; } else { @@ -531,16 +594,16 @@ const doRenderBonusPoints = function () { document.getElementById("loyalty-points").style.display = "none"; return; } - basePoints = Math.floor(totalAmount / 1000); + basePoints = Math.floor(totalAmount / POINT_RATE_BASE); finalPoints = 0; pointsDetail = []; if (basePoints > 0) { finalPoints = basePoints; pointsDetail.push("기본: " + basePoints + "p"); } - if (new Date().getDay() === 2) { + if (new Date().getDay() === TUESDAY_DAY_OF_WEEK) { if (basePoints > 0) { - finalPoints = basePoints * 2; + finalPoints = basePoints * POINT_MULTIPLIER_TUESDAY; pointsDetail.push("화요일 2배"); } } @@ -568,24 +631,38 @@ const doRenderBonusPoints = function () { } } if (hasKeyboard && hasMouse) { - finalPoints = finalPoints + 50; - pointsDetail.push("키보드+마우스 세트 +50p"); + finalPoints = finalPoints + POINT_BONUS_KEYBOARD_MOUSE_SET; + pointsDetail.push("키보드+마우스 세트 +" + POINT_BONUS_KEYBOARD_MOUSE_SET + "p"); } if (hasKeyboard && hasMouse && hasMonitorArm) { - finalPoints = finalPoints + 100; - pointsDetail.push("풀세트 구매 +100p"); - } - if (itemCounts >= 30) { - finalPoints = finalPoints + 100; - pointsDetail.push("대량구매(30개+) +100p"); + finalPoints = finalPoints + POINT_BONUS_FULL_SET; + pointsDetail.push("풀세트 구매 +" + POINT_BONUS_FULL_SET + "p"); + } + if (itemCounts >= MIN_QUANTITY_FOR_POINT_BONUS_TIER3) { + finalPoints = finalPoints + POINT_BONUS_QUANTITY_TIER3; + pointsDetail.push( + "대량구매(" + MIN_QUANTITY_FOR_POINT_BONUS_TIER3 + "개+) +" + POINT_BONUS_QUANTITY_TIER3 + "p" + ); } else { - if (itemCounts >= 20) { - finalPoints = finalPoints + 50; - pointsDetail.push("대량구매(20개+) +50p"); + if (itemCounts >= MIN_QUANTITY_FOR_POINT_BONUS_TIER2) { + finalPoints = finalPoints + POINT_BONUS_QUANTITY_TIER2; + pointsDetail.push( + "대량구매(" + + MIN_QUANTITY_FOR_POINT_BONUS_TIER2 + + "개+) +" + + POINT_BONUS_QUANTITY_TIER2 + + "p" + ); } else { - if (itemCounts >= 10) { - finalPoints = finalPoints + 20; - pointsDetail.push("대량구매(10개+) +20p"); + if (itemCounts >= MIN_QUANTITY_FOR_POINT_BONUS_TIER1) { + finalPoints = finalPoints + POINT_BONUS_QUANTITY_TIER1; + pointsDetail.push( + "대량구매(" + + MIN_QUANTITY_FOR_POINT_BONUS_TIER1 + + "개+) +" + + POINT_BONUS_QUANTITY_TIER1 + + "p" + ); } } } @@ -625,10 +702,10 @@ const handleStockInfoUpdate = function () { let messageOptimizer; infoMsg = ""; totalStock = onGetStockTotal(); - if (totalStock < 30) { + if (totalStock < MIN_QUANTITY_FOR_BULK_DISCOUNT) { } PRODUCT_LIST.forEach(item => { - if (item.q < 5) { + if (item.q < LOW_STOCK_THRESHOLD) { if (item.q > 0) { infoMsg = infoMsg + item.name + ": 재고 부족 (" + item.q + "개 남음)\n"; } else { From eb9b9386aa241f0736350bfb68ff25b865232a9c Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 00:53:16 +0900 Subject: [PATCH 11/55] =?UTF-8?q?test:=20=ED=99=94=EC=9A=94=EC=9D=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=ED=99=94=EC=9A=94=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=98=EA=B3=A0,=20=EB=82=98=EB=A8=B8?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EC=9B=94=EC=9A=94=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/__tests__/basic.test.js | 545 +++++++++++++++--------------- 1 file changed, 267 insertions(+), 278 deletions(-) diff --git a/src/basic/__tests__/basic.test.js b/src/basic/__tests__/basic.test.js index e5756b96e..b5ad152c4 100644 --- a/src/basic/__tests__/basic.test.js +++ b/src/basic/__tests__/basic.test.js @@ -1,7 +1,7 @@ import { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; -describe('basic 테스트', () => { +describe("basic 테스트", () => { // 공통 헬퍼 함수 const addItemsToCart = (sel, addBtn, productId, count) => { sel.value = productId; @@ -16,43 +16,43 @@ describe('basic 테스트', () => { expect(option.textContent).toContain(product.price); if (product.stock === 0) { expect(option.disabled).toBe(true); - expect(option.textContent).toContain('품절'); + expect(option.textContent).toContain("품절"); } }; const getCartItemQuantity = (cartDisp, productId) => { const item = cartDisp.querySelector(`#${productId}`); if (!item) return 0; - const qtyElement = item.querySelector('.quantity-number'); + const qtyElement = item.querySelector(".quantity-number"); return qtyElement ? parseInt(qtyElement.textContent) : 0; }; describe.each([ - { type: 'origin', loadFile: () => import('../../main.original.js'), }, - { type: 'basic', loadFile: () => import('../main.basic.js'), }, - ])('$type 장바구니 상세 기능 테스트', ({ loadFile }) => { + { type: "origin", loadFile: () => import("../../main.original.js") }, + { type: "basic", loadFile: () => import("../main.basic.js") }, + ])("$type 장바구니 상세 기능 테스트", ({ loadFile }) => { let sel, addBtn, cartDisp, sum, stockInfo, itemCount, loyaltyPoints, discountInfo; beforeEach(async () => { - vi.useRealTimers(); - vi.spyOn(window, 'alert').mockImplementation(() => {}); + vi.setSystemTime(new Date("2024-10-14")); + vi.spyOn(window, "alert").mockImplementation(() => {}); // 전체 DOM 재초기화 document.body.innerHTML = '
'; - + // 모듈 캐시 클리어 및 재로드 vi.resetModules(); await loadFile(); // DOM 요소 참조 - sel = document.getElementById('product-select'); - addBtn = document.getElementById('add-to-cart'); - cartDisp = document.getElementById('cart-items'); - sum = document.getElementById('cart-total'); - stockInfo = document.getElementById('stock-status'); - itemCount = document.getElementById('item-count'); - loyaltyPoints = document.getElementById('loyalty-points'); - discountInfo = document.getElementById('discount-info'); + sel = document.getElementById("product-select"); + addBtn = document.getElementById("add-to-cart"); + cartDisp = document.getElementById("cart-items"); + sum = document.getElementById("cart-total"); + stockInfo = document.getElementById("stock-status"); + itemCount = document.getElementById("item-count"); + loyaltyPoints = document.getElementById("loyalty-points"); + discountInfo = document.getElementById("discount-info"); }); afterEach(() => { @@ -60,15 +60,21 @@ describe('basic 테스트', () => { }); // 2. 상품 정보 테스트 - describe('2. 상품 정보', () => { - describe('2.1 상품 목록', () => { - it('5개 상품이 올바른 정보로 표시되어야 함', () => { + describe("2. 상품 정보", () => { + describe("2.1 상품 목록", () => { + it("5개 상품이 올바른 정보로 표시되어야 함", () => { const expectedProducts = [ - { id: 'p1', name: '버그 없애는 키보드', price: '10000원', stock: 50, discount: 10 }, - { id: 'p2', name: '생산성 폭발 마우스', price: '20000원', stock: 30, discount: 15 }, - { id: 'p3', name: '거북목 탈출 모니터암', price: '30000원', stock: 20, discount: 20 }, - { id: 'p4', name: '에러 방지 노트북 파우치', price: '15000원', stock: 0, discount: 5 }, - { id: 'p5', name: '코딩할 때 듣는 Lo-Fi 스피커', price: '25000원', stock: 10, discount: 25 } + { id: "p1", name: "버그 없애는 키보드", price: "10000원", stock: 50, discount: 10 }, + { id: "p2", name: "생산성 폭발 마우스", price: "20000원", stock: 30, discount: 15 }, + { id: "p3", name: "거북목 탈출 모니터암", price: "30000원", stock: 20, discount: 20 }, + { id: "p4", name: "에러 방지 노트북 파우치", price: "15000원", stock: 0, discount: 5 }, + { + id: "p5", + name: "코딩할 때 듣는 Lo-Fi 스피커", + price: "25000원", + stock: 10, + discount: 25, + }, ]; expect(sel.options.length).toBe(5); @@ -79,400 +85,388 @@ describe('basic 테스트', () => { }); }); - describe('2.2 재고 관리', () => { + describe("2.2 재고 관리", () => { it('재고가 5개 미만인 상품은 "재고 부족" 표시', async () => { // 상품5를 6개 구매하여 재고를 4개로 만듦 - addItemsToCart(sel, addBtn, 'p5', 6); + addItemsToCart(sel, addBtn, "p5", 6); - expect(stockInfo.textContent).toContain('코딩할 때 듣는 Lo-Fi 스피커'); - expect(stockInfo.textContent).toContain('재고 부족'); - expect(stockInfo.textContent).toContain('4개 남음'); + expect(stockInfo.textContent).toContain("코딩할 때 듣는 Lo-Fi 스피커"); + expect(stockInfo.textContent).toContain("재고 부족"); + expect(stockInfo.textContent).toContain("4개 남음"); }); it('재고가 0개인 상품은 "품절" 표시 및 선택 불가', () => { const p4Option = sel.querySelector('option[value="p4"]'); expect(p4Option.disabled).toBe(true); - expect(p4Option.textContent).toContain('품절'); + expect(p4Option.textContent).toContain("품절"); }); }); }); // 3. 할인 정책 테스트 - describe('3. 할인 정책', () => { - describe('3.1 개별 상품 할인', () => { - it('상품1: 10개 이상 구매 시 10% 할인', () => { - addItemsToCart(sel, addBtn, 'p1', 10); + describe("3. 할인 정책", () => { + describe("3.1 개별 상품 할인", () => { + it("상품1: 10개 이상 구매 시 10% 할인", () => { + addItemsToCart(sel, addBtn, "p1", 10); // 100,000원 -> 90,000원 - expect(sum.textContent).toContain('₩90,000'); - expect(discountInfo.textContent).toContain('10.0%'); + expect(sum.textContent).toContain("₩90,000"); + expect(discountInfo.textContent).toContain("10.0%"); }); - it('상품2: 10개 이상 구매 시 15% 할인', () => { - addItemsToCart(sel, addBtn, 'p2', 10); + it("상품2: 10개 이상 구매 시 15% 할인", () => { + addItemsToCart(sel, addBtn, "p2", 10); // 200,000원 -> 170,000원 - expect(sum.textContent).toContain('₩170,000'); - expect(discountInfo.textContent).toContain('15.0%'); + expect(sum.textContent).toContain("₩170,000"); + expect(discountInfo.textContent).toContain("15.0%"); }); - it('상품3: 10개 이상 구매 시 20% 할인', () => { - addItemsToCart(sel, addBtn, 'p3', 10); + it("상품3: 10개 이상 구매 시 20% 할인", () => { + addItemsToCart(sel, addBtn, "p3", 10); // 300,000원 -> 240,000원 - expect(sum.textContent).toContain('₩240,000'); - expect(discountInfo.textContent).toContain('20.0%'); + expect(sum.textContent).toContain("₩240,000"); + expect(discountInfo.textContent).toContain("20.0%"); }); - it('상품5: 10개 이상 구매 시 25% 할인', () => { - addItemsToCart(sel, addBtn, 'p5', 10); + it("상품5: 10개 이상 구매 시 25% 할인", () => { + addItemsToCart(sel, addBtn, "p5", 10); // 250,000원 -> 187,500원 - expect(sum.textContent).toContain('₩187,500'); - expect(discountInfo.textContent).toContain('25.0%'); + expect(sum.textContent).toContain("₩187,500"); + expect(discountInfo.textContent).toContain("25.0%"); }); }); - describe('3.2 전체 수량 할인', () => { - it('전체 30개 이상 구매 시 25% 할인 (개별 할인 무시)', () => { + describe("3.2 전체 수량 할인", () => { + it("전체 30개 이상 구매 시 25% 할인 (개별 할인 무시)", () => { // 상품1 10개, 상품2 10개, 상품3 10개 = 총 30개 - addItemsToCart(sel, addBtn, 'p1', 10); - addItemsToCart(sel, addBtn, 'p2', 10); - addItemsToCart(sel, addBtn, 'p3', 10); + addItemsToCart(sel, addBtn, "p1", 10); + addItemsToCart(sel, addBtn, "p2", 10); + addItemsToCart(sel, addBtn, "p3", 10); // 600,000원 -> 450,000원 (25% 할인) - expect(sum.textContent).toContain('₩450,000'); - expect(discountInfo.textContent).toContain('25.0%'); + expect(sum.textContent).toContain("₩450,000"); + expect(discountInfo.textContent).toContain("25.0%"); }); }); - describe('3.3 특별 할인', () => { - describe('3.3.1 화요일 할인', () => { - it('화요일에 10% 추가 할인 적용', () => { - const tuesday = new Date('2024-10-15'); // 화요일 - vi.useFakeTimers(); + describe("3.3 특별 할인", () => { + describe("3.3.1 화요일 할인", () => { + it("화요일에 10% 추가 할인 적용", () => { + const tuesday = new Date("2024-10-15"); // 화요일 vi.setSystemTime(tuesday); - sel.value = 'p1'; + sel.value = "p1"; addBtn.click(); // 10,000원 -> 9,000원 (10% 할인) - expect(sum.textContent).toContain('₩9,000'); - expect(discountInfo.textContent).toContain('10.0%'); + expect(sum.textContent).toContain("₩9,000"); + expect(discountInfo.textContent).toContain("10.0%"); // 화요일 특별 할인 배너 표시 - const tuesdayBanner = document.getElementById('tuesday-special'); - expect(tuesdayBanner.classList.contains('hidden')).toBe(false); - - vi.useRealTimers(); + const tuesdayBanner = document.getElementById("tuesday-special"); + expect(tuesdayBanner.classList.contains("hidden")).toBe(false); }); - it('화요일 할인은 다른 할인과 중복 적용', () => { - const tuesday = new Date('2024-10-15'); - vi.useFakeTimers(); + it("화요일 할인은 다른 할인과 중복 적용", () => { + const tuesday = new Date("2024-10-15"); + vi.setSystemTime(tuesday); - addItemsToCart(sel, addBtn, 'p1', 10); + addItemsToCart(sel, addBtn, "p1", 10); // 100,000원 -> 90,000원 (개별 10%) -> 81,000원 (화요일 10% 추가) - expect(sum.textContent).toContain('₩81,000'); - expect(discountInfo.textContent).toContain('19.0%'); // 총 19% 할인 - - vi.useRealTimers(); + expect(sum.textContent).toContain("₩81,000"); + expect(discountInfo.textContent).toContain("19.0%"); // 총 19% 할인 }); }); - describe('3.3.2 번개세일', () => { - it.skip('번개세일 알림 표시 및 20% 할인 적용', async () => { + describe("3.3.2 번개세일", () => { + it.skip("번개세일 알림 표시 및 20% 할인 적용", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(40000); - vi.useRealTimers(); }); - it.skip('번개세일 상품은 드롭다운에 ⚡ 아이콘 표시', async () => { + it.skip("번개세일 상품은 드롭다운에 ⚡ 아이콘 표시", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(40000); - vi.useRealTimers(); }); }); - describe('3.3.3 추천할인', () => { - it.skip('마지막 선택한 상품과 다른 상품 추천 및 5% 할인', async () => { + describe("3.3.3 추천할인", () => { + it.skip("마지막 선택한 상품과 다른 상품 추천 및 5% 할인", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - vi.useFakeTimers(); - sel.value = 'p1'; + + sel.value = "p1"; addBtn.click(); await vi.advanceTimersByTimeAsync(80000); - vi.useRealTimers(); }); - it.skip('추천할인 상품은 드롭다운에 💝 아이콘 표시', async () => { + it.skip("추천할인 상품은 드롭다운에 💝 아이콘 표시", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - vi.useFakeTimers(); - sel.value = 'p1'; + + sel.value = "p1"; addBtn.click(); await vi.advanceTimersByTimeAsync(80000); - vi.useRealTimers(); }); }); - describe('3.3.4 할인 중복', () => { - it.skip('번개세일 + 추천할인 = 25% SUPER SALE', async () => { + describe("3.3.4 할인 중복", () => { + it.skip("번개세일 + 추천할인 = 25% SUPER SALE", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(40000); - sel.value = 'p1'; + sel.value = "p1"; addBtn.click(); await vi.advanceTimersByTimeAsync(80000); - vi.useRealTimers(); }); }); }); }); // 4. 포인트 적립 시스템 테스트 - describe('4. 포인트 적립 시스템', () => { - describe('4.1 기본 적립', () => { - it('최종 결제 금액의 0.1% 포인트 적립', () => { - sel.value = 'p1'; + describe("4. 포인트 적립 시스템", () => { + describe("4.1 기본 적립", () => { + it("최종 결제 금액의 0.1% 포인트 적립", () => { + sel.value = "p1"; addBtn.click(); // 10,000원 -> 10포인트 - expect(loyaltyPoints.textContent).toContain('10p'); + expect(loyaltyPoints.textContent).toContain("10p"); }); }); - describe('4.2 추가 적립', () => { - it('화요일 구매 시 기본 포인트 2배', () => { - const tuesday = new Date('2024-10-15'); - vi.useFakeTimers(); + describe("4.2 추가 적립", () => { + it("화요일 구매 시 기본 포인트 2배", () => { + const tuesday = new Date("2024-10-15"); + vi.setSystemTime(tuesday); - sel.value = 'p1'; + sel.value = "p1"; addBtn.click(); // 9,000원 (화요일 10% 할인) -> 9포인트 * 2 = 18포인트 - expect(loyaltyPoints.textContent).toContain('18p'); - expect(loyaltyPoints.textContent).toContain('화요일 2배'); - - vi.useRealTimers(); + expect(loyaltyPoints.textContent).toContain("18p"); + expect(loyaltyPoints.textContent).toContain("화요일 2배"); }); - it('키보드+마우스 세트 구매 시 +50p', () => { - sel.value = 'p1'; + it("키보드+마우스 세트 구매 시 +50p", () => { + sel.value = "p1"; addBtn.click(); - sel.value = 'p2'; + sel.value = "p2"; addBtn.click(); // 30,000원 -> 30포인트 + 50포인트 = 80포인트 - expect(loyaltyPoints.textContent).toContain('80p'); - expect(loyaltyPoints.textContent).toContain('키보드+마우스 세트'); + expect(loyaltyPoints.textContent).toContain("80p"); + expect(loyaltyPoints.textContent).toContain("키보드+마우스 세트"); }); - it('풀세트(키보드+마우스+모니터암) 구매 시 +100p', () => { - sel.value = 'p1'; + it("풀세트(키보드+마우스+모니터암) 구매 시 +100p", () => { + sel.value = "p1"; addBtn.click(); - sel.value = 'p2'; + sel.value = "p2"; addBtn.click(); - sel.value = 'p3'; + sel.value = "p3"; addBtn.click(); // 60,000원 -> 60포인트 + 50포인트(세트) + 100포인트(풀세트) = 210포인트 - expect(loyaltyPoints.textContent).toContain('210p'); - expect(loyaltyPoints.textContent).toContain('풀세트 구매'); + expect(loyaltyPoints.textContent).toContain("210p"); + expect(loyaltyPoints.textContent).toContain("풀세트 구매"); }); - it('수량별 보너스 - 10개 이상 +20p', () => { - addItemsToCart(sel, addBtn, 'p1', 10); + it("수량별 보너스 - 10개 이상 +20p", () => { + addItemsToCart(sel, addBtn, "p1", 10); // 90,000원 (10% 할인) -> 90포인트 + 20포인트 = 110포인트 - expect(loyaltyPoints.textContent).toContain('110p'); - expect(loyaltyPoints.textContent).toContain('대량구매(10개+)'); + expect(loyaltyPoints.textContent).toContain("110p"); + expect(loyaltyPoints.textContent).toContain("대량구매(10개+)"); }); - it('수량별 보너스 - 20개 이상 +50p', () => { - addItemsToCart(sel, addBtn, 'p1', 20); + it("수량별 보너스 - 20개 이상 +50p", () => { + addItemsToCart(sel, addBtn, "p1", 20); // 180,000원 (10% 할인) -> 180포인트 + 50포인트 = 230포인트 - expect(loyaltyPoints.textContent).toContain('230p'); - expect(loyaltyPoints.textContent).toContain('대량구매(20개+)'); + expect(loyaltyPoints.textContent).toContain("230p"); + expect(loyaltyPoints.textContent).toContain("대량구매(20개+)"); }); - it('수량별 보너스 - 30개 이상 +100p', () => { - addItemsToCart(sel, addBtn, 'p1', 30); + it("수량별 보너스 - 30개 이상 +100p", () => { + addItemsToCart(sel, addBtn, "p1", 30); // 225,000원 (25% 할인) -> 225포인트 + 100포인트 = 325포인트 - expect(loyaltyPoints.textContent).toContain('325p'); - expect(loyaltyPoints.textContent).toContain('대량구매(30개+)'); + expect(loyaltyPoints.textContent).toContain("325p"); + expect(loyaltyPoints.textContent).toContain("대량구매(30개+)"); }); }); - describe('4.3 포인트 표시', () => { - it('포인트 적립 내역 상세 표시', () => { - sel.value = 'p1'; + describe("4.3 포인트 표시", () => { + it("포인트 적립 내역 상세 표시", () => { + sel.value = "p1"; addBtn.click(); - sel.value = 'p2'; + sel.value = "p2"; addBtn.click(); const pointsText = loyaltyPoints.textContent; - expect(pointsText).toContain('기본:'); - expect(pointsText).toContain('키보드+마우스 세트'); + expect(pointsText).toContain("기본:"); + expect(pointsText).toContain("키보드+마우스 세트"); }); }); }); // 5. UI/UX 요구사항 테스트 - describe('5. UI/UX 요구사항', () => { - describe('5.1 레이아웃', () => { - it('필수 레이아웃 요소가 존재해야 함', () => { + describe("5. UI/UX 요구사항", () => { + describe("5.1 레이아웃", () => { + it("필수 레이아웃 요소가 존재해야 함", () => { // 헤더 - expect(document.querySelector('h1').textContent).toContain('🛒 Hanghae Online Store'); - expect(document.querySelector('.text-5xl').textContent).toContain('Shopping Cart'); + expect(document.querySelector("h1").textContent).toContain("🛒 Hanghae Online Store"); + expect(document.querySelector(".text-5xl").textContent).toContain("Shopping Cart"); // 좌측: 상품 선택 및 장바구니 - expect(document.querySelector('#product-select')).toBeTruthy(); - expect(document.querySelector('#cart-items')).toBeTruthy(); + expect(document.querySelector("#product-select")).toBeTruthy(); + expect(document.querySelector("#cart-items")).toBeTruthy(); // 우측: 주문 요약 - expect(document.querySelector('#cart-total')).toBeTruthy(); - expect(document.querySelector('#loyalty-points')).toBeTruthy(); + expect(document.querySelector("#cart-total")).toBeTruthy(); + expect(document.querySelector("#loyalty-points")).toBeTruthy(); // 도움말 버튼 - const helpButton = document.querySelector('.fixed.top-4.right-4'); + const helpButton = document.querySelector(".fixed.top-4.right-4"); expect(helpButton).toBeTruthy(); }); }); - describe('5.2 상품 선택 영역', () => { - it('할인 중인 상품 강조 표시 확인', async () => { + describe("5.2 상품 선택 영역", () => { + it("할인 중인 상품 강조 표시 확인", async () => { // 현재 화요일 테스트 또는 일반 상황에서의 강조 표시만 확인 const options = Array.from(sel.options); - + // 품절 상품이 비활성화되어 있는지 확인 const disabledOption = options.find(opt => opt.disabled); if (disabledOption) { - expect(disabledOption.textContent).toContain('품절'); + expect(disabledOption.textContent).toContain("품절"); } }); }); - describe('5.3 장바구니 영역', () => { - it('장바구니 아이템 카드 형식 확인', () => { - sel.value = 'p1'; + describe("5.3 장바구니 영역", () => { + it("장바구니 아이템 카드 형식 확인", () => { + sel.value = "p1"; addBtn.click(); - const cartItem = cartDisp.querySelector('#p1'); + const cartItem = cartDisp.querySelector("#p1"); // 상품 이미지 - expect(cartItem.querySelector('.bg-gradient-black')).toBeTruthy(); + expect(cartItem.querySelector(".bg-gradient-black")).toBeTruthy(); // 상품명 - expect(cartItem.querySelector('h3').textContent).toContain('버그 없애는 키보드'); + expect(cartItem.querySelector("h3").textContent).toContain("버그 없애는 키보드"); // 수량 조절 버튼 expect(cartItem.querySelector('.quantity-change[data-change="1"]')).toBeTruthy(); expect(cartItem.querySelector('.quantity-change[data-change="-1"]')).toBeTruthy(); // 제거 버튼 - expect(cartItem.querySelector('.remove-item')).toBeTruthy(); + expect(cartItem.querySelector(".remove-item")).toBeTruthy(); }); - it('첫 번째 상품은 상단 여백 없음', () => { - sel.value = 'p1'; + it("첫 번째 상품은 상단 여백 없음", () => { + sel.value = "p1"; addBtn.click(); const firstItem = cartDisp.firstElementChild; - expect(firstItem.className).toContain('first:pt-0'); + expect(firstItem.className).toContain("first:pt-0"); }); - it('마지막 상품은 하단 테두리 없음', () => { - sel.value = 'p1'; + it("마지막 상품은 하단 테두리 없음", () => { + sel.value = "p1"; addBtn.click(); const lastItem = cartDisp.lastElementChild; - expect(lastItem.className).toContain('last:border-b-0'); + expect(lastItem.className).toContain("last:border-b-0"); }); }); - describe('5.5 도움말 모달', () => { - it('도움말 버튼 클릭 시 모달 표시', async () => { - const helpButton = document.querySelector('.fixed.top-4.right-4'); - const modal = document.querySelector('.fixed.inset-0'); - const slidePanel = document.querySelector('.fixed.right-0.top-0'); + describe("5.5 도움말 모달", () => { + it("도움말 버튼 클릭 시 모달 표시", async () => { + const helpButton = document.querySelector(".fixed.top-4.right-4"); + const modal = document.querySelector(".fixed.inset-0"); + const slidePanel = document.querySelector(".fixed.right-0.top-0"); // 초기 상태: 숨김 - expect(modal.classList.contains('hidden')).toBe(true); - expect(slidePanel.classList.contains('translate-x-full')).toBe(true); + expect(modal.classList.contains("hidden")).toBe(true); + expect(slidePanel.classList.contains("translate-x-full")).toBe(true); // 클릭 후: 표시 await userEvent.click(helpButton); - expect(modal.classList.contains('hidden')).toBe(false); - expect(slidePanel.classList.contains('translate-x-full')).toBe(false); + expect(modal.classList.contains("hidden")).toBe(false); + expect(slidePanel.classList.contains("translate-x-full")).toBe(false); }); - it('배경 클릭 시 모달 닫기', async () => { - const helpButton = document.querySelector('.fixed.top-4.right-4'); - const modal = document.querySelector('.fixed.inset-0'); + it("배경 클릭 시 모달 닫기", async () => { + const helpButton = document.querySelector(".fixed.top-4.right-4"); + const modal = document.querySelector(".fixed.inset-0"); // 모달 열기 await userEvent.click(helpButton); - expect(modal.classList.contains('hidden')).toBe(false); + expect(modal.classList.contains("hidden")).toBe(false); // 배경 클릭으로 닫기 await userEvent.click(modal); - expect(modal.classList.contains('hidden')).toBe(true); + expect(modal.classList.contains("hidden")).toBe(true); }); }); }); // 6. 기능 요구사항 테스트 - describe('6. 기능 요구사항', () => { - describe('6.1 상품 추가', () => { - it('선택한 상품을 장바구니에 추가', () => { - sel.value = 'p2'; + describe("6. 기능 요구사항", () => { + describe("6.1 상품 추가", () => { + it("선택한 상품을 장바구니에 추가", () => { + sel.value = "p2"; addBtn.click(); expect(cartDisp.children.length).toBe(1); - expect(cartDisp.querySelector('#p2')).toBeTruthy(); + expect(cartDisp.querySelector("#p2")).toBeTruthy(); }); - it('이미 있는 상품은 수량 증가', () => { - sel.value = 'p3'; + it("이미 있는 상품은 수량 증가", () => { + sel.value = "p3"; addBtn.click(); addBtn.click(); expect(cartDisp.children.length).toBe(1); - const qty = cartDisp.querySelector('.quantity-number').textContent; - expect(qty).toBe('2'); + const qty = cartDisp.querySelector(".quantity-number").textContent; + expect(qty).toBe("2"); }); - it('재고 초과 시 알림 표시', () => { + it("재고 초과 시 알림 표시", () => { // 재고가 10개인 상품5를 11개 추가 시도 - addItemsToCart(sel, addBtn, 'p5', 11); - + addItemsToCart(sel, addBtn, "p5", 11); + // 장바구니에는 10개만 있어야 함 - const qty = getCartItemQuantity(cartDisp, 'p5'); + const qty = getCartItemQuantity(cartDisp, "p5"); expect(qty).toBeLessThanOrEqual(10); }); - it('품절 상품은 선택 불가', () => { - sel.value = 'p4'; + it("품절 상품은 선택 불가", () => { + sel.value = "p4"; addBtn.click(); expect(cartDisp.children.length).toBe(0); }); }); - describe('6.2 수량 변경', () => { - it('+/- 버튼으로 수량 조절', async () => { - sel.value = 'p1'; + describe("6.2 수량 변경", () => { + it("+/- 버튼으로 수량 조절", async () => { + sel.value = "p1"; addBtn.click(); const increaseBtn = cartDisp.querySelector('.quantity-change[data-change="1"]'); @@ -480,28 +474,28 @@ describe('basic 테스트', () => { // 증가 await userEvent.click(increaseBtn); - expect(cartDisp.querySelector('.quantity-number').textContent).toBe('2'); + expect(cartDisp.querySelector(".quantity-number").textContent).toBe("2"); // 감소 await userEvent.click(decreaseBtn); - expect(cartDisp.querySelector('.quantity-number').textContent).toBe('1'); + expect(cartDisp.querySelector(".quantity-number").textContent).toBe("1"); }); - it('재고 한도 내에서만 증가 가능', async () => { + it("재고 한도 내에서만 증가 가능", async () => { // 재고 10개인 상품5를 10개 추가 - addItemsToCart(sel, addBtn, 'p5', 10); + addItemsToCart(sel, addBtn, "p5", 10); const increaseBtn = cartDisp.querySelector('.quantity-change[data-change="1"]'); - const qtyBefore = getCartItemQuantity(cartDisp, 'p5'); - + const qtyBefore = getCartItemQuantity(cartDisp, "p5"); + await userEvent.click(increaseBtn); - - const qtyAfter = getCartItemQuantity(cartDisp, 'p5'); + + const qtyAfter = getCartItemQuantity(cartDisp, "p5"); expect(qtyAfter).toBe(qtyBefore); // 수량이 증가하지 않아야 함 }); - it('수량 0이 되면 자동 제거', async () => { - sel.value = 'p1'; + it("수량 0이 되면 자동 제거", async () => { + sel.value = "p1"; addBtn.click(); const decreaseBtn = cartDisp.querySelector('.quantity-change[data-change="-1"]'); @@ -511,164 +505,159 @@ describe('basic 테스트', () => { }); }); - describe('6.3 상품 제거', () => { - it('Remove 버튼 클릭 시 즉시 제거', async () => { - sel.value = 'p2'; + describe("6.3 상품 제거", () => { + it("Remove 버튼 클릭 시 즉시 제거", async () => { + sel.value = "p2"; addBtn.click(); - const removeBtn = cartDisp.querySelector('.remove-item'); + const removeBtn = cartDisp.querySelector(".remove-item"); await userEvent.click(removeBtn); expect(cartDisp.children.length).toBe(0); }); - it.skip('제거된 수량만큼 재고 복구', async () => { + it.skip("제거된 수량만큼 재고 복구", async () => { // 원본 코드의 재고 업데이트 버그로 인해 스킵 - addItemsToCart(sel, addBtn, 'p5', 5); + addItemsToCart(sel, addBtn, "p5", 5); - const removeBtn = cartDisp.querySelector('.remove-item'); + const removeBtn = cartDisp.querySelector(".remove-item"); await userEvent.click(removeBtn); - + // 재고가 복구되어야 하지만 원본 코드에서는 제대로 업데이트되지 않음 }); }); - describe('6.4 실시간 계산', () => { - it('수량 변경 시 즉시 재계산', async () => { - sel.value = 'p1'; + describe("6.4 실시간 계산", () => { + it("수량 변경 시 즉시 재계산", async () => { + sel.value = "p1"; addBtn.click(); - expect(sum.textContent).toContain('₩10,000'); + expect(sum.textContent).toContain("₩10,000"); const increaseBtn = cartDisp.querySelector('.quantity-change[data-change="1"]'); await userEvent.click(increaseBtn); - expect(sum.textContent).toContain('₩20,000'); + expect(sum.textContent).toContain("₩20,000"); }); - it('할인 정책 자동 적용', () => { - addItemsToCart(sel, addBtn, 'p1', 10); + it("할인 정책 자동 적용", () => { + addItemsToCart(sel, addBtn, "p1", 10); - expect(discountInfo.textContent).toContain('10.0%'); - expect(sum.textContent).toContain('₩90,000'); + expect(discountInfo.textContent).toContain("10.0%"); + expect(sum.textContent).toContain("₩90,000"); }); - it('포인트 실시간 업데이트', async () => { - sel.value = 'p1'; + it("포인트 실시간 업데이트", async () => { + sel.value = "p1"; addBtn.click(); - expect(loyaltyPoints.textContent).toContain('10p'); + expect(loyaltyPoints.textContent).toContain("10p"); const increaseBtn = cartDisp.querySelector('.quantity-change[data-change="1"]'); await userEvent.click(increaseBtn); - expect(loyaltyPoints.textContent).toContain('20p'); + expect(loyaltyPoints.textContent).toContain("20p"); }); }); - describe('6.5 상태 관리', () => { - it('장바구니 상품 수 표시', () => { - expect(itemCount.textContent).toContain('0 items'); + describe("6.5 상태 관리", () => { + it("장바구니 상품 수 표시", () => { + expect(itemCount.textContent).toContain("0 items"); - addItemsToCart(sel, addBtn, 'p1', 5); + addItemsToCart(sel, addBtn, "p1", 5); - expect(itemCount.textContent).toContain('5 items'); + expect(itemCount.textContent).toContain("5 items"); }); - it('재고 부족/품절 상태 표시', () => { + it("재고 부족/품절 상태 표시", () => { // 상품5를 재고 부족 상태로 만듦 - addItemsToCart(sel, addBtn, 'p5', 6); + addItemsToCart(sel, addBtn, "p5", 6); - expect(stockInfo.textContent).toContain('재고 부족'); - expect(stockInfo.textContent).toContain('4개 남음'); + expect(stockInfo.textContent).toContain("재고 부족"); + expect(stockInfo.textContent).toContain("4개 남음"); // 상품4는 품절 - expect(stockInfo.textContent).toContain('에러 방지 노트북 파우치: 품절'); + expect(stockInfo.textContent).toContain("에러 방지 노트북 파우치: 품절"); }); }); }); // 8. 예외 처리 테스트 - describe('8. 예외 처리', () => { - describe('8.1 재고 부족', () => { - it('장바구니 추가 시 재고 확인', () => { + describe("8. 예외 처리", () => { + describe("8.1 재고 부족", () => { + it("장바구니 추가 시 재고 확인", () => { // 재고 10개인 상품을 11개 추가 시도 - addItemsToCart(sel, addBtn, 'p5', 11); - + addItemsToCart(sel, addBtn, "p5", 11); + // 장바구니에는 최대 재고 수량만큼만 담김 - const qty = getCartItemQuantity(cartDisp, 'p5'); + const qty = getCartItemQuantity(cartDisp, "p5"); expect(qty).toBeLessThanOrEqual(10); }); - it('수량 증가 시 재고 확인', async () => { - addItemsToCart(sel, addBtn, 'p5', 10); + it("수량 증가 시 재고 확인", async () => { + addItemsToCart(sel, addBtn, "p5", 10); const increaseBtn = cartDisp.querySelector('.quantity-change[data-change="1"]'); await userEvent.click(increaseBtn); - expect(window.alert).toHaveBeenCalledWith('재고가 부족합니다.'); + expect(window.alert).toHaveBeenCalledWith("재고가 부족합니다."); }); }); - describe('8.2 빈 장바구니', () => { - it('장바구니가 비어있을 때 포인트 섹션 숨김', () => { + describe("8.2 빈 장바구니", () => { + it("장바구니가 비어있을 때 포인트 섹션 숨김", () => { expect(cartDisp.children.length).toBe(0); - expect(loyaltyPoints.style.display).toBe('none'); + expect(loyaltyPoints.style.display).toBe("none"); }); - it('주문 요약에 기본값 표시', () => { - expect(sum.textContent).toContain('₩0'); - expect(itemCount.textContent).toContain('0 items'); + it("주문 요약에 기본값 표시", () => { + expect(sum.textContent).toContain("₩0"); + expect(itemCount.textContent).toContain("0 items"); }); }); - describe('8.3 동시성 이슈', () => { - it.skip('번개세일과 추천할인이 같은 상품에 적용 시 최대 25%', async () => { + describe("8.3 동시성 이슈", () => { + it.skip("번개세일과 추천할인이 같은 상품에 적용 시 최대 25%", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(40000); - sel.value = 'p1'; + sel.value = "p1"; addBtn.click(); await vi.advanceTimersByTimeAsync(80000); - vi.useRealTimers(); }); }); }); // 복잡한 시나리오 테스트 - describe('복잡한 통합 시나리오', () => { - it('화요일 + 풀세트 + 대량구매 시나리오', () => { - const tuesday = new Date('2024-10-15'); - vi.useFakeTimers(); + describe("복잡한 통합 시나리오", () => { + it("화요일 + 풀세트 + 대량구매 시나리오", () => { + const tuesday = new Date("2024-10-15"); + vi.setSystemTime(tuesday); // 키보드 10개, 마우스 10개, 모니터암 10개 - addItemsToCart(sel, addBtn, 'p1', 10); - addItemsToCart(sel, addBtn, 'p2', 10); - addItemsToCart(sel, addBtn, 'p3', 10); + addItemsToCart(sel, addBtn, "p1", 10); + addItemsToCart(sel, addBtn, "p2", 10); + addItemsToCart(sel, addBtn, "p3", 10); // 총액 확인: 600,000원 -> 25% 할인 -> 450,000원 -> 화요일 10% -> 405,000원 - expect(sum.textContent).toContain('₩405,000'); + expect(sum.textContent).toContain("₩405,000"); // 포인트 확인: 405포인트(기본) * 2(화요일) + 50(세트) + 100(풀세트) + 100(30개) = 1060포인트 - expect(loyaltyPoints.textContent).toContain('1060p'); - - vi.useRealTimers(); + expect(loyaltyPoints.textContent).toContain("1060p"); }); - it.skip('번개세일 + 추천할인 + 화요일 시나리오', async () => { + it.skip("번개세일 + 추천할인 + 화요일 시나리오", async () => { // 원본 코드의 타이머 구현 문제로 인해 스킵 - const tuesday = new Date('2024-10-15'); - vi.useFakeTimers(); + const tuesday = new Date("2024-10-15"); + vi.setSystemTime(tuesday); await vi.advanceTimersByTimeAsync(40000); - sel.value = 'p1'; + sel.value = "p1"; addBtn.click(); await vi.advanceTimersByTimeAsync(80000); - - vi.useRealTimers(); }); }); }); -}); \ No newline at end of file +}); From 4ecb59f03eb25a24d5a7e275237bc24c404dbf5d Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 01:30:52 +0900 Subject: [PATCH 12/55] =?UTF-8?q?refactor:=20=EB=A9=94=EC=9D=B8=20UI=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 5 +- src/basic/components/Header.js | 15 +++ src/basic/components/MainLayout.js | 84 +++++++++++++ src/basic/components/ManualToggle.js | 102 +++++++++++++++ src/basic/main.basic.js | 180 +++------------------------ 5 files changed, 221 insertions(+), 165 deletions(-) create mode 100644 src/basic/components/Header.js create mode 100644 src/basic/components/MainLayout.js create mode 100644 src/basic/components/ManualToggle.js diff --git a/.prettierrc b/.prettierrc index 5fd80f367..3aa054bef 100644 --- a/.prettierrc +++ b/.prettierrc @@ -9,5 +9,8 @@ "bracketSpacing": true, "bracketSameLine": false, "arrowParens": "avoid", - "endOfLine": "lf" + "endOfLine": "lf", + "htmlWhitespaceSensitivity": "css", + "vueIndentScriptAndStyle": false, + "embeddedLanguageFormatting": "auto" } diff --git a/src/basic/components/Header.js b/src/basic/components/Header.js new file mode 100644 index 000000000..a10fbaaa3 --- /dev/null +++ b/src/basic/components/Header.js @@ -0,0 +1,15 @@ +/** + * 헤더 컴포넌트 생성 함수 + * @returns {HTMLElement} 생성된 헤더 요소 + */ +export function Header() { + const header = document.createElement("div"); + header.className = "mb-8"; + header.innerHTML = ` +

🛒 Hanghae Online Store

+
Shopping Cart
+

🛍️ 0 items in cart

+ `; + + return header; +} diff --git a/src/basic/components/MainLayout.js b/src/basic/components/MainLayout.js new file mode 100644 index 000000000..979ffdf1b --- /dev/null +++ b/src/basic/components/MainLayout.js @@ -0,0 +1,84 @@ +/** + * 메인 레이아웃 컴포넌트 생성 함수 + * @returns {Object} 생성된 요소들과 참조 + */ +export function MainLayout() { + const select = document.createElement("select"); + select.id = "product-select"; + select.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; + + const gridContainer = document.createElement("div"); + gridContainer.className = + "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; + + const leftColumn = document.createElement("div"); + leftColumn["className"] = "bg-white border border-gray-200 p-8 overflow-y-auto"; + + const selectorContainer = document.createElement("div"); + selectorContainer.className = "mb-6 pb-6 border-b border-gray-200"; + + const addButton = document.createElement("button"); + addButton.id = "add-to-cart"; + addButton.innerHTML = "Add to Cart"; + addButton.className = + "w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all"; + + const stockInfo = document.createElement("div"); + stockInfo.id = "stock-status"; + stockInfo.className = "text-xs text-red-500 mt-3 whitespace-pre-line"; + + selectorContainer.appendChild(select); + selectorContainer.appendChild(addButton); + selectorContainer.appendChild(stockInfo); + + const cartDisplay = document.createElement("div"); + cartDisplay.id = "cart-items"; + + leftColumn.appendChild(selectorContainer); + leftColumn.appendChild(cartDisplay); + + const rightColumn = document.createElement("div"); + rightColumn.className = "bg-black text-white p-8 flex flex-col"; + rightColumn.innerHTML = ` +

Order Summary

+
+
+
+
+
+
+ Total +
₩0
+
+
적립 포인트: 0p
+
+ +
+
+ +

+ Free shipping on all orders.
+ Earn loyalty points with purchase. +

+ `; + + gridContainer.appendChild(leftColumn); + gridContainer.appendChild(rightColumn); + + return { + gridContainer, + select, + addButton, + stockInfo, + cartDisplay, + leftColumn, + rightColumn, + }; +} diff --git a/src/basic/components/ManualToggle.js b/src/basic/components/ManualToggle.js new file mode 100644 index 000000000..61ea36dc4 --- /dev/null +++ b/src/basic/components/ManualToggle.js @@ -0,0 +1,102 @@ +/** + * 매뉴얼 토글 컴포넌트 생성 함수 + * @returns {Object} 생성된 요소들과 참조 + */ +export function createManualToggle() { + const manualToggle = document.createElement("button"); + manualToggle.className = + "fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50"; + manualToggle.innerHTML = ` + + + + `; + + const manualOverlay = document.createElement("div"); + manualOverlay.className = "fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300"; + + const manualColumn = document.createElement("div"); + manualColumn.className = + "fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300"; + manualColumn.innerHTML = ` + +

📖 이용 안내

+
+

💰 할인 정책

+
+
+

개별 상품

+

+ • 키보드 10개↑: 10%
+ • 마우스 10개↑: 15%
+ • 모니터암 10개↑: 20%
+ • 스피커 10개↑: 25% +

+
+
+

전체 수량

+

• 30개 이상: 25%

+
+
+

특별 할인

+

+ • 화요일: +10%
+ • ⚡번개세일: 20%
+ • 💝추천할인: 5% +

+
+
+
+
+

🎁 포인트 적립

+
+
+

기본

+

• 구매액의 0.1%

+
+
+

추가

+

+ • 화요일: 2배
+ • 키보드+마우스: +50p
+ • 풀세트: +100p
+ • 10개↑: +20p / 20개↑: +50p / 30개↑: +100p +

+
+
+
+
+

💡 TIP

+

+ • 화요일 대량구매 = MAX 혜택
+ • ⚡+💝 중복 가능
+ • 상품4 = 품절 +

+
+ `; + + // 이벤트 리스너 설정 + manualToggle.onclick = function () { + manualOverlay.classList.toggle("hidden"); + manualColumn.classList.toggle("translate-x-full"); + }; + + manualOverlay.onclick = function (e) { + if (e.target === manualOverlay) { + manualOverlay.classList.add("hidden"); + manualColumn.classList.add("translate-x-full"); + } + }; + + // 오버레이에 컬럼 추가 + manualOverlay.appendChild(manualColumn); + + return { + manualToggle, + manualOverlay, + }; +} diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 543230f4d..9476f0575 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -42,6 +42,9 @@ import { SUGGESTION_SALE_MAX_DELAY, } from "./data/time.data.js"; import { TUESDAY_DAY_OF_WEEK } from "./data/date.data.js"; +import { Header } from "./components/Header.js"; +import { createManualToggle } from "./components/ManualToggle.js"; +import { MainLayout } from "./components/MainLayout.js"; // 상태 변수 let itemCounts; @@ -55,185 +58,34 @@ let addButton; let cartDisplay; function main() { - let root; - let header; - let gridContainer; - let leftColumn; - let selectorContainer; - let rightColumn; - let manualToggle; - let manualOverlay; - let manualColumn; let lightningDelay; totalAmount = 0; itemCounts = 0; lastSelect = null; - root = document.getElementById("app"); + const root = document.getElementById("app"); - header = document.createElement("div"); - header.className = "mb-8"; - header.innerHTML = ` -

🛒 Hanghae Online Store

-
Shopping Cart
-

🛍️ 0 items in cart

- `; + // 매뉴얼 토글 컴포넌트 생성 + const { manualToggle, manualOverlay } = createManualToggle(); - select = document.createElement("select"); - select.id = "product-select"; - select.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; + // 메인 레이아웃 컴포넌트 생성 + const mainLayoutComponent = MainLayout(); + select = mainLayoutComponent.select; + addButton = mainLayoutComponent.addButton; + stockInfo = mainLayoutComponent.stockInfo; + cartDisplay = mainLayoutComponent.cartDisplay; - gridContainer = document.createElement("div"); - gridContainer.className = - "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; - - leftColumn = document.createElement("div"); - leftColumn["className"] = "bg-white border border-gray-200 p-8 overflow-y-auto"; - - selectorContainer = document.createElement("div"); - selectorContainer.className = "mb-6 pb-6 border-b border-gray-200"; - - addButton = document.createElement("button"); - stockInfo = document.createElement("div"); - addButton.id = "add-to-cart"; - stockInfo.id = "stock-status"; - stockInfo.className = "text-xs text-red-500 mt-3 whitespace-pre-line"; - addButton.innerHTML = "Add to Cart"; - addButton.className = - "w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all"; - selectorContainer.appendChild(select); - selectorContainer.appendChild(addButton); - selectorContainer.appendChild(stockInfo); - leftColumn.appendChild(selectorContainer); - cartDisplay = document.createElement("div"); - leftColumn.appendChild(cartDisplay); - cartDisplay.id = "cart-items"; - rightColumn = document.createElement("div"); - rightColumn.className = "bg-black text-white p-8 flex flex-col"; - rightColumn.innerHTML = ` -

Order Summary

-
-
-
-
-
-
- Total -
₩0
-
-
적립 포인트: 0p
-
- -
-
- -

- Free shipping on all orders.
- Earn loyalty points with purchase. -

- `; - manualToggle = document.createElement("button"); - manualToggle.onclick = function () { - manualOverlay.classList.toggle("hidden"); - manualColumn.classList.toggle("translate-x-full"); - }; - manualToggle.className = - "fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50"; - manualToggle.innerHTML = ` - - - - `; - manualOverlay = document.createElement("div"); - manualOverlay.className = "fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300"; - manualOverlay.onclick = function (e) { - if (e.target === manualOverlay) { - manualOverlay.classList.add("hidden"); - manualColumn.classList.add("translate-x-full"); - } - }; - manualColumn = document.createElement("div"); - manualColumn.className = - "fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300"; - manualColumn.innerHTML = ` - -

📖 이용 안내

-
-

💰 할인 정책

-
-
-

개별 상품

-

- • 키보드 10개↑: 10%
- • 마우스 10개↑: 15%
- • 모니터암 10개↑: 20%
- • 스피커 10개↑: 25% -

-
-
-

전체 수량

-

• 30개 이상: 25%

-
-
-

특별 할인

-

- • 화요일: +10%
- • ⚡번개세일: 20%
- • 💝추천할인: 5% -

-
-
-
-
-

🎁 포인트 적립

-
-
-

기본

-

• 구매액의 0.1%

-
-
-

추가

-

- • 화요일: 2배
- • 키보드+마우스: +50p
- • 풀세트: +100p
- • 10개↑: +20p / 20개↑: +50p / 30개↑: +100p -

-
-
-
-
-

💡 TIP

-

- • 화요일 대량구매 = MAX 혜택
- • ⚡+💝 중복 가능
- • 상품4 = 품절 -

-
- `; - gridContainer.appendChild(leftColumn); - gridContainer.appendChild(rightColumn); - manualOverlay.appendChild(manualColumn); - root.appendChild(header); - root.appendChild(gridContainer); + root.appendChild(Header()); + root.appendChild(mainLayoutComponent.gridContainer); root.appendChild(manualToggle); root.appendChild(manualOverlay); + let initStock = 0; for (let i = 0; i < PRODUCT_LIST.length; i++) { initStock += PRODUCT_LIST[i].q; } + onUpdateSelectOptions(); handleCalculateCartStuff(); lightningDelay = Math.random() * LIGHTNING_SALE_MAX_DELAY; From 44a84dc5eb943ff1db1d63c01b2933679069f53e Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 02:10:26 +0900 Subject: [PATCH 13/55] =?UTF-8?q?refactor:=20=ED=83=80=EC=9D=B4=EB=A8=B8?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/components/LightningSaleTimer.js | 74 ++++++++++++++ src/basic/components/SuggestionSaleTimer.js | 92 +++++++++++++++++ src/basic/components/TimerManager.js | 105 ++++++++++++++++++++ src/basic/main.basic.js | 64 ++---------- 4 files changed, 280 insertions(+), 55 deletions(-) create mode 100644 src/basic/components/LightningSaleTimer.js create mode 100644 src/basic/components/SuggestionSaleTimer.js create mode 100644 src/basic/components/TimerManager.js diff --git a/src/basic/components/LightningSaleTimer.js b/src/basic/components/LightningSaleTimer.js new file mode 100644 index 000000000..ea3a567ec --- /dev/null +++ b/src/basic/components/LightningSaleTimer.js @@ -0,0 +1,74 @@ +import { DISCOUNT_RATE_LIGHTNING } from "../data/discount.data.js"; +import { PRODUCT_LIST } from "../data/product.data.js"; +import { LIGHTNING_SALE_MAX_DELAY, LIGHTNING_SALE_INTERVAL } from "../data/time.data.js"; + +/** + * 번개세일 타이머를 관리하는 컴포넌트 + * @param {Function} onUpdateSelectOptions - 상품 옵션 업데이트 콜백 + * @param {Function} doUpdatePricesInCart - 장바구니 가격 업데이트 콜백 + * @returns {Object} 타이머 시작/중지 메서드를 포함한 객체 + */ +export function createLightningSaleTimer(onUpdateSelectOptions, doUpdatePricesInCart) { + let lightningIntervalId = null; + let lightningTimeoutId = null; + + /** + * 번개세일 이벤트를 실행합니다 + */ + function executeLightningSale() { + const luckyIdx = Math.floor(Math.random() * PRODUCT_LIST.length); + const luckyItem = PRODUCT_LIST[luckyIdx]; + + if (luckyItem.q > 0 && !luckyItem.onSale) { + luckyItem.val = Math.round((luckyItem.originalVal * (100 - DISCOUNT_RATE_LIGHTNING)) / 100); + luckyItem.onSale = true; + + alert( + "⚡번개세일! " + luckyItem.name + "이(가) " + DISCOUNT_RATE_LIGHTNING + "% 할인 중입니다!" + ); + + onUpdateSelectOptions(); + doUpdatePricesInCart(); + } + } + + /** + * 번개세일 타이머를 시작합니다 + */ + function startLightningSaleTimer() { + const lightningDelay = Math.random() * LIGHTNING_SALE_MAX_DELAY; + + lightningTimeoutId = setTimeout(() => { + lightningIntervalId = setInterval(executeLightningSale, LIGHTNING_SALE_INTERVAL); + }, lightningDelay); + } + + /** + * 번개세일 타이머를 중지합니다 + */ + function stopLightningSaleTimer() { + if (lightningTimeoutId) { + clearTimeout(lightningTimeoutId); + lightningTimeoutId = null; + } + + if (lightningIntervalId) { + clearInterval(lightningIntervalId); + lightningIntervalId = null; + } + } + + /** + * 번개세일 타이머를 재시작합니다 + */ + function restartLightningSaleTimer() { + stopLightningSaleTimer(); + startLightningSaleTimer(); + } + + return { + start: startLightningSaleTimer, + stop: stopLightningSaleTimer, + restart: restartLightningSaleTimer, + }; +} diff --git a/src/basic/components/SuggestionSaleTimer.js b/src/basic/components/SuggestionSaleTimer.js new file mode 100644 index 000000000..cdd1cadbf --- /dev/null +++ b/src/basic/components/SuggestionSaleTimer.js @@ -0,0 +1,92 @@ +import { DISCOUNT_RATE_SUGGESTION } from "../data/discount.data.js"; +import { PRODUCT_LIST } from "../data/product.data.js"; +import { SUGGESTION_SALE_INTERVAL, SUGGESTION_SALE_MAX_DELAY } from "../data/time.data.js"; + +/** + * 추천할인 타이머를 관리하는 컴포넌트 + * @param {Function} onUpdateSelectOptions - 상품 옵션 업데이트 콜백 + * @param {Function} doUpdatePricesInCart - 장바구니 가격 업데이트 콜백 + * @param {Object} state - 앱 상태 객체 (lastSelect, cartDisplay 포함) + * @returns {Object} 타이머 시작/중지 메서드를 포함한 객체 + */ +export function createSuggestionSaleTimer(onUpdateSelectOptions, doUpdatePricesInCart, state) { + let suggestionIntervalId = null; + let suggestionTimeoutId = null; + + /** + * 추천할인 이벤트를 실행합니다 + */ + function executeSuggestionSale() { + // 장바구니가 비어있거나 마지막 선택한 상품이 없으면 실행하지 않음 + if (state.cartDisplay.children.length === 0 || !state.lastSelect) { + return; + } + + // 마지막 선택한 상품과 다른 상품 중 추천할 상품 찾기 + let suggest = null; + for (let k = 0; k < PRODUCT_LIST.length; k++) { + if (PRODUCT_LIST[k].id !== state.lastSelect) { + if (PRODUCT_LIST[k].q > 0 && !PRODUCT_LIST[k].suggestSale) { + suggest = PRODUCT_LIST[k]; + break; + } + } + } + + if (suggest) { + alert( + "💝 " + + suggest.name + + "은(는) 어떠세요? 지금 구매하시면 " + + DISCOUNT_RATE_SUGGESTION + + "% 추가 할인!" + ); + + suggest.val = Math.round((suggest.val * (100 - DISCOUNT_RATE_SUGGESTION)) / 100); + suggest.suggestSale = true; + + onUpdateSelectOptions(); + doUpdatePricesInCart(); + } + } + + /** + * 추천할인 타이머를 시작합니다 + */ + function startSuggestionSaleTimer() { + const suggestionDelay = Math.random() * SUGGESTION_SALE_MAX_DELAY; + + suggestionTimeoutId = setTimeout(() => { + suggestionIntervalId = setInterval(executeSuggestionSale, SUGGESTION_SALE_INTERVAL); + }, suggestionDelay); + } + + /** + * 추천할인 타이머를 중지합니다 + */ + function stopSuggestionSaleTimer() { + if (suggestionTimeoutId) { + clearTimeout(suggestionTimeoutId); + suggestionTimeoutId = null; + } + + if (suggestionIntervalId) { + clearInterval(suggestionIntervalId); + suggestionIntervalId = null; + } + } + + /** + * 추천할인 타이머를 재시작합니다 + */ + function restartSuggestionSaleTimer() { + stopSuggestionSaleTimer(); + startSuggestionSaleTimer(); + } + + return { + start: startSuggestionSaleTimer, + stop: stopSuggestionSaleTimer, + restart: restartSuggestionSaleTimer, + }; +} diff --git a/src/basic/components/TimerManager.js b/src/basic/components/TimerManager.js new file mode 100644 index 000000000..e1c42bc30 --- /dev/null +++ b/src/basic/components/TimerManager.js @@ -0,0 +1,105 @@ +import { createLightningSaleTimer } from "./LightningSaleTimer.js"; +import { createSuggestionSaleTimer } from "./SuggestionSaleTimer.js"; + +/** + * 모든 타이머를 통합 관리하는 컴포넌트 + * @param {Function} onUpdateSelectOptions - 상품 옵션 업데이트 콜백 + * @param {Function} doUpdatePricesInCart - 장바구니 가격 업데이트 콜백 + * @param {Object} state - 앱 상태 객체 (lastSelect, cartDisplay 포함) + * @returns {Object} 모든 타이머 시작/중지 메서드를 포함한 객체 + */ +export function createTimerManager(onUpdateSelectOptions, doUpdatePricesInCart, state) { + // 개별 타이머 인스턴스 생성 + const lightningSaleTimer = createLightningSaleTimer(onUpdateSelectOptions, doUpdatePricesInCart); + const suggestionSaleTimer = createSuggestionSaleTimer( + onUpdateSelectOptions, + doUpdatePricesInCart, + state + ); + + /** + * 모든 타이머를 시작합니다 + */ + function startAllTimers() { + lightningSaleTimer.start(); + suggestionSaleTimer.start(); + } + + /** + * 모든 타이머를 중지합니다 + */ + function stopAllTimers() { + lightningSaleTimer.stop(); + suggestionSaleTimer.stop(); + } + + /** + * 모든 타이머를 재시작합니다 + */ + function restartAllTimers() { + stopAllTimers(); + startAllTimers(); + } + + /** + * 번개세일 타이머만 시작합니다 + */ + function startLightningTimer() { + lightningSaleTimer.start(); + } + + /** + * 번개세일 타이머만 중지합니다 + */ + function stopLightningTimer() { + lightningSaleTimer.stop(); + } + + /** + * 추천할인 타이머만 시작합니다 + */ + function startSuggestionTimer() { + suggestionSaleTimer.start(); + } + + /** + * 추천할인 타이머만 중지합니다 + */ + function stopSuggestionTimer() { + suggestionSaleTimer.stop(); + } + + /** + * 타이머 상태를 확인합니다 + */ + function getTimerStatus() { + return { + lightningSale: { + isRunning: lightningSaleTimer.isRunning ? lightningSaleTimer.isRunning() : false, + }, + suggestionSale: { + isRunning: suggestionSaleTimer.isRunning ? suggestionSaleTimer.isRunning() : false, + }, + }; + } + + return { + // 전체 타이머 관리 + startAll: startAllTimers, + stopAll: stopAllTimers, + restartAll: restartAllTimers, + + // 개별 타이머 관리 + startLightning: startLightningTimer, + stopLightning: stopLightningTimer, + startSuggestion: startSuggestionTimer, + stopSuggestion: stopSuggestionTimer, + + // 상태 확인 + getStatus: getTimerStatus, + + // 개별 타이머 인스턴스 (고급 사용자용) + lightningSaleTimer, + suggestionSaleTimer, + }; +} diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 9476f0575..3cdb21f85 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -45,6 +45,7 @@ import { TUESDAY_DAY_OF_WEEK } from "./data/date.data.js"; import { Header } from "./components/Header.js"; import { createManualToggle } from "./components/ManualToggle.js"; import { MainLayout } from "./components/MainLayout.js"; +import { createTimerManager } from "./components/TimerManager.js"; // 상태 변수 let itemCounts; @@ -58,8 +59,6 @@ let addButton; let cartDisplay; function main() { - let lightningDelay; - totalAmount = 0; itemCounts = 0; lastSelect = null; @@ -81,61 +80,16 @@ function main() { root.appendChild(manualToggle); root.appendChild(manualOverlay); - let initStock = 0; - for (let i = 0; i < PRODUCT_LIST.length; i++) { - initStock += PRODUCT_LIST[i].q; - } - onUpdateSelectOptions(); handleCalculateCartStuff(); - lightningDelay = Math.random() * LIGHTNING_SALE_MAX_DELAY; - setTimeout(() => { - setInterval(() => { - const luckyIdx = Math.floor(Math.random() * PRODUCT_LIST.length); - const luckyItem = PRODUCT_LIST[luckyIdx]; - if (luckyItem.q > 0 && !luckyItem.onSale) { - luckyItem.val = Math.round((luckyItem.originalVal * (100 - DISCOUNT_RATE_LIGHTNING)) / 100); - luckyItem.onSale = true; - alert( - "⚡번개세일! " + luckyItem.name + "이(가) " + DISCOUNT_RATE_LIGHTNING + "% 할인 중입니다!" - ); - onUpdateSelectOptions(); - doUpdatePricesInCart(); - } - }, LIGHTNING_SALE_INTERVAL); - }, lightningDelay); - setTimeout(() => { - setInterval(() => { - if (cartDisplay.children.length === 0) { - } - if (lastSelect) { - let suggest = null; - for (let k = 0; k < PRODUCT_LIST.length; k++) { - if (PRODUCT_LIST[k].id !== lastSelect) { - if (PRODUCT_LIST[k].q > 0) { - if (!PRODUCT_LIST[k].suggestSale) { - suggest = PRODUCT_LIST[k]; - break; - } - } - } - } - if (suggest) { - alert( - "💝 " + - suggest.name + - "은(는) 어떠세요? 지금 구매하시면 " + - DISCOUNT_RATE_SUGGESTION + - "% 추가 할인!" - ); - suggest.val = Math.round((suggest.val * (100 - DISCOUNT_RATE_SUGGESTION)) / 100); - suggest.suggestSale = true; - onUpdateSelectOptions(); - doUpdatePricesInCart(); - } - } - }, SUGGESTION_SALE_INTERVAL); - }, Math.random() * SUGGESTION_SALE_MAX_DELAY); + + // 타이머 매니저 생성 및 모든 타이머 시작 + const timerManager = createTimerManager(onUpdateSelectOptions, doUpdatePricesInCart, { + lastSelect, + cartDisplay, + }); + + timerManager.startAll(); } function onUpdateSelectOptions() { From 53a9d9f9f747cee01d640678ed394196465ed6f7 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 02:24:47 +0900 Subject: [PATCH 14/55] =?UTF-8?q?chore:=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snapshot.html | 950 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 950 insertions(+) create mode 100644 snapshot.html diff --git a/snapshot.html b/snapshot.html new file mode 100644 index 000000000..a40551d82 --- /dev/null +++ b/snapshot.html @@ -0,0 +1,950 @@ + + + + + + + Hanghae Shopping Cart + + + + + + +
+ +
+

+ 🛒 Hanghae Online Store +

+
Shopping Cart
+

🛍️ 0 items in cart

+
+
+
+
+ +
+ 에러 방지 노트북 파우치: 품절 +
+
+
+
+
+

Order Summary

+
+
+
+
+
+
+ Total +
₩0
+
+ +
+ +
+
+ +

+ Free shipping on all orders.
+ Earn loyalty points with purchase. +

+
+
+ + +
+ + + From b70992b489973057c3aaa827bb1ef129d07be204 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 02:59:30 +0900 Subject: [PATCH 15/55] =?UTF-8?q?refactor:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20App=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/App.js | 27 +++++ src/basic/components/Header.js | 24 +++-- src/basic/components/MainLayout.js | 106 ++++++-------------- src/basic/components/OrderSummary.js | 39 ++++++++ src/basic/components/ProductSelector.js | 29 ++++++ src/basic/lib/Component.js | 125 ++++++++++++++++++++++++ src/basic/main.basic.js | 56 +++++------ 7 files changed, 286 insertions(+), 120 deletions(-) create mode 100644 src/basic/App.js create mode 100644 src/basic/components/OrderSummary.js create mode 100644 src/basic/components/ProductSelector.js create mode 100644 src/basic/lib/Component.js diff --git a/src/basic/App.js b/src/basic/App.js new file mode 100644 index 000000000..c88ce9ec1 --- /dev/null +++ b/src/basic/App.js @@ -0,0 +1,27 @@ +import Header from "./components/Header.js"; +import MainLayout from "./components/MainLayout.js"; +import Component from "./lib/Component.js"; + +export default class App extends Component { + mounted() { + this.renderChildren({ + header: { + selector: "#header", + Component: Header, + }, + mainLayout: { + selector: "#main-layout", + Component: MainLayout, + }, + }); + } + + template() { + return /* HTML */ ` + +
+
+
+ `; + } +} diff --git a/src/basic/components/Header.js b/src/basic/components/Header.js index a10fbaaa3..a6622a810 100644 --- a/src/basic/components/Header.js +++ b/src/basic/components/Header.js @@ -1,15 +1,19 @@ +import Component from "../lib/Component"; + /** * 헤더 컴포넌트 생성 함수 * @returns {HTMLElement} 생성된 헤더 요소 */ -export function Header() { - const header = document.createElement("div"); - header.className = "mb-8"; - header.innerHTML = ` -

🛒 Hanghae Online Store

-
Shopping Cart
-

🛍️ 0 items in cart

- `; - - return header; +export default class Header extends Component { + template() { + return /* HTML */ ` +
+

+ 🛒 Hanghae Online Store +

+
Shopping Cart
+

🛍️ 0 items in cart

+
+ `; + } } diff --git a/src/basic/components/MainLayout.js b/src/basic/components/MainLayout.js index 979ffdf1b..6fdc28052 100644 --- a/src/basic/components/MainLayout.js +++ b/src/basic/components/MainLayout.js @@ -1,84 +1,34 @@ +import Component from "../lib/Component"; +import OrderSummary from "./OrderSummary"; +import ProductSelector from "./ProductSelector"; + /** * 메인 레이아웃 컴포넌트 생성 함수 * @returns {Object} 생성된 요소들과 참조 */ -export function MainLayout() { - const select = document.createElement("select"); - select.id = "product-select"; - select.className = "w-full p-3 border border-gray-300 rounded-lg text-base mb-3"; - - const gridContainer = document.createElement("div"); - gridContainer.className = - "grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden"; - - const leftColumn = document.createElement("div"); - leftColumn["className"] = "bg-white border border-gray-200 p-8 overflow-y-auto"; - - const selectorContainer = document.createElement("div"); - selectorContainer.className = "mb-6 pb-6 border-b border-gray-200"; - - const addButton = document.createElement("button"); - addButton.id = "add-to-cart"; - addButton.innerHTML = "Add to Cart"; - addButton.className = - "w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all"; - - const stockInfo = document.createElement("div"); - stockInfo.id = "stock-status"; - stockInfo.className = "text-xs text-red-500 mt-3 whitespace-pre-line"; - - selectorContainer.appendChild(select); - selectorContainer.appendChild(addButton); - selectorContainer.appendChild(stockInfo); - - const cartDisplay = document.createElement("div"); - cartDisplay.id = "cart-items"; - - leftColumn.appendChild(selectorContainer); - leftColumn.appendChild(cartDisplay); - - const rightColumn = document.createElement("div"); - rightColumn.className = "bg-black text-white p-8 flex flex-col"; - rightColumn.innerHTML = ` -

Order Summary

-
-
-
-
-
-
- Total -
₩0
-
-
적립 포인트: 0p
-
- +export default class MainLayout extends Component { + mounted() { + this.renderChildren({ + productSelector: { + selector: "#leftColumn", + Component: ProductSelector, + }, + orderSummary: { + selector: "#rightColumn", + Component: OrderSummary, + }, + }); + } + + template() { + return /* HTML */ ` +
+ +
+ + +
-
- -

- Free shipping on all orders.
- Earn loyalty points with purchase. -

- `; - - gridContainer.appendChild(leftColumn); - gridContainer.appendChild(rightColumn); - - return { - gridContainer, - select, - addButton, - stockInfo, - cartDisplay, - leftColumn, - rightColumn, - }; + `; + } } diff --git a/src/basic/components/OrderSummary.js b/src/basic/components/OrderSummary.js new file mode 100644 index 000000000..88e2c0497 --- /dev/null +++ b/src/basic/components/OrderSummary.js @@ -0,0 +1,39 @@ +import Component from "../lib/Component"; + +export default class OrderSummary extends Component { + template() { + return /* HTML */ `
+

Order Summary

+
+
+
+
+
+
+ Total +
₩0
+
+
+ 적립 포인트: 0p +
+
+ +
+
+ +

+ Free shipping on all orders.
+ Earn loyalty points with purchase. +

+
`; + } +} diff --git a/src/basic/components/ProductSelector.js b/src/basic/components/ProductSelector.js new file mode 100644 index 000000000..e8adddf62 --- /dev/null +++ b/src/basic/components/ProductSelector.js @@ -0,0 +1,29 @@ +import Component from "../lib/Component"; + +export default class ProductSelector extends Component { + template() { + return /* HTML */ ` +
+
+ --> + + + + + + +
+
+
+
+ `; + } +} diff --git a/src/basic/lib/Component.js b/src/basic/lib/Component.js new file mode 100644 index 000000000..2774e43f5 --- /dev/null +++ b/src/basic/lib/Component.js @@ -0,0 +1,125 @@ +export default class Component { + $target; // 이 컴포넌트가 렌더링될 부모 DOM 요소 + props; // 부모로부터 받은 props (속성) + state; // 이 컴포넌트가 관리하는 상태 + + constructor($target, props) { + this.$target = $target; // 부모 DOM 요소 지정 + this.props = props; // props 지정 + this.state = {}; // 초기 상태 설정 + this.child = new Map(); // 하위 컴포넌트 저장 (중복 인스턴스 방지) + + this.setup(); // 초기 상태 설정 + this.render(); // 초기 렌더링 + } + + setup() { + // 🔷 초기 상태를 정의하거나 비동기 데이터 요청 등 초기화 작업 + } + + mounted() { + // 🔷 DOM이 렌더링된 후 실행할 로직 (ex: DOM 접근, 포커스 설정 등) + // 하위 컴포넌트 렌더링 + } + + /** + * 자식 컴포넌트들을 설정 객체로 한번에 렌더링하는 메서드 + * @param {Object} childrenConfig - 자식 컴포넌트 설정 객체 + */ + renderChildren(childrenConfig) { + Object.entries(childrenConfig).forEach(([childKey, config]) => { + const $container = document.querySelector(config.selector); + + if (!$container) { + console.warn(`Container not found: ${config.selector}`); + return; + } + + if (!this.child.get(childKey)) { + // 새로운 컴포넌트 인스턴스 생성 + const childInstance = new config.Component($container, config.props || {}); + this.addChild(childInstance, childKey); + } else { + // 기존 컴포넌트 인스턴스 재사용 + const childInstance = this.child.get(childKey); + childInstance.$target = $container; + childInstance.render(); + } + }); + } + + template() { + // 🔷 현재 상태와 props를 기반으로 HTML 문자열 반환 + // (렌더링할 UI를 정의) + return ""; + } + + unmount() { + // 🔷 컴포넌트가 언마운트될 때 실행할 로직 + // 하위 컴포넌트 정리 등 + + this.cleanup(); + this.child.forEach(child => child.cleanup?.()); + this.child.clear(); + this.$target.innerHTML = ""; + } + + cleanup() { + // 🔷 기존 이벤트/자원을 정리하는 훅 + // 하위 클래스에서 오버라이드 + } + + render() { + // 🔷 template()로부터 HTML 문자열을 받아 + // 부모 DOM에 렌더링하고, mounted()를 호출 + + // 🔷 기존 자원 정리 + // this.cleanup(); + + // 🔷 새로운 렌더링 시작 + + this.$target.innerHTML = this.template(); + + // 🔷 새 이벤트 바인딩 + this.setEvent(); + + // 🔷 후처리 + this.mounted?.(); + } + + setEvent() { + // 🔷 이벤트를 바인딩하는 메서드 (addEvent를 주로 활용) + } + + setState(newState) { + // 🔷 상태를 업데이트하고 render()를 호출해 화면을 갱신 + this.state = { ...this.state, ...newState }; + this.render(); + } + + getState() { + return this.state; + } + + addChild(childInstance, key) { + this.child.set(key, childInstance); + } + + removeChild(key) { + const child = this.child.get(key); + if (child?.cleanup) { + child.cleanup(); + } + this.child.delete(key); + } + + destroy() { + // 🔷 컴포넌트 완전 제거 + // 외부에서 강제로 unmount할 때 호출 + // 하위 컴포넌트 정리 및 부모 DOM 정리 + this.cleanup(); + this.child.forEach(child => child.cleanup?.()); + this.child.clear(); + this.$target.innerHTML = ""; + } +} diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 3cdb21f85..f510b22ed 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,14 +1,27 @@ +import App from "./App.js"; +import { createManualToggle } from "./components/ManualToggle.js"; +import { createTimerManager } from "./components/TimerManager.js"; +import { TUESDAY_DAY_OF_WEEK } from "./data/date.data.js"; import { + DISCOUNT_RATE_BULK, + DISCOUNT_RATE_LIGHTNING, DISCOUNT_RATE_PRODUCT_1, DISCOUNT_RATE_PRODUCT_2, DISCOUNT_RATE_PRODUCT_3, DISCOUNT_RATE_PRODUCT_4, DISCOUNT_RATE_PRODUCT_5, - DISCOUNT_RATE_BULK, - DISCOUNT_RATE_TUESDAY, - DISCOUNT_RATE_LIGHTNING, DISCOUNT_RATE_SUGGESTION, + DISCOUNT_RATE_TUESDAY, } from "./data/discount.data.js"; +import { + POINT_BONUS_FULL_SET, + POINT_BONUS_KEYBOARD_MOUSE_SET, + POINT_BONUS_QUANTITY_TIER1, + POINT_BONUS_QUANTITY_TIER2, + POINT_BONUS_QUANTITY_TIER3, + POINT_MULTIPLIER_TUESDAY, + POINT_RATE_BASE, +} from "./data/point.data.js"; import { PRODUCT_1, PRODUCT_2, @@ -18,34 +31,14 @@ import { PRODUCT_LIST, } from "./data/product.data.js"; import { - MIN_QUANTITY_FOR_DISCOUNT, + LOW_STOCK_THRESHOLD, MIN_QUANTITY_FOR_BULK_DISCOUNT, + MIN_QUANTITY_FOR_DISCOUNT, MIN_QUANTITY_FOR_POINT_BONUS_TIER1, MIN_QUANTITY_FOR_POINT_BONUS_TIER2, MIN_QUANTITY_FOR_POINT_BONUS_TIER3, - LOW_STOCK_THRESHOLD, TOTAL_STOCK_WARNING_THRESHOLD, } from "./data/quantity.data.js"; -import { - POINT_RATE_BASE, - POINT_BONUS_KEYBOARD_MOUSE_SET, - POINT_BONUS_FULL_SET, - POINT_BONUS_QUANTITY_TIER1, - POINT_BONUS_QUANTITY_TIER2, - POINT_BONUS_QUANTITY_TIER3, - POINT_MULTIPLIER_TUESDAY, -} from "./data/point.data.js"; -import { - LIGHTNING_SALE_MAX_DELAY, - LIGHTNING_SALE_INTERVAL, - SUGGESTION_SALE_INTERVAL, - SUGGESTION_SALE_MAX_DELAY, -} from "./data/time.data.js"; -import { TUESDAY_DAY_OF_WEEK } from "./data/date.data.js"; -import { Header } from "./components/Header.js"; -import { createManualToggle } from "./components/ManualToggle.js"; -import { MainLayout } from "./components/MainLayout.js"; -import { createTimerManager } from "./components/TimerManager.js"; // 상태 변수 let itemCounts; @@ -63,20 +56,19 @@ function main() { itemCounts = 0; lastSelect = null; + // 앱 진입점 const root = document.getElementById("app"); + new App(root); // 매뉴얼 토글 컴포넌트 생성 const { manualToggle, manualOverlay } = createManualToggle(); // 메인 레이아웃 컴포넌트 생성 - const mainLayoutComponent = MainLayout(); - select = mainLayoutComponent.select; - addButton = mainLayoutComponent.addButton; - stockInfo = mainLayoutComponent.stockInfo; - cartDisplay = mainLayoutComponent.cartDisplay; + select = document.querySelector("#product-select"); + addButton = document.querySelector("#add-to-cart"); + stockInfo = document.querySelector("#stock-status"); + cartDisplay = document.querySelector("#cart-items"); - root.appendChild(Header()); - root.appendChild(mainLayoutComponent.gridContainer); root.appendChild(manualToggle); root.appendChild(manualOverlay); From c74713a1224a2a478ae2961f1d0bce1b11798ba6 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Tue, 29 Jul 2025 02:59:44 +0900 Subject: [PATCH 16/55] =?UTF-8?q?refactor:=20=EC=8A=A4=EB=83=85=EC=83=B7?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snapshot.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/snapshot.html b/snapshot.html index a40551d82..5a1a5cc62 100644 --- a/snapshot.html +++ b/snapshot.html @@ -783,6 +783,7 @@ >
+

🛒 Hanghae Online Store @@ -790,9 +791,14 @@

Shopping Cart

🛍️ 0 items in cart

+ +
+
+
+ + > + ${PRODUCT_LIST.map(product => ProductSelectOption(product)).join("")} + + ${quantity} + +
+
+
+
+ ${cartItem.onSale || cartItem.suggestSale + ? '₩' + + cartItem.originalVal.toLocaleString() + + ' ₩' + + cartItem.val.toLocaleString() + + "" + : "₩" + cartItem.val.toLocaleString()} +
+ Remove +
+
`; +} diff --git a/src/basic/store/appStore.js b/src/basic/store/appStore.js new file mode 100644 index 000000000..96dba1a40 --- /dev/null +++ b/src/basic/store/appStore.js @@ -0,0 +1,13 @@ +import Store from "../lib/Store"; + +const appState = { + itemCounts: 0, + lastSelect: null, + totalAmount: 0, +}; + +const appStore = new Store(appState); + +export const resetAppStore = () => appStore.setState(appState); + +export default appStore; diff --git a/src/basic/store/cartStore.js b/src/basic/store/cartStore.js new file mode 100644 index 000000000..c97a8f2e2 --- /dev/null +++ b/src/basic/store/cartStore.js @@ -0,0 +1,19 @@ +import Store from "../lib/Store"; + +/** + * cartItem + * + * { + * product... + * quantity + * } + */ +const cartState = { + cartItems: [], + lastSelect: null, + selectedProductId: null, // 선택된 상품 ID 추가 +}; + +const cartStore = new Store(cartState); + +export default cartStore; From 33cce7b7d017e08ad61b30ca314e96976af5107b Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Wed, 30 Jul 2025 23:10:04 +0900 Subject: [PATCH 22/55] =?UTF-8?q?chore:=20advanced=20=EC=95=B1=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 8 +- eslint.config.js | 209 ++++++++- index.advanced.html | 2 +- package.json | 12 + pnpm-lock.yaml | 777 ++++++++++++++++++++++++++++++++- src/advanced/App.tsx | 32 ++ src/advanced/main.advanced.js | 0 src/advanced/main.advanced.tsx | 18 + tsconfig.json | 38 ++ vite.config.js | 15 + 10 files changed, 1098 insertions(+), 13 deletions(-) create mode 100644 src/advanced/App.tsx delete mode 100644 src/advanced/main.advanced.js create mode 100644 src/advanced/main.advanced.tsx create mode 100644 tsconfig.json diff --git a/.prettierrc b/.prettierrc index 3aa054bef..5fd65ffa4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "semi": true, "trailingComma": "es5", - "singleQuote": false, + "singleQuote": true, "printWidth": 100, "tabWidth": 2, "useTabs": false, @@ -12,5 +12,9 @@ "endOfLine": "lf", "htmlWhitespaceSensitivity": "css", "vueIndentScriptAndStyle": false, - "embeddedLanguageFormatting": "auto" + "embeddedLanguageFormatting": "auto", + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrder": ["^react$", "^react-", "^@/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true } diff --git a/eslint.config.js b/eslint.config.js index a1f2e2c4b..5916bb02f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,9 @@ import js from "@eslint/js"; -import prettier from "eslint-plugin-prettier"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsparser from "@typescript-eslint/parser"; import prettierConfig from "eslint-config-prettier"; import importPlugin from "eslint-plugin-import"; +import prettier from "eslint-plugin-prettier"; export default [ js.configs.recommended, @@ -130,7 +132,11 @@ export default [ "error", { groups: ["builtin", "external", "internal", "parent", "sibling", "index"], - "newlines-between": "never", + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, }, ], @@ -175,7 +181,204 @@ export default [ }, }, { - files: ["**/*.test.js"], + files: ["**/*.ts", "**/*.tsx"], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + console: "readonly", + document: "readonly", + window: "readonly", + alert: "readonly", + setTimeout: "readonly", + setInterval: "readonly", + Math: "readonly", + Date: "readonly", + parseInt: "readonly", + location: "readonly", + }, + }, + plugins: { + prettier, + import: importPlugin, + "@typescript-eslint": tseslint, + }, + rules: { + // Prettier 규칙 + "prettier/prettier": "error", + + // TypeScript 관련 + "@typescript-eslint/no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + ignoreRestSiblings: true, + argsIgnorePattern: "^_", + }, + ], + + // Import/Export 관련 - 내부/외부 라이브러리 분리 및 알파벳 순 정렬 + "import/no-unresolved": "off", + "import/prefer-default-export": "off", + "import/no-default-export": "off", + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type", + ], + pathGroups: [ + { + pattern: "react", + group: "external", + position: "before", + }, + { + pattern: "react-*", + group: "external", + position: "after", + }, + { + pattern: "@/**", + group: "internal", + position: "after", + }, + { + pattern: "@/lib/**", + group: "internal", + position: "after", + }, + { + pattern: "@/components/**", + group: "internal", + position: "after", + }, + { + pattern: "@/store/**", + group: "internal", + position: "after", + }, + { + pattern: "@/types/**", + group: "internal", + position: "after", + }, + { + pattern: "@/data/**", + group: "internal", + position: "after", + }, + { + pattern: "@/utils/**", + group: "internal", + position: "after", + }, + { + pattern: "@/hooks/**", + group: "internal", + position: "after", + }, + ], + pathGroupsExcludedImportTypes: ["react"], + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + + // 기타 규칙들 + "no-var": "error", + "prefer-const": "error", + "no-redeclare": "error", + "no-shadow": "error", + "no-undef": "error", + "no-console": "warn", + "no-alert": "error", + "no-debugger": "error", + "no-empty": "error", + "no-eval": "error", + "no-implied-eval": "error", + "no-new-func": "error", + "no-script-url": "error", + "object-shorthand": ["error", "always"], + "prefer-template": "error", + "template-curly-spacing": "error", + "no-useless-concat": "error", + curly: ["error", "all"], + "no-else-return": ["error", { allowElseIf: false }], + "no-lonely-if": "error", + "no-unneeded-ternary": ["error", { defaultAssignment: false }], + "no-nested-ternary": "error", + "space-before-function-paren": [ + "error", + { + anonymous: "always", + named: "never", + asyncArrow: "always", + }, + ], + "keyword-spacing": [ + "error", + { + before: true, + after: true, + }, + ], + "max-len": [ + "warn", + { + code: 120, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreComments: true, + }, + ], + "max-lines-per-function": ["warn", { max: 80 }], + complexity: ["warn", { max: 12 }], + "max-depth": ["warn", { max: 4 }], + "max-params": ["warn", { max: 4 }], + "consistent-return": "error", + "default-case": "error", + "no-case-declarations": "error", + "no-fallthrough": "error", + "no-param-reassign": ["error", { props: false }], + "no-return-assign": ["error", "always"], + "no-sequences": "error", + "no-throw-literal": "error", + "no-unused-expressions": [ + "error", + { + allowShortCircuit: false, + allowTernary: false, + allowTaggedTemplates: false, + }, + ], + "no-useless-call": "error", + "no-useless-return": "error", + "prefer-promise-reject-errors": ["error", { allowEmptyReject: true }], + radix: "error", + yoda: "error", + }, + }, + { + files: ["**/*.test.js", "**/*.test.ts", "**/*.test.tsx"], languageOptions: { globals: { describe: "readonly", diff --git a/index.advanced.html b/index.advanced.html index 4af4b87d6..cac631a9e 100644 --- a/index.advanced.html +++ b/index.advanced.html @@ -35,6 +35,6 @@
- + diff --git a/package.json b/package.json index e790b7ad3..01127ed37 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,12 @@ "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "@vitejs/plugin-react": "^4.7.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", "eslint-config-airbnb-base": "^15.0.0", @@ -36,6 +42,7 @@ "jsdom": "^26.1.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", + "typescript": "^5.8.3", "vite": "^7.0.5", "vitest": "^3.2.4" }, @@ -47,5 +54,10 @@ "*.{json,md}": [ "prettier --write" ] + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zustand": "^5.0.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60abef0cb..f8fe5b6da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,16 @@ settings: importers: .: + dependencies: + react: + specifier: ^19.1.1 + version: 19.1.1 + react-dom: + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + zustand: + specifier: ^5.0.6 + version: 5.0.6(@types/react@19.1.9)(react@19.1.1) devDependencies: '@eslint/js': specifier: ^9.32.0 @@ -17,6 +27,24 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) + '@trivago/prettier-plugin-sort-imports': + specifier: ^5.2.2 + version: 5.2.2(prettier@3.6.2) + '@types/react': + specifier: ^19.1.9 + version: 19.1.9 + '@types/react-dom': + specifier: ^19.1.7 + version: 19.1.7(@types/react@19.1.9) + '@typescript-eslint/eslint-plugin': + specifier: ^8.38.0 + version: 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.38.0 + version: 8.38.0(eslint@9.32.0)(typescript@5.8.3) + '@vitejs/plugin-react': + specifier: ^4.7.0 + version: 4.7.0(vite@7.0.5(yaml@2.8.0)) '@vitest/ui': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) @@ -25,13 +53,13 @@ importers: version: 9.32.0 eslint-config-airbnb-base: specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.32.0(eslint@9.32.0))(eslint@9.32.0) + version: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0))(eslint@9.32.0) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.32.0) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(eslint@9.32.0) + version: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0) eslint-plugin-prettier: specifier: ^5.5.3 version: 5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2) @@ -50,6 +78,9 @@ importers: prettier: specifier: ^3.6.2 version: 3.6.2 + typescript: + specifier: ^5.8.3 + version: 5.8.3 vite: specifier: ^7.0.5 version: 7.0.5(yaml@2.8.0) @@ -62,6 +93,10 @@ packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -69,14 +104,89 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.2': + resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.27.6': resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -319,9 +429,31 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -329,6 +461,9 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.45.1': resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} cpu: [arm] @@ -446,9 +581,37 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@trivago/prettier-plugin-sort-imports@5.2.2': + resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} + engines: {node: '>18.12'} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -464,6 +627,79 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/react-dom@19.1.7': + resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.9': + resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} + + '@typescript-eslint/eslint-plugin@8.38.0': + resolution: {integrity: sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.38.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.38.0': + resolution: {integrity: sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.38.0': + resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.38.0': + resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.38.0': + resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/type-utils@8.38.0': + resolution: {integrity: sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.38.0': + resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.38.0': + resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.38.0': + resolution: {integrity: sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.38.0': + resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -591,10 +827,18 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -615,6 +859,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + caniuse-lite@1.0.30001731: + resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} + chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} @@ -663,6 +910,9 @@ packages: confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -674,6 +924,9 @@ packages: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -743,6 +996,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + electron-to-chromium@1.5.192: + resolution: {integrity: sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -790,6 +1046,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -913,12 +1173,19 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.4.6: resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: @@ -968,6 +1235,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} @@ -984,6 +1255,10 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1004,6 +1279,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1056,6 +1334,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1189,6 +1471,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1208,6 +1493,11 @@ packages: canvas: optional: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1221,6 +1511,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1261,6 +1556,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -1272,6 +1570,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1287,6 +1589,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1309,6 +1615,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} @@ -1430,9 +1739,25 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + peerDependencies: + react: ^19.1.1 + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -1458,6 +1783,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -1469,6 +1798,9 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -1488,10 +1820,18 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -1660,6 +2000,12 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -1683,10 +2029,21 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1836,6 +2193,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} @@ -1845,10 +2205,33 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.6: + resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@adobe/css-tools@4.4.3': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -1863,10 +2246,114 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.28.2 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.2': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.2 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.27.6': {} + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -2022,12 +2509,38 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + '@pkgr/core@0.2.9': {} '@polka/url@1.0.0-next.29': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.45.1': optional: true @@ -2115,8 +2628,41 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.6.2)': + dependencies: + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.2 + javascript-natural-sort: 0.7.1 + lodash: 4.17.21 + prettier: 3.6.2 + transitivePeerDependencies: + - supports-color + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.2 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.2 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -2129,6 +2675,119 @@ snapshots: '@types/json5@0.0.29': {} + '@types/react-dom@19.1.7(@types/react@19.1.9)': + dependencies: + '@types/react': 19.1.9 + + '@types/react@19.1.9': + dependencies: + csstype: 3.1.3 + + '@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.38.0(eslint@9.32.0)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/type-utils': 8.38.0(eslint@9.32.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.38.0 + eslint: 9.32.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1 + eslint: 9.32.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.38.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) + '@typescript-eslint/types': 8.38.0 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.38.0': + dependencies: + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 + + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.38.0(eslint@9.32.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.32.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.38.0': {} + + '@typescript-eslint/typescript-estree@8.38.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.38.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.38.0(eslint@9.32.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0) + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + eslint: 9.32.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.38.0': + dependencies: + '@typescript-eslint/types': 8.38.0 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@4.7.0(vite@7.0.5(yaml@2.8.0))': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.0.5(yaml@2.8.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -2286,10 +2945,21 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001731 + electron-to-chromium: 1.5.192 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -2311,6 +2981,8 @@ snapshots: callsites@3.1.0: {} + caniuse-lite@1.0.30001731: {} + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -2356,6 +3028,8 @@ snapshots: confusing-browser-globals@1.0.11: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2369,6 +3043,8 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 + csstype@3.1.3: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -2434,6 +3110,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + electron-to-chromium@1.5.192: {} + emoji-regex@10.4.0: {} entities@6.0.1: {} @@ -2553,13 +3231,15 @@ snapshots: '@esbuild/win32-ia32': 0.25.8 '@esbuild/win32-x64': 0.25.8 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(eslint@9.32.0))(eslint@9.32.0): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0))(eslint@9.32.0): dependencies: confusing-browser-globals: 1.0.11 eslint: 9.32.0 - eslint-plugin-import: 2.32.0(eslint@9.32.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0) object.assign: 4.1.7 object.entries: 1.1.9 semver: 6.3.1 @@ -2576,16 +3256,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.32.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.32.0): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.38.0(eslint@9.32.0)(typescript@5.8.3) eslint: 9.32.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(eslint@9.32.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint@9.32.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -2596,7 +3277,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.32.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.32.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -2607,6 +3288,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.38.0(eslint@9.32.0)(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -2700,10 +3383,22 @@ snapshots: fast-diff@1.3.0: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2750,6 +3445,8 @@ snapshots: functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} + get-east-asian-width@1.3.0: {} get-intrinsic@1.3.0: @@ -2776,6 +3473,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2791,6 +3492,8 @@ snapshots: gopd@1.2.0: {} + graphemer@1.4.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -2839,6 +3542,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -2973,6 +3678,8 @@ snapshots: isexe@2.0.0: {} + javascript-natural-sort@0.7.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -3008,6 +3715,8 @@ snapshots: - supports-color - utf-8-validate + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -3018,6 +3727,8 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3073,6 +3784,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lz-string@1.5.0: {} magic-string@0.30.17: @@ -3081,6 +3796,8 @@ snapshots: math-intrinsics@1.1.0: {} + merge2@1.4.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3094,6 +3811,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + minimist@1.2.8: {} mrmime@2.0.1: {} @@ -3106,6 +3827,8 @@ snapshots: natural-compare@1.4.0: {} + node-releases@2.0.19: {} + nwsapi@2.2.20: {} object-inspect@1.13.4: {} @@ -3225,8 +3948,19 @@ snapshots: punycode@2.3.1: {} + queue-microtask@1.2.3: {} + + react-dom@19.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + scheduler: 0.26.0 + react-is@17.0.2: {} + react-refresh@0.17.0: {} + + react@19.1.1: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -3265,6 +3999,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} rollup@4.45.1: @@ -3295,6 +4031,10 @@ snapshots: rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -3320,8 +4060,12 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.26.0: {} + semver@6.3.1: {} + semver@7.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -3503,6 +4247,10 @@ snapshots: dependencies: punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -3547,6 +4295,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript@5.8.3: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -3554,6 +4304,12 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -3714,6 +4470,13 @@ snapshots: xmlchars@2.2.0: {} + yallist@3.1.1: {} + yaml@2.8.0: {} yocto-queue@0.1.0: {} + + zustand@5.0.6(@types/react@19.1.9)(react@19.1.1): + optionalDependencies: + '@types/react': 19.1.9 + react: 19.1.1 diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx new file mode 100644 index 000000000..d38b7f468 --- /dev/null +++ b/src/advanced/App.tsx @@ -0,0 +1,32 @@ +import { ReactElement } from 'react'; + +import Cart from '@/components/Cart'; +import Header from '@/components/Header'; +import OrderSummary from '@/components/OrderSummary'; +import ProductSelector from '@/components/ProductSelector'; +import { useCartStore } from '@/store/cartStore'; +import { useProductStore } from '@/store/productStore'; + +function App(): ReactElement { + const { cartItems, addToCart, updateQuantity, removeFromCart } = useCartStore(); + const { products } = useProductStore(); + + return ( +
+
+ +
+
+ +
+ +
+ + +
+
+
+ ); +} + +export default App; diff --git a/src/advanced/main.advanced.js b/src/advanced/main.advanced.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/advanced/main.advanced.tsx b/src/advanced/main.advanced.tsx new file mode 100644 index 000000000..a0dd1ea72 --- /dev/null +++ b/src/advanced/main.advanced.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { createRoot } from 'react-dom/client'; + +import App from '@/advanced/App'; + +const container = document.getElementById('app'); +if (!container) { + throw new Error('App container not found'); +} + +const root = createRoot(container); + +root.render( + + + +); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..e9201bda8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@/advanced/*": ["src/advanced/*"], + "@/components/*": ["src/advanced/components/*"], + "@/store/*": ["src/advanced/store/*"], + "@/types/*": ["src/advanced/types/*"], + "@/data/*": ["src/advanced/data/*"], + "@/utils/*": ["src/advanced/utils/*"], + "@/hooks/*": ["src/advanced/hooks/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/vite.config.js b/vite.config.js index 496383c53..4ca29640a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,21 @@ import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import path from "path"; export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + "@/advanced": path.resolve(__dirname, "./src/advanced"), + "@/components": path.resolve(__dirname, "./src/advanced/components"), + "@/store": path.resolve(__dirname, "./src/advanced/store"), + "@/types": path.resolve(__dirname, "./src/advanced/types"), + "@/data": path.resolve(__dirname, "./src/advanced/data"), + "@/utils": path.resolve(__dirname, "./src/advanced/utils"), + "@/hooks": path.resolve(__dirname, "./src/advanced/hooks"), + }, + }, test: { globals: true, environment: "jsdom", From a48684898342fec7a6fd7a419edc1b42d746fbd4 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Wed, 30 Jul 2025 23:59:50 +0900 Subject: [PATCH 23/55] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20UI=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snapshot.html | 41 +- snapshot2.html | 1201 +++++++++++++++++ src/advanced/App.tsx | 41 +- src/advanced/components/cart/CartItem.tsx | 5 + src/advanced/components/cart/CartItems.tsx | 7 + src/advanced/components/layout/Divider.tsx | 5 + src/advanced/components/layout/Header.tsx | 24 + .../components/layout/HelpOverlay.tsx | 94 ++ src/advanced/components/layout/HelpToggle.tsx | 16 + src/advanced/components/layout/MainLayout.tsx | 9 + .../components/layout/SectionLayout.tsx | 5 + .../components/order/OrderSummary.tsx | 25 + .../components/order/SummaryDetail.tsx | 29 + src/advanced/components/product/AddButton.tsx | 9 + .../components/product/ProductSelect.tsx | 22 + .../components/product/StockStatus.tsx | 5 + src/advanced/data/product.data.ts | 55 + src/advanced/hooks/useOrderSummary.ts | 9 + src/advanced/types/index.ts | 45 + src/advanced/utils/cart.util.ts | 5 + src/advanced/utils/format.util.ts | 6 + 21 files changed, 1617 insertions(+), 41 deletions(-) create mode 100644 snapshot2.html create mode 100644 src/advanced/components/cart/CartItem.tsx create mode 100644 src/advanced/components/cart/CartItems.tsx create mode 100644 src/advanced/components/layout/Divider.tsx create mode 100644 src/advanced/components/layout/Header.tsx create mode 100644 src/advanced/components/layout/HelpOverlay.tsx create mode 100644 src/advanced/components/layout/HelpToggle.tsx create mode 100644 src/advanced/components/layout/MainLayout.tsx create mode 100644 src/advanced/components/layout/SectionLayout.tsx create mode 100644 src/advanced/components/order/OrderSummary.tsx create mode 100644 src/advanced/components/order/SummaryDetail.tsx create mode 100644 src/advanced/components/product/AddButton.tsx create mode 100644 src/advanced/components/product/ProductSelect.tsx create mode 100644 src/advanced/components/product/StockStatus.tsx create mode 100644 src/advanced/data/product.data.ts create mode 100644 src/advanced/hooks/useOrderSummary.ts create mode 100644 src/advanced/types/index.ts create mode 100644 src/advanced/utils/cart.util.ts create mode 100644 src/advanced/utils/format.util.ts diff --git a/snapshot.html b/snapshot.html index 5a1a5cc62..5b24106e6 100644 --- a/snapshot.html +++ b/snapshot.html @@ -13,20 +13,20 @@ extend: { fontFamily: { sans: [ - "-apple-system", - "BlinkMacSystemFont", - "Helvetica Neue", - "Arial", - "sans-serif", + '-apple-system', + 'BlinkMacSystemFont', + 'Helvetica Neue', + 'Arial', + 'sans-serif', ], }, letterSpacing: { - "extra-wide": "0.3em", - "super-wide": "0.25em", + 'extra-wide': '0.3em', + 'super-wide': '0.25em', }, - fontSize: { "2xs": "0.625rem" }, - maxHeight: { 800: "800px" }, - backgroundImage: { "gradient-black": "linear-gradient(135deg, #000 0%, #333 100%)" }, + fontSize: { '2xs': '0.625rem' }, + maxHeight: { 800: '800px' }, + backgroundImage: { 'gradient-black': 'linear-gradient(135deg, #000 0%, #333 100%)' }, }, }, }; @@ -150,7 +150,7 @@ } ::after, ::before { - --tw-content: ""; + --tw-content: ''; } :host, html { @@ -203,7 +203,7 @@ pre, samp { font-family: - ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-feature-settings: normal; font-variation-settings: normal; @@ -251,9 +251,9 @@ text-transform: none; } button, - input:where([type="button"]), - input:where([type="reset"]), - input:where([type="submit"]) { + input:where([type='button']), + input:where([type='reset']), + input:where([type='submit']) { -webkit-appearance: button; background-color: transparent; background-image: none; @@ -271,7 +271,7 @@ ::-webkit-outer-spin-button { height: auto; } - [type="search"] { + [type='search'] { -webkit-appearance: textfield; outline-offset: -2px; } @@ -325,7 +325,7 @@ opacity: 1; color: #9ca3af; } - [role="button"], + [role='button'], button { cursor: pointer; } @@ -348,7 +348,7 @@ max-width: 100%; height: auto; } - [hidden]:where(:not([hidden="until-found"])) { + [hidden]:where(:not([hidden='until-found'])) { display: none; } .fixed { @@ -809,8 +809,9 @@

- +
+ 에러 방지 노트북 파우치: 품절 코딩할 때 듣는 Lo-Fi 스피커: 재고 부족 (3개 남음) +
+

+
+
+
+
+
+
+

+ 코딩할 때 듣는 Lo-Fi 스피커 +

+

PRODUCT

+

₩25,000

+
+ + 7 + +
+
+
+
+ ₩25,000 +
+ Remove +
+
+
+
+
+
+
+

생산성 폭발 마우스

+

PRODUCT

+

₩20,000

+
+ + 4 + +
+
+
+
+ ₩20,000 +
+ Remove +
+
+
+
+ + +
+ +

Order Summary

+ + +
+ +
+
+ 코딩할 때 듣는 Lo-Fi 스피커 x 7 + ₩175,000 +
+ +
+ 생산성 폭발 마우스 x 4 + ₩80,000 +
+ +
+ +
+ Subtotal + ₩255,000 +
+ +
+ Shipping + Free +
+
+ + +
+ +
+ + +
+
+ Total +
₩255,000
+
+
+
적립 포인트: 275p
+
기본: 255p, 대량구매(10개+) +20p
+
+
+ +
+
+ + +

+ Free shipping on all orders.
+ Earn loyalty points with purchase. +

+
+
+ + +
+ + + diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx index d38b7f468..b77dcce9a 100644 --- a/src/advanced/App.tsx +++ b/src/advanced/App.tsx @@ -1,31 +1,30 @@ -import { ReactElement } from 'react'; +import React, { ReactElement } from 'react'; -import Cart from '@/components/Cart'; -import Header from '@/components/Header'; -import OrderSummary from '@/components/OrderSummary'; -import ProductSelector from '@/components/ProductSelector'; -import { useCartStore } from '@/store/cartStore'; -import { useProductStore } from '@/store/productStore'; +import CartItems from '@/advanced/components/cart/CartItems'; +import Header from '@/advanced/components/layout/Header'; +import HelpOverlay from '@/advanced/components/layout/HelpOverlay'; +import HelpToggle from '@/advanced/components/layout/HelpToggle'; +import MainLayout from '@/advanced/components/layout/MainLayout'; +import SectionLayout from '@/advanced/components/layout/SectionLayout'; +import OrderSummary from '@/advanced/components/order/OrderSummary'; +import ProductSelect from '@/advanced/components/product/ProductSelect'; function App(): ReactElement { - const { cartItems, addToCart, updateQuantity, removeFromCart } = useCartStore(); - const { products } = useProductStore(); - return ( -
+
-
-
- -
+ + + + + + + -
- - -
-
-
+ + + ); } diff --git a/src/advanced/components/cart/CartItem.tsx b/src/advanced/components/cart/CartItem.tsx new file mode 100644 index 000000000..06fe61207 --- /dev/null +++ b/src/advanced/components/cart/CartItem.tsx @@ -0,0 +1,5 @@ +import { ReactElement } from 'react'; + +export default function CartItem(): ReactElement { + return
CartItem
; +} diff --git a/src/advanced/components/cart/CartItems.tsx b/src/advanced/components/cart/CartItems.tsx new file mode 100644 index 000000000..0d5eb7a93 --- /dev/null +++ b/src/advanced/components/cart/CartItems.tsx @@ -0,0 +1,7 @@ +import { ReactElement } from 'react'; + +export default function CartItems(): ReactElement { + // const { cartItems } = useCartStore(); + + return
; +} diff --git a/src/advanced/components/layout/Divider.tsx b/src/advanced/components/layout/Divider.tsx new file mode 100644 index 000000000..e8937afb8 --- /dev/null +++ b/src/advanced/components/layout/Divider.tsx @@ -0,0 +1,5 @@ +import { ReactElement } from 'react'; + +export default function Divider(): ReactElement { + return
; +} diff --git a/src/advanced/components/layout/Header.tsx b/src/advanced/components/layout/Header.tsx new file mode 100644 index 000000000..2eaac4b92 --- /dev/null +++ b/src/advanced/components/layout/Header.tsx @@ -0,0 +1,24 @@ +import { ReactElement } from 'react'; + +import { useCartStore } from '@/advanced/store/cartStore'; +import { getCartTotalCount } from '@/advanced/utils/cart.util'; + +function Header(): ReactElement { + const { cartItems } = useCartStore(); + + const totalCount = getCartTotalCount(cartItems); + + return ( +
+

+ 🛒 Hanghae Online Store +

+
Shopping Cart
+

+ 🛍️ {totalCount} items in cart +

+
+ ); +} + +export default Header; diff --git a/src/advanced/components/layout/HelpOverlay.tsx b/src/advanced/components/layout/HelpOverlay.tsx new file mode 100644 index 000000000..c7e2ac64f --- /dev/null +++ b/src/advanced/components/layout/HelpOverlay.tsx @@ -0,0 +1,94 @@ +import { ReactElement } from 'react'; + +export default function HelpOverlay(): ReactElement { + return ( + <> + {/* Backdrop */} +
+ + {/* Overlay Content */} +
+ {/* Close Button */} + + +

📖 이용 안내

+ + {/* 할인 정책 섹션 */} +
+

💰 할인 정책

+
+
+

개별 상품

+

+ • 키보드 10개↑: 10% +
+ • 마우스 10개↑: 15% +
+ • 모니터암 10개↑: 20% +
• 스피커 10개↑: 25% +

+
+ +
+

전체 수량

+

• 30개 이상: 25%

+
+ +
+

특별 할인

+

+ • 화요일: +10% +
+ • ⚡번개세일: 20% +
• 💝추천할인: 5% +

+
+
+
+ + {/* 포인트 적립 섹션 */} +
+

🎁 포인트 적립

+
+
+

기본

+

• 구매액의 0.1%

+
+ +
+

추가

+

+ • 화요일: 2배 +
+ • 키보드+마우스: +50p +
+ • 풀세트: +100p +
• 10개↑: +20p / 20개↑: +50p / 30개↑: +100p +

+
+
+
+ + {/* 팁 섹션 */} +
+

💡 TIP

+

+ • 화요일 대량구매 = MAX 혜택 +
+ • ⚡+💝 중복 가능 +
• 상품4 = 품절 +

+
+
+ + ); +} diff --git a/src/advanced/components/layout/HelpToggle.tsx b/src/advanced/components/layout/HelpToggle.tsx new file mode 100644 index 000000000..6375e2a56 --- /dev/null +++ b/src/advanced/components/layout/HelpToggle.tsx @@ -0,0 +1,16 @@ +import { ReactElement } from 'react'; + +export default function HelpToggle(): ReactElement { + return ( + + ); +} diff --git a/src/advanced/components/layout/MainLayout.tsx b/src/advanced/components/layout/MainLayout.tsx new file mode 100644 index 000000000..73f11d1d6 --- /dev/null +++ b/src/advanced/components/layout/MainLayout.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren, ReactElement } from 'react'; + +export default function MainLayout({ children }: PropsWithChildren): ReactElement { + return ( +
+ {children} +
+ ); +} diff --git a/src/advanced/components/layout/SectionLayout.tsx b/src/advanced/components/layout/SectionLayout.tsx new file mode 100644 index 000000000..ec4724c53 --- /dev/null +++ b/src/advanced/components/layout/SectionLayout.tsx @@ -0,0 +1,5 @@ +import { PropsWithChildren, ReactElement } from 'react'; + +export default function SectionLayout({ children }: PropsWithChildren): ReactElement { + return
{children}
; +} diff --git a/src/advanced/components/order/OrderSummary.tsx b/src/advanced/components/order/OrderSummary.tsx new file mode 100644 index 000000000..703ec9d85 --- /dev/null +++ b/src/advanced/components/order/OrderSummary.tsx @@ -0,0 +1,25 @@ +import { ReactElement } from 'react'; + +import SummaryDetail from '@/advanced/components/order/SummaryDetail'; + +export default function OrderSummary(): ReactElement { + return ( +
+

Order Summary

+ +
+ +
+ + + +

+ Free shipping on all orders. +
+ Earn loyalty points with purchase. +

+
+ ); +} diff --git a/src/advanced/components/order/SummaryDetail.tsx b/src/advanced/components/order/SummaryDetail.tsx new file mode 100644 index 000000000..b85a2f3fc --- /dev/null +++ b/src/advanced/components/order/SummaryDetail.tsx @@ -0,0 +1,29 @@ +import { ReactElement } from 'react'; + +import Divider from '@/advanced/components/layout/Divider'; +import useOrderSummary from '@/advanced/hooks/useOrderSummary'; +import formatPrice from '@/advanced/utils/format.util'; + +export default function SummaryDetail(): ReactElement { + const { subTotal } = useOrderSummary(); + + const formattedSubTotal = formatPrice(subTotal); + + return ( +
+ {/* 주문 요약 내용 */} + + + +
+ Subtotal + {formattedSubTotal} +
+ +
+ Shipping + Free +
+
+ ); +} diff --git a/src/advanced/components/product/AddButton.tsx b/src/advanced/components/product/AddButton.tsx new file mode 100644 index 000000000..dc2869d1e --- /dev/null +++ b/src/advanced/components/product/AddButton.tsx @@ -0,0 +1,9 @@ +import { ReactElement } from 'react'; + +export default function AddButton(): ReactElement { + return ( + + ); +} diff --git a/src/advanced/components/product/ProductSelect.tsx b/src/advanced/components/product/ProductSelect.tsx new file mode 100644 index 000000000..cef72546a --- /dev/null +++ b/src/advanced/components/product/ProductSelect.tsx @@ -0,0 +1,22 @@ +import { ReactElement } from 'react'; + +import AddButton from '@/advanced/components/product/AddButton'; +import StockStatus from '@/advanced/components/product/StockStatus'; +import { PRODUCT_LIST } from '@/advanced/data/product.data'; + +export default function ProductSelect(): ReactElement { + return ( +
+ + + +
+ ); +} diff --git a/src/advanced/components/product/StockStatus.tsx b/src/advanced/components/product/StockStatus.tsx new file mode 100644 index 000000000..5df9fe8f9 --- /dev/null +++ b/src/advanced/components/product/StockStatus.tsx @@ -0,0 +1,5 @@ +import { ReactElement } from 'react'; + +export default function StockStatus(): ReactElement { + return
; +} diff --git a/src/advanced/data/product.data.ts b/src/advanced/data/product.data.ts new file mode 100644 index 000000000..f948dda00 --- /dev/null +++ b/src/advanced/data/product.data.ts @@ -0,0 +1,55 @@ +import { Product } from '../types'; + +export const PRODUCT_1 = 'p1'; +export const PRODUCT_2 = 'p2'; +export const PRODUCT_3 = 'p3'; +export const PRODUCT_4 = 'p4'; +export const PRODUCT_5 = 'p5'; + +export const PRODUCT_LIST: Product[] = [ + { + id: 'p1', + name: '버그 없애는 키보드', + val: 10000, + originalVal: 10000, + q: 50, + onSale: false, + suggestSale: false, + }, + { + id: 'p2', + name: '생산성 폭발 마우스', + val: 20000, + originalVal: 20000, + q: 30, + onSale: false, + suggestSale: false, + }, + { + id: 'p3', + name: '거북목 탈출 모니터암', + val: 30000, + originalVal: 30000, + q: 20, + onSale: false, + suggestSale: false, + }, + { + id: 'p4', + name: '에러 방지 노트북 파우치', + val: 15000, + originalVal: 15000, + q: 0, + onSale: false, + suggestSale: false, + }, + { + id: 'p5', + name: '코딩할 때 듣는 Lo-Fi 스피커', + val: 25000, + originalVal: 25000, + q: 10, + onSale: false, + suggestSale: false, + }, +]; diff --git a/src/advanced/hooks/useOrderSummary.ts b/src/advanced/hooks/useOrderSummary.ts new file mode 100644 index 000000000..a20c91dc9 --- /dev/null +++ b/src/advanced/hooks/useOrderSummary.ts @@ -0,0 +1,9 @@ +import { useCartStore } from '@/advanced/store/cartStore'; + +export default function useOrderSummary() { + const { cartItems } = useCartStore(); + + const subTotal = cartItems.reduce((acc, item) => acc + item.price, 0); + + return { subTotal }; +} diff --git a/src/advanced/types/index.ts b/src/advanced/types/index.ts new file mode 100644 index 000000000..abff0e063 --- /dev/null +++ b/src/advanced/types/index.ts @@ -0,0 +1,45 @@ +// Product 관련 타입 +export interface Product { + id: string; + name: string; + val: number; + originalVal: number; + q: number; + onSale: boolean; + suggestSale: boolean; +} + +// Cart Item 타입 +export interface CartItem { + id: string; + name: string; + price: number; + originalPrice: number; + quantity: number; + onSale: boolean; + suggestSale: boolean; +} + +// 할인 관련 타입 +export interface DiscountInfo { + name: string; + discount: number; +} + +// 포인트 관련 타입 +export interface PointInfo { + basePoints: number; + finalPoints: number; + details: string[]; +} + +// 주문 요약 타입 +export interface OrderSummary { + subtotal: number; + totalAmount: number; + itemCount: number; + discountRate: number; + savedAmount: number; + itemDiscounts: DiscountInfo[]; + points: PointInfo; +} diff --git a/src/advanced/utils/cart.util.ts b/src/advanced/utils/cart.util.ts new file mode 100644 index 000000000..e45c6fd09 --- /dev/null +++ b/src/advanced/utils/cart.util.ts @@ -0,0 +1,5 @@ +import { CartItem } from "@/advanced/types"; + +export function getCartTotalCount(cartItems: CartItem[]): number { + return cartItems.reduce((acc, item) => acc + item.quantity, 0); +} diff --git a/src/advanced/utils/format.util.ts b/src/advanced/utils/format.util.ts new file mode 100644 index 000000000..dc7cf691c --- /dev/null +++ b/src/advanced/utils/format.util.ts @@ -0,0 +1,6 @@ +export default function formatPrice(price: number): string { + return price.toLocaleString('ko-KR', { + style: 'currency', + currency: 'KRW', + }); +} From 11f87dd721e99d09a027d8df33c979c523be5eee Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Thu, 31 Jul 2025 00:07:32 +0900 Subject: [PATCH 24/55] =?UTF-8?q?feat:=20=ED=86=A0=EA=B8=80=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=A0=88=EC=9D=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/advanced/App.tsx | 5 ++++- src/advanced/components/layout/Header.tsx | 9 ++++----- src/advanced/components/layout/HelpToggle.tsx | 11 ++++++++++- src/advanced/hooks/useOrderSummary.ts | 8 ++++---- src/advanced/store/layoutStore.ts | 17 +++++++++++++++++ 5 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 src/advanced/store/layoutStore.ts diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx index b77dcce9a..4093f4485 100644 --- a/src/advanced/App.tsx +++ b/src/advanced/App.tsx @@ -8,8 +8,11 @@ import MainLayout from '@/advanced/components/layout/MainLayout'; import SectionLayout from '@/advanced/components/layout/SectionLayout'; import OrderSummary from '@/advanced/components/order/OrderSummary'; import ProductSelect from '@/advanced/components/product/ProductSelect'; +import { useLayoutStore } from '@/advanced/store/layoutStore'; function App(): ReactElement { + const { showHelpOverlay } = useLayoutStore(); + return (
@@ -22,8 +25,8 @@ function App(): ReactElement { + {showHelpOverlay && } - ); } diff --git a/src/advanced/components/layout/Header.tsx b/src/advanced/components/layout/Header.tsx index 2eaac4b92..da5709caa 100644 --- a/src/advanced/components/layout/Header.tsx +++ b/src/advanced/components/layout/Header.tsx @@ -1,12 +1,11 @@ import { ReactElement } from 'react'; -import { useCartStore } from '@/advanced/store/cartStore'; -import { getCartTotalCount } from '@/advanced/utils/cart.util'; - function Header(): ReactElement { - const { cartItems } = useCartStore(); + // const { cartItems } = useCartStore(); + + // const totalCount = getCartTotalCount(cartItems); - const totalCount = getCartTotalCount(cartItems); + const totalCount = 0; return (
diff --git a/src/advanced/components/layout/HelpToggle.tsx b/src/advanced/components/layout/HelpToggle.tsx index 6375e2a56..4b788aefa 100644 --- a/src/advanced/components/layout/HelpToggle.tsx +++ b/src/advanced/components/layout/HelpToggle.tsx @@ -1,8 +1,17 @@ import { ReactElement } from 'react'; +import { useLayoutStore } from '@/advanced/store/layoutStore'; + export default function HelpToggle(): ReactElement { + const { showHelpOverlay, setShowHelpOverlay } = useLayoutStore(); + + const handleToggleHelpOverlay = () => setShowHelpOverlay(!showHelpOverlay); + return ( - + + {quantity} + + +
+
+
+
+ {price} +
+ + Remove + +
+
+ ); +} diff --git a/src/advanced/components/cart/index.ts b/src/advanced/components/cart/index.ts new file mode 100644 index 000000000..1357d09d1 --- /dev/null +++ b/src/advanced/components/cart/index.ts @@ -0,0 +1 @@ +export { default as CartList } from '@/advanced/components/cart/CartList'; diff --git a/src/advanced/components/product/AddButton.tsx b/src/advanced/components/product/AddButton.tsx index dc2869d1e..87f27cf0a 100644 --- a/src/advanced/components/product/AddButton.tsx +++ b/src/advanced/components/product/AddButton.tsx @@ -1,8 +1,23 @@ import { ReactElement } from 'react'; +import { useCartStore } from '@/advanced/store/useCartStore'; +import { useProductStore } from '@/advanced/store/useProductStore'; + export default function AddButton(): ReactElement { + const { selectedProduct } = useProductStore(); + const { addCartItem } = useCartStore(); + + const handleAddToCart = () => { + if (!selectedProduct) return; + + addCartItem(selectedProduct); + }; + return ( - ); diff --git a/src/advanced/components/product/ProductSelect.tsx b/src/advanced/components/product/ProductSelect.tsx index 2e3640090..a20ba5ae3 100644 --- a/src/advanced/components/product/ProductSelect.tsx +++ b/src/advanced/components/product/ProductSelect.tsx @@ -1,4 +1,4 @@ -import { ReactElement } from 'react'; +import { ReactElement, useEffect } from 'react'; import AddButton from '@/advanced/components/product/AddButton'; import StockStatus from '@/advanced/components/product/StockStatus'; @@ -7,11 +7,26 @@ import { ProductStatus } from '@/advanced/types/product.type'; import { createProductText, getProductStatus } from '@/advanced/utils/product.util'; export default function ProductSelect(): ReactElement { - const { products } = useProductStore(); + const { products, setSelectedProduct } = useProductStore(); + + const handleChangeSelect = (e: React.ChangeEvent) => { + const product = products.find(product => product.id === e.target.value); + + if (product) { + setSelectedProduct(product); + } + }; + + const initializeSelectedProduct = () => setSelectedProduct(products[0]); + + useEffect(() => { + initializeSelectedProduct(); + }, []); return (
+
diff --git a/src/advanced/store/useCartStore.ts b/src/advanced/store/useCartStore.ts new file mode 100644 index 000000000..f11ecc4de --- /dev/null +++ b/src/advanced/store/useCartStore.ts @@ -0,0 +1,70 @@ +import { create } from 'zustand'; + +import { CartItem } from '@/advanced/types/cart.type'; +import { Product } from '@/advanced/types/product.type'; + +interface CartState { + cartItems: CartItem[]; +} + +interface CartActions { + addCartItem: (product: Product) => void; + + hasProductInCart: (product: Product) => boolean; +} + +export const useCartStore = create((set, get) => ({ + cartItems: [], + + hasProductInCart: (product: Product) => { + return get().cartItems.some(p => p.id === product.id); + }, + + addCartItem: (product: Product) => { + set(state => { + const productInCart = state.hasProductInCart(product); + + if (productInCart) { + const newProducts = state.cartItems.map(item => + item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item + ); + + return { cartItems: newProducts }; + } + + return { cartItems: [...state.cartItems, { ...product, quantity: 1 }] }; + }); + }, + + increaseProductQuantity: (product: Product) => { + set(state => { + const productInCart = state.hasProductInCart(product); + + if (productInCart) { + const newProducts = state.cartItems.map(item => + item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item + ); + + return { cartItems: newProducts }; + } + + return { cartItems: [...state.cartItems, { ...product, quantity: 1 }] }; + }); + }, + + decreaseProductQuantity: (product: Product) => { + set(state => { + const productInCart = state.hasProductInCart(product); + + if (!productInCart) return state; + + const newProducts = state.cartItems + .map(item => + item.id === product.id ? { ...item, quantity: Math.max(item.quantity - 1, 0) } : item + ) + .filter(item => item.quantity > 0); + + return { cartItems: newProducts }; + }); + }, +})); diff --git a/src/advanced/store/useProductStore.ts b/src/advanced/store/useProductStore.ts index 06f9b701b..c573d5e84 100644 --- a/src/advanced/store/useProductStore.ts +++ b/src/advanced/store/useProductStore.ts @@ -4,15 +4,21 @@ import { Product } from '@/advanced/types/product.type'; interface ProductState { products: Product[]; + selectedProduct: Product | null; } interface ProductActions { setProducts: (products: Product[]) => void; + setSelectedProduct: (product: Product) => void; } export const useProductStore = create(set => ({ products: [], + selectedProduct: null, + setSelectedProduct: (product: Product) => { + set({ selectedProduct: product }); + }, setProducts: (products: Product[]) => { set({ products }); }, diff --git a/src/advanced/types/index.ts b/src/advanced/types/index.ts index 996f8e322..2fec802d0 100644 --- a/src/advanced/types/index.ts +++ b/src/advanced/types/index.ts @@ -1,14 +1,3 @@ -// Cart Item 타입 -export interface CartItem { - id: string; - name: string; - price: number; - originalPrice: number; - quantity: number; - onSale: boolean; - suggestSale: boolean; -} - // 할인 관련 타입 export interface DiscountInfo { name: string; diff --git a/src/advanced/utils/product.util.ts b/src/advanced/utils/product.util.ts index d8b8337dc..7f32e7953 100644 --- a/src/advanced/utils/product.util.ts +++ b/src/advanced/utils/product.util.ts @@ -13,15 +13,18 @@ export function getProductStatus(product: Product): ProductStatus { export function createProductText(product: Product): string { const status: ProductStatus = getProductStatus(product); + const discountText = product.onSale ? '⚡SALE' : product.suggestSale ? '💝추천' : ''; + const formatters = { - [ProductStatus.OUT_OF_STOCK]: () => `${product.name} - ${product.price}원 (품절)`, + [ProductStatus.OUT_OF_STOCK]: () => + `${product.name} - ${product.price}원 (품절) ${discountText}`, [ProductStatus.SUPER_SALE]: () => `⚡💝${product.name} - ${product.originalPrice}원 → ${product.price}원 (${getSuperSaleRate()}% SUPER SALE!)`, [ProductStatus.LIGHTNING_SALE]: () => `⚡${product.name} - ${product.originalPrice}원 → ${product.price}원 (${DISCOUNT_RATE_LIGHTNING}% SALE!)`, [ProductStatus.SUGGESTION_SALE]: () => `💝${product.name} - ${product.originalPrice}원 → ${product.price}원 (${DISCOUNT_RATE_SUGGESTION}% 추천할인!)`, - [ProductStatus.NORMAL]: () => `${product.name} - ${product.price}원`, + [ProductStatus.NORMAL]: () => `${product.name} - ${product.price}원 ${discountText}`, }; return formatters[status](); From d3046437c26e6d7e7635192d1778cda7b86aab47 Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Thu, 31 Jul 2025 01:06:21 +0900 Subject: [PATCH 28/55] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=9D=84=20=EA=B0=9C=EC=84=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EB=B0=B0=EB=9F=B4=20=EC=9D=B5?= =?UTF-8?q?=EC=8A=A4=ED=8F=AC=ED=8A=B8=20=ED=8C=A8=ED=84=B4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/advanced/App.tsx | 19 ++++++++++--------- src/advanced/components/cart/CartList.tsx | 2 +- src/advanced/components/layout/HelpToggle.tsx | 2 +- src/advanced/components/layout/index.ts | 5 +++++ src/advanced/components/order/index.ts | 1 + src/advanced/components/product/AddButton.tsx | 3 +-- .../components/product/ProductSelect.tsx | 2 +- src/advanced/components/product/index.ts | 1 + src/advanced/store/index.ts | 3 +++ src/advanced/store/useCartStore.ts | 4 +++- src/advanced/store/useLayoutStore.ts | 4 +++- src/advanced/store/useProductStore.ts | 4 +++- 12 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 src/advanced/components/layout/index.ts create mode 100644 src/advanced/components/order/index.ts create mode 100644 src/advanced/components/product/index.ts create mode 100644 src/advanced/store/index.ts diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx index 9215a9b5d..369c1e4a2 100644 --- a/src/advanced/App.tsx +++ b/src/advanced/App.tsx @@ -2,15 +2,16 @@ import React, { useEffect } from 'react'; import getProducts from '@/advanced/api/getProducts'; import { CartList } from '@/advanced/components/cart'; -import Header from '@/advanced/components/layout/Header'; -import HelpOverlay from '@/advanced/components/layout/HelpOverlay'; -import HelpToggle from '@/advanced/components/layout/HelpToggle'; -import MainLayout from '@/advanced/components/layout/MainLayout'; -import SectionLayout from '@/advanced/components/layout/SectionLayout'; -import OrderSummary from '@/advanced/components/order/OrderSummary'; -import ProductSelect from '@/advanced/components/product/ProductSelect'; -import { useLayoutStore } from '@/advanced/store/useLayoutStore'; -import { useProductStore } from '@/advanced/store/useProductStore'; +import { + Header, + HelpOverlay, + HelpToggle, + MainLayout, + SectionLayout, +} from '@/advanced/components/layout'; +import { OrderSummary } from '@/advanced/components/order'; +import { ProductSelect } from '@/advanced/components/product'; +import { useLayoutStore, useProductStore } from '@/advanced/store'; function App() { const { showHelpOverlay } = useLayoutStore(); diff --git a/src/advanced/components/cart/CartList.tsx b/src/advanced/components/cart/CartList.tsx index 5f0d29d7e..b0752e7d2 100644 --- a/src/advanced/components/cart/CartList.tsx +++ b/src/advanced/components/cart/CartList.tsx @@ -1,7 +1,7 @@ import { ReactElement } from 'react'; import CartListItem from '@/advanced/components/cart/CartListItem'; -import { useCartStore } from '@/advanced/store/useCartStore'; +import { useCartStore } from '@/advanced/store'; export default function CartList(): ReactElement { const { cartItems } = useCartStore(); diff --git a/src/advanced/components/layout/HelpToggle.tsx b/src/advanced/components/layout/HelpToggle.tsx index 304a2ba29..5b9f07ea6 100644 --- a/src/advanced/components/layout/HelpToggle.tsx +++ b/src/advanced/components/layout/HelpToggle.tsx @@ -1,6 +1,6 @@ import { ReactElement } from 'react'; -import { useLayoutStore } from '@/advanced/store/useLayoutStore'; +import { useLayoutStore } from '@/advanced/store'; export default function HelpToggle(): ReactElement { const { showHelpOverlay, setShowHelpOverlay } = useLayoutStore(); diff --git a/src/advanced/components/layout/index.ts b/src/advanced/components/layout/index.ts new file mode 100644 index 000000000..b5501c60d --- /dev/null +++ b/src/advanced/components/layout/index.ts @@ -0,0 +1,5 @@ +export { default as Header } from '@/advanced/components/layout/Header'; +export { default as HelpOverlay } from '@/advanced/components/layout/HelpOverlay'; +export { default as HelpToggle } from '@/advanced/components/layout/HelpToggle'; +export { default as MainLayout } from '@/advanced/components/layout/MainLayout'; +export { default as SectionLayout } from '@/advanced/components/layout/SectionLayout'; diff --git a/src/advanced/components/order/index.ts b/src/advanced/components/order/index.ts new file mode 100644 index 000000000..a34698981 --- /dev/null +++ b/src/advanced/components/order/index.ts @@ -0,0 +1 @@ +export { default as OrderSummary } from '@/advanced/components/order/OrderSummary'; diff --git a/src/advanced/components/product/AddButton.tsx b/src/advanced/components/product/AddButton.tsx index 87f27cf0a..8d3c379f1 100644 --- a/src/advanced/components/product/AddButton.tsx +++ b/src/advanced/components/product/AddButton.tsx @@ -1,7 +1,6 @@ import { ReactElement } from 'react'; -import { useCartStore } from '@/advanced/store/useCartStore'; -import { useProductStore } from '@/advanced/store/useProductStore'; +import { useCartStore, useProductStore } from '@/advanced/store'; export default function AddButton(): ReactElement { const { selectedProduct } = useProductStore(); diff --git a/src/advanced/components/product/ProductSelect.tsx b/src/advanced/components/product/ProductSelect.tsx index a20ba5ae3..76933110d 100644 --- a/src/advanced/components/product/ProductSelect.tsx +++ b/src/advanced/components/product/ProductSelect.tsx @@ -2,7 +2,7 @@ import { ReactElement, useEffect } from 'react'; import AddButton from '@/advanced/components/product/AddButton'; import StockStatus from '@/advanced/components/product/StockStatus'; -import { useProductStore } from '@/advanced/store/useProductStore'; +import { useProductStore } from '@/advanced/store'; import { ProductStatus } from '@/advanced/types/product.type'; import { createProductText, getProductStatus } from '@/advanced/utils/product.util'; diff --git a/src/advanced/components/product/index.ts b/src/advanced/components/product/index.ts new file mode 100644 index 000000000..3fb888114 --- /dev/null +++ b/src/advanced/components/product/index.ts @@ -0,0 +1 @@ +export { default as ProductSelect } from '@/advanced/components/product/ProductSelect'; diff --git a/src/advanced/store/index.ts b/src/advanced/store/index.ts new file mode 100644 index 000000000..a4c610e6a --- /dev/null +++ b/src/advanced/store/index.ts @@ -0,0 +1,3 @@ +export { default as useCartStore } from '@/advanced/store/useCartStore'; +export { default as useLayoutStore } from '@/advanced/store/useLayoutStore'; +export { default as useProductStore } from '@/advanced/store/useProductStore'; diff --git a/src/advanced/store/useCartStore.ts b/src/advanced/store/useCartStore.ts index f11ecc4de..a7a0838c0 100644 --- a/src/advanced/store/useCartStore.ts +++ b/src/advanced/store/useCartStore.ts @@ -13,7 +13,7 @@ interface CartActions { hasProductInCart: (product: Product) => boolean; } -export const useCartStore = create((set, get) => ({ +const useCartStore = create((set, get) => ({ cartItems: [], hasProductInCart: (product: Product) => { @@ -68,3 +68,5 @@ export const useCartStore = create((set, get) => ({ }); }, })); + +export default useCartStore; diff --git a/src/advanced/store/useLayoutStore.ts b/src/advanced/store/useLayoutStore.ts index cc6f754e7..1ec55e14f 100644 --- a/src/advanced/store/useLayoutStore.ts +++ b/src/advanced/store/useLayoutStore.ts @@ -8,10 +8,12 @@ interface LayoutActions { setShowHelpOverlay: (showHelpOverlay: boolean) => void; } -export const useLayoutStore = create(set => ({ +const useLayoutStore = create(set => ({ showHelpOverlay: false, setShowHelpOverlay: (showHelpOverlay: boolean) => { set({ showHelpOverlay }); }, })); + +export default useLayoutStore; diff --git a/src/advanced/store/useProductStore.ts b/src/advanced/store/useProductStore.ts index c573d5e84..22b2646a3 100644 --- a/src/advanced/store/useProductStore.ts +++ b/src/advanced/store/useProductStore.ts @@ -12,7 +12,7 @@ interface ProductActions { setSelectedProduct: (product: Product) => void; } -export const useProductStore = create(set => ({ +const useProductStore = create(set => ({ products: [], selectedProduct: null, @@ -23,3 +23,5 @@ export const useProductStore = create(set => ({ set({ products }); }, })); + +export default useProductStore; From 05a4f75ae988089dab346772d46d13bf6e67b59e Mon Sep 17 00:00:00 2001 From: Angie Lee Date: Thu, 31 Jul 2025 01:11:12 +0900 Subject: [PATCH 29/55] =?UTF-8?q?feat:=20=EC=84=A0=ED=83=9D=EB=90=9C=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=8B=9C=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/advanced/components/product/ProductSelect.tsx | 12 +++++++----- src/advanced/store/useProductStore.ts | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/advanced/components/product/ProductSelect.tsx b/src/advanced/components/product/ProductSelect.tsx index 76933110d..745f59512 100644 --- a/src/advanced/components/product/ProductSelect.tsx +++ b/src/advanced/components/product/ProductSelect.tsx @@ -7,7 +7,7 @@ import { ProductStatus } from '@/advanced/types/product.type'; import { createProductText, getProductStatus } from '@/advanced/utils/product.util'; export default function ProductSelect(): ReactElement { - const { products, setSelectedProduct } = useProductStore(); + const { products, selectedProduct, setSelectedProduct } = useProductStore(); const handleChangeSelect = (e: React.ChangeEvent) => { const product = products.find(product => product.id === e.target.value); @@ -17,11 +17,11 @@ export default function ProductSelect(): ReactElement { } }; - const initializeSelectedProduct = () => setSelectedProduct(products[0]); - useEffect(() => { - initializeSelectedProduct(); - }, []); + if (!selectedProduct && products.length > 0) { + setSelectedProduct(products[0]); + } + }, [products, selectedProduct]); return (
@@ -29,9 +29,11 @@ export default function ProductSelect(): ReactElement { onChange={handleChangeSelect} id="product-select" className="w-full p-3 border border-gray-300 rounded-lg text-base mb-3" + value={selectedProduct?.id || ''} > {products.map(product => (
`; + } +} diff --git a/src/basic/components/order/PointInfo.js b/src/basic/components/order/PointInfo.js new file mode 100644 index 000000000..e4448e5b2 --- /dev/null +++ b/src/basic/components/order/PointInfo.js @@ -0,0 +1,12 @@ +import Component from '../../lib/Component.js'; + +export default class PointInfo extends Component { + template() { + return ` +

+ Free shipping on all orders.
+ Earn loyalty points with purchase. +

+ `; + } +} diff --git a/src/basic/components/order/SpecialDiscount.js b/src/basic/components/order/SpecialDiscount.js new file mode 100644 index 000000000..3bc0613ec --- /dev/null +++ b/src/basic/components/order/SpecialDiscount.js @@ -0,0 +1,5 @@ +export default function SpecialDiscount() { + return ` +
+ `; +} diff --git a/src/basic/components/order/SummaryDetail.js b/src/basic/components/order/SummaryDetail.js new file mode 100644 index 000000000..97fddd78d --- /dev/null +++ b/src/basic/components/order/SummaryDetail.js @@ -0,0 +1,11 @@ +import Component from '../../lib/Component.js'; + +export default class SummaryDetail extends Component { + template() { + return ` +
+ +
+ `; + } +} diff --git a/src/basic/components/order/index.js b/src/basic/components/order/index.js new file mode 100644 index 000000000..73af114df --- /dev/null +++ b/src/basic/components/order/index.js @@ -0,0 +1,8 @@ +export { default as BasicDiscount } from './BasicDiscount.js'; +export { default as CartTotal } from './CartTotal.js'; +export { default as DiscountBanner } from './DiscountBanner.js'; +export { default as DiscountInfo } from './DiscountInfo.js'; +export { default as OrderSummary } from './OrderSummary.js'; +export { default as PointInfo } from './PointInfo.js'; +export { default as SpecialDiscount } from './SpecialDiscount.js'; +export { default as SummaryDetail } from './SummaryDetail.js'; diff --git a/src/basic/components/product/AddButton.js b/src/basic/components/product/AddButton.js new file mode 100644 index 000000000..fe96b107b --- /dev/null +++ b/src/basic/components/product/AddButton.js @@ -0,0 +1,10 @@ +export default function AddButton() { + return ` + + `; +} diff --git a/src/basic/components/product/ProductSection.js b/src/basic/components/product/ProductSection.js new file mode 100644 index 000000000..c759ed5a9 --- /dev/null +++ b/src/basic/components/product/ProductSection.js @@ -0,0 +1,29 @@ +import Component from '../../lib/Component.js'; +import SectionLayout from '../layout/SectionLayout.js'; +import AddButton from './AddButton.js'; +import ProductSelect from './ProductSelect.js'; +import StockStatus from './StockStatus.js'; + +export default class ProductSection extends Component { + mounted() { + this.renderChildren({ + productSelect: { + selector: '#product-select-container', + Component: ProductSelect, + }, + }); + } + + template() { + return SectionLayout( + 'bg-white border border-gray-200 p-8 overflow-y-auto', + ` +
+
+ ${AddButton()} ${StockStatus()} +
+
+ ` + ); + } +} diff --git a/src/basic/components/product/ProductSelect.js b/src/basic/components/product/ProductSelect.js new file mode 100644 index 000000000..6d483ac8e --- /dev/null +++ b/src/basic/components/product/ProductSelect.js @@ -0,0 +1,44 @@ +import { PRODUCT_LIST } from '../../data/product.data.js'; +import { TOTAL_STOCK_WARNING_THRESHOLD } from '../../data/quantity.data.js'; +import Component from '../../lib/Component.js'; +import { calculateTotalStock } from '../../utils/stockCalculator.util.js'; + +// ProductSelectOption 함수 +function ProductSelectOption(product) { + const { id, name, val, q } = product; + let iconClass = ''; + + // 상품별 아이콘 설정 + if (name.includes('⚡')) iconClass = ' lightning-sale'; + if (name.includes('💝')) iconClass = ' suggestion-sale'; + + return ` + + `; +} + +export default class ProductSelect extends Component { + mounted() { + const totalStock = calculateTotalStock(PRODUCT_LIST); + const $select = document.querySelector('#product-select'); + + if (totalStock < TOTAL_STOCK_WARNING_THRESHOLD) { + $select.style.borderColor = 'orange'; + } else { + $select.style.borderColor = ''; + } + } + + template() { + return /* HTML */ ` + + `; + } +} diff --git a/src/basic/components/product/StockStatus.js b/src/basic/components/product/StockStatus.js new file mode 100644 index 000000000..a17faa795 --- /dev/null +++ b/src/basic/components/product/StockStatus.js @@ -0,0 +1,9 @@ +export default function StockStatus() { + return ` +
+
+ `; +} diff --git a/src/basic/components/product/index.js b/src/basic/components/product/index.js new file mode 100644 index 000000000..cfaffe48f --- /dev/null +++ b/src/basic/components/product/index.js @@ -0,0 +1,4 @@ +export { default as AddButton } from './AddButton.js'; +export { default as ProductSection } from './ProductSection.js'; +export { default as ProductSelect } from './ProductSelect.js'; +export { default as StockStatus } from './StockStatus.js'; diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 944d4bebc..bf7c5436e 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,11 +1,14 @@ import App from './App.js'; -import { createTimerManager } from './components/TimerManager.js'; import CartItem from './components/cart/CartItem.js'; import ProductPrice from './components/cart/ProductPrice.js'; +import Divider from './components/layout/Divider.js'; +import { createTimerManager } from './components/layout/TimerManager.js'; import { DISCOUNT_RATE_TUESDAY } from './data/discount.data.js'; import { PRODUCT_LIST } from './data/product.data.js'; import { MIN_QUANTITY_FOR_DISCOUNT } from './data/quantity.data.js'; import { useDiscount, useOrderSummary, usePoint, useStock } from './hooks/index.js'; +import AppState from './store/appStore.js'; +import DOMManager from './store/domManager.js'; import { getCartItemsArray, hasValidProduct, @@ -19,60 +22,6 @@ import { isProductDiscountEligible } from './utils/discount.util.js'; import { createProductName, findProductById } from './utils/product.util.js'; import { validateStockAvailability } from './utils/stock.util.js'; -// ============================================================================ -// 애플리케이션 상태 관리 -// ============================================================================ -class AppState { - constructor() { - this.totalQuantity = 0; - this.lastSelectedProductId = null; - this.totalPrice = 0; - } - - updateState(newState) { - Object.assign(this, newState); - } - - getState() { - return { - totalQuantity: this.totalQuantity, - lastSelectedProductId: this.lastSelectedProductId, - totalPrice: this.totalPrice, - }; - } -} - -// ============================================================================ -// DOM 요소 관리 -// ============================================================================ -class DOMManager { - constructor() { - this.elements = { - stockStatus: null, - productSelect: null, - addToCartButton: null, - cartItemsContainer: null, - }; - } - - initialize() { - this.elements = { - stockStatus: document.querySelector('#stock-status'), - productSelect: document.querySelector('#product-select'), - addToCartButton: document.querySelector('#add-to-cart'), - cartItemsContainer: document.querySelector('#cart-items'), - }; - } - - getElement(name) { - return this.elements[name]; - } - - getAllElements() { - return this.elements; - } -} - // ============================================================================ // 전역 인스턴스 (최소화된 전역 상태) // ============================================================================ @@ -154,9 +103,9 @@ const updateOrderSummaryDisplay = (orderList, subTotal, itemDiscounts, isTuesday ) .join(''); - // 소계 + // 소계 섹션에 Divider 사용 const subtotalSection = ` -
+ ${Divider()}
Subtotal ₩${subTotal.toLocaleString()} @@ -184,21 +133,22 @@ const updateOrderSummaryDisplay = (orderList, subTotal, itemDiscounts, isTuesday const generateDiscountDetailsHTML = (itemDiscounts, isTuesday, totalPrice) => { let discountHTML = ''; - // 개별 할인 정보 + // 개별 할인 정보 (BasicDiscount 컴포넌트 사용) if (itemDiscounts.length > 0) { discountHTML += itemDiscounts - .map( - item => ` -
- ${item.name} (${MIN_QUANTITY_FOR_DISCOUNT}개↑) - -${item.discount}% -
- ` - ) + .map(item => { + // BasicDiscount 컴포넌트를 활용한 구조 + return ` +
+ ${item.name} (${MIN_QUANTITY_FOR_DISCOUNT}개↑) + -${item.discount}% +
+ `; + }) .join(''); } - // 화요일 특별 할인 + // 화요일 특별 할인 (SpecialDiscount 컴포넌트 사용) if (isTuesday && totalPrice > 0) { discountHTML += `
@@ -211,6 +161,37 @@ const generateDiscountDetailsHTML = (itemDiscounts, isTuesday, totalPrice) => { return discountHTML; }; +/** + * 기본 할인 정보 표시 (BasicDiscount 컴포넌트 사용) + */ +const updateBasicDiscountDisplay = itemDiscounts => { + const basicDiscountElement = document.getElementById('basic-discount'); + if (!basicDiscountElement) return; + + if (itemDiscounts.length > 0) { + const discountInfo = itemDiscounts.map(item => `${item.name}: -${item.discount}%`).join(', '); + basicDiscountElement.textContent = `기본 할인 적용: ${discountInfo}`; + basicDiscountElement.style.display = 'block'; + } else { + basicDiscountElement.style.display = 'none'; + } +}; + +/** + * 특별 할인 정보 표시 (SpecialDiscount 컴포넌트 사용) + */ +const updateSpecialDiscountDisplay = isTuesday => { + const specialDiscountElement = document.getElementById('special-discount'); + if (!specialDiscountElement) return; + + if (isTuesday) { + specialDiscountElement.textContent = `🌟 화요일 특별 할인 ${DISCOUNT_RATE_TUESDAY}% 적용 중!`; + specialDiscountElement.style.display = 'block'; + } else { + specialDiscountElement.style.display = 'none'; + } +}; + /** * 총액 표시 업데이트 */ @@ -338,6 +319,8 @@ const calculateCartAndUpdateUI = () => { updateDiscountInfoDisplay(cartData.originalTotalPrice, cartData.totalDiscountRate); updateStockStatusDisplay(cartData.stockStatusMessage); updateTuesdaySpecialDisplay(cartData.isTuesday); + updateBasicDiscountDisplay(cartData.itemDiscounts); + updateSpecialDiscountDisplay(cartData.isTuesday); // 할인 시각적 표시 업데이트 (공통 함수 사용) if (cartItemsContainer) { diff --git a/src/basic/store/appStore.js b/src/basic/store/appStore.js index 96dba1a40..ad67a5769 100644 --- a/src/basic/store/appStore.js +++ b/src/basic/store/appStore.js @@ -1,13 +1,21 @@ -import Store from "../lib/Store"; +class AppState { + constructor() { + this.totalQuantity = 0; + this.lastSelectedProductId = null; + this.totalPrice = 0; + } -const appState = { - itemCounts: 0, - lastSelect: null, - totalAmount: 0, -}; + updateState(newState) { + Object.assign(this, newState); + } -const appStore = new Store(appState); + getState() { + return { + totalQuantity: this.totalQuantity, + lastSelectedProductId: this.lastSelectedProductId, + totalPrice: this.totalPrice, + }; + } +} -export const resetAppStore = () => appStore.setState(appState); - -export default appStore; +export default AppState; diff --git a/src/basic/store/domManager.js b/src/basic/store/domManager.js new file mode 100644 index 000000000..2d3a2650b --- /dev/null +++ b/src/basic/store/domManager.js @@ -0,0 +1,29 @@ +class DOMManager { + constructor() { + this.elements = { + stockStatus: null, + productSelect: null, + addToCartButton: null, + cartItemsContainer: null, + }; + } + + initialize() { + this.elements = { + stockStatus: document.querySelector('#stock-status'), + productSelect: document.querySelector('#product-select'), + addToCartButton: document.querySelector('#add-to-cart'), + cartItemsContainer: document.querySelector('#cart-items'), + }; + } + + getElement(name) { + return this.elements[name]; + } + + getAllElements() { + return this.elements; + } +} + +export default DOMManager;