diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..5ee7abd87 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm exec lint-staged diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..d4c0dd53d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "useTabs": false, + "endOfLine": "lf" +} \ 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/05-clean-code-theory-lesson.md b/docs/05-clean-code-theory-lesson.md index a0aacfdf2..1e8902526 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'; - + // 콘솔 로깅 console.log('할인 적용: ' + curItem.name); } ``` #### 클린코드 원칙 + - **Model**: 데이터와 비즈니스 로직 - **View**: UI 렌더링 - **Controller**: 사용자 입력 처리와 조정 @@ -159,10 +180,12 @@ function calcCart() { ### 3.2 의존성 역전 원칙 #### 문제점 + - 고수준 모듈이 저수준 모듈에 직접 의존 - DOM 요소에 직접 접근하여 테스트 어려움 #### 해결 방법 + ```javascript // ✅ 인터페이스를 통한 의존성 주입 class CartService { @@ -185,11 +208,13 @@ class CartService { ### 4.2 리팩토링 우선순위 1. **가독성 개선** + - 변수명 개선 - 함수 분리 - 주석 대신 자명한 코드 2. **구조 개선** + - 중복 제거 - 모듈화 - 의존성 정리 @@ -202,6 +227,7 @@ class CartService { ## Part 5: AI 시대의 코드 리뷰 체크리스트 ### 5.1 즉시 거부해야 할 코드 + - [ ] 전역 변수 남용 - [ ] 하드코딩된 값 - [ ] 300줄 이상의 함수 @@ -209,6 +235,7 @@ class CartService { - [ ] 테스트 불가능한 구조 ### 5.2 개선 요청할 코드 + - [ ] 불명확한 변수명 - [ ] 주석 없는 복잡한 로직 - [ ] 일관성 없는 코딩 스타일 @@ -216,6 +243,7 @@ class CartService { - [ ] 에러 처리 부재 ### 5.3 AI 생성 코드 평가 기준 + 1. **정확성**: 요구사항을 충족하는가? 2. **가독성**: 다른 개발자가 이해할 수 있는가? 3. **유지보수성**: 변경이 쉬운가? @@ -225,20 +253,24 @@ class CartService { ## 실습 과제 가이드 ### Phase 1: 문제 인식 + - cart-tailwind.html의 안티패턴 10개 찾기 - 각 문제가 야기할 수 있는 실제 버그 시나리오 작성 ### Phase 2: 설계 + - 클린 아키텍처로 재설계 - 모듈 다이어그램 작성 - 인터페이스 정의 ### Phase 3: 구현 + - 테스트 먼저 작성 (TDD) - 점진적 리팩토링 - 코드 리뷰와 개선 ### Phase 4: 검증 + - 성능 비교 - 가독성 평가 - 확장성 테스트 @@ -246,17 +278,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/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..5bc23efbf --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,71 @@ +import js from '@eslint/js'; // JavaScript 기본 규칙들 (no-unused-vars 등) +import { defineConfig } from 'eslint/config'; // ESLint 설정 타입 안전성 제공 +import prettierConfig from 'eslint-config-prettier'; // ESLint와 Prettier 규칙 충돌 방지 +import prettier from 'eslint-plugin-prettier'; // Prettier 포맷팅을 ESLint 규칙으로 적용 +import pluginReact from 'eslint-plugin-react'; // React/JSX 전용 규칙들 (Hook 규칙, JSX 문법 등) +import simpleImportSort from 'eslint-plugin-simple-import-sort'; // import/export 문 자동 정렬 +import globals from 'globals'; // 브라우저/Node.js 전역변수 정의 (window, document 등) +import tseslint from 'typescript-eslint'; // TypeScript 코드 검사 및 타입 관련 규칙 + +export default defineConfig([ + // 기본 파일 타입 및 언어 설정 + { + files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], // 검사할 파일 확장자 지정 + languageOptions: { + globals: globals.browser, // 브라우저 환경 전역변수 사용 가능 (window, document 등) + ecmaVersion: 12, // ES2021 문법 지원 + sourceType: 'module', // ES6 모듈 시스템 사용 + parserOptions: { + ecmaFeatures: { + jsx: true, // JSX 문법 파싱 활성화 + }, + }, + }, + }, + + // 기본 권장 설정들 + js.configs.recommended, // JavaScript 기본 권장 규칙 (문법 오류, 일반적인 실수 방지) + ...tseslint.configs.recommended, // TypeScript 권장 규칙 (타입 안전성, TS 모범 사례) + pluginReact.configs.flat.recommended, // React 권장 규칙 (Hook 규칙, JSX 모범 사례) + prettierConfig, // Prettier와 충돌하는 ESLint 규칙들 비활성화 + + // 커스텀 플러그인 및 규칙 설정 + { + plugins: { + 'simple-import-sort': simpleImportSort, // import 문 정렬 기능 활성화 + prettier, // Prettier 포맷팅 검사 기능 활성화 + }, + rules: { + // === 코드 포맷팅 관련 === + 'prettier/prettier': 'error', // Prettier 규칙 위반시 에러 (일관된 코드 포맷팅) + + // === Import/Export 정리 === + 'simple-import-sort/imports': 'error', // import 문을 알파벳순으로 정렬 (가독성 향상) + 'simple-import-sort/exports': 'error', // export 문을 알파벳순으로 정렬 + + // === 변수 및 코드 품질 === + 'no-unused-vars': 'off', // 기본 no-unused-vars 비활성화 + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], // TypeScript 전용 unused vars 규칙 사용 + 'no-console': 'warn', // console.log 사용 경고 (운영환경 배포시 제거 필요) + 'no-var': 'error', // var 사용 금지 (let/const 사용 강제) + 'prefer-const': 'error', // 재할당 없는 변수는 const 사용 강제 + 'react/react-in-jsx-scope': 'off', // React 17 이상에서는 필요 없음 + 'react/jsx-uses-react': 'off', // React 17 이상에서는 필요 없음 + + // === 코드 스타일 통일 === + eqeqeq: ['error', 'always'], // === 연산자 사용 강제 (타입 안전성) + }, + settings: { + react: { + version: 'detect', // React 버전 자동 감지 (버전별 규칙 적용) + }, + }, + }, +]); diff --git a/index.advanced.html b/index.advanced.html index a070c3355..d1cf2a098 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.html b/index.html index da107a22e..f21f9d2d2 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..b1e93070f 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,52 @@ "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,.jsx,.ts,.tsx", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "prepare": "husky", + "gh-pages": "pnpm build && gh-pages -d ./dist" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md,css,html}": [ + "prettier --write" + ] + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "@vitejs/plugin-react": "^4.3.4", "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.3", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "gh-pages": "^6.3.0", + "globals": "^16.3.0", + "husky": "^9.1.7", "jsdom": "^26.1.0", + "lint-staged": "^16.1.2", + "prettier": "^3.6.2", + "tsc-alias": "^1.8.16", + "tsconfig-paths": "^4.2.0", + "typescript-eslint": "^8.38.0", "vite": "^7.0.5", "vitest": "^3.2.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49a2140ea..f33f8b758 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,31 +7,102 @@ 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) devDependencies: + '@eslint/js': + specifier: ^9.32.0 + version: 9.32.0 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) + '@types/react': + specifier: ^18.3.12 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.23) + '@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.3.4 + 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) + 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) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.32.0) + eslint-plugin-react-hooks: + specifier: ^5.0.0 + version: 5.2.0(eslint@9.32.0) + eslint-plugin-simple-import-sort: + specifier: ^12.1.1 + version: 12.1.1(eslint@9.32.0) + gh-pages: + specifier: ^6.3.0 + version: 6.3.0 + 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 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript-eslint: + specifier: ^8.38.0 + version: 8.38.0(eslint@9.32.0)(typescript@5.8.3) 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: '@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==} @@ -39,14 +110,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'} @@ -231,12 +377,99 @@ 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/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} + '@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] @@ -354,6 +587,18 @@ packages: '@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==} @@ -363,6 +608,85 @@ 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==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + + '@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==} @@ -397,14 +721,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 +758,17 @@ 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'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + 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==} @@ -420,14 +776,98 @@ 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-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + 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'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + 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'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + 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==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + 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'} + 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'} + + 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'} @@ -440,10 +880,26 @@ 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'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + 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 +907,34 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + 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'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -458,10 +942,25 @@ 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'} + 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@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -478,35 +977,216 @@ 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==} + + 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'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + 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'} + + electron-to-chromium@1.5.193: + resolution: {integrity: sha512-eePuBZXM9OVCwfYUhd2OzESeNGnWmLyeu0XAEjf7xjijNjHFdeJSzuRUGN4ueT2tEYo5YqjHramKEFxz67p3XA==} + + email-addresses@5.0.0: + resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} + + 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-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-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + 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'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + 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-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-simple-import-sort@12.1.1: + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + + 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-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: @@ -518,47 +1198,333 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} engines: {node: '>=8'} - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + 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==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + 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==} + + 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'} + + 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'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gh-pages@6.3.0: + resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==} + engines: {node: '>=10'} + hasBin: true + + 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'} + + 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'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + 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'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} https-proxy-agent@7.0.6: 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'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + 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'} + 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-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + 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'} + + is-fullwidth-code-point@5.0.0: + 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'} + 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==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + 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,15 +1534,83 @@ 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==} + + 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==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + 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@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + 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'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} 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 @@ -584,10 +1618,40 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + math-intrinsics@1.1.0: + 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'} + + 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==} + + 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==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -595,17 +1659,114 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + + 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==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + 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.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'} + + optionator@0.9.4: + 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@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + 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'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -616,37 +1777,146 @@ 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 + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + 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} + 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} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + 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@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + 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'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - rollup@4.45.1: - resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + 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-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@5.1.0: + 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==} + + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true 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'} + + 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==} @@ -654,13 +1924,77 @@ 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'} + + 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'} + + shebang-regex@3.0.0: + 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==} + 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'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + 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,20 +2005,75 @@ 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'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + 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'} + 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==} + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + supports-color@7.2.0: 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==} + 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 +2103,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 +2119,74 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + 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'} + + typescript-eslint@8.38.0: + resolution: {integrity: sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==} + 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@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'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + 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==} + 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 +2280,40 @@ 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'} + 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,10 +2333,27 @@ 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'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + 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) @@ -861,10 +2368,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)': @@ -963,10 +2574,95 @@ 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/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 @@ -1054,6 +2750,27 @@ snapshots: '@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 @@ -1062,52 +2779,170 @@ snapshots: '@types/estree@1.0.8': {} - '@vitest/expect@3.2.4': + '@types/json-schema@7.0.15': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.23)': dependencies: - '@types/chai': 5.2.2 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - tinyrainbow: 2.0.0 + '@types/react': 18.3.23 - '@vitest/mocker@3.2.4(vite@7.0.5)': + '@types/react@18.3.23': dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 7.0.5 + '@types/prop-types': 15.7.15 + csstype: 3.1.3 - '@vitest/pretty-format@3.2.4': + '@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: - tinyrainbow: 2.0.0 + '@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 - '@vitest/runner@3.2.4': + '@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.8.3)': dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.0.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) + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1 + eslint: 9.32.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@vitest/snapshot@3.2.4': + '@typescript-eslint/project-service@8.38.0(typescript@5.8.3)': dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 - pathe: 2.0.3 + '@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 - '@vitest/spy@3.2.4': + '@typescript-eslint/scope-manager@8.38.0': dependencies: - tinyspy: 4.0.3 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 - '@vitest/ui@3.2.4(vitest@3.2.4)': + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.8.3)': dependencies: - '@vitest/utils': 3.2.4 - fflate: 0.8.2 - flatted: 3.3.3 + 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 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@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(yaml@2.8.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/ui@3.2.4(vitest@3.2.4)': + dependencies: + '@vitest/utils': 3.2.4 + fflate: 0.8.2 + flatted: 3.3.3 pathe: 2.0.3 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,26 +2950,166 @@ 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: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 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-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + 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 + 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 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.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: {} + + async@3.2.6: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + 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.193 + 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: + 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: {} + + caniuse-lite@1.0.30001731: {} + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -1153,14 +3128,57 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.4.1: {} + check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + 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@13.1.0: {} + + commander@14.0.0: {} + + commander@9.5.0: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + 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: @@ -1168,11 +3186,31 @@ 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 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@4.4.1: dependencies: ms: 2.1.3 @@ -1181,16 +3219,153 @@ snapshots: deep-eql@5.0.2: {} + 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: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + 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 + + electron-to-chromium@1.5.193: {} + + email-addresses@5.0.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-iterator-helpers@1.2.1: + 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-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.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 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + 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 @@ -1220,55 +3395,500 @@ snapshots: '@esbuild/win32-ia32': 0.25.8 '@esbuild/win32-x64': 0.25.8 - estree-walker@3.0.3: + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + 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-plugin-react-hooks@5.2.0(eslint@9.32.0): + dependencies: + eslint: 9.32.0 + + eslint-plugin-react@7.37.5(eslint@9.32.0): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.32.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-plugin-simple-import-sort@12.1.1(eslint@9.32.0): + dependencies: + 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-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 + + fflate@0.8.2: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filename-reserved-regex@2.0.0: {} + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + 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: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + 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: {} + + gensync@1.0.0-beta.2: {} + + 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 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gh-pages@6.3.0: + dependencies: + async: 3.2.6 + commander: 13.1.0 + email-addresses: 5.0.0 + filenamify: 4.3.0 + find-cache-dir: 3.3.2 + fs-extra: 11.3.0 + globby: 11.1.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.3.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.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 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + husky@9.1.7: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + 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: {} + + 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-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.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: - '@types/estree': 1.0.8 + is-extglob: 2.1.1 - expect-type@1.2.2: {} + is-map@2.0.3: {} - fdir@6.4.6(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 + is-negative-zero@2.0.3: {} - fflate@0.8.2: {} + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - flatted@3.3.3: {} + is-number@7.0.0: {} - fsevents@2.3.3: - optional: true + is-potential-custom-element-name@1.0.1: {} - has-flag@4.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 - html-encoding-sniffer@4.0.0: + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: dependencies: - whatwg-encoding: 3.1.1 + call-bound: 1.0.4 - http-proxy-agent@7.0.2: + is-string@1.1.1: dependencies: - agent-base: 7.1.4 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - https-proxy-agent@7.0.6: + is-symbol@1.1.1: dependencies: - agent-base: 7.1.4 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 - iconv-lite@0.6.3: + is-typed-array@1.1.15: dependencies: - safer-buffer: 2.1.2 + which-typed-array: 1.1.19 - indent-string@4.0.0: {} + is-weakmap@2.0.2: {} - is-potential-custom-element-name@1.0.1: {} + 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: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 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,61 +3916,351 @@ snapshots: - supports-color - utf-8-validate + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.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@5.0.0: + dependencies: + p-locate: 4.1.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 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + loupe@3.2.0: {} 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: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + 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 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + mrmime@2.0.1: {} ms@2.1.3: {} + mylas@2.1.13: {} + + nano-spawn@1.0.2: {} + nanoid@3.3.11: {} + natural-compare@1.4.0: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + nwsapi@2.2.20: {} + object-assign@4.1.1: {} + + 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.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 + + 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 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.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: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.3: {} + pidtree@0.6.0: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + possible-typed-array-names@1.1.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 ansi-styles: 5.2.0 react-is: 17.0.2 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} + queue-lit@1.5.2: {} + + 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@16.13.1: {} + react-is@17.0.2: {} + react-refresh@0.17.0: {} + + react@19.1.1: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + redent@3.0.0: dependencies: 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-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.5: + 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 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + rollup@4.45.1: dependencies: '@types/estree': 1.0.8 @@ -1379,40 +4289,214 @@ 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 + 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 + 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 + 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: {} + sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 + slash@3.0.0: {} + + 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: {} + 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: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string.prototype.matchall@4.0.12: + 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 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.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 + strip-json-comments@3.1.1: {} + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + 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 +4518,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 +4532,106 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.2.4: + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.10.1 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + 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 + + typescript-eslint@8.38.0(eslint@9.32.0)(typescript@5.8.3): + dependencies: + '@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) + '@typescript-eslint/parser': 8.38.0(eslint@9.32.0)(typescript@5.8.3) + '@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) + eslint: 9.32.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + 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 + + universalify@2.0.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 + + 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 +4646,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 +4656,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 +4680,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 +4717,72 @@ 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 + 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: {} + + yallist@3.1.1: {} + + yaml@2.8.0: {} + + yocto-queue@0.1.0: {} diff --git a/src/App.css b/src/App.css new file mode 100644 index 000000000..b4fd97112 --- /dev/null +++ b/src/App.css @@ -0,0 +1,44 @@ +/* 기본 스타일 */ +body { + margin: 0; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', + 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* 애니메이션 */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.3s ease-out; +} + +/* 버튼 호버 효과 */ +button { + transition: all 0.2s ease-in-out; +} + +button:hover { + transform: translateY(-1px); +} + +/* 카드 호버 효과 */ +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 000000000..981b54544 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,127 @@ +import React, { useState } from 'react'; +import { ProductSelector } from './components/ProductSelector'; +import { Cart } from './components/Cart'; +import { AddToCartButton } from './components/AddToCartButton'; +import { OrderSummary } from './components/OrderSummary'; +import { TuesdayBanner } from './components/TuesdayBanner'; +import { useProducts } from './hooks/useProducts'; +import { useCart } from './hooks/useCart'; +import { Product } from './types'; +import './App.css'; + +function App() { + const { products, updateStock, restoreStock, getProduct } = useProducts(); + const { + cartItems, + totalItems, + totalAmount, + addToCart, + removeFromCart, + increaseQuantity, + decreaseQuantity, + } = useCart(); + + const [selectedProductId, setSelectedProductId] = useState(''); + + const selectedProduct = selectedProductId ? getProduct(selectedProductId) || null : null; + + const handleProductSelect = (productId: string) => { + setSelectedProductId(productId); + }; + + const handleAddToCart = (product: Product) => { + // 재고 확인 + if (product.stock > 0) { + addToCart(product); + updateStock(product.id, 1); + setSelectedProductId(''); // 선택 초기화 + } + }; + + const handleRemoveFromCart = (productId: string) => { + const item = cartItems.find((item) => item.product.id === productId); + if (item) { + restoreStock(productId, item.quantity); + removeFromCart(productId); + } + }; + + const handleIncreaseQuantity = (productId: string) => { + const item = cartItems.find((item) => item.product.id === productId); + if (item && item.product.stock > item.quantity) { + increaseQuantity(productId); + updateStock(productId, 1); + } + }; + + const handleDecreaseQuantity = (productId: string) => { + const item = cartItems.find((item) => item.product.id === productId); + if (item) { + decreaseQuantity(productId); + restoreStock(productId, 1); + } + }; + + return ( +
+ {/* 헤더 */} +
+
+

🛒 Hanghae Online Store

+
+
+ + {/* 메인 컨텐츠 */} +
+ {/* 화요일 할인 배너 */} + + +
+ {/* 좌측: 상품 선택 및 장바구니 */} +
+
+

상품 선택

+
+ + +
+
+ +
+

장바구니

+ +
+
+ + {/* 우측: 주문 요약 */} +
+
+ +
+ +
+

포인트 적립

+

포인트 적립 기능이 여기에 들어갈 예정입니다.

+
+
+
+
+ + {/* 도움말 버튼 */} + +
+ ); +} + +export default App; diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx new file mode 100644 index 000000000..c1363a713 --- /dev/null +++ b/src/advanced/App.tsx @@ -0,0 +1,21 @@ +import ShoppingCart from './components/cart/ShoppingCart'; +import GuideToggle from './components/guide/GuideToggle'; +import Header from './components/layout/Header'; +import Layout from './components/layout/Layout'; +import OrderSummary from './components/order/OrderSummary'; +import { CartProvider } from './contexts/CartContext'; + +const App = () => { + return ( + +
+ + + + + + + ); +}; + +export default App; diff --git a/src/advanced/components/cart/ProductPicker.tsx b/src/advanced/components/cart/ProductPicker.tsx new file mode 100644 index 000000000..ea60d19f5 --- /dev/null +++ b/src/advanced/components/cart/ProductPicker.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; + +import { useCart } from '../../contexts/CartContext'; +import { PRODUCTS } from '../../lib/product'; +import { Product } from '../../lib/product'; + +const ProductPicker = () => { + const { products, addToCart, setSelectedProduct } = useCart(); + const [selectedProduct, setSelectedProductLocal] = useState(''); + + const handleProductSelect = (productId: string, event?: React.ChangeEvent) => { + event?.stopPropagation(); + setSelectedProductLocal(productId); + setSelectedProduct(productId); + }; + + const handleAddToCart = (event?: React.MouseEvent) => { + event?.stopPropagation(); + if (selectedProduct) { + addToCart(selectedProduct); + } + }; + + const getProductDisplayName = (product: Product) => { + const baseName = product.name; + const originalPrice = product.price.toLocaleString(); + const currentPrice = Math.round(product.price * (1 - product.discount)).toLocaleString(); + + let displayName = ''; + let discountText = ''; + + // 할인 상태에 따른 텍스트 추가 + if (product.lightningSale && product.recommendationSale) { + discountText = '⚡💝'; + displayName = `${discountText}${baseName} - ${originalPrice}원 → ${currentPrice}원 (25% SUPER SALE!)`; + } else if (product.lightningSale) { + discountText = '⚡'; + displayName = `${discountText}${baseName} - ${originalPrice}원 → ${currentPrice}원 (20% SALE!)`; + } else if (product.recommendationSale) { + discountText = '💝'; + displayName = `${discountText}${baseName} - ${originalPrice}원 → ${currentPrice}원 (5% 추천할인!)`; + } else { + displayName = `${baseName} - ${currentPrice}원`; + } + + // 품절 상태 확인 + if (product.stock === 0) { + displayName += ' (품절)'; + } + + return displayName; + }; + + const getProductClassName = (product: Product) => { + if (product.stock === 0) { + return 'text-gray-400'; + } + + if (product.lightningSale && product.recommendationSale) { + return 'text-purple-600 font-bold'; + } else if (product.lightningSale) { + return 'text-red-500 font-bold'; + } else if (product.recommendationSale) { + return 'text-blue-500 font-bold'; + } + + return ''; + }; + + const getStockWarningMessage = () => { + const lowStockProducts = products.filter((product) => product.stock < 5 && product.stock > 0); + const outOfStockProducts = products.filter((product) => product.stock === 0); + + let message = ''; + + lowStockProducts.forEach((product) => { + message += `${product.name}: 재고 부족 (${product.stock}개 남음)\n`; + }); + + outOfStockProducts.forEach((product) => { + message += `${product.name}: 품절\n`; + }); + + return message; + }; + + const getTotalStock = () => { + return products.reduce((total, product) => total + product.stock, 0); + }; + + const totalStock = getTotalStock(); + const stockWarningMessage = getStockWarningMessage(); + + return ( +
+ + +
+ {stockWarningMessage} +
+
+ ); +}; + +export default ProductPicker; diff --git a/src/advanced/components/cart/ShoppingCart.tsx b/src/advanced/components/cart/ShoppingCart.tsx new file mode 100644 index 000000000..a3556a12f --- /dev/null +++ b/src/advanced/components/cart/ShoppingCart.tsx @@ -0,0 +1,148 @@ +import { useCart } from '../../contexts/CartContext'; +import { Product } from '../../lib/product'; +import ProductPicker from './ProductPicker'; + +const ShoppingCart = () => { + const { cartItems, updateQuantity, removeFromCart } = useCart(); + + const handleQuantityChange = (productId: string, change: number, event?: React.MouseEvent) => { + event?.stopPropagation(); + const item = cartItems.find((item) => item.product.id === productId); + if (item) { + const newQuantity = item.quantity + change; + updateQuantity(productId, newQuantity); + } + }; + + const handleRemoveItem = (productId: string, event?: React.MouseEvent) => { + event?.stopPropagation(); + removeFromCart(productId); + }; + + const getProductDisplayName = (product: Product) => { + let icon = ''; + if (product.lightningSale && product.recommendationSale) { + icon = '⚡💝'; + } else if (product.lightningSale) { + icon = '⚡'; + } else if (product.recommendationSale) { + icon = '💝'; + } + return `${icon}${product.name}`; + }; + + const getPriceDisplay = (product: Product) => { + const originalPrice = product.price; + const discountedPrice = Math.round(product.price * (1 - product.discount)); + + if (product.lightningSale || product.recommendationSale) { + let colorClass = ''; + if (product.lightningSale && product.recommendationSale) { + colorClass = 'text-purple-600'; + } else if (product.lightningSale) { + colorClass = 'text-red-500'; + } else if (product.recommendationSale) { + colorClass = 'text-blue-500'; + } + + return ( + <> + ₩{originalPrice.toLocaleString()}{' '} + ₩{discountedPrice.toLocaleString()} + + ); + } + + return `₩${originalPrice.toLocaleString()}`; + }; + + const getTotalPriceDisplay = (product: Product, quantity: number) => { + const originalTotal = product.price * quantity; + const discountedTotal = Math.round(product.price * (1 - product.discount) * quantity); + + if (product.lightningSale || product.recommendationSale) { + let colorClass = ''; + if (product.lightningSale && product.recommendationSale) { + colorClass = 'text-purple-600'; + } else if (product.lightningSale) { + colorClass = 'text-red-500'; + } else if (product.recommendationSale) { + colorClass = 'text-blue-500'; + } + + return ( + <> + ₩{originalTotal.toLocaleString()}{' '} + ₩{discountedTotal.toLocaleString()} + + ); + } + + return `₩${originalTotal.toLocaleString()}`; + }; + + return ( +
+ +
+ {cartItems.length === 0 ? ( +
장바구니가 비어있습니다.
+ ) : ( + cartItems.map((item) => ( +
+
+
+
+
+

+ {getProductDisplayName(item.product)} +

+

PRODUCT

+

{getPriceDisplay(item.product)}

+
+ + + {item.quantity} + + +
+
+
+
+ {getTotalPriceDisplay(item.product, item.quantity)} +
+ +
+
+ )) + )} +
+
+ ); +}; + +export default ShoppingCart; diff --git a/src/advanced/components/guide/GuideToggle.tsx b/src/advanced/components/guide/GuideToggle.tsx new file mode 100644 index 000000000..c74400aee --- /dev/null +++ b/src/advanced/components/guide/GuideToggle.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; + +import ShoppingGuide from './ShoppingGuide'; + +const GuideToggle = () => { + const [isOpen, setIsOpen] = useState(false); + + const handleToggle = (event?: React.MouseEvent) => { + event?.stopPropagation(); + setIsOpen((prev: boolean) => !prev); + }; + + const handleClose = (event?: React.MouseEvent) => { + event?.stopPropagation(); + setIsOpen(false); + }; + + return ( + <> + + + + ); +}; + +export default GuideToggle; diff --git a/src/advanced/components/guide/ShoppingGuide.tsx b/src/advanced/components/guide/ShoppingGuide.tsx new file mode 100644 index 000000000..1c613dbf3 --- /dev/null +++ b/src/advanced/components/guide/ShoppingGuide.tsx @@ -0,0 +1,115 @@ +interface ShoppingGuideProps { + isOpen: boolean; + onClose: () => void; +} + +const ShoppingGuide = ({ isOpen, onClose }: ShoppingGuideProps) => { + const handleOverlayClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const handleCloseClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClose(); + }; + + if (!isOpen) return null; + + return ( + <> + {/* Overlay */} +
+ + {/* Manual Column */} +
+ + +

📖 이용 안내

+ +
+

💰 할인 정책

+
+
+

개별 상품

+

+ • 키보드 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 = 품절 +

+
+
+ + ); +}; + +export default ShoppingGuide; diff --git a/src/advanced/components/layout/Header.tsx b/src/advanced/components/layout/Header.tsx new file mode 100644 index 000000000..bacd0dead --- /dev/null +++ b/src/advanced/components/layout/Header.tsx @@ -0,0 +1,21 @@ +import { useCart } from '../../contexts/CartContext'; + +const Header = () => { + const { cartItems } = useCart(); + + const itemCount = cartItems.reduce((total, item) => total + item.quantity, 0); + + return ( +
+

+ 🛒 Hanghae Online Store +

+
Shopping Cart
+

+ 🛍️ {itemCount} items in cart +

+
+ ); +}; + +export default Header; diff --git a/src/advanced/components/layout/Layout.tsx b/src/advanced/components/layout/Layout.tsx new file mode 100644 index 000000000..efc1cf093 --- /dev/null +++ b/src/advanced/components/layout/Layout.tsx @@ -0,0 +1,9 @@ +const Layout = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +export default Layout; diff --git a/src/advanced/components/order/OrderSummary.tsx b/src/advanced/components/order/OrderSummary.tsx new file mode 100644 index 000000000..6a558d897 --- /dev/null +++ b/src/advanced/components/order/OrderSummary.tsx @@ -0,0 +1,141 @@ +import { useCart } from '../../contexts/CartContext'; + +const OrderSummary = () => { + const { cartItems, getDiscountedAmount, getDiscountBreakdown, getPoints } = useCart(); + + const calculateSubtotal = () => { + return cartItems.reduce((total, item) => { + return total + item.product.price * item.quantity; + }, 0); + }; + + const subtotal = calculateSubtotal(); + const total = getDiscountedAmount(); + const points = getPoints(); + const isTuesday = new Date().getDay() === 2; + const discountBreakdown = getDiscountBreakdown(); + + return ( +
+

Order Summary

+
+
+ {cartItems.length === 0 ? ( +
+ 장바구니가 비어있습니다. +
+ ) : ( + cartItems.map((item) => { + const originalPrice = item.product.price * item.quantity; + const discountedPrice = originalPrice * (1 - item.product.discount); + return ( +
+ + {item.product.name} x {item.quantity} + + ₩{Math.round(discountedPrice).toLocaleString()} +
+ ); + }) + )} + + {cartItems.length > 0 && ( + <> +
+
+ Subtotal + ₩{subtotal.toLocaleString()} +
+ + {/* 할인 정보 표시 */} + {discountBreakdown.individualDiscount > 0 && ( +
+ 개별 상품 할인 + -₩{discountBreakdown.individualDiscount.toLocaleString()} +
+ )} + + {discountBreakdown.lightningSaleDiscount > 0 && ( +
+ ⚡ 번개세일 할인 + -₩{discountBreakdown.lightningSaleDiscount.toLocaleString()} +
+ )} + + {discountBreakdown.recommendationDiscount > 0 && ( +
+ 💝 추천할인 + -₩{discountBreakdown.recommendationDiscount.toLocaleString()} +
+ )} + + {discountBreakdown.totalBulkDiscount > 0 && ( +
+ 전체 수량 할인 (30개 이상) + -₩{discountBreakdown.totalBulkDiscount.toLocaleString()} +
+ )} + + {discountBreakdown.tuesdayDiscount > 0 && ( +
+ 화요일 특별 할인 + -₩{discountBreakdown.tuesdayDiscount.toLocaleString()} +
+ )} + +
+ Shipping + Free +
+ + )} +
+
+
+
+
+ Total +
₩{Math.round(total).toLocaleString()}
+
+ {cartItems.length > 0 && ( +
+
+ 적립 포인트: {points.total}p +
+
+ 기본: {points.base}p{points.tuesday > 0 && `, 화요일 보너스 +${points.tuesday}p`} + {points.set > 0 && `, 키보드+마우스 세트 +${points.set}p`} + {points.fullSet > 0 && `, 풀세트 구매 +${points.fullSet}p`} +
+
+ )} +
+ {isTuesday && cartItems.length > 0 && ( +
+
+ 🎉 + Tuesday Special 10% Applied +
+
+ )} +
+
+ +

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

+
+ ); +}; + +export default OrderSummary; diff --git a/src/advanced/contexts/CartContext.tsx b/src/advanced/contexts/CartContext.tsx new file mode 100644 index 000000000..f4630afc9 --- /dev/null +++ b/src/advanced/contexts/CartContext.tsx @@ -0,0 +1,597 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; + +import { + calculateFinalDiscount, + calculateIndividualDiscount, + calculateLightningSaleDiscount, + calculateRecommendationDiscount, + calculateTotalBulkDiscount, + calculateTuesdayDiscount, + Discount, + getDiscountStyle, +} from '../lib/discount'; +import { CartItem, initialProducts, Product } from '../lib/product'; + +interface CartContextType { + products: Product[]; + cartItems: CartItem[]; + selectedProductId: string | null; + addToCart: (productId: string) => void; + removeFromCart: (productId: string) => void; + updateQuantity: (productId: string, quantity: number) => void; + setSelectedProduct: (productId: string | null) => void; + getCartItemCount: () => number; + getTotalAmount: () => number; + getDiscountedAmount: () => number; + getAppliedDiscounts: () => Discount[]; + getDiscountBreakdown: () => { + subtotal: number; + individualDiscount: number; + totalBulkDiscount: number; + tuesdayDiscount: number; + lightningSaleDiscount: number; + recommendationDiscount: number; + finalAmount: number; + }; + getPoints: () => { + base: number; + tuesday: number; + set: number; + fullSet: number; + total: number; + }; + getDiscountStyle: (productId: string) => { icon: string; className: string }; + lightningSaleProductId: string | null; + recommendationProductId: string | null; +} + +const CartContext = createContext(undefined); + +export const useCart = (): CartContextType => { + const context = useContext(CartContext); + if (!context) { + throw new Error('useCart must be used within a CartProvider'); + } + return context; +}; + +interface CartProviderProps { + children: ReactNode; +} + +export const CartProvider = ({ children }: CartProviderProps) => { + const [products, setProducts] = useState(initialProducts); + const [cartItems, setCartItems] = useState([]); + const [selectedProductId, setSelectedProductId] = useState(null); + const [lightningSaleProductId, setLightningSaleProductId] = useState(null); + const [recommendationProductId, setRecommendationProductId] = useState(null); + const [lastSelectedProduct, setLastSelectedProduct] = useState(null); + + // 타이머 관리를 위한 ref + const lightningSaleTimerRef = useRef(null); + const recommendationTimerRef = useRef(null); + const initialLightningTimerRef = useRef(null); + const initialRecommendationTimerRef = useRef(null); + + // 무작위 상품 선택 (재고가 있는 상품만) + const getRandomProductWithStock = useCallback(() => { + const availableProducts = products.filter((product) => product.stock > 0); + if (availableProducts.length === 0) return null; + + const randomIndex = Math.floor(Math.random() * availableProducts.length); + return availableProducts[randomIndex].id; + }, [products]); + + // 번개세일 시작 + const startLightningSale = useCallback(() => { + const productId = getRandomProductWithStock(); + if (!productId) return; + + // 이미 번개세일 중인 상품은 제외 (원본과 동일) + const product = products.find((p: Product) => p.id === productId); + if (!product || product.lightningSale) return; + + setLightningSaleProductId(productId); + + // 상품에 번개세일 상태 적용 + setProducts((prevProducts: Product[]) => + prevProducts.map((product: Product) => + product.id === productId + ? { ...product, lightningSale: true, price: Math.round(product.price * 0.8) } + : product, + ), + ); + + // 알림창 즉시 표시 (원본과 동일) + alert(`⚡번개세일! ${product.name}이(가) 20% 할인 중입니다!`); + }, [products]); // getRandomProductWithStock 제거, products만 의존 + + // 추천할인 시작 + const startRecommendation = useCallback(() => { + if (!lastSelectedProduct) return; // 마지막 선택 상품이 없으면 실행하지 않음 (원본과 동일) + + // 마지막 선택 상품과 다른 상품 선택 + const otherProducts = products.filter( + (product: Product) => + product.id !== lastSelectedProduct && product.stock > 0 && !product.recommendationSale, + ); + + if (otherProducts.length === 0) return; + + const randomIndex = Math.floor(Math.random() * otherProducts.length); + const productId = otherProducts[randomIndex].id; + + setRecommendationProductId(productId); + + // 상품에 추천할인 상태 적용 + setProducts((prevProducts: Product[]) => + prevProducts.map((product: Product) => + product.id === productId + ? { ...product, recommendationSale: true, price: Math.round(product.price * 0.95) } + : product, + ), + ); + + // 알림창 즉시 표시 (원본과 동일) + const product = products.find((p: Product) => p.id === productId); + if (product) { + alert(`💝 ${product.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`); + } + }, [lastSelectedProduct, products]); + + // 번개세일 종료 + const stopLightningSale = useCallback(() => { + if (lightningSaleProductId) { + setProducts((prevProducts: Product[]) => + prevProducts.map((product: Product) => + product.id === lightningSaleProductId + ? { ...product, lightningSale: false, price: product.price / 0.8 } + : product, + ), + ); + } + setLightningSaleProductId(null); + }, [lightningSaleProductId]); + + // 추천할인 종료 + const stopRecommendation = useCallback(() => { + if (recommendationProductId) { + setProducts((prevProducts: Product[]) => + prevProducts.map((product: Product) => + product.id === recommendationProductId + ? { ...product, recommendationSale: false, price: product.price / 0.95 } + : product, + ), + ); + } + setRecommendationProductId(null); + }, [recommendationProductId]); + + // 번개세일 타이머 (30초마다 - 원본과 동일) + useEffect(() => { + // 기존 타이머 정리 + if (lightningSaleTimerRef.current) { + clearInterval(lightningSaleTimerRef.current); + } + + const lightningSaleTimer = setInterval(() => { + if (lightningSaleProductId) { + stopLightningSale(); + } + startLightningSale(); + }, 30000); // 30초로 변경 (원본과 동일) + + lightningSaleTimerRef.current = lightningSaleTimer; + + return () => { + if (lightningSaleTimerRef.current) { + clearInterval(lightningSaleTimerRef.current); + } + }; + }, []); // 의존성 배열을 비워서 한 번만 실행 + + // 추천할인 타이머 (60초마다 - 원본과 동일) + useEffect(() => { + // 기존 타이머 정리 + if (recommendationTimerRef.current) { + clearInterval(recommendationTimerRef.current); + } + + const recommendationTimer = setInterval(() => { + if (recommendationProductId) { + stopRecommendation(); + } + startRecommendation(); + }, 60000); // 60초로 변경 (원본과 동일) + + recommendationTimerRef.current = recommendationTimer; + + return () => { + if (recommendationTimerRef.current) { + clearInterval(recommendationTimerRef.current); + } + }; + }, []); // 의존성 배열을 비워서 한 번만 실행 + + // 초기 번개세일 시작 (0~10초 사이 - 원본과 동일) + useEffect(() => { + // 기존 타이머 정리 + if (initialLightningTimerRef.current) { + clearTimeout(initialLightningTimerRef.current); + } + + const initialDelay = Math.random() * 10000; // 0~10초 (원본과 동일) + const timer = setTimeout(() => { + startLightningSale(); + }, initialDelay); + + initialLightningTimerRef.current = timer; + + return () => { + if (initialLightningTimerRef.current) { + clearTimeout(initialLightningTimerRef.current); + } + }; + }, []); // 의존성 배열을 비워서 한 번만 실행 + + // 초기 추천할인 시작 (0~20초 사이 - 원본과 동일) + useEffect(() => { + // 기존 타이머 정리 + if (initialRecommendationTimerRef.current) { + clearTimeout(initialRecommendationTimerRef.current); + } + + const initialDelay = Math.random() * 20000; // 0~20초 (원본과 동일) + const timer = setTimeout(() => { + startRecommendation(); + }, initialDelay); + + initialRecommendationTimerRef.current = timer; + + return () => { + if (initialRecommendationTimerRef.current) { + clearTimeout(initialRecommendationTimerRef.current); + } + }; + }, []); // 의존성 배열을 비워서 한 번만 실행 + + // 장바구니에 상품 추가 + const addToCart = useCallback( + (productId: string) => { + const product = products.find((p: Product) => p.id === productId); + + if (!product) { + return; + } + + if (product.stock === 0) { + return; + } + + setCartItems((prevItems: CartItem[]) => { + const existingItem = prevItems.find((item: CartItem) => item.product.id === productId); + + if (existingItem) { + // 이미 있는 상품이면 수량 증가 + if (existingItem.quantity < product.stock) { + return prevItems.map((item: CartItem) => + item.product.id === productId ? { ...item, quantity: item.quantity + 1 } : item, + ); + } + return prevItems; // 재고 부족 + } else { + // 새 상품 추가 + const newItem = { + product, + quantity: 1, + appliedDiscounts: [], + }; + return [...prevItems, newItem]; + } + }); + + // 재고 감소 + setProducts((prevProducts: Product[]) => + prevProducts.map((p: Product) => (p.id === productId ? { ...p, stock: p.stock - 1 } : p)), + ); + }, + [products], + ); + + // 장바구니에서 상품 제거 + const removeFromCart = useCallback((productId: string) => { + setCartItems((prevItems: CartItem[]) => { + const itemToRemove = prevItems.find((item: CartItem) => item.product.id === productId); + if (!itemToRemove) return prevItems; + + // 재고 복구 + setProducts((prevProducts: Product[]) => + prevProducts.map((p: Product) => + p.id === productId ? { ...p, stock: p.stock + itemToRemove.quantity } : p, + ), + ); + + return prevItems.filter((item: CartItem) => item.product.id !== productId); + }); + }, []); + + // 수량 변경 + const updateQuantity = useCallback( + (productId: string, newQuantity: number) => { + if (newQuantity <= 0) { + removeFromCart(productId); + return; + } + + const product = products.find((p: Product) => p.id === productId); + if (!product) return; + + const currentItem = cartItems.find((item: CartItem) => item.product.id === productId); + if (!currentItem) return; + + const quantityDiff = newQuantity - currentItem.quantity; + const availableStock = product.stock + currentItem.quantity; + + if (newQuantity > availableStock) return; // 재고 초과 + + setCartItems((prevItems: CartItem[]) => + prevItems.map((item: CartItem) => + item.product.id === productId ? { ...item, quantity: newQuantity } : item, + ), + ); + + // 재고 조정 + setProducts((prevProducts: Product[]) => + prevProducts.map((p: Product) => + p.id === productId ? { ...p, stock: p.stock - quantityDiff } : p, + ), + ); + }, + [products, cartItems, removeFromCart], + ); + + // 선택된 상품 설정 + const setSelectedProduct = useCallback((productId: string | null) => { + setSelectedProductId(productId); + if (productId) { + setLastSelectedProduct(productId); + } + }, []); + + // 장바구니 아이템 개수 + const getCartItemCount = useCallback(() => { + return cartItems.reduce((total: number, item: CartItem) => total + item.quantity, 0); + }, [cartItems]); + + // 총 금액 계산 (할인 적용 전) + const getTotalAmount = useCallback(() => { + return cartItems.reduce((total: number, item: CartItem) => { + return total + item.product.price * item.quantity; + }, 0); + }, [cartItems]); + + // 할인 적용된 최종 금액 계산 + const getDiscountedAmount = useCallback(() => { + const subtotal = getTotalAmount(); + const totalQuantity = getCartItemCount(); + + // 개별 상품 할인 계산 + const individualDiscounts = cartItems.map((item: CartItem) => + calculateIndividualDiscount(item.product.price, item.quantity, item.product.discount), + ); + + // 번개세일 할인 계산 + const lightningSaleDiscounts = cartItems.map((item: CartItem) => + calculateLightningSaleDiscount( + item.product.id, + item.product.price, + item.quantity, + lightningSaleProductId, + ), + ); + + // 추천할인 계산 + const recommendationDiscounts = cartItems.map((item: CartItem) => + calculateRecommendationDiscount( + item.product.id, + item.product.price, + item.quantity, + recommendationProductId, + ), + ); + + const discountResult = calculateFinalDiscount( + subtotal, + totalQuantity, + individualDiscounts, + lightningSaleDiscounts.reduce((sum: number, discount: number): number => sum + discount, 0), + recommendationDiscounts.reduce((sum: number, discount: number): number => sum + discount, 0), + ); + return discountResult.finalAmount; + }, [ + cartItems, + getTotalAmount, + getCartItemCount, + lightningSaleProductId, + recommendationProductId, + ]); + + // 적용된 할인 목록 (현재는 빈 배열, 추후 확장) + const getAppliedDiscounts = useCallback(() => { + return [] as Discount[]; + }, []); + + // 할인 세부 정보 제공 + const getDiscountBreakdown = useCallback(() => { + const subtotal = getTotalAmount(); + const totalQuantity = getCartItemCount(); + + // 개별 상품 할인 계산 + const individualDiscounts = cartItems.map((item: CartItem) => + calculateIndividualDiscount(item.product.price, item.quantity, item.product.discount), + ); + const individualDiscount = individualDiscounts.reduce( + (sum: number, discount: number): number => sum + discount, + 0, + ); + + // 번개세일 할인 계산 + const lightningSaleDiscounts = cartItems.map((item: CartItem) => + calculateLightningSaleDiscount( + item.product.id, + item.product.price, + item.quantity, + lightningSaleProductId, + ), + ); + const lightningSaleDiscount = lightningSaleDiscounts.reduce( + (sum: number, discount: number): number => sum + discount, + 0, + ); + + // 추천할인 계산 + const recommendationDiscounts = cartItems.map((item: CartItem) => + calculateRecommendationDiscount( + item.product.id, + item.product.price, + item.quantity, + recommendationProductId, + ), + ); + const recommendationDiscount = recommendationDiscounts.reduce( + (sum: number, discount: number): number => sum + discount, + 0, + ); + + // 개별 할인 적용 후 금액 + const afterIndividualDiscount = subtotal - individualDiscount; + + // 전체 수량 할인 계산 + const totalBulkDiscount = calculateTotalBulkDiscount(afterIndividualDiscount, totalQuantity); + + // 화요일 할인 계산 + const tuesdayDiscount = calculateTuesdayDiscount( + afterIndividualDiscount - totalBulkDiscount - lightningSaleDiscount - recommendationDiscount, + ); + + const finalAmount = + afterIndividualDiscount - + totalBulkDiscount - + lightningSaleDiscount - + recommendationDiscount - + tuesdayDiscount; + + return { + subtotal, + individualDiscount, + totalBulkDiscount, + tuesdayDiscount, + lightningSaleDiscount, + recommendationDiscount, + finalAmount, + }; + }, [ + cartItems, + getTotalAmount, + getCartItemCount, + lightningSaleProductId, + recommendationProductId, + ]); + + // 포인트 계산 (original과 동일) + const getPoints = useCallback(() => { + const finalAmount = getDiscountedAmount(); + const basePoints = Math.floor(finalAmount / 1000); + let finalPoints = 0; + const pointsDetail: string[] = []; + + // 기본 포인트 + if (basePoints > 0) { + finalPoints = basePoints; + pointsDetail.push(`기본: ${basePoints}p`); + } + + // 화요일 2배 + const today = new Date(); + const isTuesday = today.getDay() === 2; + if (isTuesday && basePoints > 0) { + finalPoints = basePoints * 2; + pointsDetail.push('화요일 2배'); + } + + // 세트 구매 보너스 (키보드 + 마우스) + const hasKeyboard = cartItems.some((item: CartItem) => item.product.id === 'p1'); + const hasMouse = cartItems.some((item: CartItem) => item.product.id === 'p2'); + if (hasKeyboard && hasMouse) { + finalPoints += 50; + pointsDetail.push('키보드+마우스 세트 +50p'); + } + + // 풀세트 구매 보너스 (키보드 + 마우스 + 모니터암) + const hasMonitorArm = cartItems.some((item: CartItem) => item.product.id === 'p3'); + if (hasKeyboard && hasMouse && hasMonitorArm) { + finalPoints += 100; + pointsDetail.push('풀세트 구매 +100p'); + } + + // 대량구매 보너스 + const totalQuantity = getCartItemCount(); + if (totalQuantity >= 30) { + finalPoints += 100; + pointsDetail.push('대량구매(30개+) +100p'); + } else if (totalQuantity >= 20) { + finalPoints += 50; + pointsDetail.push('대량구매(20개+) +50p'); + } else if (totalQuantity >= 10) { + finalPoints += 20; + pointsDetail.push('대량구매(10개+) +20p'); + } + + return { + base: basePoints, + tuesday: isTuesday && basePoints > 0 ? basePoints : 0, + set: hasKeyboard && hasMouse ? 50 : 0, + fullSet: hasKeyboard && hasMouse && hasMonitorArm ? 100 : 0, + bulk: totalQuantity >= 30 ? 100 : totalQuantity >= 20 ? 50 : totalQuantity >= 10 ? 20 : 0, + total: finalPoints, + details: pointsDetail, + }; + }, [cartItems, getDiscountedAmount, getCartItemCount]); + + // 할인 스타일 가져오기 + const getDiscountStyleForProduct = useCallback( + (productId: string) => { + return getDiscountStyle(productId, lightningSaleProductId, recommendationProductId); + }, + [lightningSaleProductId, recommendationProductId], + ); + + const value = { + products, + cartItems, + selectedProductId, + addToCart, + removeFromCart, + updateQuantity, + setSelectedProduct, + getCartItemCount, + getTotalAmount, + getDiscountedAmount, + getAppliedDiscounts, + getDiscountBreakdown, + getPoints, + getDiscountStyle: getDiscountStyleForProduct, + lightningSaleProductId, + recommendationProductId, + }; + + return {children}; +}; diff --git a/src/advanced/hooks/useCart.ts b/src/advanced/hooks/useCart.ts new file mode 100644 index 000000000..4e49a872c --- /dev/null +++ b/src/advanced/hooks/useCart.ts @@ -0,0 +1,147 @@ +import { useState, useCallback } from 'react'; +import { Product, CartItem, initialProducts } from '../lib/product'; + +interface UseCartReturn { + products: Product[]; + cartItems: CartItem[]; + selectedProductId: string | null; + addToCart: (productId: string) => void; + removeFromCart: (productId: string) => void; + updateQuantity: (productId: string, quantity: number) => void; + setSelectedProduct: (productId: string | null) => void; + getCartItemCount: () => number; + getTotalAmount: () => number; +} + +export const useCart = (): UseCartReturn => { + const [products, setProducts] = useState(initialProducts); + const [cartItems, setCartItems] = useState([]); + const [selectedProductId, setSelectedProductId] = useState(null); + + // 장바구니에 상품 추가 + const addToCart = useCallback( + (productId: string) => { + console.log('addToCart called with productId:', productId); // 디버깅용 + const product = products.find((p: Product) => p.id === productId); + console.log('found product:', product); // 디버깅용 + + if (!product) { + console.log('Product not found'); // 디버깅용 + return; + } + + if (product.stock === 0) { + console.log('Product out of stock'); // 디버깅용 + return; + } + + setCartItems((prevItems: CartItem[]) => { + const existingItem = prevItems.find((item: CartItem) => item.product.id === productId); + + if (existingItem) { + // 이미 있는 상품이면 수량 증가 + if (existingItem.quantity < product.stock) { + return prevItems.map((item: CartItem) => + item.product.id === productId ? { ...item, quantity: item.quantity + 1 } : item, + ); + } + return prevItems; // 재고 부족 + } else { + // 새 상품 추가 + const newItem = { + product, + quantity: 1, + appliedDiscounts: [], + }; + console.log('Adding new item to cart:', newItem); // 디버깅용 + return [...prevItems, newItem]; + } + }); + + // 재고 감소 + setProducts((prevProducts: Product[]) => + prevProducts.map((p: Product) => (p.id === productId ? { ...p, stock: p.stock - 1 } : p)), + ); + }, + [products], + ); + + // 장바구니에서 상품 제거 + const removeFromCart = useCallback((productId: string) => { + setCartItems((prevItems: CartItem[]) => { + const itemToRemove = prevItems.find((item: CartItem) => item.product.id === productId); + if (!itemToRemove) return prevItems; + + // 재고 복구 + setProducts((prevProducts: Product[]) => + prevProducts.map((p: Product) => + p.id === productId ? { ...p, stock: p.stock + itemToRemove.quantity } : p, + ), + ); + + return prevItems.filter((item: CartItem) => item.product.id !== productId); + }); + }, []); + + // 수량 변경 + const updateQuantity = useCallback( + (productId: string, newQuantity: number) => { + if (newQuantity <= 0) { + removeFromCart(productId); + return; + } + + const product = products.find((p: Product) => p.id === productId); + if (!product) return; + + const currentItem = cartItems.find((item: CartItem) => item.product.id === productId); + if (!currentItem) return; + + const quantityDiff = newQuantity - currentItem.quantity; + const availableStock = product.stock + currentItem.quantity; + + if (newQuantity > availableStock) return; // 재고 초과 + + setCartItems((prevItems: CartItem[]) => + prevItems.map((item: CartItem) => + item.product.id === productId ? { ...item, quantity: newQuantity } : item, + ), + ); + + // 재고 조정 + setProducts((prevProducts: Product[]) => + prevProducts.map((p: Product) => (p.id === productId ? { ...p, stock: p.stock - quantityDiff } : p)), + ); + }, + [products, cartItems, removeFromCart], + ); + + // 선택된 상품 설정 + const setSelectedProduct = useCallback((productId: string | null) => { + setSelectedProductId(productId); + }, []); + + // 장바구니 아이템 개수 + const getCartItemCount = useCallback(() => { + return cartItems.reduce((total: number, item: CartItem) => total + item.quantity, 0); + }, [cartItems]); + + // 총 금액 계산 (할인 적용 전) + const getTotalAmount = useCallback(() => { + return cartItems.reduce((total: number, item: CartItem) => { + return total + item.product.price * item.quantity; + }, 0); + }, [cartItems]); + + return { + products, + cartItems, + selectedProductId, + addToCart, + removeFromCart, + updateQuantity, + setSelectedProduct, + getCartItemCount, + getTotalAmount, + }; +}; diff --git a/src/advanced/hooks/useDiscount.ts b/src/advanced/hooks/useDiscount.ts new file mode 100644 index 000000000..6f77307a3 --- /dev/null +++ b/src/advanced/hooks/useDiscount.ts @@ -0,0 +1,160 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { DiscountState } from '../lib/discount'; +import { PRODUCTS } from '../lib/product'; + +export const useDiscount = () => { + const [discountState, setDiscountState] = useState({ + lightningSale: { + isActive: false, + productId: null, + startTime: null, + }, + recommendation: { + isActive: false, + productId: null, + startTime: null, + }, + }); + + const [lastSelectedProduct, setLastSelectedProduct] = useState(null); + + // 무작위 상품 선택 (재고가 있는 상품만) + const getRandomProductWithStock = useCallback(() => { + const availableProducts = PRODUCTS.filter((product) => product.stock > 0); + if (availableProducts.length === 0) return null; + + const randomIndex = Math.floor(Math.random() * availableProducts.length); + return availableProducts[randomIndex].id; + }, []); + + // 번개세일 시작 + const startLightningSale = useCallback(() => { + const productId = getRandomProductWithStock(); + if (!productId) return; + + setDiscountState((prev) => ({ + ...prev, + lightningSale: { + isActive: true, + productId, + startTime: Date.now(), + }, + })); + + // 알림창 표시 + const product = PRODUCTS.find((p) => p.id === productId); + if (product) { + alert(`⚡ 번개세일! ${product.name} 20% 할인!`); + } + }, [getRandomProductWithStock]); + + // 추천할인 시작 + const startRecommendation = useCallback(() => { + if (!lastSelectedProduct) return; + + // 마지막 선택 상품과 다른 상품 선택 + const otherProducts = PRODUCTS.filter( + (product) => product.id !== lastSelectedProduct && product.stock > 0, + ); + + if (otherProducts.length === 0) return; + + const randomIndex = Math.floor(Math.random() * otherProducts.length); + const productId = otherProducts[randomIndex].id; + + setDiscountState((prev) => ({ + ...prev, + recommendation: { + isActive: true, + productId, + startTime: Date.now(), + }, + })); + + // 알림창 표시 + const product = PRODUCTS.find((p) => p.id === productId); + if (product) { + alert(`💝 추천할인! ${product.name} 5% 추가 할인!`); + } + }, [lastSelectedProduct]); + + // 번개세일 종료 + const stopLightningSale = useCallback(() => { + setDiscountState((prev) => ({ + ...prev, + lightningSale: { + isActive: false, + productId: null, + startTime: null, + }, + })); + }, []); + + // 추천할인 종료 + const stopRecommendation = useCallback(() => { + setDiscountState((prev) => ({ + ...prev, + recommendation: { + isActive: false, + productId: null, + startTime: null, + }, + })); + }, []); + + // 마지막 선택 상품 업데이트 + const updateLastSelectedProduct = useCallback((productId: string) => { + setLastSelectedProduct(productId); + }, []); + + // 번개세일 타이머 (30초마다) + useEffect(() => { + const lightningSaleTimer = setInterval(() => { + if (discountState.lightningSale.isActive) { + stopLightningSale(); + } + startLightningSale(); + }, 30000); + + return () => clearInterval(lightningSaleTimer); + }, [discountState.lightningSale.isActive, startLightningSale, stopLightningSale]); + + // 추천할인 타이머 (60초마다) + useEffect(() => { + const recommendationTimer = setInterval(() => { + if (discountState.recommendation.isActive) { + stopRecommendation(); + } + startRecommendation(); + }, 60000); + + return () => clearInterval(recommendationTimer); + }, [discountState.recommendation.isActive, startRecommendation, stopRecommendation]); + + // 초기 번개세일 시작 (0~10초 사이) + useEffect(() => { + const initialDelay = Math.random() * 10000; // 0~10초 + const timer = setTimeout(() => { + startLightningSale(); + }, initialDelay); + + return () => clearTimeout(timer); + }, [startLightningSale]); + + // 초기 추천할인 시작 (0~20초 사이) + useEffect(() => { + const initialDelay = Math.random() * 20000; // 0~20초 + const timer = setTimeout(() => { + startRecommendation(); + }, initialDelay); + + return () => clearTimeout(timer); + }, [startRecommendation]); + + return { + discountState, + lastSelectedProduct, + updateLastSelectedProduct, + }; +}; diff --git a/src/advanced/lib/discount.ts b/src/advanced/lib/discount.ts new file mode 100644 index 000000000..9a5ccc32d --- /dev/null +++ b/src/advanced/lib/discount.ts @@ -0,0 +1,176 @@ +// 할인 타입 정의 +export interface Discount { + id: string; + name: string; + rate: number; + description: string; + icon?: string; +} + +// 할인 정책 상수 +export const DISCOUNT_POLICIES = { + BULK_THRESHOLD: 10, // 개별 상품 할인 기준 수량 + TOTAL_BULK_THRESHOLD: 30, // 전체 수량 할인 기준 + TUESDAY_DISCOUNT_RATE: 0.1, // 화요일 할인율 + LIGHTNING_SALE_RATE: 0.2, // 번개세일 할인율 + RECOMMENDATION_RATE: 0.05, // 추천할인 할인율 + TOTAL_BULK_RATE: 0.25, // 전체 수량 할인율 + SUPER_SALE_RATE: 0.25, // SUPER SALE 할인율 (번개세일 + 추천할인) +} as const; + +// 할인 계산 결과 +export interface DiscountResult { + subtotal: number; + appliedDiscounts: Discount[]; + finalAmount: number; + discountAmount: number; +} + +// 할인 상태 관리 +export interface DiscountState { + lightningSale: { + isActive: boolean; + productId: string | null; + startTime: number | null; + }; + recommendation: { + isActive: boolean; + productId: string | null; + startTime: number | null; + }; +} + +// 개별 상품 할인 계산 (original과 동일) +export const calculateIndividualDiscount = ( + price: number, + quantity: number, + discountRate: number, +): number => { + if (quantity >= DISCOUNT_POLICIES.BULK_THRESHOLD) { + return price * quantity * discountRate; + } + return 0; +}; + +// 전체 수량 할인 계산 (original과 동일) +export const calculateTotalBulkDiscount = (subtotal: number, totalQuantity: number): number => { + if (totalQuantity >= DISCOUNT_POLICIES.TOTAL_BULK_THRESHOLD) { + return subtotal * DISCOUNT_POLICIES.TOTAL_BULK_RATE; + } + return 0; +}; + +// 화요일 할인 계산 (original과 동일) +export const calculateTuesdayDiscount = (amount: number): number => { + const today = new Date(); + if (today.getDay() === 2) { + // 화요일 (0=일요일, 1=월요일, 2=화요일) + return amount * DISCOUNT_POLICIES.TUESDAY_DISCOUNT_RATE; + } + return 0; +}; + +// 번개세일 할인 계산 (original과 동일) +export const calculateLightningSaleDiscount = ( + productId: string, + price: number, + quantity: number, + lightningSaleProductId: string | null, +): number => { + if (lightningSaleProductId === productId) { + return price * quantity * DISCOUNT_POLICIES.LIGHTNING_SALE_RATE; + } + return 0; +}; + +// 추천할인 계산 (original과 동일) +export const calculateRecommendationDiscount = ( + productId: string, + price: number, + quantity: number, + recommendationProductId: string | null, +): number => { + if (recommendationProductId === productId) { + return price * quantity * DISCOUNT_POLICIES.RECOMMENDATION_RATE; + } + return 0; +}; + +// 할인 적용 순서 및 최종 계산 (original과 동일한 순서) +export const calculateFinalDiscount = ( + subtotal: number, + totalQuantity: number, + individualDiscounts: number[], + lightningSaleDiscount: number = 0, + recommendationDiscount: number = 0, +): DiscountResult => { + // 1. 개별 상품 할인 계산 + const individualDiscountTotal = individualDiscounts.reduce((sum, discount) => sum + discount, 0); + let currentAmount = subtotal - individualDiscountTotal; + + // 2. 전체 수량 할인 계산 (30개 이상) + if (totalQuantity >= DISCOUNT_POLICIES.TOTAL_BULK_THRESHOLD) { + currentAmount = subtotal * (1 - DISCOUNT_POLICIES.TOTAL_BULK_RATE); + } + + // 3. 번개세일과 추천할인 계산 + let specialDiscount = 0; + const hasLightningSale = lightningSaleDiscount > 0; + const hasRecommendation = recommendationDiscount > 0; + + if (hasLightningSale && hasRecommendation) { + // SUPER SALE: 25% 할인 + specialDiscount = Math.max(lightningSaleDiscount, recommendationDiscount) * 1.25; + } else { + specialDiscount = lightningSaleDiscount + recommendationDiscount; + } + + currentAmount -= specialDiscount; + + // 4. 화요일 할인 계산 (마지막에 적용) + const tuesdayDiscount = calculateTuesdayDiscount(currentAmount); + currentAmount -= tuesdayDiscount; + + const finalAmount = currentAmount; + const totalDiscount = subtotal - finalAmount; + + return { + subtotal, + appliedDiscounts: [], // 실제 할인 객체는 별도로 관리 + finalAmount, + discountAmount: totalDiscount, + }; +}; + +// 할인 아이콘 및 스타일 가져오기 (original과 동일) +export const getDiscountStyle = ( + productId: string, + lightningSaleProductId: string | null, + recommendationProductId: string | null, +): { icon: string; className: string } => { + const hasLightningSale = lightningSaleProductId === productId; + const hasRecommendation = recommendationProductId === productId; + const isSuperSale = hasLightningSale && hasRecommendation; + + if (isSuperSale) { + return { + icon: '⚡💝', + className: 'text-purple-600 font-bold', + }; + } else if (hasLightningSale) { + return { + icon: '⚡', + className: 'text-red-500 font-bold', + }; + } else if (hasRecommendation) { + return { + icon: '💝', + className: 'text-blue-500 font-bold', + }; + } + + return { + icon: '', + className: '', + }; +}; diff --git a/src/advanced/lib/product.ts b/src/advanced/lib/product.ts new file mode 100644 index 000000000..c53f4af07 --- /dev/null +++ b/src/advanced/lib/product.ts @@ -0,0 +1,68 @@ +// 상품 타입 +export interface Product { + id: string; + name: string; + price: number; + stock: number; + discount: number; + lightningSale?: boolean; + recommendationSale?: boolean; +} + +// 장바구니 아이템 타입 +export interface CartItem { + product: Product; + quantity: number; + appliedDiscounts: string[]; +} + +export const PRODUCTS = [ + { + id: 'p1', + name: '버그 없애는 키보드', + price: 10000, + stock: 50, + discount: 0.1, + lightningSale: false, + recommendationSale: false, + }, + { + id: 'p2', + name: '생산성 폭발 마우스', + price: 20000, + stock: 30, + discount: 0.15, + lightningSale: false, + recommendationSale: false, + }, + { + id: 'p3', + name: '거북목 탈출 모니터암', + price: 30000, + stock: 20, + discount: 0.2, + lightningSale: false, + recommendationSale: false, + }, + { + id: 'p4', + name: '에러 방지 노트북 파우치', + price: 15000, + stock: 0, + discount: 0.05, + lightningSale: false, + recommendationSale: false, + }, + { + id: 'p5', + name: '코딩할 때 듣는 Lo-Fi 스피커', + price: 25000, + stock: 10, + discount: 0.25, + lightningSale: false, + recommendationSale: false, + }, +]; + +// useCart 훅에서 사용할 initialProducts +export const initialProducts: Product[] = PRODUCTS; diff --git a/src/advanced/main.tsx b/src/advanced/main.tsx new file mode 100644 index 000000000..574ccf640 --- /dev/null +++ b/src/advanced/main.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement); + +root.render( + + + , +); diff --git a/src/basic/components/cart/CartDisplay.js b/src/basic/components/cart/CartDisplay.js new file mode 100644 index 000000000..e27f4c60b --- /dev/null +++ b/src/basic/components/cart/CartDisplay.js @@ -0,0 +1,9 @@ +/** + * 장바구니 표시 영역 컴포넌트 + * 장바구니 아이템들을 표시하는 컨테이너를 생성합니다. + */ +export function createCartDisplay() { + const cartDisplayElement = document.createElement('div'); + cartDisplayElement.id = 'cart-items'; + return cartDisplayElement; +} diff --git a/src/basic/components/cart/CartItem.js b/src/basic/components/cart/CartItem.js new file mode 100644 index 000000000..f3275508e --- /dev/null +++ b/src/basic/components/cart/CartItem.js @@ -0,0 +1,52 @@ +/** + * 장바구니 아이템 컴포넌트 + * 개별 장바구니 아이템을 렌더링합니다. + */ +export function renderCartItem(item, product) { + const newItem = document.createElement('div'); + newItem.id = item.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'; + + const saleIcon = + product.onSale && product.suggestSale + ? '⚡💝' + : product.onSale + ? '⚡' + : product.suggestSale + ? '💝' + : ''; + + const priceDisplay = + product.onSale || product.suggestSale + ? `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}` + : `₩${product.price.toLocaleString()}`; + + newItem.innerHTML = ` +
+
+
+
+

${saleIcon}${product.name}

+

PRODUCT

+

${priceDisplay}

+
+ + ${item.quantity} + +
+
+
+
${priceDisplay}
+ Remove +
+ `; + + return newItem; +} diff --git a/src/basic/components/cart/index.js b/src/basic/components/cart/index.js new file mode 100644 index 000000000..830419dda --- /dev/null +++ b/src/basic/components/cart/index.js @@ -0,0 +1,5 @@ +/** + * Cart 컴포넌트들의 barrel export + */ +export { createCartDisplay } from './CartDisplay.js'; +export { renderCartItem } from './CartItem.js'; diff --git a/src/basic/components/index.js b/src/basic/components/index.js new file mode 100644 index 000000000..c9ec2d033 --- /dev/null +++ b/src/basic/components/index.js @@ -0,0 +1,18 @@ +/** + * 모든 컴포넌트들의 barrel export + */ + +// Layout 컴포넌트들 +export { createHeader, createGridContainer } from './layout/index.js'; + +// Product 컴포넌트들 +export { createProductSelector, createStockInfo } from './product/index.js'; + +// Cart 컴포넌트들 +export { createCartDisplay, renderCartItem } from './cart/index.js'; + +// Order 컴포넌트들 +export { createRightColumn, renderDiscountInfo, renderLoyaltyPoints } from './order/index.js'; + +// Modal 컴포넌트들 +export { createManualOverlay, createManualToggle, createManualColumn } from './modal/index.js'; diff --git a/src/basic/components/layout/GridContainer.js b/src/basic/components/layout/GridContainer.js new file mode 100644 index 000000000..bb5e093fe --- /dev/null +++ b/src/basic/components/layout/GridContainer.js @@ -0,0 +1,12 @@ +/** + * 그리드 컨테이너 컴포넌트 + * 왼쪽 컬럼과 오른쪽 컬럼을 감싸는 그리드 레이아웃을 생성합니다. + */ +export function createGridContainer(leftColumn, rightColumn) { + const gridContainer = document.createElement('div'); + gridContainer.className = + 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden'; + gridContainer.appendChild(leftColumn); + gridContainer.appendChild(rightColumn); + return gridContainer; +} diff --git a/src/basic/components/layout/Header.js b/src/basic/components/layout/Header.js new file mode 100644 index 000000000..1d2e032f9 --- /dev/null +++ b/src/basic/components/layout/Header.js @@ -0,0 +1,14 @@ +/** + * 헤더 컴포넌트 + * 쇼핑 카트 페이지의 상단 헤더를 생성합니다. + */ +export function createHeader() { + 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/layout/index.js b/src/basic/components/layout/index.js new file mode 100644 index 000000000..18f02431d --- /dev/null +++ b/src/basic/components/layout/index.js @@ -0,0 +1,5 @@ +/** + * Layout 컴포넌트들의 barrel export + */ +export { createHeader } from './Header.js'; +export { createGridContainer } from './GridContainer.js'; diff --git a/src/basic/components/modal/ManualColumn.js b/src/basic/components/modal/ManualColumn.js new file mode 100644 index 000000000..4961cc86d --- /dev/null +++ b/src/basic/components/modal/ManualColumn.js @@ -0,0 +1,70 @@ +/** + * 이용 안내 컬럼 컴포넌트 + * 모달의 실제 내용을 표시하는 컬럼을 생성합니다. + */ +export function createManualColumn() { + 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 manual-column'; + 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 = 품절 +

+
+ `; + return manualColumn; +} diff --git a/src/basic/components/modal/ManualOverlay.js b/src/basic/components/modal/ManualOverlay.js new file mode 100644 index 000000000..b7933b37a --- /dev/null +++ b/src/basic/components/modal/ManualOverlay.js @@ -0,0 +1,19 @@ +/** + * 이용 안내 오버레이 컴포넌트 + * 모달의 배경 오버레이를 생성합니다. + */ +export function createManualOverlay() { + const manualOverlay = document.createElement('div'); + manualOverlay.className = + 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300 manual-overlay'; + manualOverlay.onclick = function (e) { + if (e.target === manualOverlay) { + manualOverlay.classList.add('hidden'); + const manualColumn = document.querySelector('.manual-column'); + if (manualColumn) { + manualColumn.classList.add('translate-x-full'); + } + } + }; + return manualOverlay; +} diff --git a/src/basic/components/modal/ManualToggle.js b/src/basic/components/modal/ManualToggle.js new file mode 100644 index 000000000..19c58899d --- /dev/null +++ b/src/basic/components/modal/ManualToggle.js @@ -0,0 +1,23 @@ +/** + * 이용 안내 토글 버튼 컴포넌트 + * 모달을 열고 닫는 토글 버튼을 생성합니다. + */ +export function createManualToggle() { + const manualToggle = document.createElement('button'); + manualToggle.onclick = function () { + const manualOverlay = document.querySelector('.manual-overlay'); + const manualColumn = document.querySelector('.manual-column'); + if (manualOverlay && manualColumn) { + 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 = ` + + + + `; + return manualToggle; +} diff --git a/src/basic/components/modal/index.js b/src/basic/components/modal/index.js new file mode 100644 index 000000000..cdd65036c --- /dev/null +++ b/src/basic/components/modal/index.js @@ -0,0 +1,6 @@ +/** + * Modal 컴포넌트들의 barrel export + */ +export { createManualOverlay } from './ManualOverlay.js'; +export { createManualToggle } from './ManualToggle.js'; +export { createManualColumn } from './ManualColumn.js'; diff --git a/src/basic/components/order/DiscountInfo.js b/src/basic/components/order/DiscountInfo.js new file mode 100644 index 000000000..3c16428c0 --- /dev/null +++ b/src/basic/components/order/DiscountInfo.js @@ -0,0 +1,25 @@ +/** + * 할인 정보 렌더링 컴포넌트 + * 할인 정보를 표시하는 컴포넌트입니다. + */ +export function renderDiscountInfo(discRate, originalTotal, totalAmount) { + const discountInfoDiv = document.getElementById('discount-info'); + if (!discountInfoDiv) return; + + discountInfoDiv.innerHTML = ''; + + if (discRate > 0 && totalAmount > 0) { + const savedAmount = originalTotal - totalAmount; + discountInfoDiv.innerHTML = ` +
+
+ 총 할인율 + ${(discRate * 100).toFixed(1)}% +
+
₩${Math.round( + savedAmount, + ).toLocaleString()} 할인되었습니다
+
+ `; + } +} diff --git a/src/basic/components/order/LoyaltyPoints.js b/src/basic/components/order/LoyaltyPoints.js new file mode 100644 index 000000000..209c0ddc7 --- /dev/null +++ b/src/basic/components/order/LoyaltyPoints.js @@ -0,0 +1,18 @@ +/** + * 포인트 정보 렌더링 컴포넌트 + * 적립 포인트 정보를 표시하는 컴포넌트입니다. + */ +export function renderLoyaltyPoints(points, pointInfo) { + const loyaltyPointsDiv = document.getElementById('loyalty-points'); + if (!loyaltyPointsDiv) return; + + if (points > 0) { + loyaltyPointsDiv.innerHTML = + `
적립 포인트: ${points}p
` + + `
${pointInfo.detailText}
`; + loyaltyPointsDiv.style.display = 'block'; + } else { + loyaltyPointsDiv.textContent = '적립 포인트: 0p'; + loyaltyPointsDiv.style.display = 'block'; + } +} diff --git a/src/basic/components/order/OrderSummary.js b/src/basic/components/order/OrderSummary.js new file mode 100644 index 000000000..953114eaa --- /dev/null +++ b/src/basic/components/order/OrderSummary.js @@ -0,0 +1,73 @@ +/** + * 주문 요약 컴포넌트 + * 오른쪽 컬럼의 주문 요약 섹션을 생성합니다. + */ +export function createRightColumn() { + const rightColumn = document.createElement('div'); + rightColumn.className = 'bg-black text-white p-8 flex flex-col'; + rightColumn.innerHTML = + createOrderSummaryHeader() + + createOrderSummaryDetails() + + createCheckoutButton() + + createPointsNotice(); + return rightColumn; +} + +/** + * 주문 요약 헤더 컴포넌트 + */ +function createOrderSummaryHeader() { + return ` +

Order Summary

+ `; +} + +/** + * 주문 요약 상세 영역 컴포넌트 + */ +function createOrderSummaryDetails() { + return ` +
+
+
+
+
+
+ Total +
₩0
+
+
적립 포인트: 0p
+
+ +
+
+ `; +} + +/** + * 체크아웃 버튼 컴포넌트 + */ +function createCheckoutButton() { + return ` + + `; +} + +/** + * 포인트 안내 컴포넌트 + */ +function createPointsNotice() { + return ` +

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

+ `; +} diff --git a/src/basic/components/order/index.js b/src/basic/components/order/index.js new file mode 100644 index 000000000..392927139 --- /dev/null +++ b/src/basic/components/order/index.js @@ -0,0 +1,6 @@ +/** + * Order 컴포넌트들의 barrel export + */ +export { createRightColumn } from './OrderSummary.js'; +export { renderDiscountInfo } from './DiscountInfo.js'; +export { renderLoyaltyPoints } from './LoyaltyPoints.js'; diff --git a/src/basic/components/product/ProductSelector.js b/src/basic/components/product/ProductSelector.js new file mode 100644 index 000000000..f2db31599 --- /dev/null +++ b/src/basic/components/product/ProductSelector.js @@ -0,0 +1,10 @@ +/** + * 상품 선택기 컴포넌트 + * 사용자가 상품을 선택할 수 있는 드롭다운을 생성합니다. + */ +export function createProductSelector() { + const productSelector = document.createElement('select'); + productSelector.id = 'product-select'; + productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3'; + return productSelector; +} diff --git a/src/basic/components/product/StockInfo.js b/src/basic/components/product/StockInfo.js new file mode 100644 index 000000000..7725d1b48 --- /dev/null +++ b/src/basic/components/product/StockInfo.js @@ -0,0 +1,10 @@ +/** + * 재고 정보 컴포넌트 + * 상품의 재고 상태를 표시하는 컴포넌트를 생성합니다. + */ +export function createStockInfo() { + const stockInfoElement = document.createElement('div'); + stockInfoElement.id = 'stock-status'; + stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line'; + return stockInfoElement; +} diff --git a/src/basic/components/product/index.js b/src/basic/components/product/index.js new file mode 100644 index 000000000..96f16eda7 --- /dev/null +++ b/src/basic/components/product/index.js @@ -0,0 +1,5 @@ +/** + * Product 컴포넌트들의 barrel export + */ +export { createProductSelector } from './ProductSelector.js'; +export { createStockInfo } from './StockInfo.js'; diff --git a/src/basic/constants/UIConstants.js b/src/basic/constants/UIConstants.js new file mode 100644 index 000000000..3e9c8f209 --- /dev/null +++ b/src/basic/constants/UIConstants.js @@ -0,0 +1,12 @@ +/** + * UI 관련 상수 정의 + */ +export const UI_CONSTANTS = { + LOW_STOCK_THRESHOLD: 5, //재고 부족 기준 수량 + TOTAL_STOCK_THRESHOLD: 50, //총 재고 기준 수량 + TUESDAY: 2, //화요일 판매 할인 적용 여부 + LIGHTNING_SALE_INTERVAL: 30000, //번개 판매 주기 + LIGHTNING_SALE_DELAY: 10000, //번개 판매 지연 시간 + SUGGEST_SALE_INTERVAL: 60000, //추천 판매 주기 + SUGGEST_SALE_DELAY: 20000, //추천 판매 지연 시간 +}; diff --git a/src/basic/constants/discountInfo.js b/src/basic/constants/discountInfo.js new file mode 100644 index 000000000..f5938529d --- /dev/null +++ b/src/basic/constants/discountInfo.js @@ -0,0 +1,17 @@ +/** + * 할인 관련 상수 정의 + */ +export const DISCOUNT_THRESHOLDS = { + INDIVIDUAL_ITEM: 10, + BULK_PURCHASE: 30, +}; + +export const DISCOUNT_RATES = { + KEYBOARD: 0.1, + MOUSE: 0.15, + MONITOR_ARM: 0.2, + LAPTOP_CASE: 0.05, + SPEAKER: 0.25, + BULK_PURCHASE: 0.25, + TUESDAY: 0.1, +}; diff --git a/src/basic/constants/index.js b/src/basic/constants/index.js new file mode 100644 index 000000000..1d285eef7 --- /dev/null +++ b/src/basic/constants/index.js @@ -0,0 +1,4 @@ +export * from './productInfo.js'; +export * from './discountInfo.js'; +export * from './pointRate.js'; +export * from './UIConstants.js'; diff --git a/src/basic/constants/pointRate.js b/src/basic/constants/pointRate.js new file mode 100644 index 000000000..e07a815e4 --- /dev/null +++ b/src/basic/constants/pointRate.js @@ -0,0 +1,12 @@ +/** + * 포인트 관련 상수 정의 + */ +export const POINT_RATES = { + BASE_RATE: 0.001, // 0.1% (1000원당 1포인트) + TUESDAY_MULTIPLIER: 2, + SET_BONUS: 50, + FULL_SET_BONUS: 100, + QUANTITY_BONUS_10: 20, + QUANTITY_BONUS_20: 50, + QUANTITY_BONUS_30: 100, +}; diff --git a/src/basic/constants/productInfo.js b/src/basic/constants/productInfo.js new file mode 100644 index 000000000..bbf755fd3 --- /dev/null +++ b/src/basic/constants/productInfo.js @@ -0,0 +1,34 @@ +/** + * 상품 관련 상수 정의 + */ +export const PRODUCT_IDS = { + KEYBOARD: 'p1', + MOUSE: 'p2', + MONITOR_ARM: 'p3', + LAPTOP_CASE: 'p4', + SPEAKER: 'p5', +}; + +export const PRODUCT_NAMES = { + [PRODUCT_IDS.KEYBOARD]: '버그 없애는 키보드', + [PRODUCT_IDS.MOUSE]: '생산성 폭발 마우스', + [PRODUCT_IDS.MONITOR_ARM]: '거북목 탈출 모니터암', + [PRODUCT_IDS.LAPTOP_CASE]: '에러 방지 노트북 파우치', + [PRODUCT_IDS.SPEAKER]: '코딩할 때 듣는 Lo-Fi 스피커', +}; + +export const PRODUCT_PRICES = { + [PRODUCT_IDS.KEYBOARD]: 10000, + [PRODUCT_IDS.MOUSE]: 20000, + [PRODUCT_IDS.MONITOR_ARM]: 30000, + [PRODUCT_IDS.LAPTOP_CASE]: 15000, + [PRODUCT_IDS.SPEAKER]: 25000, +}; + +export const INITIAL_STOCK = { + [PRODUCT_IDS.KEYBOARD]: 50, + [PRODUCT_IDS.MOUSE]: 30, + [PRODUCT_IDS.MONITOR_ARM]: 20, + [PRODUCT_IDS.LAPTOP_CASE]: 0, + [PRODUCT_IDS.SPEAKER]: 10, +}; diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 825eae3a5..7a341a357 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,763 +1,487 @@ -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 +// 상수 import +import { DISCOUNT_THRESHOLDS, UI_CONSTANTS } from './constants/index.js'; + +// productStore import +import { productStore, productStoreActions } from './store/productStore.js'; + +// DiscountService import +import { + calculateTotalDiscountRate, + createDiscountInfo, +} from './services/discount/DiscountService.js'; + +// PointService import +import { createPointInfo } from './services/point/PointService.js'; + +// CartService import +import { + createInitialCartState, + addItemToCart, + updateCartItemQuantity, + removeItemFromCart, +} from './services/cart/CartService.js'; + +// cartStore import +import { cartStore, cartStoreActions } from './store/cartStore.js'; + +// Components import (새로운 폴더 구조) +import { + createHeader, + createGridContainer, + createProductSelector, + createStockInfo, + createCartDisplay, + createRightColumn, + createManualOverlay, + createManualToggle, + createManualColumn, + renderDiscountInfo, + renderLoyaltyPoints, +} from './components/index.js'; + +// Utils import +import { + createAddToCartButton, + createSelectorContainer, + createLeftColumn, +} from './utils/UIRenderer.js'; + +// EventHandler import +import { setupEventListeners } from './utils/EventHandler.js'; + +// TimerHandler import +import { setupAllTimers } from './utils/TimerHandler.js'; + +// Renderers import +import { + renderProductOptions, + renderOrderSummaryDetails, + renderTuesdaySpecial, + renderTotalAmount, + renderItemCount, + renderStockMessages, + updateCartPrices, +} from './utils/renderers/index.js'; + +// UI 요소들 (cartStore와 분리) +let stockInfoElement; +let productSelector; +let addToCartButton; +let cartDisplayElement; +let orderSummaryElement; + +// CartService를 위한 상태 관리 +let cartState = createInitialCartState(); + +// productStore를 CartService에서 사용하기 위한 인터페이스 +const productService = { + getProductById: (productId) => productStoreActions.getProductById(productId), + decreaseStock: (productId, quantity) => { + const success = productStoreActions.decreaseStock(productId, quantity); + return { success, products: productStore.products }; + }, + increaseStock: (productId, quantity) => { + const success = productStoreActions.increaseStock(productId, quantity); + return { success, products: productStore.products }; + }, +}; + function main() { - var root; - var header; - var gridContainer; - var leftColumn; - var selectorContainer; - var rightColumn; - var manualToggle; - var manualOverlay; - var manualColumn; - var 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 - } - ] - 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'); - 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'; - selectorContainer.appendChild(sel); - selectorContainer.appendChild(addBtn); - selectorContainer.appendChild(stockInfo); - leftColumn.appendChild(selectorContainer); - 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'; - rightColumn.innerHTML = ` -

Order Summary

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

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

- `; - sum = rightColumn.querySelector('#cart-total'); - 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); + // cartStore 초기화 + cartStoreActions.reset(); + + // productStore 초기화 + productStoreActions.initializeProducts(); + + // CartService 상태 초기화 + cartState = createInitialCartState(); + + const root = document.getElementById('app'); + + // UI 컴포넌트들 생성 + const header = createHeader(); + + productSelector = createProductSelector(); + addToCartButton = createAddToCartButton(); + stockInfoElement = createStockInfo(); + + const selectorContainer = createSelectorContainer( + productSelector, + addToCartButton, + stockInfoElement, + ); + cartDisplayElement = createCartDisplay(); + + const leftColumn = createLeftColumn(selectorContainer, cartDisplayElement); + const rightColumn = createRightColumn(); + + const manualOverlay = createManualOverlay(); + const manualToggle = createManualToggle(); + const manualColumn = createManualColumn(); + + const gridContainer = createGridContainer(leftColumn, rightColumn); + + // DOM에 요소들 추가 manualOverlay.appendChild(manualColumn); root.appendChild(header); root.appendChild(gridContainer); root.appendChild(manualToggle); root.appendChild(manualOverlay); - var initStock = 0; - for (var i = 0; i < prodList.length; i++) { - initStock += prodList[i].q; - } - onUpdateSelectOptions(); - handleCalculateCartStuff(); - lightningDelay = Math.random() * 10000; - setTimeout(() => { - setInterval(function () { - var luckyIdx = Math.floor(Math.random() * prodList.length); - var luckyItem = prodList[luckyIdx]; - if (luckyItem.q > 0 && !luckyItem.onSale) { - luckyItem.val = Math.round(luckyItem.originalVal * 80 / 100); - luckyItem.onSale = true; - alert('⚡번개세일! ' + luckyItem.name + '이(가) 20% 할인 중입니다!'); - onUpdateSelectOptions(); - doUpdatePricesInCart(); - } - }, 30000); - }, lightningDelay); - setTimeout(function () { - setInterval(function () { - if (cartDisp.children.length === 0) { - } - if (lastSel) { - var suggest = null; - for (var k = 0; k < prodList.length; k++) { - if (prodList[k].id !== lastSel) { - if (prodList[k].q > 0) { - if (!prodList[k].suggestSale) { - suggest = prodList[k]; - break; - } - } - } - } - if (suggest) { - alert('💝 ' + suggest.name + '은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!'); - suggest.val = Math.round(suggest.val * (100 - 5) / 100); - suggest.suggestSale = true; - onUpdateSelectOptions(); - doUpdatePricesInCart(); - } - } - }, 60000); - }, Math.random() * 20000); -}; -var sum -function onUpdateSelectOptions() { - var totalStock; - var opt; - var discountText; - sel.innerHTML = ''; - totalStock = 0; - for (var idx = 0; idx < prodList.length; idx++) { - var _p = prodList[idx]; - totalStock = totalStock + _p.q; - } - for (var i = 0; i < prodList.length; i++) { - (function() { - var item = prodList[i]; - opt = document.createElement("option") - opt.value = item.id; - discountText = ''; - if (item.onSale) discountText += ' ⚡SALE'; - if (item.suggestSale) discountText += ' 💝추천'; - if (item.q === 0) { - opt.textContent = item.name + ' - ' + item.val + '원 (품절)' + discountText - opt.disabled = true - opt.className = 'text-gray-400'; - } else { - if (item.onSale && item.suggestSale) { - opt.textContent = '⚡💝' + item.name + ' - ' + item.originalVal + '원 → ' + item.val + '원 (25% SUPER SALE!)'; - opt.className = 'text-purple-600 font-bold'; - } else if (item.onSale) { - opt.textContent = '⚡' + item.name + ' - ' + item.originalVal + '원 → ' + item.val + '원 (20% SALE!)'; - opt.className = 'text-red-500 font-bold'; - } else if (item.suggestSale) { - opt.textContent = '💝' + item.name + ' - ' + item.originalVal + '원 → ' + item.val + '원 (5% 추천할인!)'; - opt.className = 'text-blue-500 font-bold'; - } else { - opt.textContent = item.name + ' - ' + item.val + '원' + discountText; - } - } - sel.appendChild(opt); - })(); - } - if (totalStock < 50) { - sel.style.borderColor = 'orange'; - } else { - sel.style.borderColor = ''; - } + + // orderSummaryElement 참조 설정 + orderSummaryElement = rightColumn.querySelector('#cart-total'); + + // 상품 옵션, 장바구니, 가격 등 초기 렌더링 + updateProductOptions(); + calculateCartSummary(); + + // 타이머 설정 + setupAllTimers({ + products: productStore.products, + cartDisplayElement, + lastSelectedProductId: cartState.lastSelectedProductId, + updateProductOptions, + updateCartPrices, + }); } -function handleCalculateCartStuff() { - var cartItems; - var subTot; - var itemDiscounts; - var lowStockItems; - var idx; - var originalTotal; - var bulkDisc; - var itemDisc; - var savedAmount; - var summaryDetails; - var totalDiv; - var loyaltyPointsDiv; - var points; - var discountInfoDiv; - var itemCountElement; - var previousCount; - var stockMsg; - var pts; - var hasP1; - var hasP2; - var loyaltyDiv; - totalAmt = 0; - itemCnt = 0; - originalTotal = totalAmt - cartItems = cartDisp.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); - } - } + +// 장바구니 내 각 상품별 합계/할인 계산 +function processCartItems(cartItems) { + let totalAmount = 0; + let itemCount = 0; + let subTot = 0; + const itemDiscounts = []; + for (let i = 0; i < cartItems.length; i++) { - (function () { - var curItem; - for (var j = 0; j < prodList.length; j++) { - if (prodList[j].id === cartItems[i].id) { - curItem = prodList[j]; - break; - } - } - var qtyElem = cartItems[i].querySelector('.quantity-number'); - var q; - var itemTot; - var disc; - q = parseInt(qtyElem.textContent); - itemTot = curItem.val * q; - disc = 0; - itemCnt += q; - subTot += itemTot; - var itemDiv = cartItems[i]; - var priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs'); - priceElems.forEach(function (elem) { - if (elem.classList.contains('text-lg')) { - elem.style.fontWeight = q >= 10 ? 'bold' : 'normal'; - } - }); - if (q >= 10) { - if (curItem.id === PRODUCT_ONE) { - disc = 10 / 100; - } else { - if (curItem.id === p2) { - disc = 15 / 100; - } else { - if (curItem.id === product_3) { - disc = 20 / 100; - } else { - if (curItem.id === p4) { - disc = 5 / 100; - } else { - if (curItem.id === PRODUCT_5) { - disc = 25 / 100; - } - } - } - } - } - if (disc > 0) { - itemDiscounts.push({name: curItem.name, discount: disc * 100}); - } + // 상품 찾기 + let curItem; + for (let j = 0; j < productStore.products.length; j++) { + if (productStore.products[j].id === cartItems[i].id) { + curItem = productStore.products[j]; + break; } - totalAmt += itemTot * (1 - disc); - })(); - } - let discRate = 0; - var originalTotal = subTot; - if (itemCnt >= 30) { - totalAmt = subTot * 75 / 100; - discRate = 25 / 100; - } else { - discRate = (subTot - totalAmt) / subTot; - } - const today = new Date(); - var isTuesday = today.getDay() === 2; - var tuesdaySpecial = document.getElementById('tuesday-special'); - if (isTuesday) { - if (totalAmt > 0) { - totalAmt = totalAmt * 90 / 100; - discRate = 1 - (totalAmt / originalTotal); - tuesdaySpecial.classList.remove('hidden'); - } else { - tuesdaySpecial.classList.add('hidden'); } - } else { - tuesdaySpecial.classList.add('hidden'); - } - document.getElementById('item-count').textContent = '🛍️ ' + itemCnt + ' items in cart'; - summaryDetails = document.getElementById('summary-details'); - summaryDetails.innerHTML = ''; - if (subTot > 0) { - for (let i = 0; i < cartItems.length; i++) { - var curItem; - for (var j = 0; j < prodList.length; j++) { - if (prodList[j].id === cartItems[i].id) { - curItem = prodList[j]; - break; - } + + const quantityElem = cartItems[i].querySelector('.quantity-number'); + const quantity = parseInt(quantityElem.textContent); + const itemTot = curItem.price * quantity; + + itemCount += quantity; + subTot += itemTot; + + // UI 스타일 조정 (10개 이상시 볼드 처리) + const itemDiv = cartItems[i]; + const priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs'); + priceElems.forEach(function (elem) { + if (elem.classList.contains('text-lg')) { + elem.style.fontWeight = quantity >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM ? 'bold' : 'normal'; } - var qtyElem = cartItems[i].querySelector('.quantity-number'); - var q = parseInt(qtyElem.textContent); - var itemTotal = curItem.val * q; - summaryDetails.innerHTML += ` -
- ${curItem.name} x ${q} - ₩${itemTotal.toLocaleString()} -
- `; - } - summaryDetails.innerHTML += ` -
-
- Subtotal - ₩${subTot.toLocaleString()} -
- `; - if (itemCnt >= 30) { - summaryDetails.innerHTML += ` -
- 🎉 대량구매 할인 (30개 이상) - -25% -
- `; - } else if (itemDiscounts.length > 0) { - itemDiscounts.forEach(function (item) { - summaryDetails.innerHTML += ` -
- ${item.name} (10개↑) - -${item.discount}% -
- `; - }); + }); + + // 개별 할인 계산 - productStore 사용 + const disc = productStoreActions.calculateItemDiscount(curItem.id, quantity); + if (disc > 0) { + itemDiscounts.push({ name: curItem.name, discount: disc * 100 }); } - if (isTuesday) { - if (totalAmt > 0) { - summaryDetails.innerHTML += ` -
- 🌟 화요일 추가 할인 - -10% -
- `; - } - } - summaryDetails.innerHTML += ` -
- Shipping - Free -
- `; - } - totalDiv = sum.querySelector('.text-2xl'); - if (totalDiv) { - totalDiv.textContent = '₩' + Math.round(totalAmt).toLocaleString(); + + totalAmount += itemTot * (1 - disc); + } + + return { + totalAmount, + itemCount, + subTot, + itemDiscounts, + }; +} + +// 할인 총합 계산 (대량구매 할인 + 화요일 할인) +function calculateTotalDiscount(subTot, itemCount, currentAmount) { + return calculateTotalDiscountRate(itemCount, subTot, currentAmount); +} + +// 주문 요약 상세 내역 갱신 +function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts) { + // createDiscountInfo를 사용하여 올바른 할인 정보 생성 + const discountInfo = createDiscountInfo(itemDiscounts, itemCount); + renderOrderSummaryDetails(cartItems, productStore.products, subTot, discountInfo); +} + +// 상품 선택 옵션 렌더링 및 재고 상태 표시 +function updateProductOptions() { + renderProductOptions(productSelector, productStore.products); + + // productStore의 getTotalStock 함수 사용 + const totalStock = productStoreActions.getTotalStock(); + + if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) { + productSelector.style.borderColor = 'orange'; + } else { + productSelector.style.borderColor = ''; } - loyaltyPointsDiv = document.getElementById('loyalty-points'); +} + +// 장바구니, 할인, 포인트 등 계산 및 화면 갱신 +function calculateCartSummary() { + let points; + let previousCount; + + const cartItems = cartDisplayElement.children; + + // 장바구니 내 각 상품별 합계/할인 계산 + const { + totalAmount: calcTotalAmount, + itemCount: calcItemCount, + subTot, + itemDiscounts, + } = processCartItems(cartItems); + + // cartStore 상태 업데이트 + cartStore.totalAmount = calcTotalAmount; + cartStore.itemCount = calcItemCount; + + const originalTotal = subTot; + + // 할인 총합 계산 적용 + const { finalAmount, discountRate, isTuesday } = calculateTotalDiscount( + subTot, + cartStore.itemCount, + cartStore.totalAmount, + ); + cartStore.totalAmount = finalAmount; + const discRate = discountRate; + + // 화요일 특별 할인 UI 표시 + renderTuesdaySpecial(isTuesday, cartStore.totalAmount); + + // 장바구니 수량 표시 갱신 + renderItemCount(cartStore.itemCount); + + // 주문 요약(상품별, 할인, 배송 등) 갱신 + updateOrderSummary(cartItems, subTot, cartStore.itemCount, itemDiscounts); + + // 총 결제 금액 표시 갱신 + renderTotalAmount(cartStore.totalAmount, orderSummaryElement); + + // 적립 포인트 표시 갱신 - PointService 사용 + const loyaltyPointsDiv = document.getElementById('loyalty-points'); if (loyaltyPointsDiv) { - points = Math.floor(totalAmt / 1000); - if (points > 0) { - loyaltyPointsDiv.textContent = '적립 포인트: ' + points + 'p'; - loyaltyPointsDiv.style.display = 'block'; - } else { - loyaltyPointsDiv.textContent = '적립 포인트: 0p'; - loyaltyPointsDiv.style.display = 'block'; - } - } - discountInfoDiv = document.getElementById('discount-info'); - discountInfoDiv.innerHTML = ''; - if (discRate > 0 && totalAmt > 0) { - savedAmount = originalTotal - totalAmt; - discountInfoDiv.innerHTML = ` -
-
- 총 할인율 - ${(discRate * 100).toFixed(1)}% -
-
₩${Math.round(savedAmount).toLocaleString()} 할인되었습니다
-
- `; - } - itemCountElement = document.getElementById('item-count'); + // cartStore에서 장바구니 아이템 정보 추출 + const cartItemsData = Array.from(cartItems).map((item) => { + const productId = item.id; + const product = productService.getProductById(productId); + const quantity = parseInt(item.querySelector('.quantity-number').textContent); + return { + id: productId, + quantity, + name: product ? product.name : '', + price: product ? product.price : 0, + }; + }); + + // PointService를 사용하여 포인트 계산 + const pointInfo = createPointInfo(cartStore.totalAmount, cartItemsData); + points = pointInfo.totalPoints; + + // cartStore에 포인트 업데이트 + cartStoreActions.updateBonusPoints(points); + + renderLoyaltyPoints(points, pointInfo); + } + + // 할인 정보 표시 갱신 + renderDiscountInfo(discRate, originalTotal, cartStore.totalAmount); + + // 장바구니 수량 변화 애니메이션 표시 + const itemCountElement = document.getElementById('item-count'); if (itemCountElement) { previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0); - itemCountElement.textContent = '🛍️ ' + itemCnt + ' items in cart'; - if (previousCount !== itemCnt) { + if (previousCount !== cartStore.itemCount) { itemCountElement.setAttribute('data-changed', 'true'); } } - stockMsg = ''; - for (var stockIdx = 0; stockIdx < prodList.length; stockIdx++) { - var item = prodList[stockIdx]; - if (item.q < 5) { - if (item.q > 0) { - stockMsg = stockMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n'; - } else { - stockMsg = stockMsg + item.name + ': 품절\n'; - } - } - } - stockInfo.textContent = stockMsg; - handleStockInfoUpdate(); - doRenderBonusPoints(); + + // 재고 부족/품절 안내 메시지 갱신 + updateStockMessages(); + + renderBonusPoints(); } -var doRenderBonusPoints = function() { - var basePoints; - var finalPoints; - var pointsDetail; - var hasKeyboard; - var hasMouse; - var hasMonitorArm; - var nodes; - if (cartDisp.children.length === 0) { + +// 적립 포인트 계산 및 상세 내역 표시 +const renderBonusPoints = function () { + if (cartDisplayElement.children.length === 0) { document.getElementById('loyalty-points').style.display = 'none'; return; } - basePoints = Math.floor(totalAmt / 1000) - finalPoints = 0; - pointsDetail = []; - if (basePoints > 0) { - finalPoints = basePoints; - pointsDetail.push('기본: ' + basePoints + 'p'); - } - if (new Date().getDay() === 2) { - if (basePoints > 0) { - finalPoints = basePoints * 2; - pointsDetail.push('화요일 2배'); - } - } - hasKeyboard = false; - hasMouse = false; - hasMonitorArm = false; - nodes = cartDisp.children; - for (const node of nodes) { - var product = null; - for (var pIdx = 0; pIdx < prodList.length; pIdx++) { - if (prodList[pIdx].id === node.id) { - product = prodList[pIdx]; - break; - } - } - if (!product) continue; - if (product.id === PRODUCT_ONE) { - hasKeyboard = true; - } else if (product.id === p2) { - hasMouse = true; - } else if (product.id === product_3) { - hasMonitorArm = true; - } - } - if (hasKeyboard && hasMouse) { - finalPoints = finalPoints + 50; - pointsDetail.push('키보드+마우스 세트 +50p'); - } - if (hasKeyboard && hasMouse && hasMonitorArm) { - finalPoints = finalPoints + 100; - pointsDetail.push('풀세트 구매 +100p'); - } - if (itemCnt >= 30) { - finalPoints = finalPoints + 100; - pointsDetail.push('대량구매(30개+) +100p'); - } else { - if (itemCnt >= 20) { - finalPoints = finalPoints + 50; - pointsDetail.push('대량구매(20개+) +50p'); - } else { - if (itemCnt >= 10) { - finalPoints = finalPoints + 20; - pointsDetail.push('대량구매(10개+) +20p'); - } - } - } - bonusPts = finalPoints; - var ptsTag = document.getElementById('loyalty-points'); + + // cartStore에서 장바구니 아이템 정보 추출 + const cartItems = cartDisplayElement.children; + const cartItemsData = Array.from(cartItems).map((item) => { + const productId = item.id; + const product = productService.getProductById(productId); + const quantity = parseInt(item.querySelector('.quantity-number').textContent); + return { + id: productId, + quantity, + name: product ? product.name : '', + price: product ? product.price : 0, + }; + }); + + // PointService를 사용하여 포인트 정보 생성 + const pointInfo = createPointInfo(cartStore.totalAmount, cartItemsData); + cartStore.bonusPoints = pointInfo.totalPoints; + + const ptsTag = document.getElementById('loyalty-points'); + if (ptsTag) { - if (bonusPts > 0) { - ptsTag.innerHTML = '
적립 포인트: ' + bonusPts + 'p
' + - '
' + pointsDetail.join(', ') + '
'; + if (cartStore.bonusPoints > 0) { + ptsTag.innerHTML = + `
적립 포인트: ${cartStore.bonusPoints}p
` + + `
${pointInfo.detailText}
`; ptsTag.style.display = 'block'; } else { ptsTag.textContent = '적립 포인트: 0p'; - ptsTag.style.display = 'block' + ptsTag.style.display = 'block'; } } +}; + +// 재고 부족/품절 안내 메시지 생성 및 표시 +function updateStockMessages() { + // 재고 부족 상품 조회 + const lowStockProducts = productStoreActions.getLowStockProducts(); + // 품절 상품 조회 + const outOfStockProducts = productStoreActions.getOutOfStockProducts(); + + renderStockMessages(lowStockProducts, outOfStockProducts, stockInfoElement); } -function onGetStockTotal() { - var sum; - var i; - var currentProduct; - sum = 0; - for (i = 0; i < prodList.length; i++) { - currentProduct = prodList[i]; - sum += currentProduct.q; - } - return sum; -} -var handleStockInfoUpdate = function() { - var infoMsg; - var totalStock; - var messageOptimizer; - infoMsg = ''; - totalStock = onGetStockTotal(); - if (totalStock < 30) { + +// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산 +// updateCartPrices 함수는 utils/renderers/CartRenderer.js에서 import됨 + +// CartService를 사용한 장바구니 아이템 추가 함수 +function addItemToCartUI(productId, quantity = 1) { + const { + success, + cartState: newCartState, + message, + } = addItemToCart(cartState, productId, quantity, productService); + + if (success) { + // CartService 상태 업데이트 + cartState = newCartState; + + // DOM에 아이템 추가 + addItemToCartDOM(productId, quantity); + calculateCartSummary(); + } else { + alert(message || '재고가 부족하거나 상품을 찾을 수 없습니다.'); } - prodList.forEach(function (item) { - if (item.q < 5) { - if (item.q > 0) { - infoMsg = infoMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n'; - } else { - infoMsg = infoMsg + item.name + ': 품절\n'; - } - } - }); - stockInfo.textContent = infoMsg; } -function doUpdatePricesInCart() { - var totalCount = 0, j = 0; - var cartItems; - while (cartDisp.children[j]) { - var 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); - } - 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++) { - if (prodList[productIdx].id === itemId) { - product = prodList[productIdx]; - break; - } - } - if (product) { - var priceDiv = cartItems[i].querySelector('.text-lg'); - var nameDiv = cartItems[i].querySelector('h3'); - if (product.onSale && product.suggestSale) { - 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; - } else if (product.suggestSale) { - priceDiv.innerHTML = '₩' + product.originalVal.toLocaleString() + ' ₩' + product.val.toLocaleString() + ''; - nameDiv.textContent = '💝' + product.name; - } else { - priceDiv.textContent = '₩' + product.val.toLocaleString(); - nameDiv.textContent = product.name; - } - } + +// DOM에 장바구니 아이템 추가 +function addItemToCartDOM(productId, quantity = 1) { + const product = productService.getProductById(productId); + if (!product) return; + + // 기존 아이템이 있는지 확인 + const existingItem = cartDisplayElement.querySelector(`#${productId}`); + + if (existingItem) { + // 기존 아이템 수량 증가 + const qtyElement = existingItem.querySelector('.quantity-number'); + const currentQty = parseInt(qtyElement.textContent); + qtyElement.textContent = currentQty + quantity; + } else { + // 새 아이템 생성 + const newItem = createCartItemElement(product, quantity); + cartDisplayElement.appendChild(newItem); } - handleCalculateCartStuff(); } -main(); -addBtn.addEventListener("click", function () { - var selItem = sel.value - var hasItem = false; - for (var idx = 0; idx < prodList.length; idx++) { - if (prodList[idx].id === selItem) { - hasItem = true; - break; - } - } - if (!selItem || !hasItem) { - return; - } - var itemToAdd = null; - for (var 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']); - if (item) { - var qtyElem = item.querySelector('.quantity-number') - var newQty = parseInt(qtyElem['textContent']) + 1 - if (newQty <= itemToAdd.q + parseInt(qtyElem.textContent)) { - qtyElem.textContent = newQty; - itemToAdd['q']-- - } else { - alert('재고가 부족합니다.'); - } - } else { - var 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.innerHTML = ` -
-
+ +// 장바구니 아이템 DOM 요소 생성 +function createCartItemElement(product, quantity) { + const itemDiv = document.createElement('div'); + itemDiv.id = product.id; + itemDiv.className = + 'cart-item bg-white rounded-lg shadow-md p-4 mb-4 border-b border-gray-200 first:pt-0 last:border-b-0'; + + itemDiv.innerHTML = ` +
+
+
+ ${product.name.charAt(0)}
-

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

-

PRODUCT

-

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

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

${product.name}

+

₩${product.price.toLocaleString()}

- `; - cartDisp.appendChild(newItem); - itemToAdd.q--; - } - handleCalculateCartStuff(); - 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++) { - 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 (newQty > 0 && newQty <= prod.q + currentQty) { - qtyElem.textContent = newQty; - prod.q -= qtyChange; - } else if (newQty <= 0) { - prod.q += currentQty; - itemElem.remove(); - } else { - alert('재고가 부족합니다.'); - } - } else if (tgt.classList.contains('remove-item')) { - var qtyElem = itemElem.querySelector('.quantity-number'); - var remQty = parseInt(qtyElem.textContent); - prod.q += remQty; - itemElem.remove(); - } - if (prod && prod.q < 5) { - } - handleCalculateCartStuff(); - onUpdateSelectOptions(); - } -}); \ No newline at end of file +
+
+ + ${quantity} + + +
+
+ `; + + return itemDiv; +} + +main(); + +// 이벤트 핸들러 래퍼 함수들 +function updateCartItemQuantityHandler(productId, newQuantity) { + const { success, cartState: newCartState } = updateCartItemQuantity( + cartState, + productId, + newQuantity, + productService, + ); + if (success) { + cartState = newCartState; + } + return { success, cartState: newCartState }; +} + +function removeItemFromCartHandler(productId) { + const { success, cartState: newCartState } = removeItemFromCart( + cartState, + productId, + productService, + ); + if (success) { + cartState = newCartState; + } + return { success, cartState: newCartState }; +} + +// 이벤트 리스너 설정 +setupEventListeners( + { + addToCartButton, + productSelector, + cartDisplayElement, + }, + { + addItemToCartUI, + updateCartItemQuantity: updateCartItemQuantityHandler, + removeItemFromCart: removeItemFromCartHandler, + calculateCartSummary, + updateProductOptions, + }, +); diff --git a/src/basic/services/cart/CartService.js b/src/basic/services/cart/CartService.js new file mode 100644 index 000000000..c531535a4 --- /dev/null +++ b/src/basic/services/cart/CartService.js @@ -0,0 +1,330 @@ +/** + * 장바구니 관련 비즈니스 로직을 담당하는 함수들 + */ + +/** + * 장바구니 상태 초기화 + */ +export function createInitialCartState() { + return { + items: [], + totalAmount: 0, + itemCount: 0, + lastSelectedProductId: null, + }; +} + +/** + * 장바구니에 상품 추가 + */ +export function addItemToCart(cartState, productId, quantity, productService) { + const product = productService.getProductById(productId); + + if (!product) { + return { + success: false, + message: '상품을 찾을 수 없습니다.', + cartState, + }; + } + + if (product.quantity < quantity) { + return { + success: false, + message: '재고가 부족합니다.', + cartState, + }; + } + + const existingItem = cartState.items.find((item) => item.id === productId); + + if (existingItem) { + // 기존 상품의 추가 수량이 재고를 초과하는지 확인 + if (product.quantity < quantity) { + return { + success: false, + message: '재고가 부족합니다.', + cartState, + }; + } + + // 기존 상품 수량 증가 + const updatedItems = cartState.items.map((item) => + item.id === productId ? { ...item, quantity: item.quantity + quantity } : item, + ); + + const newCartState = { + ...cartState, + items: updatedItems, + lastSelectedProductId: productId, + }; + + // 재고 감소 + const stockResult = productService.decreaseStock(productId, quantity); + if (!stockResult.success) { + return { + success: false, + message: stockResult.message || '재고 업데이트에 실패했습니다.', + cartState, + }; + } + + return { + success: true, + cartState: updateCartSummary(newCartState, productService), + }; + } else { + // 새 상품 추가 + const newItem = { + id: productId, + name: product.name, + price: product.price, + originalPrice: product.originalPrice || product.price, + quantity, + onSale: product.onSale || false, + suggestSale: product.suggestSale || false, + }; + + const newCartState = { + ...cartState, + items: [...cartState.items, newItem], + lastSelectedProductId: productId, + }; + + // 재고 감소 + const stockResult = productService.decreaseStock(productId, quantity); + if (!stockResult.success) { + return { + success: false, + message: stockResult.message || '재고 업데이트에 실패했습니다.', + cartState, + }; + } + + return { + success: true, + cartState: updateCartSummary(newCartState, productService), + }; + } +} + +/** + * 장바구니에서 상품 수량 변경 + */ +export function updateCartItemQuantity(cartState, productId, newQuantity, productService) { + const item = cartState.items.find((item) => item.id === productId); + if (!item) { + return { + success: false, + message: '상품을 찾을 수 없습니다.', + cartState, + }; + } + + if (newQuantity <= 0) { + // 상품 제거 + return removeItemFromCart(cartState, productId, productService); + } + + const quantityDifference = newQuantity - item.quantity; + const product = productService.getProductById(productId); + + if (!product) { + return { + success: false, + message: '상품 정보를 찾을 수 없습니다.', + cartState, + }; + } + + // 수량 증가 시 재고 확인 + if (quantityDifference > 0 && product.quantity < quantityDifference) { + return { + success: false, + message: '재고가 부족합니다.', + cartState, + }; + } + + // 수량 변경 + const updatedItems = cartState.items.map((item) => + item.id === productId ? { ...item, quantity: newQuantity } : item, + ); + + const newCartState = { + ...cartState, + items: updatedItems, + }; + + // 재고 업데이트 (양수면 감소, 음수면 증가) + const stockResult = productService.decreaseStock(productId, quantityDifference); + if (!stockResult.success) { + return { + success: false, + message: stockResult.message || '재고 업데이트에 실패했습니다.', + cartState, + }; + } + + return { + success: true, + cartState: updateCartSummary(newCartState, productService), + }; +} + +/** + * 장바구니에서 상품 제거 + */ +export function removeItemFromCart(cartState, productId, productService) { + const itemIndex = cartState.items.findIndex((item) => item.id === productId); + if (itemIndex === -1) { + return { + success: false, + message: '상품을 찾을 수 없습니다.', + cartState, + }; + } + + const item = cartState.items[itemIndex]; + + // 재고 복구 + const stockResult = productService.increaseStock(productId, item.quantity); + if (!stockResult.success) { + return { + success: false, + message: stockResult.message || '재고 복구에 실패했습니다.', + cartState, + }; + } + + // 장바구니에서 제거 + const newItems = cartState.items.filter((item) => item.id !== productId); + const newCartState = { + ...cartState, + items: newItems, + }; + + return { + success: true, + cartState: updateCartSummary(newCartState, productService), + }; +} + +/** + * 장바구니 비우기 + */ +export function clearCart(cartState, productService) { + // 모든 상품의 재고 복구 + cartState.items.forEach((item) => { + productService.increaseStock(item.id, item.quantity); + }); + + const newCartState = { + ...cartState, + items: [], + }; + + return updateCartSummary(newCartState, productService); +} + +/** + * 장바구니 요약 정보 업데이트 + */ +export function updateCartSummary(cartState, productService) { + const itemCount = cartState.items.reduce((total, item) => total + item.quantity, 0); + + // 각 상품별 합계 계산 + let totalAmount = 0; + + cartState.items.forEach((item) => { + const product = productService.getProductById(item.id); + if (product) { + // 현재 상품의 실제 가격 사용 (할인이 적용된 가격) + const itemTotal = product.price * item.quantity; + totalAmount += itemTotal; + } + }); + + return { + ...cartState, + itemCount, + totalAmount, + }; +} + +/** + * 장바구니 상태 조회 + */ +export function getCartState(cartState, productService, discountService, pointService) { + const updatedCartState = updateCartSummary(cartState, productService); + + // 포인트 정보 계산 (pointService 주입 필요) + const pointInfo = { + totalPoints: 0, + details: [], + displayText: '적립 포인트: 0p', + }; + + // 할인 정보 계산 (discountService 주입 필요) + const discountInfo = []; + + return { + items: [...updatedCartState.items], + summary: { + subtotal: updatedCartState.items.reduce((total, item) => { + const product = productService.getProductById(item.id); + return total + (product ? product.originalPrice || product.price : 0) * item.quantity; + }, 0), + totalAmount: updatedCartState.totalAmount, + itemCount: updatedCartState.itemCount, + itemDiscounts: [], + discountRate: 0, + isTuesday: false, + }, + pointInfo, + discountInfo, + lastSelectedProductId: updatedCartState.lastSelectedProductId, + }; +} + +/** + * 장바구니가 비어있는지 확인 + */ +export function isCartEmpty(cartState) { + return cartState.items.length === 0; +} + +/** + * 특정 상품이 장바구니에 있는지 확인 + */ +export function hasCartItem(cartState, productId) { + return cartState.items.some((item) => item.id === productId); +} + +/** + * 장바구니 아이템 수 조회 + */ +export function getCartItemCount(cartState) { + return cartState.items.reduce((total, item) => total + item.quantity, 0); +} + +/** + * 총 금액 조회 + */ +export function getCartTotalAmount(cartState) { + return cartState.totalAmount; +} + +/** + * 장바구니 아이템 조회 + */ +export function getCartItems(cartState) { + return [...cartState.items]; +} + +/** + * 특정 상품의 장바구니 아이템 조회 + */ +export function getCartItem(cartState, productId) { + return cartState.items.find((item) => item.id === productId); +} diff --git a/src/basic/services/discount/DiscountService.js b/src/basic/services/discount/DiscountService.js new file mode 100644 index 000000000..6faec6632 --- /dev/null +++ b/src/basic/services/discount/DiscountService.js @@ -0,0 +1,180 @@ +import { DISCOUNT_THRESHOLDS, DISCOUNT_RATES, UI_CONSTANTS } from '../../constants/index.js'; + +/** + * 할인 관련 비즈니스 로직을 담당하는 함수들 + */ + +/** + * 화요일 여부 확인 + */ +export function checkIsTuesday() { + return new Date().getDay() === UI_CONSTANTS.TUESDAY; +} + +/** + * 개별 상품 할인 계산 + */ +export function calculateItemDiscount(productId, quantity, calculateItemDiscountFn) { + return calculateItemDiscountFn(productId, quantity); +} + +/** + * 대량구매 할인 계산 + */ +export function calculateBulkDiscount(itemCount) { + if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) { + return DISCOUNT_RATES.BULK_PURCHASE; + } + return 0; +} + +/** + * 화요일 할인 계산 + */ +export function calculateTuesdayDiscount() { + if (checkIsTuesday()) { + return DISCOUNT_RATES.TUESDAY; + } + return 0; +} + +/** + * 총 할인율 계산 + */ +export function calculateTotalDiscountRate(itemCount, subtotal, currentAmount) { + let finalAmount = currentAmount; + let discountRate = 0; + + // 대량구매 할인 적용 + const bulkDiscount = calculateBulkDiscount(itemCount); + if (bulkDiscount > 0) { + finalAmount = subtotal * (1 - bulkDiscount); + discountRate = bulkDiscount; + } else { + discountRate = (subtotal - finalAmount) / subtotal; + } + + // 화요일 할인 적용 + const tuesdayDiscount = calculateTuesdayDiscount(); + if (tuesdayDiscount > 0 && finalAmount > 0) { + finalAmount = finalAmount * (1 - tuesdayDiscount); + discountRate = 1 - finalAmount / subtotal; + } + + return { + finalAmount, + discountRate, + isTuesday: checkIsTuesday(), + }; +} + +/** + * 할인 정보 생성 + */ +export function createDiscountInfo(itemDiscounts, itemCount) { + const discountInfo = []; + + // 대량구매 할인 정보 + if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) { + discountInfo.push({ + type: 'bulk', + name: '대량구매 할인 (30개 이상)', + rate: DISCOUNT_RATES.BULK_PURCHASE * 100, + }); + } else if (itemDiscounts.length > 0) { + // 개별 상품 할인 정보 + itemDiscounts.forEach((item) => { + discountInfo.push({ + type: 'individual', + name: `${item.name} (10개↑)`, + rate: item.discount, + }); + }); + } + + // 화요일 할인 정보 + if (checkIsTuesday()) { + discountInfo.push({ + type: 'tuesday', + name: '화요일 추가 할인', + rate: DISCOUNT_RATES.TUESDAY * 100, + }); + } + + return discountInfo; +} + +/** + * 할인된 금액 계산 + */ +export function calculateDiscountedAmount(originalAmount, discountRate) { + return originalAmount * (1 - discountRate); +} + +/** + * 절약된 금액 계산 + */ +export function calculateSavedAmount(originalAmount, finalAmount) { + return originalAmount - finalAmount; +} + +/** + * 할인율 적용 + */ +export function applyDiscountRate(amount, discountRate) { + return amount * (1 - discountRate); +} + +/** + * 복합 할인율 계산 + */ +export function calculateCompoundDiscountRate(discountRates) { + if (discountRates.length === 0) return 0; + + // 복합 할인율 = 1 - (1 - 할인율1) * (1 - 할인율2) * ... + const compoundRate = discountRates.reduce((rate, discount) => { + return rate * (1 - discount); + }, 1); + + return 1 - compoundRate; +} + +/** + * 할인 가능 여부 확인 + */ +export function canApplyDiscount(productId, quantity) { + if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) { + return false; + } + + // 상품별 할인 가능 여부 확인 + const discountableProducts = [ + 'p1', // 키보드 + 'p2', // 마우스 + 'p3', // 모니터암 + 'p4', // 노트북 파우치 + 'p5', // 스피커 + ]; + + return discountableProducts.includes(productId); +} + +/** + * 최대 할인율 계산 + */ +export function calculateMaxDiscountRate(productId, quantity, itemCount) { + let maxDiscount = 0; + + // 개별 상품 할인 + if (quantity >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) { + const itemDiscount = calculateItemDiscount(productId, quantity, () => 0); + maxDiscount = Math.max(maxDiscount, itemDiscount); + } + + // 대량구매 할인 + if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) { + maxDiscount = Math.max(maxDiscount, DISCOUNT_RATES.BULK_PURCHASE); + } + + return maxDiscount; +} diff --git a/src/basic/services/point/PointService.js b/src/basic/services/point/PointService.js new file mode 100644 index 000000000..0f8cea0b9 --- /dev/null +++ b/src/basic/services/point/PointService.js @@ -0,0 +1,230 @@ +import { + POINT_RATES, + PRODUCT_IDS, + DISCOUNT_THRESHOLDS, + UI_CONSTANTS, +} from '../../constants/index.js'; + +/** + * 포인트 관련 비즈니스 로직을 담당하는 함수들 + */ + +/** + * 화요일 여부 확인 + */ +export function checkIsTuesday() { + return new Date().getDay() === UI_CONSTANTS.TUESDAY; +} + +/** + * 기본 포인트 계산 + */ +export function calculateBasePoints(totalAmount) { + return Math.floor(totalAmount * POINT_RATES.BASE_RATE); +} + +/** + * 화요일 2배 포인트 적용 + */ +export function applyTuesdayMultiplier(basePoints) { + if (checkIsTuesday() && basePoints > 0) { + return basePoints * POINT_RATES.TUESDAY_MULTIPLIER; + } + return basePoints; +} + +/** + * 상품 조합 보너스 포인트 계산 + */ +export function calculateProductCombinationBonus(cartItems) { + let bonusPoints = 0; + const productIds = cartItems.map((item) => item.id); + + // 키보드+마우스 세트 보너스 + const hasKeyboard = productIds.includes(PRODUCT_IDS.KEYBOARD); + const hasMouse = productIds.includes(PRODUCT_IDS.MOUSE); + const hasMonitorArm = productIds.includes(PRODUCT_IDS.MONITOR_ARM); + + if (hasKeyboard && hasMouse) { + bonusPoints += POINT_RATES.SET_BONUS; + } + + // 풀세트 보너스 + if (hasKeyboard && hasMouse && hasMonitorArm) { + bonusPoints += POINT_RATES.FULL_SET_BONUS; + } + + return bonusPoints; +} + +/** + * 수량별 보너스 포인트 계산 + */ +export function calculateQuantityBonus(itemCount) { + if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) { + return POINT_RATES.QUANTITY_BONUS_30; + } else if (itemCount >= 20) { + return POINT_RATES.QUANTITY_BONUS_20; + } else if (itemCount >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) { + return POINT_RATES.QUANTITY_BONUS_10; + } + return 0; +} + +/** + * 총 포인트 계산 + */ +export function calculateTotalPoints(totalAmount, cartItems) { + let finalPoints = 0; + const pointsDetail = []; + + // 기본 포인트 계산 + const basePoints = calculateBasePoints(totalAmount); + if (basePoints > 0) { + finalPoints = basePoints; + pointsDetail.push(`기본: ${basePoints}p`); + } + + // 화요일 2배 포인트 적용 + const tuesdayPoints = applyTuesdayMultiplier(basePoints); + if (tuesdayPoints !== basePoints) { + finalPoints = tuesdayPoints; + pointsDetail.push('화요일 2배'); + } + + // 상품 조합 보너스 + const combinationBonus = calculateProductCombinationBonus(cartItems); + if (combinationBonus > 0) { + finalPoints += combinationBonus; + + // 키보드+마우스 세트 보너스 (50p) + const hasKeyboard = cartItems.some((item) => item.id === PRODUCT_IDS.KEYBOARD); + const hasMouse = cartItems.some((item) => item.id === PRODUCT_IDS.MOUSE); + if (hasKeyboard && hasMouse) { + pointsDetail.push('키보드+마우스 세트 +50p'); + } + + // 풀세트 보너스 (100p) - 키보드+마우스+모니터암 + const hasMonitorArm = cartItems.some((item) => item.id === PRODUCT_IDS.MONITOR_ARM); + if (hasKeyboard && hasMouse && hasMonitorArm) { + pointsDetail.push('풀세트 구매 +100p'); + } + } + + // 수량별 보너스 + const itemCount = cartItems.reduce((total, item) => total + item.quantity, 0); + const quantityBonus = calculateQuantityBonus(itemCount); + if (quantityBonus > 0) { + finalPoints += quantityBonus; + if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) { + pointsDetail.push('대량구매(30개+) +100p'); + } else if (itemCount >= 20) { + pointsDetail.push('대량구매(20개+) +50p'); + } else { + pointsDetail.push('대량구매(10개+) +20p'); + } + } + + return { + totalPoints: finalPoints, + details: pointsDetail, + }; +} + +/** + * 포인트 정보 생성 + */ +export function createPointInfo(totalAmount, cartItems) { + if (cartItems.length === 0) { + return { + totalPoints: 0, + details: [], + displayText: '적립 포인트: 0p', + }; + } + + const { totalPoints, details } = calculateTotalPoints(totalAmount, cartItems); + + return { + totalPoints, + details, + displayText: `적립 포인트: ${totalPoints}p`, + detailText: details.join(', '), + }; +} + +/** + * 포인트 적립 가능 여부 확인 + */ +// export function canEarnPoints(totalAmount) { +// return totalAmount > 0; +// } + +/** + * 포인트 적립율 계산 + */ +// export function calculatePointRate(totalAmount, cartItems) { +// if (!canEarnPoints(totalAmount)) { +// return 0; +// } + +// const { totalPoints } = calculateTotalPoints(totalAmount, cartItems); +// return totalPoints / totalAmount; +// } + +/** + * 포인트 적립 예상 금액 계산 + */ +// export function calculateExpectedPoints(totalAmount, cartItems) { +// const { totalPoints } = calculateTotalPoints(totalAmount, cartItems); +// return totalPoints; +// } + +/** + * 포인트 적립 내역 생성 + */ +// export function createPointHistory(totalAmount, cartItems) { +// const history = []; + +// // 기본 포인트 +// const basePoints = calculateBasePoints(totalAmount); +// if (basePoints > 0) { +// history.push({ +// type: 'base', +// description: '기본 적립', +// points: basePoints, +// }); +// } + +// // 화요일 보너스 +// if (checkIsTuesday() && basePoints > 0) { +// history.push({ +// type: 'tuesday', +// description: '화요일 2배 보너스', +// points: basePoints, +// }); +// } + +// // 상품 조합 보너스 +// const combinationBonus = calculateProductCombinationBonus(cartItems); +// if (combinationBonus > 0) { +// history.push({ +// type: 'combination', +// description: '상품 조합 보너스', +// points: combinationBonus, +// }); +// } + +// // 수량 보너스 +// const itemCount = cartItems.reduce((total, item) => total + item.quantity, 0); +// const quantityBonus = calculateQuantityBonus(itemCount); +// if (quantityBonus > 0) { +// history.push({ +// type: 'quantity', +// description: '수량 보너스', +// points: quantityBonus, +// }); +// } + +// return history; +// } diff --git a/src/basic/services/product/ProductService.js b/src/basic/services/product/ProductService.js new file mode 100644 index 000000000..715515fa4 --- /dev/null +++ b/src/basic/services/product/ProductService.js @@ -0,0 +1,224 @@ +import { + PRODUCT_IDS, + PRODUCT_NAMES, + PRODUCT_PRICES, + INITIAL_STOCK, + DISCOUNT_THRESHOLDS, + DISCOUNT_RATES, +} from '../../constants/index.js'; + +/** + * 상품 관련 비즈니스 로직을 담당하는 함수들 + */ + +/** + * 상품 목록 + */ +export function initializeProducts() { + return [ + { + id: PRODUCT_IDS.KEYBOARD, + name: PRODUCT_NAMES[PRODUCT_IDS.KEYBOARD], + price: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD], + quantity: INITIAL_STOCK[PRODUCT_IDS.KEYBOARD], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.MOUSE, + name: PRODUCT_NAMES[PRODUCT_IDS.MOUSE], + price: PRODUCT_PRICES[PRODUCT_IDS.MOUSE], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MOUSE], + quantity: INITIAL_STOCK[PRODUCT_IDS.MOUSE], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.MONITOR_ARM, + name: PRODUCT_NAMES[PRODUCT_IDS.MONITOR_ARM], + price: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM], + quantity: INITIAL_STOCK[PRODUCT_IDS.MONITOR_ARM], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.LAPTOP_CASE, + name: PRODUCT_NAMES[PRODUCT_IDS.LAPTOP_CASE], + price: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE], + quantity: INITIAL_STOCK[PRODUCT_IDS.LAPTOP_CASE], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.SPEAKER, + name: PRODUCT_NAMES[PRODUCT_IDS.SPEAKER], + price: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER], + quantity: INITIAL_STOCK[PRODUCT_IDS.SPEAKER], + onSale: false, + suggestSale: false, + }, + ]; +} + +/** + * 상품 ID로 상품 조회 + */ +export function getProductById(products, productId) { + return products.find((product) => product.id === productId); +} + +/** + * 모든 상품 조회 + */ +export function getAllProducts(products) { + return [...products]; +} + +/** + * 상품 재고 감소 + */ +export function decreaseStock(products, productId, quantity = 1) { + const product = getProductById(products, productId); + if (product && product.quantity >= quantity) { + const updatedProducts = products.map((p) => + p.id === productId ? { ...p, quantity: p.quantity - quantity } : p, + ); + return { success: true, products: updatedProducts }; + } + return { success: false, products }; +} + +/** + * 상품 재고 증가 + */ +export function increaseStock(products, productId, quantity = 1) { + const product = getProductById(products, productId); + if (product) { + const updatedProducts = products.map((p) => + p.id === productId ? { ...p, quantity: p.quantity + quantity } : p, + ); + return { success: true, products: updatedProducts }; + } + return { success: false, products }; +} + +/** + * 상품 할인 적용 + */ +export function applySale(products, productId, discountRate) { + const product = getProductById(products, productId); + if (product) { + const updatedProducts = products.map((p) => + p.id === productId + ? { + ...p, + price: Math.round(p.originalPrice * (1 - discountRate)), + onSale: true, + } + : p, + ); + return { success: true, products: updatedProducts }; + } + return { success: false, products }; +} + +/** + * 상품 추천 할인 적용 + */ +export function applySuggestSale(products, productId, discountRate) { + const product = getProductById(products, productId); + if (product) { + const updatedProducts = products.map((p) => + p.id === productId + ? { + ...p, + price: Math.round(p.price * (1 - discountRate)), + suggestSale: true, + } + : p, + ); + return { success: true, products: updatedProducts }; + } + return { success: false, products }; +} + +/** + * 상품 할인 초기화 + */ +export function resetSale(products, productId) { + const product = getProductById(products, productId); + if (product) { + const updatedProducts = products.map((p) => + p.id === productId + ? { + ...p, + price: p.originalPrice, + onSale: false, + suggestSale: false, + } + : p, + ); + return { success: true, products: updatedProducts }; + } + return { success: false, products }; +} + +/** + * 재고 부족 상품 조회 + */ +export function getLowStockProducts(products) { + return products.filter( + (product) => product.quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM && product.quantity > 0, + ); +} + +/** + * 품절 상품 조회 + */ +export function getOutOfStockProducts(products) { + return products.filter((product) => product.quantity === 0); +} + +/** + * 총 재고 수량 계산 + */ +export function getTotalStock(products) { + return products.reduce((total, product) => total + product.quantity, 0); +} + +/** + * 개별 상품 할인율 계산 + */ +export function calculateItemDiscount(productId, quantity) { + if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) { + return 0; + } + + const discountMap = { + [PRODUCT_IDS.KEYBOARD]: DISCOUNT_RATES.KEYBOARD, + [PRODUCT_IDS.MOUSE]: DISCOUNT_RATES.MOUSE, + [PRODUCT_IDS.MONITOR_ARM]: DISCOUNT_RATES.MONITOR_ARM, + [PRODUCT_IDS.LAPTOP_CASE]: DISCOUNT_RATES.LAPTOP_CASE, + [PRODUCT_IDS.SPEAKER]: DISCOUNT_RATES.SPEAKER, + }; + + return discountMap[productId] || 0; +} + +/** + * 상품 가격 업데이트 + */ +export function updateProductPrice(products, productId, newPrice) { + return products.map((p) => (p.id === productId ? { ...p, price: newPrice } : p)); +} + +/** + * 상품 상태 업데이트 + */ +export function updateProductState(products, productId, updates) { + return products.map((p) => (p.id === productId ? { ...p, ...updates } : p)); +} diff --git a/src/basic/store/cartStore.js b/src/basic/store/cartStore.js new file mode 100644 index 000000000..9686fa768 --- /dev/null +++ b/src/basic/store/cartStore.js @@ -0,0 +1,67 @@ +export const cartStore = { + products: [], + totalAmount: 0, + itemCount: 0, + bonusPoints: 0, + lastSelectedId: null, +}; + +export const cartStoreActions = { + getProducts() { + return cartStore.products; + }, + + setProducts(products) { + cartStore.products = products.map((p) => ({ + ...p, + originalQuantity: p.quantity, // 최초 재고 보존 + quantity: p.quantity, + })); + }, + + addToCart(productId, quantity = 1) { + if (quantity <= 0) return false; + + const product = cartStore.products.find((p) => p.id === productId); + if (!product || product.quantity < quantity) return false; + + product.quantity -= quantity; + cartStore.itemCount += quantity; + cartStore.totalAmount += product.price * quantity; + cartStore.lastSelectedId = productId; + + return true; + }, + + removeFromCart(productId, quantity = 1) { + if (quantity <= 0) return false; + + const product = cartStore.products.find((p) => p.id === productId); + if (!product) return false; + + product.quantity += quantity; + cartStore.itemCount -= quantity; + cartStore.totalAmount -= product.price * quantity; + + return true; + }, + + updateBonusPoints(points) { + cartStore.bonusPoints = points; + }, + + setLastSelectedId(productId) { + cartStore.lastSelectedId = productId; + }, + + reset() { + cartStore.totalAmount = 0; + cartStore.itemCount = 0; + cartStore.bonusPoints = 0; + cartStore.lastSelectedId = null; + cartStore.products = cartStore.products.map((p) => ({ + ...p, + quantity: p.originalQuantity, + })); + }, +}; diff --git a/src/basic/store/productStore.js b/src/basic/store/productStore.js new file mode 100644 index 000000000..c350a276d --- /dev/null +++ b/src/basic/store/productStore.js @@ -0,0 +1,252 @@ +import { + PRODUCT_IDS, + PRODUCT_NAMES, + PRODUCT_PRICES, + INITIAL_STOCK, + DISCOUNT_THRESHOLDS, + DISCOUNT_RATES, +} from '../constants/index.js'; + +export const productStore = { + products: [], + selectedProductId: null, + lowStockProducts: [], + outOfStockProducts: [], + totalStock: 0, +}; + +export const productStoreActions = { + getProducts() { + return productStore.products; + }, + + getProductById(productId) { + return productStore.products.find((product) => product.id === productId); + }, + + setProducts(products) { + productStore.products = products.map((product) => ({ + ...product, + originalQuantity: product.quantity, + originalPrice: product.price, + })); + this.updateDerivedState(); + }, + + initializeProducts() { + const products = [ + { + id: PRODUCT_IDS.KEYBOARD, + name: PRODUCT_NAMES[PRODUCT_IDS.KEYBOARD], + price: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD], + quantity: INITIAL_STOCK[PRODUCT_IDS.KEYBOARD], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.MOUSE, + name: PRODUCT_NAMES[PRODUCT_IDS.MOUSE], + price: PRODUCT_PRICES[PRODUCT_IDS.MOUSE], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MOUSE], + quantity: INITIAL_STOCK[PRODUCT_IDS.MOUSE], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.MONITOR_ARM, + name: PRODUCT_NAMES[PRODUCT_IDS.MONITOR_ARM], + price: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM], + quantity: INITIAL_STOCK[PRODUCT_IDS.MONITOR_ARM], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.LAPTOP_CASE, + name: PRODUCT_NAMES[PRODUCT_IDS.LAPTOP_CASE], + price: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE], + quantity: INITIAL_STOCK[PRODUCT_IDS.LAPTOP_CASE], + onSale: false, + suggestSale: false, + }, + { + id: PRODUCT_IDS.SPEAKER, + name: PRODUCT_NAMES[PRODUCT_IDS.SPEAKER], + price: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER], + originalPrice: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER], + quantity: INITIAL_STOCK[PRODUCT_IDS.SPEAKER], + onSale: false, + suggestSale: false, + }, + ]; + + this.setProducts(products); + }, + + decreaseStock(productId, quantity = 1) { + const product = this.getProductById(productId); + if (!product || product.quantity < quantity) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId ? { ...p, quantity: p.quantity - quantity } : p, + ); + + this.updateDerivedState(); + return true; + }, + + increaseStock(productId, quantity = 1) { + const product = this.getProductById(productId); + if (!product) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId ? { ...p, quantity: p.quantity + quantity } : p, + ); + + this.updateDerivedState(); + return true; + }, + + applySale(productId, discountRate) { + const product = this.getProductById(productId); + if (!product) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId + ? { + ...p, + price: Math.round(p.originalPrice * (1 - discountRate)), + onSale: true, + } + : p, + ); + + return true; + }, + + applySuggestSale(productId, discountRate) { + const product = this.getProductById(productId); + if (!product) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId + ? { + ...p, + price: Math.round(p.price * (1 - discountRate)), + suggestSale: true, + } + : p, + ); + + return true; + }, + + resetSale(productId) { + const product = this.getProductById(productId); + if (!product) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId + ? { + ...p, + price: p.originalPrice, + onSale: false, + suggestSale: false, + } + : p, + ); + + return true; + }, + + updateProductPrice(productId, newPrice) { + const product = this.getProductById(productId); + if (!product) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId ? { ...p, price: newPrice } : p, + ); + + return true; + }, + + updateProductState(productId, updates) { + const product = this.getProductById(productId); + if (!product) return false; + + productStore.products = productStore.products.map((p) => + p.id === productId ? { ...p, ...updates } : p, + ); + + this.updateDerivedState(); + return true; + }, + + setSelectedProductId(productId) { + productStore.selectedProductId = productId; + }, + + getSelectedProduct() { + return this.getProductById(productStore.selectedProductId); + }, + + getLowStockProducts() { + return productStore.lowStockProducts; + }, + + getOutOfStockProducts() { + return productStore.outOfStockProducts; + }, + + getTotalStock() { + return productStore.totalStock; + }, + + calculateItemDiscount(productId, quantity) { + if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) { + return 0; + } + + const discountMap = { + [PRODUCT_IDS.KEYBOARD]: DISCOUNT_RATES.KEYBOARD, + [PRODUCT_IDS.MOUSE]: DISCOUNT_RATES.MOUSE, + [PRODUCT_IDS.MONITOR_ARM]: DISCOUNT_RATES.MONITOR_ARM, + [PRODUCT_IDS.LAPTOP_CASE]: DISCOUNT_RATES.LAPTOP_CASE, + [PRODUCT_IDS.SPEAKER]: DISCOUNT_RATES.SPEAKER, + }; + + return discountMap[productId] || 0; + }, + + updateDerivedState() { + // Update low stock products + productStore.lowStockProducts = productStore.products.filter( + (product) => product.quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM && product.quantity > 0, + ); + + // Update out of stock products + productStore.outOfStockProducts = productStore.products.filter( + (product) => product.quantity === 0, + ); + + // Update total stock + productStore.totalStock = productStore.products.reduce( + (total, product) => total + product.quantity, + 0, + ); + }, + + reset() { + productStore.products = productStore.products.map((p) => ({ + ...p, + quantity: p.originalQuantity, + price: p.originalPrice, + onSale: false, + suggestSale: false, + })); + productStore.selectedProductId = null; + this.updateDerivedState(); + }, +}; diff --git a/src/basic/utils/EventHandler.js b/src/basic/utils/EventHandler.js new file mode 100644 index 000000000..13a89f377 --- /dev/null +++ b/src/basic/utils/EventHandler.js @@ -0,0 +1,175 @@ +// EventHandler.js - 이벤트 핸들러 관리 유틸리티 + +import { cartStoreActions } from '../store/cartStore.js'; +import { getProductById } from '../services/product/ProductService.js'; +import { UI_CONSTANTS } from '../constants/index.js'; + +/** + * 장바구니 추가 버튼 클릭 이벤트 핸들러 + * @param {HTMLSelectElement} productSelector - 상품 선택 요소 + * @param {Function} addItemToCartUI - 장바구니 추가 UI 함수 + */ +export function handleAddToCartClick(productSelector, addItemToCartUI) { + const selectedProductId = productSelector.value; + + if (!selectedProductId) { + return; + } + + // 장바구니에 상품 추가 + addItemToCartUI(selectedProductId, 1); +} + +/** + * 장바구니 내 수량 변경 이벤트 핸들러 + * @param {Event} event - 클릭 이벤트 + * @param {Object} handlers - 핸들러 함수들 + */ +export function handleQuantityChange(event, handlers) { + const { updateCartItemQuantity, removeItemFromCart, calculateCartSummary, updateProductOptions } = + handlers; + + const { target } = event; + const { productId } = target.dataset; + const quantityChange = parseInt(target.dataset.change); + + const itemElement = document.getElementById(productId); + const quantityElement = itemElement.querySelector('.quantity-number'); + const currentQuantity = parseInt(quantityElement.textContent); + const newQuantity = currentQuantity + quantityChange; + + if (newQuantity > 0) { + // 수량 변경 + const result = updateCartItemQuantity(productId, newQuantity); + if (result.success) { + quantityElement.textContent = newQuantity; + } else { + alert(result.message || '재고가 부족합니다.'); + } + } else { + // 상품 제거 + const result = removeItemFromCart(productId); + if (result.success) { + itemElement.remove(); + } else { + alert(result.message || '상품 제거에 실패했습니다.'); + } + } + + // 재고 상태 확인 및 알림 + checkStockStatus(productId); + + // UI 업데이트 + calculateCartSummary(); + updateProductOptions(); +} + +/** + * 장바구니 내 상품 제거 이벤트 핸들러 + * @param {Event} event - 클릭 이벤트 + * @param {Object} handlers - 핸들러 함수들 + */ +export function handleRemoveItem(event, handlers) { + const { removeItemFromCart, calculateCartSummary, updateProductOptions } = handlers; + + const { target } = event; + const { productId } = target.dataset; + + const itemElement = document.getElementById(productId); + + // 상품 제거 + const result = removeItemFromCart(productId); + if (result.success) { + itemElement.remove(); + } else { + alert(result.message || '상품 제거에 실패했습니다.'); + } + + // 재고 상태 확인 및 알림 + checkStockStatus(productId); + + // UI 업데이트 + calculateCartSummary(); + updateProductOptions(); +} + +/** + * 재고 상태 확인 및 알림 + * @param {string} productId - 상품 ID + */ +function checkStockStatus(productId) { + const product = getProductById(cartStoreActions.getProducts(), productId); + + if (product && product.quantity < UI_CONSTANTS.LOW_STOCK_THRESHOLD) { + // 재고 부족 알림 (필요시 추가 구현) + console.log(`${product.name}의 재고가 부족합니다.`); + } +} + +/** + * 장바구니 이벤트 위임 핸들러 + * @param {Event} event - 클릭 이벤트 + * @param {Object} handlers - 핸들러 함수들 + */ +export function handleCartItemClick(event, handlers) { + const { target } = event; + + if (target.classList.contains('quantity-change')) { + handleQuantityChange(event, handlers); + } else if (target.classList.contains('remove-item')) { + handleRemoveItem(event, handlers); + } +} + +/** + * 상품 선택 변경 이벤트 핸들러 + * @param {Event} event - change 이벤트 + * @param {Function} updateProductOptions - 상품 옵션 업데이트 함수 + */ +export function handleProductSelectionChange(event, updateProductOptions) { + const selectedProductId = event.target.value; + + if (selectedProductId) { + // 마지막 선택된 상품 ID 저장 + cartStoreActions.setLastSelectedProductId(selectedProductId); + } + + // 상품 옵션 업데이트 + updateProductOptions(); +} + +/** + * 이벤트 리스너 등록 함수 + * @param {Object} elements - DOM 요소들 + * @param {Object} handlers - 핸들러 함수들 + */ +export function setupEventListeners(elements, handlers) { + const { addToCartButton, productSelector, cartDisplayElement } = elements; + const { + addItemToCartUI, + updateCartItemQuantity, + removeItemFromCart, + calculateCartSummary, + updateProductOptions, + } = handlers; + + // 장바구니 추가 버튼 클릭 이벤트 + addToCartButton.addEventListener('click', () => { + handleAddToCartClick(productSelector, addItemToCartUI); + }); + + // 상품 선택 변경 이벤트 + productSelector.addEventListener('change', (event) => { + handleProductSelectionChange(event, updateProductOptions); + }); + + // 장바구니 아이템 클릭 이벤트 (위임) + cartDisplayElement.addEventListener('click', (event) => { + handleCartItemClick(event, { + updateCartItemQuantity, + removeItemFromCart, + calculateCartSummary, + updateProductOptions, + }); + }); +} diff --git a/src/basic/utils/TimerHandler.js b/src/basic/utils/TimerHandler.js new file mode 100644 index 000000000..c2fe0dc70 --- /dev/null +++ b/src/basic/utils/TimerHandler.js @@ -0,0 +1,105 @@ +// TimerHandler.js - 타이머 이벤트 핸들러 관리 유틸리티 + +import { UI_CONSTANTS } from '../constants/index.js'; +import { applySale, applySuggestSale } from '../services/product/ProductService.js'; + +/** + * 번개 세일 타이머 설정 + * @param {Array} products - 상품 목록 + * @param {Function} updateProductOptions - 상품 옵션 업데이트 함수 + * @param {Function} updateCartPrices - 장바구니 가격 업데이트 함수 + */ +export function setupLightningSaleTimer(products, updateProductOptions, updateCartPrices) { + const lightningDelay = Math.random() * UI_CONSTANTS.LIGHTNING_SALE_DELAY; + + setTimeout(() => { + setInterval(() => { + const luckyIndex = Math.floor(Math.random() * products.length); + const luckyItem = products[luckyIndex]; + + if (luckyItem.quantity > 0 && !luckyItem.onSale) { + const result = applySale(products, luckyItem.id, 0.2); + if (result.success) { + // products 배열 업데이트 + Object.assign(products, result.products); + alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`); + updateProductOptions(); + updateCartPrices(); + } + } + }, UI_CONSTANTS.LIGHTNING_SALE_INTERVAL); + }, lightningDelay); +} + +/** + * 추천 할인 타이머 설정 + * @param {Array} products - 상품 목록 + * @param {HTMLElement} cartDisplayElement - 장바구니 표시 요소 + * @param {string} lastSelectedProductId - 마지막 선택된 상품 ID + * @param {Function} updateProductOptions - 상품 옵션 업데이트 함수 + * @param {Function} updateCartPrices - 장바구니 가격 업데이트 함수 + */ +export function setupSuggestSaleTimer( + products, + cartDisplayElement, + lastSelectedProductId, + updateProductOptions, + updateCartPrices, +) { + setTimeout(() => { + setInterval(() => { + if (lastSelectedProductId && cartDisplayElement.children.length > 0) { + let suggestProduct = null; + + for (let i = 0; i < products.length; i++) { + const product = products[i]; + if ( + product.id !== lastSelectedProductId && + product.quantity > 0 && + !product.suggestSale + ) { + suggestProduct = product; + break; + } + } + + if (suggestProduct) { + alert(`💝 ${suggestProduct.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`); + const result = applySuggestSale(products, suggestProduct.id, 0.05); + if (result.success) { + // products 배열 업데이트 + Object.assign(products, result.products); + updateProductOptions(); + updateCartPrices(); + } + } + } + }, UI_CONSTANTS.SUGGEST_SALE_INTERVAL); + }, Math.random() * UI_CONSTANTS.SUGGEST_SALE_DELAY); +} + +/** + * 모든 타이머 설정 + * @param {Object} config - 타이머 설정 객체 + */ +export function setupAllTimers(config) { + const { + products, + cartDisplayElement, + lastSelectedProductId, + updateProductOptions, + updateCartPrices, + } = config; + + // 번개 세일 타이머 설정 + setupLightningSaleTimer(products, updateProductOptions, updateCartPrices); + + // 추천 할인 타이머 설정 + setupSuggestSaleTimer( + products, + cartDisplayElement, + lastSelectedProductId, + updateProductOptions, + updateCartPrices, + ); +} diff --git a/src/basic/utils/UIRenderer.js b/src/basic/utils/UIRenderer.js new file mode 100644 index 000000000..16d0ea661 --- /dev/null +++ b/src/basic/utils/UIRenderer.js @@ -0,0 +1,33 @@ +/** + * UI 렌더링 컴포넌트들 (남은 컴포넌트들) + * 대부분의 컴포넌트는 src/basic/components/ 폴더로 이동되었습니다. + */ + +// 장바구니 추가 버튼 컴포넌트 +export function createAddToCartButton() { + const addToCartButton = document.createElement('button'); + addToCartButton.id = 'add-to-cart'; + addToCartButton.innerHTML = 'Add to Cart'; + addToCartButton.className = + 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all'; + return addToCartButton; +} + +// 상품 선택 컨테이너 컴포넌트 +export function createSelectorContainer(productSelector, addToCartButton, stockInfoElement) { + const selectorContainer = document.createElement('div'); + selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200'; + selectorContainer.appendChild(productSelector); + selectorContainer.appendChild(addToCartButton); + selectorContainer.appendChild(stockInfoElement); + return selectorContainer; +} + +// 왼쪽 컬럼 컴포넌트 +export function createLeftColumn(selectorContainer, cartDisplayElement) { + const leftColumn = document.createElement('div'); + leftColumn.className = 'bg-white border border-gray-200 p-8 overflow-y-auto'; + leftColumn.appendChild(selectorContainer); + leftColumn.appendChild(cartDisplayElement); + return leftColumn; +} diff --git a/src/basic/utils/renderers/CartRenderer.js b/src/basic/utils/renderers/CartRenderer.js new file mode 100644 index 000000000..e7f9625fd --- /dev/null +++ b/src/basic/utils/renderers/CartRenderer.js @@ -0,0 +1,47 @@ +/** + * 장바구니 렌더링 관련 함수들 + */ + +/** + * 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산 + */ +export function updateCartPrices(cartDisplayElement, productList, calculateCartSummary) { + const cartItems = cartDisplayElement.children; + + const productMap = productList.reduce((map, product) => { + map[product.id] = product; + return map; + }, {}); + + // 각 카트 아이템을 순회하며 가격/이름 업데이트 (productMap 사용) + for (const cartItem of cartItems) { + const product = productMap[cartItem.id]; + + if (product) { + const priceDiv = cartItem.querySelector('.text-lg'); + const nameDiv = cartItem.querySelector('h3'); + renderProductPrice(product, priceDiv, nameDiv); + } + } + + calculateCartSummary(); +} + +/** + * 상품 가격 렌더링 로직 분리 + */ +function renderProductPrice(product, priceDiv, nameDiv) { + if (product.onSale && product.suggestSale) { + priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`; + nameDiv.textContent = `⚡💝${product.name}`; + } else if (product.onSale) { + priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`; + nameDiv.textContent = `⚡${product.name}`; + } else if (product.suggestSale) { + priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`; + nameDiv.textContent = `💝${product.name}`; + } else { + priceDiv.textContent = `₩${product.price.toLocaleString()}`; + nameDiv.textContent = product.name; + } +} diff --git a/src/basic/utils/renderers/OrderRenderer.js b/src/basic/utils/renderers/OrderRenderer.js new file mode 100644 index 000000000..55cb1bc64 --- /dev/null +++ b/src/basic/utils/renderers/OrderRenderer.js @@ -0,0 +1,119 @@ +/** + * 주문 렌더링 관련 함수들 + */ + +/** + * 주문 요약 상세 렌더링 컴포넌트 + */ +export function renderOrderSummaryDetails(cartItems, productList, subTot, itemDiscounts) { + const summaryDetails = document.getElementById('summary-details'); + if (!summaryDetails) return; + + summaryDetails.innerHTML = ''; + + if (subTot > 0) { + // 각 상품별 정보 표시 + for (let i = 0; i < cartItems.length; i++) { + let curItem; + for (let j = 0; j < productList.length; j++) { + if (productList[j].id === cartItems[i].id) { + curItem = productList[j]; + break; + } + } + + const quantityElem = cartItems[i].querySelector('.quantity-number'); + const quantity = parseInt(quantityElem.textContent); + const itemTotal = curItem.price * quantity; + + summaryDetails.innerHTML += ` +
+ ${curItem.name} x ${quantity} + ₩${itemTotal.toLocaleString()} +
+ `; + } + + // 소계 표시 + summaryDetails.innerHTML += ` +
+
+ Subtotal + ₩${subTot.toLocaleString()} +
+ `; + + // 할인 정보 표시 + itemDiscounts.forEach(function (discount) { + const colorClass = discount.type === 'tuesday' ? 'text-purple-400' : 'text-green-400'; + const icon = discount.type === 'tuesday' ? '🌟' : discount.type === 'bulk' ? '🎉' : ''; + summaryDetails.innerHTML += ` +
+ ${icon} ${discount.name} + -${discount.rate}% +
+ `; + }); + + // 배송비 표시 + summaryDetails.innerHTML += ` +
+ Shipping + Free +
+ `; + } +} + +/** + * 화요일 특별 할인 UI 렌더링 컴포넌트 + */ +export function renderTuesdaySpecial(isTuesday, totalAmount) { + const tuesdaySpecial = document.getElementById('tuesday-special'); + if (!tuesdaySpecial) return; + + if (isTuesday && totalAmount > 0) { + tuesdaySpecial.classList.remove('hidden'); + } else { + tuesdaySpecial.classList.add('hidden'); + } +} + +/** + * 총 결제 금액 렌더링 컴포넌트 + */ +export function renderTotalAmount(totalAmount, orderSummaryElement) { + const totalDiv = orderSummaryElement.querySelector('.text-2xl'); + if (totalDiv) { + totalDiv.textContent = `₩${Math.round(totalAmount).toLocaleString()}`; + } +} + +/** + * 장바구니 수량 렌더링 컴포넌트 + */ +export function renderItemCount(itemCount) { + const itemCountElement = document.getElementById('item-count'); + if (itemCountElement) { + itemCountElement.textContent = `🛍️ ${itemCount} items in cart`; + } +} + +/** + * 재고 메시지 렌더링 컴포넌트 + */ +export function renderStockMessages(lowStockProducts, outOfStockProducts, stockInfoElement) { + let stockMsg = ''; + + // 재고 부족 상품 메시지 + lowStockProducts.forEach((item) => { + stockMsg += `${item.name}: 재고 부족 (${item.quantity}개 남음)\n`; + }); + + // 품절 상품 메시지 + outOfStockProducts.forEach((item) => { + stockMsg += `${item.name}: 품절\n`; + }); + + stockInfoElement.textContent = stockMsg; +} diff --git a/src/basic/utils/renderers/ProductRenderer.js b/src/basic/utils/renderers/ProductRenderer.js new file mode 100644 index 000000000..1e9014ec4 --- /dev/null +++ b/src/basic/utils/renderers/ProductRenderer.js @@ -0,0 +1,62 @@ +/** + * 상품 렌더링 관련 함수들 + */ +import { UI_CONSTANTS } from '../../constants/UIConstants.js'; + +/** + * 상품 옵션 렌더링 컴포넌트 + * 상품 선택 드롭다운의 옵션들을 렌더링합니다. + */ +export function renderProductOptions(productSelector, productList) { + productSelector.innerHTML = ''; + + for (let i = 0; i < productList.length; i++) { + const item = productList[i]; + const opt = document.createElement('option'); + opt.value = item.id; + let discountText = ''; + + if (item.onSale) discountText += ' ⚡SALE'; + if (item.suggestSale) discountText += ' 💝추천'; + + if (item.quantity === 0) { + opt.textContent = `${item.name} - ${item.price}원 (품절)${discountText}`; + opt.disabled = true; + opt.className = 'text-gray-400'; + } else { + if (item.onSale && item.suggestSale) { + opt.textContent = `⚡💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (25% SUPER SALE!)`; + opt.className = 'text-purple-600 font-bold'; + } else if (item.onSale) { + opt.textContent = `⚡${item.name} - ${item.originalPrice}원 → ${item.price}원 (20% SALE!)`; + opt.className = 'text-red-500 font-bold'; + } else if (item.suggestSale) { + opt.textContent = `💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (5% 추천할인!)`; + opt.className = 'text-blue-500 font-bold'; + } else { + opt.textContent = `${item.name} - ${item.price}원${discountText}`; + } + } + productSelector.appendChild(opt); + } +} + +/** + * 상품 가격 렌더링 로직 분리 + * 상품의 가격과 이름을 렌더링합니다. + */ +export function renderProductPrice(product, priceDiv, nameDiv) { + if (product.onSale && product.suggestSale) { + priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`; + nameDiv.textContent = `⚡💝${product.name}`; + } else if (product.onSale) { + priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`; + nameDiv.textContent = `⚡${product.name}`; + } else if (product.suggestSale) { + priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`; + nameDiv.textContent = `💝${product.name}`; + } else { + priceDiv.textContent = `₩${product.price.toLocaleString()}`; + nameDiv.textContent = product.name; + } +} diff --git a/src/basic/utils/renderers/index.js b/src/basic/utils/renderers/index.js new file mode 100644 index 000000000..49f4e60f2 --- /dev/null +++ b/src/basic/utils/renderers/index.js @@ -0,0 +1,12 @@ +/** + * Renderers 컴포넌트들의 barrel export + */ +export { renderProductOptions, renderProductPrice } from './ProductRenderer.js'; +export { updateCartPrices } from './CartRenderer.js'; +export { + renderOrderSummaryDetails, + renderTuesdaySpecial, + renderTotalAmount, + renderItemCount, + renderStockMessages, +} from './OrderRenderer.js'; diff --git a/src/components/AddToCartButton.tsx b/src/components/AddToCartButton.tsx new file mode 100644 index 000000000..3a8fd1c10 --- /dev/null +++ b/src/components/AddToCartButton.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Product } from '../types'; + +interface AddToCartButtonProps { + selectedProduct: Product | null; + onAddToCart: (product: Product) => void; + disabled?: boolean; +} + +export const AddToCartButton: React.FC = ({ + selectedProduct, + onAddToCart, + disabled = false, +}) => { + const handleClick = () => { + if (selectedProduct) { + onAddToCart(selectedProduct); + } + }; + + return ( + + ); +}; diff --git a/src/components/Cart.tsx b/src/components/Cart.tsx new file mode 100644 index 000000000..953817193 --- /dev/null +++ b/src/components/Cart.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { CartItem } from '../types'; + +interface CartProps { + cartItems: CartItem[]; + onIncreaseQuantity: (productId: string) => void; + onDecreaseQuantity: (productId: string) => void; + onRemoveItem: (productId: string) => void; +} + +export const Cart: React.FC = ({ + cartItems, + onIncreaseQuantity, + onDecreaseQuantity, + onRemoveItem, +}) => { + const formatPrice = (price: number) => { + return `₩${price.toLocaleString()}`; + }; + + if (cartItems.length === 0) { + return ( +
+
🛒
+

장바구니가 비어있습니다

+

상품을 선택하고 추가해보세요!

+
+ ); + } + + return ( +
+

장바구니 ({cartItems.length}개 상품)

+ +
+ {cartItems.map((item, index) => ( +
+
+ {/* 상품 정보 */} +
+
+ {/* 상품 이미지 */} +
+ {item.product.name.charAt(0)} +
+ + {/* 상품명과 가격 */} +
+

{item.product.name}

+

{formatPrice(item.product.price)}

+
+
+
+ + {/* 수량 조절 */} +
+ + + + {item.quantity} + + + +
+ + {/* 총액 */} +
+

+ {formatPrice(item.product.price * item.quantity)} +

+
+ + {/* 제거 버튼 */} + +
+
+ ))} +
+
+ ); +}; diff --git a/src/components/OrderSummary.tsx b/src/components/OrderSummary.tsx new file mode 100644 index 000000000..f81492295 --- /dev/null +++ b/src/components/OrderSummary.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { CartItem, Discount } from '../types'; +import { calculateDiscountedTotal } from '../utils/discount'; + +interface OrderSummaryProps { + cartItems: CartItem[]; +} + +export const OrderSummary: React.FC = ({ cartItems }) => { + const { subtotal, totalDiscount, finalTotal, discounts } = calculateDiscountedTotal(cartItems); + + const formatPrice = (price: number) => { + return `₩${price.toLocaleString()}`; + }; + + const formatDiscountRate = (rate: number) => { + return `${(rate * 100).toFixed(1)}%`; + }; + + return ( +
+

주문 요약

+ + {/* 기본 정보 */} +
+
+ 총 상품 수: + + {cartItems.reduce((sum, item) => sum + item.quantity, 0)}개 + +
+
+ 상품 종류: + {cartItems.length}종 +
+
+ + {/* 할인 정보 */} + {discounts.length > 0 && ( +
+

적용된 할인

+
+ {discounts.map((discount, index) => ( +
+ {discount.description} + + -{formatDiscountRate(discount.rate)} + +
+ ))} +
+
+ )} + + {/* 금액 정보 */} +
+
+ 상품 금액: + {formatPrice(subtotal)} +
+ + {totalDiscount > 0 && ( +
+ 할인 금액: + -{formatPrice(totalDiscount)} +
+ )} + +
+ 최종 결제 금액: + {formatPrice(finalTotal)} +
+
+ + {/* 화요일 할인 배너 */} + {discounts.some((d) => d.type === 'tuesday') && ( +
+
+ 🎉 + + 화요일 특별 할인이 적용되었습니다! + +
+
+ )} +
+ ); +}; diff --git a/src/components/ProductSelector.tsx b/src/components/ProductSelector.tsx new file mode 100644 index 000000000..21f4629a3 --- /dev/null +++ b/src/components/ProductSelector.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Product } from '../types'; + +interface ProductSelectorProps { + products: Product[]; + selectedProductId: string; + onProductSelect: (productId: string) => void; +} + +export const ProductSelector: React.FC = ({ + products, + selectedProductId, + onProductSelect, +}) => { + const formatPrice = (price: number) => { + return `₩${price.toLocaleString()}`; + }; + + const getProductDisplayText = (product: Product) => { + let text = `${product.name} - ${formatPrice(product.price)}`; + + if (product.stock === 0) { + text += ' (품절)'; + } else if (product.stock < 5) { + text += ` (${product.stock}개 남음)`; + } + + return text; + }; + + return ( +
+ + + + + {/* 재고 상태 표시 */} + {products.some((p) => p.stock < 5 && p.stock > 0) && ( +
+

재고 부족 상품

+
    + {products + .filter((p) => p.stock < 5 && p.stock > 0) + .map((product) => ( +
  • + {product.name}: {product.stock}개 남음 +
  • + ))} +
+
+ )} + + {/* 품절 상품 표시 */} + {products.some((p) => p.stock === 0) && ( +
+

품절 상품

+
    + {products + .filter((p) => p.stock === 0) + .map((product) => ( +
  • {product.name}: 품절
  • + ))} +
+
+ )} +
+ ); +}; diff --git a/src/components/TuesdayBanner.tsx b/src/components/TuesdayBanner.tsx new file mode 100644 index 000000000..fe789819e --- /dev/null +++ b/src/components/TuesdayBanner.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { isTuesday } from '../utils/discount'; + +export const TuesdayBanner: React.FC = () => { + if (!isTuesday()) { + return null; + } + + return ( +
+
+
+ 🎉 +
+

화요일 특별 할인!

+

모든 상품 10% 추가 할인

+
+
+
+
10%
+
추가 할인
+
+
+
+ ); +}; diff --git a/src/data/products.ts b/src/data/products.ts new file mode 100644 index 000000000..81ce5b91b --- /dev/null +++ b/src/data/products.ts @@ -0,0 +1,54 @@ +import { Product } from '../types'; + +export const PRODUCTS: Product[] = [ + { + 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로 상품 찾기 +export const getProductById = (id: string): Product | undefined => { + return PRODUCTS.find((product) => product.id === id); +}; + +// 재고 부족 상품 찾기 (5개 미만) +export const getLowStockProducts = (): Product[] => { + return PRODUCTS.filter((product) => product.stock > 0 && product.stock < 5); +}; + +// 품절 상품 찾기 +export const getOutOfStockProducts = (): Product[] => { + return PRODUCTS.filter((product) => product.stock === 0); +}; diff --git a/src/hooks/useCart.ts b/src/hooks/useCart.ts new file mode 100644 index 000000000..039088c5d --- /dev/null +++ b/src/hooks/useCart.ts @@ -0,0 +1,101 @@ +import { useState, useCallback, useMemo } from 'react'; +import { CartItem, Product } from '../types'; + +export const useCart = () => { + const [cartItems, setCartItems] = useState([]); + + // 상품 추가 + const addToCart = useCallback((product: Product, quantity: number = 1) => { + setCartItems((prevItems) => { + const existingItem = prevItems.find((item) => item.product.id === product.id); + + if (existingItem) { + // 이미 있는 상품이면 수량 증가 + return prevItems.map((item) => + item.product.id === product.id ? { ...item, quantity: item.quantity + quantity } : item, + ); + } else { + // 새로운 상품이면 추가 + return [...prevItems, { product, quantity }]; + } + }); + }, []); + + // 상품 제거 + const removeFromCart = useCallback((productId: string) => { + setCartItems((prevItems) => prevItems.filter((item) => item.product.id !== productId)); + }, []); + + // 수량 변경 + const updateQuantity = useCallback( + (productId: string, newQuantity: number) => { + if (newQuantity <= 0) { + removeFromCart(productId); + return; + } + + setCartItems((prevItems) => + prevItems.map((item) => + item.product.id === productId ? { ...item, quantity: newQuantity } : item, + ), + ); + }, + [removeFromCart], + ); + + // 수량 증가 + const increaseQuantity = useCallback((productId: string) => { + setCartItems((prevItems) => + prevItems.map((item) => + item.product.id === productId ? { ...item, quantity: item.quantity + 1 } : item, + ), + ); + }, []); + + // 수량 감소 + const decreaseQuantity = useCallback((productId: string) => { + setCartItems( + (prevItems) => + prevItems + .map((item) => { + if (item.product.id === productId) { + const newQuantity = item.quantity - 1; + return newQuantity <= 0 ? null : { ...item, quantity: newQuantity }; + } + return item; + }) + .filter(Boolean) as CartItem[], + ); + }, []); + + // 장바구니 비우기 + const clearCart = useCallback(() => { + setCartItems([]); + }, []); + + // 계산된 값들 + const totalItems = useMemo( + () => cartItems.reduce((sum, item) => sum + item.quantity, 0), + [cartItems], + ); + + const totalAmount = useMemo( + () => cartItems.reduce((sum, item) => sum + item.product.price * item.quantity, 0), + [cartItems], + ); + + const uniqueItems = useMemo(() => cartItems.length, [cartItems]); + + return { + cartItems, + totalItems, + totalAmount, + uniqueItems, + addToCart, + removeFromCart, + updateQuantity, + increaseQuantity, + decreaseQuantity, + clearCart, + }; +}; diff --git a/src/hooks/useProducts.ts b/src/hooks/useProducts.ts new file mode 100644 index 000000000..3331da2df --- /dev/null +++ b/src/hooks/useProducts.ts @@ -0,0 +1,47 @@ +import { useState, useCallback } from 'react'; +import { Product } from '../types'; +import { PRODUCTS, getProductById } from '../data/products'; + +export const useProducts = () => { + const [products, setProducts] = useState(PRODUCTS); + + // 재고 업데이트 + const updateStock = useCallback((productId: string, quantity: number) => { + setProducts((prevProducts) => + prevProducts.map((product) => + product.id === productId + ? { ...product, stock: Math.max(0, product.stock - quantity) } + : product, + ), + ); + }, []); + + // 재고 복구 (상품 제거 시) + const restoreStock = useCallback((productId: string, quantity: number) => { + setProducts((prevProducts) => + prevProducts.map((product) => + product.id === productId ? { ...product, stock: product.stock + quantity } : product, + ), + ); + }, []); + + // 상품 가져오기 + const getProduct = useCallback((id: string) => { + return getProductById(id); + }, []); + + // 재고 부족 상품 + const lowStockProducts = products.filter((product) => product.stock > 0 && product.stock < 5); + + // 품절 상품 + const outOfStockProducts = products.filter((product) => product.stock === 0); + + return { + products, + lowStockProducts, + outOfStockProducts, + updateStock, + restoreStock, + getProduct, + }; +}; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 000000000..c018515cd --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 000000000..1d3aed209 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,28 @@ +// 상품 타입 +export interface Product { + id: string; + name: string; + price: number; + stock: number; + discount: number; +} + +// 장바구니 아이템 타입 +export interface CartItem { + product: Product; + quantity: number; +} + +// 할인 타입 +export interface Discount { + type: 'individual' | 'bulk' | 'tuesday' | 'flash' | 'recommendation'; + rate: number; + description: string; +} + +// 포인트 타입 +export interface PointBonus { + type: 'basic' | 'tuesday' | 'set' | 'fullset' | 'bulk'; + points: number; + description: string; +} diff --git a/src/utils/discount.ts b/src/utils/discount.ts new file mode 100644 index 000000000..c36e04cf7 --- /dev/null +++ b/src/utils/discount.ts @@ -0,0 +1,94 @@ +import { CartItem, Discount } from '../types'; + +// 화요일인지 확인 +export const isTuesday = (): boolean => { + return new Date().getDay() === 2; // 0=일요일, 1=월요일, 2=화요일 +}; + +// 개별 상품 할인 계산 +export const calculateIndividualDiscount = (item: CartItem): number => { + if (item.quantity >= 10) { + return item.product.discount / 100; // 퍼센트를 소수로 변환 + } + return 0; +}; + +// 전체 수량 할인 계산 (30개 이상) +export const calculateBulkDiscount = (cartItems: CartItem[]): number => { + const totalQuantity = cartItems.reduce((sum, item) => sum + item.quantity, 0); + return totalQuantity >= 30 ? 0.25 : 0; // 25% 할인 +}; + +// 화요일 할인 계산 +export const calculateTuesdayDiscount = (): number => { + return isTuesday() ? 0.1 : 0; // 10% 할인 +}; + +// 최종 할인율 계산 (중복 적용 시 복합 할인) +export const calculateTotalDiscount = (cartItems: CartItem[]): Discount[] => { + const discounts: Discount[] = []; + + // 개별 상품 할인 + cartItems.forEach((item) => { + const individualDiscount = calculateIndividualDiscount(item); + if (individualDiscount > 0) { + discounts.push({ + type: 'individual', + rate: individualDiscount, + description: `${item.product.name} ${item.quantity}개 이상 구매 할인`, + }); + } + }); + + // 전체 수량 할인 (개별 할인보다 우선) + const bulkDiscount = calculateBulkDiscount(cartItems); + if (bulkDiscount > 0) { + discounts.push({ + type: 'bulk', + rate: bulkDiscount, + description: '전체 30개 이상 구매 할인', + }); + } + + // 화요일 할인 + const tuesdayDiscount = calculateTuesdayDiscount(); + if (tuesdayDiscount > 0) { + discounts.push({ + type: 'tuesday', + rate: tuesdayDiscount, + description: '화요일 특별 할인', + }); + } + + return discounts; +}; + +// 할인 적용된 총액 계산 +export const calculateDiscountedTotal = ( + cartItems: CartItem[], +): { + subtotal: number; + totalDiscount: number; + finalTotal: number; + discounts: Discount[]; +} => { + const subtotal = cartItems.reduce((sum, item) => sum + item.product.price * item.quantity, 0); + const discounts = calculateTotalDiscount(cartItems); + + // 할인율 계산 (복합 할인) + let totalDiscountRate = 0; + discounts.forEach((discount) => { + // 복합 할인 공식: 1 - (1 - rate1) * (1 - rate2) * ... + totalDiscountRate = 1 - (1 - totalDiscountRate) * (1 - discount.rate); + }); + + const totalDiscount = subtotal * totalDiscountRate; + const finalTotal = subtotal - totalDiscount; + + return { + subtotal, + totalDiscount, + finalTotal, + discounts, + }; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..8d79f5a32 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "ES6", + "lib": ["es6", "dom", "dom.iterable", "scripthost"], + "jsx": "react-jsx", + "rootDir": "./", + "module": "es2015", // module이 node에서 동작해야하는 경우(CommonJS인 경우)에는 moduleResolution의 값은 "node"로 자동 추론된다. + "moduleResolution": "bundler", + "baseUrl": "./", + "outDir": "./build", + "esModuleInterop": true, // CommonJS와 ES 모듈 간의 상호운용성을 위한 설정 + "forceConsistentCasingInFileNames": true, // 대소문자 구분 + "allowJs": true, // 점진적 고도화(js -> ts)를 위한기능 + "checkJs": true, // js파일 에러 체크 설정 + "noEmit": true, // 컴파일시 js 변환 파일을 생성하지 않게 하는 설정 + "noEmitOnError": true, // 컴파일 에러 발생 시 js 컴파일을 막는다. + "sourceMap": true, // .js.map 파일 생성, ts로 작업 시 디버깅에 유용 + "removeComments": true, // ts 파일에 적힌 주석 모두 제거된 js 파일 생성 + "declaration": true, // ts -> js 컴파일 과정에서 타입만 따로 분리시켜 .d.ts라는 파일로 생성해줌 + "strict": true, // ts의 타입체킹을 활성화 여부 + // strict 옵셔널 + "noImplicitAny": true, // any 허용 여부(strict가 true이면 적지 않아도 됨) + "suppressImplicitAnyIndexErrors": false, // 객체에 해당 키가 없는 경우 에러 발생을 무시 여부 + "noImplicitThis": true, // 명시적 this 사용 여부 + "strictNullChecks": true, // 명시적 null, undefined 사용 여부 + "strictFunctionTypes": true, // 엄격한 함수 유형 검사 사용 여부 + "strictPropertyInitialization": true, // 클래스의 속성 초기화에 대한 검사 사용 여부 + "strictBindCallApply": true, // bind, call, apply의 엄격 검사 사용 여부 + "alwaysStrict": true, // js로 컴파일 시 "use strict"를 사용하도록 명시 + + "noUnusedLocals": true, // 사용하지 않는 지역변수가 있다면 에러 + "noUnusedParameters": true, // 사용하지 않는 파라미터가 있다면 에러 + "noImplicitReturns": true, // 함수에 return이 없는 경우 에러 + "noFallthroughCasesInSwitch": true, // Switch문이 이상하면 에러 + "skipLibCheck": true, // 타입 체킹을 스킵 + "allowImportingTsExtensions": true, // ts 파일을 import할 때 확장자를 생략할 수 있게 해줌 + "paths": { + "@/*": ["src/advanced/*"] + } + }, + + "include": ["src/advanced/**/*"], + "exclude": ["node_modules"] +} diff --git a/vite.config.js b/vite.config.js index dc9c66995..7e9247367 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,33 @@ -import { defineConfig } from 'vitest/config'; +import fs from 'fs'; +import path from 'path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +const base = process.env.NODE_ENV === 'production' ? '/front_6th_chapter2-1/' : ''; + +const entryFileName = 'index.advanced.html'; export default defineConfig({ + base, + build: { + rollupOptions: { + input: path.resolve(__dirname, entryFileName), + }, + }, + plugins: [ + react(), + { + name: 'rename-html-output', + closeBundle() { + const from = path.resolve(__dirname, `dist/${entryFileName}`); + const to = path.resolve(__dirname, 'dist/index.html'); + if (fs.existsSync(from)) fs.renameSync(from, to); + }, + }, + ], test: { globals: true, environment: 'jsdom', - setupFiles: 'src/setupTests.js' + setupFiles: 'src/setupTests.js', }, -}) +});