From 74439554d3ba58b47d40903df6d1ca3b357ed8d7 Mon Sep 17 00:00:00 2001 From: Keem <6ukeem@gmail.com> Date: Mon, 16 Mar 2026 13:31:26 +0900 Subject: [PATCH 01/13] Merge pull request #68 from hs-shell/refactor/content-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 코드 단순화, 폴더 구조 재정리 및 보안 취약점 해결 --- manifest.config.ts | 2 +- package-lock.json | 1268 ++++++++--------- package.json | 8 +- src/background.ts | 23 +- src/components/ui/popover.tsx | 2 +- src/constants/{constant.ts => links.ts} | 1 - src/content/App.tsx | 349 ----- src/content/components/Assignment.tsx | 89 -- src/content/components/PopoverHeader.tsx | 0 src/content/components/QuizTab.tsx | 86 -- src/content/components/Video.tsx | 222 --- src/hooks/useCalendarEvents.ts | 102 +- src/hooks/useCardData.ts | 124 +- src/hooks/useCourseData.tsx | 239 ++-- src/hooks/useDashboardFilters.ts | 99 ++ src/hooks/useGetCourse.ts | 48 - src/hooks/useGetCourses.ts | 24 + src/hooks/useRemainingTime.ts | 0 src/lib/attendance.ts | 7 + src/lib/cache.ts | 18 + src/lib/dateUtils.ts | 94 ++ src/lib/deduplicateInto.ts | 17 + src/lib/fetchAssign.ts | 72 +- src/lib/fetchCourseData.ts | 32 +- src/lib/fetchHtml.ts | 26 + src/lib/fetchIndexPage.ts | 49 - src/lib/fetchQuiz.ts | 93 +- src/lib/fetchVodAttendance.ts | 181 ++- src/lib/fetchVodList.ts | 40 + src/lib/filterData.tsx | 93 +- src/lib/generateKey.ts | 4 + src/lib/parseCourses.ts | 25 + src/lib/storage.ts | 27 + src/lib/stringUtils.ts | 14 + src/lib/summarizeCourseData.ts | 19 + src/lib/transformCourseData.ts | 43 + src/lib/utils.ts | 209 +-- src/mocks/loadMockData.ts | 12 + src/mocks/mockData.ts | 2 +- src/option/App.tsx | 8 +- src/option/ColorSetting.tsx | 2 +- src/option/SummaryCard.tsx | 25 +- src/option/calendar.tsx | 2 +- src/option/components/AssignCard.tsx | 2 +- src/option/components/AssignContent.tsx | 2 +- src/option/components/CourseDetailModal.tsx | 10 +- src/option/components/CourseHideModal.tsx | 2 +- src/option/components/QuizCard.tsx | 2 +- src/option/components/QuizContent.tsx | 2 +- src/option/components/Sidebar.tsx | 4 +- src/option/components/VodCard.tsx | 10 +- src/option/components/VodContent.tsx | 33 +- src/option/components/data.tsx | 13 +- src/{ => option}/lib/calendarUtils.ts | 0 src/option/lib/transformCalendarEvents.ts | 50 + src/{ => option}/pages/AssignmentPage.tsx | 0 src/{ => option}/pages/DashboardPage.tsx | 0 src/{ => option}/pages/QuizPage.tsx | 0 src/{ => option}/pages/VodPage.tsx | 0 src/popover/dashboard/Dashboard.tsx | 120 ++ .../dashboard/components/AssignList.tsx | 23 + .../components/CardFooterContent.tsx | 59 + .../dashboard/components/DashboardHeader.tsx | 156 ++ .../dashboard/components/DueDateList.tsx | 66 + .../dashboard/components/EmptyState.tsx | 16 + .../dashboard}/components/FilterBadge.tsx | 0 .../dashboard}/components/FilterItem.tsx | 0 .../dashboard}/components/FilterPanel.tsx | 2 +- .../dashboard}/components/PendingDialog.tsx | 2 +- src/popover/dashboard/components/QuizList.tsx | 19 + .../components}/StickyPopoverTrigger.tsx | 0 .../dashboard/components/TabNavigation.tsx} | 4 +- src/popover/dashboard/components/VodList.tsx | 112 ++ src/{content => popover}/index.tsx | 10 +- src/{ => popover}/lib/ShadowRootContext.tsx | 0 src/{ => popover}/lib/createShadowRoot.ts | 0 src/{ => popover}/player/App.tsx | 0 .../player/components/PlayerIframe.tsx | 13 +- .../components/PlayerPopoverContent.tsx | 27 +- .../components/PlayerPopoverTrigger.tsx | 0 .../player/components/SortableItem.tsx | 2 +- src/{content => }/types.ts | 31 +- src/utils/generate-key.ts | 3 - 83 files changed, 2097 insertions(+), 2498 deletions(-) rename src/constants/{constant.ts => links.ts} (90%) delete mode 100644 src/content/App.tsx delete mode 100644 src/content/components/Assignment.tsx delete mode 100644 src/content/components/PopoverHeader.tsx delete mode 100644 src/content/components/QuizTab.tsx delete mode 100644 src/content/components/Video.tsx create mode 100644 src/hooks/useDashboardFilters.ts delete mode 100644 src/hooks/useGetCourse.ts create mode 100644 src/hooks/useGetCourses.ts delete mode 100644 src/hooks/useRemainingTime.ts create mode 100644 src/lib/attendance.ts create mode 100644 src/lib/cache.ts create mode 100644 src/lib/dateUtils.ts create mode 100644 src/lib/deduplicateInto.ts create mode 100644 src/lib/fetchHtml.ts delete mode 100644 src/lib/fetchIndexPage.ts create mode 100644 src/lib/fetchVodList.ts create mode 100644 src/lib/generateKey.ts create mode 100644 src/lib/parseCourses.ts create mode 100644 src/lib/stringUtils.ts create mode 100644 src/lib/summarizeCourseData.ts create mode 100644 src/lib/transformCourseData.ts create mode 100644 src/mocks/loadMockData.ts rename src/{ => option}/lib/calendarUtils.ts (100%) create mode 100644 src/option/lib/transformCalendarEvents.ts rename src/{ => option}/pages/AssignmentPage.tsx (100%) rename src/{ => option}/pages/DashboardPage.tsx (100%) rename src/{ => option}/pages/QuizPage.tsx (100%) rename src/{ => option}/pages/VodPage.tsx (100%) create mode 100644 src/popover/dashboard/Dashboard.tsx create mode 100644 src/popover/dashboard/components/AssignList.tsx create mode 100644 src/popover/dashboard/components/CardFooterContent.tsx create mode 100644 src/popover/dashboard/components/DashboardHeader.tsx create mode 100644 src/popover/dashboard/components/DueDateList.tsx create mode 100644 src/popover/dashboard/components/EmptyState.tsx rename src/{content => popover/dashboard}/components/FilterBadge.tsx (100%) rename src/{content => popover/dashboard}/components/FilterItem.tsx (100%) rename src/{content => popover/dashboard}/components/FilterPanel.tsx (98%) rename src/{content => popover/dashboard}/components/PendingDialog.tsx (98%) create mode 100644 src/popover/dashboard/components/QuizList.tsx rename src/{content => popover/dashboard/components}/StickyPopoverTrigger.tsx (100%) rename src/{content/components/PopoverFooter.tsx => popover/dashboard/components/TabNavigation.tsx} (92%) create mode 100644 src/popover/dashboard/components/VodList.tsx rename src/{content => popover}/index.tsx (88%) rename src/{ => popover}/lib/ShadowRootContext.tsx (100%) rename src/{ => popover}/lib/createShadowRoot.ts (100%) rename src/{ => popover}/player/App.tsx (100%) rename src/{ => popover}/player/components/PlayerIframe.tsx (92%) rename src/{ => popover}/player/components/PlayerPopoverContent.tsx (91%) rename src/{ => popover}/player/components/PlayerPopoverTrigger.tsx (100%) rename src/{ => popover}/player/components/SortableItem.tsx (97%) rename src/{content => }/types.ts (64%) delete mode 100644 src/utils/generate-key.ts diff --git a/manifest.config.ts b/manifest.config.ts index 9346380..7e4df0f 100644 --- a/manifest.config.ts +++ b/manifest.config.ts @@ -18,7 +18,7 @@ const manifest = { content_scripts: [ { matches: ['https://learn.hansung.ac.kr/**'], - js: ['src/content/index.tsx'], + js: ['src/popover/index.tsx'], }, ], web_accessible_resources: [ diff --git a/package-lock.json b/package-lock.json index 34d37af..5fb1cea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dotbugi", - "version": "3.1.19", + "version": "4.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dotbugi", - "version": "3.1.19", + "version": "4.0.5", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -28,16 +28,13 @@ "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.7", "@types/gapi.client.calendar": "^3.0.12", - "axios": "^1.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "framer-motion": "^12.0.6", - "glob": "^11.0.1", "googleapis": "^160.0.0", "lucide-react": "^0.471.2", "motion": "^12.23.24", - "node-fetch": "^3.3.2", "react": "^18.3.1", "react-colorful": "^5.6.1", "react-dom": "^18.3.1", @@ -95,15 +92,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -227,9 +224,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -237,9 +234,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -257,27 +254,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", - "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -319,15 +316,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -363,23 +360,23 @@ } }, "node_modules/@babel/types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", - "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@crxjs/vite-plugin": { - "version": "2.0.0-beta.30", - "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.0.0-beta.30.tgz", - "integrity": "sha512-skRcaJAbDrcfKPRqgBtyeBAk19KrRqtA8lO3ZiwJgnpRPX8EICbv0iR6Jb8E3V+knXCrYTc4O5Im+r+n43f14A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.3.0.tgz", + "integrity": "sha512-+0CNVGS4bB30OoaF1vUsHVwWU1Lm7MxI0XWY9Fd/Ob+ZVTZgEFNqJ1ZC69IVwQsoYhY0sMQLvpLWiFIuDz8htg==", "dev": true, "license": "MIT", "dependencies": { @@ -394,7 +391,8 @@ "fs-extra": "^10.0.1", "jsesc": "^3.0.2", "magic-string": "^0.30.12", - "picocolors": "^1.0.0", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", "react-refresh": "^0.13.0", "rollup": "2.79.2", "rxjs": "7.5.7" @@ -425,9 +423,9 @@ } }, "node_modules/@crxjs/vite-plugin/node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", "bin": { @@ -514,7 +512,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -531,7 +528,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -548,7 +544,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -565,7 +560,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -582,7 +576,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -599,7 +592,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -616,7 +608,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -633,7 +624,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -650,7 +640,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -667,7 +656,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -684,7 +672,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -701,7 +688,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -718,7 +704,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -735,7 +720,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -752,7 +736,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -769,7 +752,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -786,7 +768,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -803,7 +784,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -820,7 +800,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -837,7 +816,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -854,7 +832,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -871,7 +848,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -888,7 +864,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -905,7 +880,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -922,7 +896,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -933,9 +906,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -975,24 +948,61 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1003,20 +1013,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1026,6 +1036,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -1039,20 +1060,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1060,13 +1097,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1348,9 +1385,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1451,7 +1488,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -2994,13 +3031,12 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3008,13 +3044,12 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3022,13 +3057,12 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3036,13 +3070,12 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3050,13 +3083,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3064,13 +3096,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3078,13 +3109,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3092,13 +3122,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3106,13 +3135,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3120,13 +3148,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3134,13 +3161,25 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3148,13 +3187,25 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3162,13 +3213,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3176,13 +3226,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3190,13 +3239,12 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3204,13 +3252,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3218,27 +3265,38 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3246,13 +3304,12 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3260,13 +3317,25 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], "license": "MIT", "optional": true, "os": [ @@ -3274,13 +3343,12 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3460,7 +3528,7 @@ "version": "22.10.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -3633,32 +3701,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3934,10 +3976,10 @@ "peer": true }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3946,6 +3988,20 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3979,9 +4035,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -4015,9 +4071,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "peer": true, @@ -4040,17 +4096,6 @@ "license": "MIT", "peer": true }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -4122,12 +4167,6 @@ "node": ">=10" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -4166,17 +4205,6 @@ "postcss": "^8.1.0" } }, - "node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4203,6 +4231,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -4232,14 +4273,12 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -4255,9 +4294,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -4275,10 +4314,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -4297,7 +4337,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -4350,9 +4390,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "version": "1.0.30001779", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz", + "integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==", "dev": true, "funding": [ { @@ -4546,18 +4586,6 @@ "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4589,12 +4617,16 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { @@ -4718,15 +4750,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4844,9 +4867,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.82", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz", - "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==", + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", "dev": true, "license": "ISC" }, @@ -4871,14 +4894,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -4916,9 +4939,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT", "peer": true @@ -4935,21 +4958,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -5015,32 +5023,32 @@ } }, "node_modules/eslint": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", - "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.18.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5052,7 +5060,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -5098,9 +5106,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5115,9 +5123,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5127,16 +5135,40 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5306,9 +5338,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", - "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { @@ -5421,39 +5453,19 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -5463,22 +5475,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -5654,29 +5650,6 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5697,30 +5670,6 @@ "license": "BSD-2-Clause", "peer": true }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "15.14.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", @@ -5857,21 +5806,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5941,9 +5875,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6057,21 +5991,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -6121,9 +6040,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6222,12 +6141,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -6274,14 +6193,18 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "peer": true, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/local-pkg": { @@ -6408,7 +6331,9 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -6417,7 +6342,9 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -6426,16 +6353,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -6589,9 +6518,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -6801,31 +6730,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -7050,12 +6954,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7067,9 +6965,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7101,17 +6999,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -7205,9 +7092,9 @@ } }, "node_modules/react-router": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz", - "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -7227,12 +7114,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz", - "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", "license": "MIT", "dependencies": { - "react-router": "7.9.1" + "react-router": "7.13.1" }, "engines": { "node": ">=20.0.0" @@ -7337,9 +7224,9 @@ } }, "node_modules/rollup": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7353,27 +7240,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.2", - "@rollup/rollup-android-arm64": "4.50.2", - "@rollup/rollup-darwin-arm64": "4.50.2", - "@rollup/rollup-darwin-x64": "4.50.2", - "@rollup/rollup-freebsd-arm64": "4.50.2", - "@rollup/rollup-freebsd-x64": "4.50.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", - "@rollup/rollup-linux-arm-musleabihf": "4.50.2", - "@rollup/rollup-linux-arm64-gnu": "4.50.2", - "@rollup/rollup-linux-arm64-musl": "4.50.2", - "@rollup/rollup-linux-loong64-gnu": "4.50.2", - "@rollup/rollup-linux-ppc64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-musl": "4.50.2", - "@rollup/rollup-linux-s390x-gnu": "4.50.2", - "@rollup/rollup-linux-x64-gnu": "4.50.2", - "@rollup/rollup-linux-x64-musl": "4.50.2", - "@rollup/rollup-openharmony-arm64": "4.50.2", - "@rollup/rollup-win32-arm64-msvc": "4.50.2", - "@rollup/rollup-win32-ia32-msvc": "4.50.2", - "@rollup/rollup-win32-x64-msvc": "4.50.2", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -7447,16 +7338,17 @@ } }, "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 10.13.0" @@ -7466,6 +7358,46 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -7476,21 +7408,10 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/shebang-command": { @@ -7630,7 +7551,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -7642,7 +7563,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "peer": true, "engines": { @@ -7780,19 +7701,11 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -7830,21 +7743,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sucrase/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -7969,20 +7867,24 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { "version": "5.37.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { @@ -7999,9 +7901,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, "license": "MIT", "peer": true, @@ -8009,7 +7911,6 @@ "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -8034,72 +7935,11 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -8321,9 +8161,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "dev": true, "license": "MIT", "engines": { @@ -8334,7 +8174,7 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -8349,9 +8189,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -8445,9 +8285,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8606,9 +8446,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "peer": true, @@ -8630,36 +8470,38 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -8678,9 +8520,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index ecc67e6..24f3429 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,13 @@ "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.7", "@types/gapi.client.calendar": "^3.0.12", - "axios": "^1.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "framer-motion": "^12.0.6", - "glob": "^11.0.1", "googleapis": "^160.0.0", "lucide-react": "^0.471.2", "motion": "^12.23.24", - "node-fetch": "^3.3.2", "react": "^18.3.1", "react-colorful": "^5.6.1", "react-dom": "^18.3.1", @@ -69,5 +66,10 @@ "typescript-eslint": "^8.18.2", "vite": "^6.3.6", "vite-plugin-pages": "^0.32.4" + }, + "overrides": { + "@crxjs/vite-plugin": { + "rollup": "^2.80.0" + } } } diff --git a/src/background.ts b/src/background.ts index 13084f1..3959c76 100644 --- a/src/background.ts +++ b/src/background.ts @@ -6,27 +6,8 @@ interface AlarmDetails { const alarmsMap: { [alarmId: string]: AlarmDetails } = {}; function parseDateTime(dateTimeStr: string): Date | null { - const parts = dateTimeStr.split(' '); - if (parts.length !== 2) { - return null; - } - - const [datePart, timePart] = parts; - const dateComponents = datePart.split('-'); - const timeComponents = timePart.split(':'); - - if (dateComponents.length !== 3 || (timeComponents.length !== 2 && timeComponents.length !== 3)) { - return null; - } - - const year = parseInt(dateComponents[0], 10); - const month = parseInt(dateComponents[1], 10) - 1; - const day = parseInt(dateComponents[2], 10); - const hour = parseInt(timeComponents[0], 10); - const minute = parseInt(timeComponents[1], 10); - const second = timeComponents.length === 3 ? parseInt(timeComponents[2], 10) : 0; - - return new Date(year, month, day, hour, minute, second); + const date = new Date(dateTimeStr.replace(' ', 'T')); + return isNaN(date.getTime()) ? null : date; } function schedulePreEventAlarm(alarmId: string, dateTimeStr: string, title: string, message: string): void { diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index 6c0c2db..c26903f 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as PopoverPrimitive from '@radix-ui/react-popover'; import { cn } from '@/lib/utils'; -import { useShadowRoot } from '@/lib/ShadowRootContext'; +import { useShadowRoot } from '@/popover/lib/ShadowRootContext'; const Popover = PopoverPrimitive.Root; diff --git a/src/constants/constant.ts b/src/constants/links.ts similarity index 90% rename from src/constants/constant.ts rename to src/constants/links.ts index 53601fd..476a15c 100644 --- a/src/constants/constant.ts +++ b/src/constants/links.ts @@ -1,7 +1,6 @@ export const BASE_LINK = 'https://learn.hansung.ac.kr' as const; export const getVodPageLink = (courseId: string) => { return BASE_LINK + `/report/ubcompletion/user_progress_a.php?id=${courseId}`; - return BASE_LINK + `/course/view.php?id=${courseId}`; }; export const getAssignPageLink = (courseId: string) => { return BASE_LINK + `/mod/assign/index.php?id=${courseId}`; diff --git a/src/content/App.tsx b/src/content/App.tsx deleted file mode 100644 index 97371a9..0000000 --- a/src/content/App.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'; -import { useEffect, useMemo, useState } from 'react'; -import icon from '@/assets/icon.png'; -import exit from '@/assets/exit.png'; -import { Assign, CourseBase, Filters, Quiz, TAB_TYPE, Vod } from './types'; -import { ListFilter, OctagonAlert, RefreshCw, Search } from 'lucide-react'; -import filter from '@/assets/filter.svg'; -import PopoverFooter from './components/PopoverFooter'; -import { Spinner } from '@/components/ui/spinner'; -import { useGetCourses } from '@/hooks/useGetCourse'; -import Video from './components/Video'; -import Assignment from './components/Assignment'; -import QuizTab from './components/QuizTab'; -import { Button } from '@/components/ui/button'; -import FilterBadge from './components/FilterBadge'; - -import FilterPanel from './components/FilterPanel'; -import { filterVods, filterAssigns, filterQuizes } from '@/lib/filterData'; -import PendingDialogWithBeforeUnload from './components/PendingDialog'; -import StickyPopoverTrigger from './StickyPopoverTrigger'; -import { useCourseData } from '@/hooks/useCourseData'; - -const attendanceOptions = ['출석', '결석']; -const submitOptions = [ - { label: '제출완료', value: true }, - { label: '제출필요', value: false }, -]; - -export default function App() { - const { courses } = useGetCourses(); - const typeCourses: CourseBase[] = courses; - - // 데이터 관련 상태를 useCourseData 커스텀 훅으로 관리 - const { vods, assigns, quizes, isPending, remainingTime, isError, updateData, setIsPending } = - useCourseData(typeCourses); - - // activeTab의 타입을 TAB_TYPE으로 지정 - const [activeTab, setActiveTab] = useState(TAB_TYPE.VIDEO); - const [isOpen, setIsOpen] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [vodSortBy] = useState('isAttendance'); - const [assignSortBy] = useState('isSubmit'); - const [quizSortBy] = useState('dueDate'); - const [isFilterOpen, setIsFilterOpen] = useState(false); - - // 필터 상태 관리 - Record을 사용하여 TAB_TYPE을 키로 지정 - const [filters, setFilters] = useState>({ - VIDEO: { courseTitles: [], attendanceStatuses: [] }, - ASSIGN: { courseTitles: [], submitStatuses: [] }, - QUIZ: { courseTitles: [] }, - }); - - useEffect(() => { - setSearchTerm(''); - }, [activeTab]); - - useEffect(() => { - setSearchTerm(''); - setIsFilterOpen(false); - }, [activeTab]); - - // 필터 옵션 추출 - const courseTitlesMap = useMemo( - () => ({ - VIDEO: Array.from(new Set(vods.map((vod) => vod.courseTitle))), - ASSIGN: Array.from(new Set(assigns.map((assign) => assign.courseTitle))), - QUIZ: Array.from(new Set(quizes.map((quiz) => quiz.courseTitle))), - }), - [vods, assigns, quizes] - ); - - // 필터 적용 - const filteredVods = useMemo(() => { - return filterVods(vods, filters[activeTab], searchTerm, vodSortBy); - }, [vods, filters, activeTab, searchTerm, vodSortBy]); - - const filteredAssigns = useMemo(() => { - return filterAssigns(assigns, filters[activeTab], searchTerm, assignSortBy); - }, [assigns, filters, activeTab, searchTerm, assignSortBy]); - - const filteredQuizes = useMemo(() => { - return filterQuizes(quizes, filters[activeTab], searchTerm, quizSortBy); - }, [quizes, filters, activeTab, searchTerm, quizSortBy]); - - // Vods용 필터 핸들러 - const handleAttendanceFilterChange = (status: string) => { - setFilters((prev) => { - const current = prev[activeTab].attendanceStatuses || []; - const updated = current.includes(status) ? current.filter((s) => s !== status) : [...current, status]; - return { - ...prev, - [activeTab]: { - ...prev[activeTab], - attendanceStatuses: updated, - }, - }; - }); - }; - - // Assigns용 필터 핸들러 - const handleSubmitFilterChange = (isSubmit: boolean) => { - setFilters((prev) => { - const current = prev[activeTab].submitStatuses || []; - const updated = current.includes(isSubmit) ? current.filter((s) => s !== isSubmit) : [...current, isSubmit]; - return { - ...prev, - [activeTab]: { - ...prev[activeTab], - submitStatuses: updated, - }, - }; - }); - }; - - // CourseTitle 필터 핸들러 - const handleCourseTitleChange = (courseTitle: string) => { - setFilters((prev) => { - const current = prev[activeTab].courseTitles; - const updated = current.includes(courseTitle) - ? current.filter((title) => title !== courseTitle) - : [...current, courseTitle]; - return { - ...prev, - [activeTab]: { - ...prev[activeTab], - courseTitles: updated, - }, - }; - }); - }; - - const isFilterSet = useMemo(() => { - const currentFilters = filters[activeTab]; - const { courseTitles, attendanceStatuses, submitStatuses } = currentFilters; - return ( - (courseTitles && courseTitles.length > 0) || - (attendanceStatuses && attendanceStatuses.length > 0) || - (submitStatuses && submitStatuses.length > 0) - ); - }, [filters, activeTab]); - - const clearFilters = () => { - setFilters((prev) => ({ - ...prev, - [activeTab]: { - courseTitles: [], - ...(activeTab === 'VIDEO' ? { attendanceStatuses: [] } : {}), - ...(activeTab === 'ASSIGN' ? { submitStatuses: [] } : {}), - }, - })); - }; - - return ( - <> - {}} /> - - - - {isOpen ? ( - { - setIsOpen(!isOpen); - e.preventDefault(); - }} - draggable={false} - className="rounded-2xl w-32 h-32 shadow-2xl shadow-zinc-900 cursor-pointer" - alt="Close" - /> - ) : ( - { - setIsOpen(!isOpen); - e.preventDefault(); - }} - draggable={false} - className="rounded-2xl w-32 h-32 shadow-2xl shadow-zinc-900 cursor-pointer" - alt="Open" - /> - )} - - - -
-
-
- {activeTab === 'VIDEO' - ? '온라인 강의 목록' - : activeTab === 'ASSIGN' - ? '과제 목록' - : activeTab === 'QUIZ' - ? '퀴즈 목록' - : '오류'} -
-
- = 30 - ? 'text-amber-500 font-semibold' - : 'text-zinc-400' - : Math.floor(remainingTime / 60) >= 1 - ? 'text-amber-500 font-semibold' - : 'text-zinc-400' - }`} - > - {remainingTime < 60 - ? `${Math.round(remainingTime)}분 전` - : `${Math.floor(remainingTime / 60)}시간 전`} - - -
-
-
- - setSearchTerm(e.target.value)} - autoFocus={true} - className="bg-zinc-50 rounded-xl border border-zinc-300 w-full text-lg h-12 pl-12 pr-4 placeholder-gray-400 font-medium py-0 outline-none focus:ring-0 focus:border-zinc-300 focus:bg-slate-50 transition-all duration-200" - /> -
-
-
- {filters[activeTab].courseTitles.map((title) => ( - handleCourseTitleChange(title)} /> - ))} - {activeTab === 'VIDEO' && - filters[activeTab].attendanceStatuses && - filters[activeTab].attendanceStatuses.map((status) => ( - handleAttendanceFilterChange(status)} - /> - ))} - {activeTab === 'ASSIGN' && - filters[activeTab].submitStatuses && - filters[activeTab].submitStatuses.map((status) => ( - handleSubmitFilterChange(status)} - /> - ))} -
- - {/* 고정된 필터 아이콘 영역 */} -
- - - - - - - - - - -
-
-
-
- {isPending ? ( -
- -
- ) : ( - <> - {isError ? ( -
- -

오류가 발생했습니다.

-

{ - location.reload(); - }} - className="py-4 text-xl font-medium underline text-zinc-500 hover:text-zinc-950 hover:cursor-pointer transition-all duration-200" - > - 페이지 새로고침 -

-
- ) : ( - <> - {activeTab === 'VIDEO' &&
- -
-
- - ); -} diff --git a/src/content/components/Assignment.tsx b/src/content/components/Assignment.tsx deleted file mode 100644 index 99a6589..0000000 --- a/src/content/components/Assignment.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { calculateDueDate, calculateRemainingTime } from '@/lib/utils'; -import { Card, CardFooter, CardHeader } from '@/components/ui/card'; -import { BadgeCheck, Clock, Siren, TriangleAlert } from 'lucide-react'; -import { Tooltip } from '@radix-ui/react-tooltip'; -import { TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import thung from '@/assets/thung.png'; -import { Assign } from '../types'; - -interface Props { - courseData: Assign[]; -} -export default function Assignment({ courseData }: Props) { - if (!courseData || courseData.length === 0) { - return ( -
- -
- 과제가 없습니다 -
-
- ); - } - - return ( -
- {courseData.map((course, index) => { - if (!course) return null; - - const isDueDateSame = true; - const timeDifference = calculateDueDate(course.dueDate!); - - return ( - window.open(`${course.url}`, '_blank')} - key={`${course.title}-${index}`} - className={`cursor-pointer w-full rounded-2xl shadow-md bg-white overflow-hidden border-0 border-l-4 ${course.isSubmit ? 'border-green-500' : timeDifference.borderColor} hover:bg-zinc-100 transition-all duration-200`} - > - -
-
{course.courseTitle}
-
{course.title}
-
-
- - - -
- - - {isDueDateSame ? timeDifference.message : '확인 필요'} - -
-
- - {calculateRemainingTime(course.dueDate)} - -
-
- {course.isSubmit ? ( - - ) : timeDifference.textColor.includes('red') ? ( - - ) : ( - - )} -
{course.isSubmit ? '제출 완료' : '제출 필요'}
-
-
-
- ); - })} -
- ); -} diff --git a/src/content/components/PopoverHeader.tsx b/src/content/components/PopoverHeader.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/content/components/QuizTab.tsx b/src/content/components/QuizTab.tsx deleted file mode 100644 index a420bb3..0000000 --- a/src/content/components/QuizTab.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { calculateDueDate, calculateRemainingTime } from '@/lib/utils'; -import { Quiz } from '../types'; -import { Card, CardFooter, CardHeader } from '@/components/ui/card'; -import { Clock, Siren, TriangleAlert } from 'lucide-react'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import thung from '@/assets/thung.png'; - -interface Props { - courseData: Quiz[]; -} - -export default function QuizTab({ courseData }: Props) { - if (!courseData || courseData.length === 0) { - return ( -
- -
- 퀴즈가 없습니다 -
-
- ); - } - - return ( -
- {courseData.map((course, index) => { - if (!course) return null; - - const isDueDateSame = true; - const timeDifference = calculateDueDate(course.dueDate!); - - return ( - window.open(`${course.url}`, '_blank')} - key={`${course.title}-${index}`} - className={`cursor-pointer w-full rounded-2xl shadow-md bg-white overflow-hidden border-0 border-l-4 ${timeDifference.borderColor} hover:bg-zinc-100 transition-all duration-200`} - > - -
-
{course.courseTitle}
-
{course.title}
-
-
- - - -
- - - {isDueDateSame ? timeDifference.message : '확인 필요'} - -
-
- - {calculateRemainingTime(course.dueDate)} - -
-
-
- {timeDifference.textColor.includes('red') ? ( - - ) : ( - - )} -
-
{'직접 확인'}
-
-
-
- ); - })} -
- ); -} diff --git a/src/content/components/Video.tsx b/src/content/components/Video.tsx deleted file mode 100644 index 685e4bf..0000000 --- a/src/content/components/Video.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useState } from 'react'; -import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; -import { Vod } from '../types'; -import { - calculateRemainingTimeByRange, - calculateTimeDifference, - formatDateString, - isCurrentDateInRange, -} from '@/lib/utils'; -import { BadgeCheck, ChevronDown, ChevronUp, Clock, Siren, TriangleAlert } from 'lucide-react'; - -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; - -import thung from '@/assets/thung.png'; - -interface Props { - courseData: Vod[]; -} - -export default function Video({ courseData }: Props) { - const [expandedCards, setExpandedCards] = useState<{ [key: string]: boolean }>({}); - - const toggleCard = (courseId: string) => { - setExpandedCards((prev) => ({ ...prev, [courseId]: !prev[courseId] })); - }; - - const groupedData = courseData.reduce( - (acc, item) => { - const key = `${item.courseId}-${item.subject}-${item.range}`; - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(item); - return acc; - }, - {} as Record - ); - - if (!courseData || courseData.length === 0) { - return ( -
- -
- 강의가 없습니다 -
-
- ); - } - - const sortedVodGroups = Object.values(groupedData).sort((groupA, groupB) => { - const firstA = groupA[0]; - const firstB = groupB[0]; - - const isAX = firstA.weeklyAttendance.toUpperCase().startsWith('X'); - const isBX = firstB.weeklyAttendance.toUpperCase().startsWith('X'); - - // X가 있는 항목을 먼저 정렬 - if (isAX && !isBX) return -1; - if (!isAX && isBX) return 1; - - const rangeA = firstA.range; - const rangeB = firstB.range; - const isRangeANull = rangeA === null; - const isRangeBNull = rangeB === null; - - // isCurrentDateInRange가 true인 항목을 먼저 정렬 (X와 O 모두) - const isCurrentDateInRangeA = isCurrentDateInRange(firstA.range); - const isCurrentDateInRangeB = isCurrentDateInRange(firstB.range); - - if (isAX) { - // X일 때는 isCurrentDateInRange가 true인 항목을 먼저 배치, 그 다음 null - if (isCurrentDateInRangeA && !isCurrentDateInRangeB) return -1; - if (!isCurrentDateInRangeA && isCurrentDateInRangeB) return 1; - if (isRangeANull && !isRangeBNull) return 1; - if (!isRangeANull && isRangeBNull) return -1; - } else { - // O일 때는 isCurrentDateInRange가 true인 항목을 먼저 배치, 그 다음 null, 그 다음 시간순 정렬 - if (isCurrentDateInRangeA && !isCurrentDateInRangeB) return -1; - if (!isCurrentDateInRangeA && isCurrentDateInRangeB) return 1; - if (isRangeANull && !isRangeBNull) return 1; - if (!isRangeANull && isRangeBNull) return -1; - - // rangeStart 날짜 기준으로 시간순으로 정렬 - if (!isRangeANull && !isRangeBNull) { - const rangeStartA = rangeA.split(' ~ ')[0]; - const rangeStartB = rangeB.split(' ~ ')[0]; - const dateA = new Date(rangeStartA); - const dateB = new Date(rangeStartB); - - if (dateA < dateB) return -1; - if (dateA > dateB) return 1; - } - } - - // courseTitle로 기본 정렬 - if (firstA.courseTitle < firstB.courseTitle) return -1; - if (firstA.courseTitle > firstB.courseTitle) return 1; - - return 0; - }); - - return ( -
- {sortedVodGroups.map((vods, index) => { - if (!vods || vods.length === 0) return null; - - const sortedVods = vods.slice().sort((a, b) => { - const isAX = a.isAttendance.toUpperCase().startsWith('X'); - const isBX = b.isAttendance.toUpperCase().startsWith('X'); - if (isAX && !isBX) return -1; - if (!isAX && isBX) return 1; - - if (a.range && b.range) { - const rangeStartA = a.range.split(' ~ ')[0]; - const rangeStartB = b.range.split(' ~ ')[0]; - const dateA = new Date(rangeStartA); - const dateB = new Date(rangeStartB); - if (dateA < dateB) return -1; - if (dateA > dateB) return 1; - } - - if (a.courseTitle < b.courseTitle) return -1; - if (a.courseTitle > b.courseTitle) return 1; - - return 0; - }); - - const item = vods[0]; - const isDueDateSame = true; - const timeDifference = calculateTimeDifference(item.range); - const isExpanded = expandedCards[`${item.title}-${index}`] || false; - - return ( - - toggleCard(`${item.title}-${index}`)} - > -
-
{item.courseTitle}
-
{item.subject}
-
- {isExpanded ? : } -
- {isExpanded && ( - - {sortedVods.map((vod, vodIndex) => { - return ( -
window.open(`${vod.url.replace('view', 'viewer')}`, '_blank', 'VodContentWindow')} - > -
- {vod.title} -
-
- {formatDateString(vod.range)},{' '} - - {vod.length} - -
-
- ); - })} -
- )} - - - -
- - - {isDueDateSame ? timeDifference.message : '확인 필요'} - -
-
- - {calculateRemainingTimeByRange(vods[0].range)} - -
-
-
- {item.weeklyAttendance.toLocaleLowerCase().trim() === 'o' ? ( - - ) : timeDifference.message.includes('시간') ? ( - - ) : ( - - )} -
-
- {item.weeklyAttendance.toLocaleLowerCase().trim() === 'o' ? '출석' : '결석'} -
-
-
-
- ); - })} -
- ); -} diff --git a/src/hooks/useCalendarEvents.ts b/src/hooks/useCalendarEvents.ts index cc5f1ac..3d04233 100644 --- a/src/hooks/useCalendarEvents.ts +++ b/src/hooks/useCalendarEvents.ts @@ -1,100 +1,30 @@ import { useState, useEffect } from 'react'; -import { startOfDay } from 'date-fns'; -import { loadDataFromStorage } from '@/lib/storage'; -import { removeSquareBrackets } from '@/lib/utils'; -import { Vod, Assign, Quiz } from '@/content/types'; +import { loadAndTransform } from '@/lib/storage'; +import { Vod, Assign, Quiz } from '@/types'; +import { CalendarEvent, vodGroupsToEvents, dueDateItemToEvent } from '@/option/lib/transformCalendarEvents'; -export type CalendarEvent = { - id: string; - type: 'vod' | 'assign' | 'quiz'; - title: string; - subject: string; - start: Date | null; - end: Date | null; -}; +export type { CalendarEvent }; function useCalendarEvents() { const [events, setEvents] = useState([]); - const loadEvents = (storageKey: string, transform: (data: T[]) => CalendarEvent[]) => { - loadDataFromStorage(storageKey, (data: string | null) => { - if (!data) return; - - let parsedData: T[]; - if (typeof data === 'string') { - try { - parsedData = JSON.parse(data); - } catch (error) { - console.error(`JSON 파싱 에러 (${storageKey}):`, error); - return; - } - } else { - parsedData = data; - } - - const eventsData = transform(parsedData); - setEvents((prev) => [...prev, ...eventsData]); - }); - }; - useEffect(() => { - loadEvents('vod', (vods) => { - const groupedData = vods.reduce( - (acc, item) => { - const key = `${item.courseId}-${item.subject}-${item.range}`; - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(item); - return acc; - }, - {} as Record - ); + const appendEvents = (newEvents: CalendarEvent[]) => { + setEvents((prev) => [...prev, ...newEvents]); + }; - return Object.entries(groupedData).map(([key, vodItems]) => { - const range = vodItems[0].range; - const [start, end] = range ? range.split(' ~ ') : [null, null]; - return { - id: key, - type: 'vod', - start: start ? new Date(start.replace(/-/g, '/')) : null, - end: end ? new Date(end.replace(/-/g, '/')) : null, - title: removeSquareBrackets(vodItems[0].courseTitle), - subject: removeSquareBrackets(vodItems[0].subject), - }; - }); - }); + loadAndTransform('vod', vodGroupsToEvents, appendEvents); - // assign 데이터 로딩 및 변환 - loadEvents('assign', (assigns) => - assigns.map((assign) => { - const dueDate = assign.dueDate; - const normalizedDate = dueDate ? startOfDay(new Date(dueDate)) : null; - return { - id: assign.courseId + assign.title + assign.dueDate, - type: 'assign', - start: normalizedDate, - end: normalizedDate, - title: removeSquareBrackets(assign.courseTitle), - subject: removeSquareBrackets(assign.title), - }; - }) + loadAndTransform( + 'assign', + (assigns) => assigns.map((a) => dueDateItemToEvent(a, 'assign')), + appendEvents, ); - // quiz 데이터 로딩 및 변환 - loadEvents('quiz', (quizzes) => - quizzes.map((quiz) => { - const dueDate = quiz.dueDate; - const normalizedDate = dueDate ? startOfDay(new Date(dueDate)) : null; - return { - id: quiz.courseId + quiz.title + quiz.dueDate, - type: 'quiz', - start: normalizedDate, - end: normalizedDate, - title: removeSquareBrackets(quiz.courseTitle), - subject: removeSquareBrackets(quiz.title), - }; - }) + loadAndTransform( + 'quiz', + (quizzes) => quizzes.map((q) => dueDateItemToEvent(q, 'quiz')), + appendEvents, ); }, []); diff --git a/src/hooks/useCardData.ts b/src/hooks/useCardData.ts index 53cf9db..693dc22 100644 --- a/src/hooks/useCardData.ts +++ b/src/hooks/useCardData.ts @@ -1,114 +1,36 @@ import { useState, useEffect } from 'react'; -import { loadDataFromStorage } from '@/lib/storage'; -import { Vod, Assign, Quiz } from '@/content/types'; +import { loadAndTransform } from '@/lib/storage'; +import { Vod, Assign, Quiz } from '@/types'; +import { CardData, summarizeVods } from '@/lib/summarizeCourseData'; -export type CardData = { - type: 'vod' | 'assign' | 'quiz'; - done: number; - total: number; -}; +export type { CardData }; -function useCardData() { - const [vodSummary, setVodSummary] = useState([]); - const [assignSummary, setAssignSummary] = useState([]); - const [quizSummary, setQuizSummary] = useState([]); - - const loadEvents = ( - storageKey: string, - transform: (data: T[]) => CardData[], - setter: (data: CardData[]) => void - ) => { - loadDataFromStorage(storageKey, (data: string | null) => { - if (!data) return; +type Summaries = Record<'vod' | 'assign' | 'quiz', CardData>; - let parsedData: T[]; - if (typeof data === 'string') { - try { - parsedData = JSON.parse(data); - } catch (error) { - console.error(`JSON 파싱 에러 (${storageKey}):`, error); - return; - } - } else { - parsedData = data; - } +const EMPTY: CardData = { done: 0, total: 0 }; - const eventsData = transform(parsedData); - setter(eventsData); - }); +function useCardData() { + const [summaries, setSummaries] = useState({ vod: EMPTY, assign: EMPTY, quiz: EMPTY }); + const updateKey = (key: keyof Summaries) => (data: CardData) => { + setSummaries((prev) => ({ ...prev, [key]: data })); }; useEffect(() => { - loadEvents( - 'vod', - (vods) => { - const groupedData = vods.reduce( - (acc, item) => { - const key = `${item.courseId}-${item.subject}-${item.range}`; - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(item); - return acc; - }, - {} as Record - ); - - let done = 0; - Object.values(groupedData).forEach((vodItems) => { - if (vodItems[0].weeklyAttendance.toLowerCase() === 'o') { - done += 1; - } - }); - - return [ - { - type: 'vod', - done, - total: Object.keys(groupedData).length, - }, - ]; - }, - setVodSummary - ); - - loadEvents( - 'assign', - (assigns) => { - const total = assigns.length; - let done = 0; - assigns.forEach((assign) => { - if (assign.isSubmit) done += 1; - }); - return [ - { - type: 'assign', - done, - total, - }, - ]; - }, - setAssignSummary - ); - - loadEvents( - 'quiz', - (quizzes) => { - const total = quizzes.length; - const done = 0; - return [ - { - type: 'quiz', - done, - total, - }, - ]; - }, - setQuizSummary - ); + loadAndTransform('vod', summarizeVods, updateKey('vod')); + + loadAndTransform('assign', (assigns) => ({ + done: assigns.filter((a) => a.isSubmit).length, + total: assigns.length, + }), updateKey('assign')); + + // QuizData에 완료 여부 필드가 없어 done은 항상 0 + loadAndTransform('quiz', (quizzes) => ({ + done: 0, + total: quizzes.length, + }), updateKey('quiz')); }, []); - return { vodSummary, assignSummary, quizSummary }; + return summaries; } export default useCardData; diff --git a/src/hooks/useCourseData.tsx b/src/hooks/useCourseData.tsx index 546cf64..4e31b95 100644 --- a/src/hooks/useCourseData.tsx +++ b/src/hooks/useCourseData.tsx @@ -1,196 +1,163 @@ import { useState, useEffect, useCallback } from 'react'; -import { Vod, Assign, Quiz, CourseBase } from '@/content/types'; +import { Vod, Assign, Quiz, CourseBase } from '@/types'; import { loadDataFromStorage, saveDataToStorage } from '@/lib/storage'; -import { requestData } from '@/lib/fetchCourseData'; +import { scrapeCourseData } from '@/lib/fetchCourseData'; import { isCurrentDateByDate, isCurrentDateInRange } from '@/lib/utils'; -import { makeAssignKey, makeQuizKey, makeVodKey } from '@/utils/generate-key'; +import { makeItemKey, makeVodKey } from '@/lib/generateKey'; +import { mergeVodWithAttendance, mergeDueDateItems } from '@/lib/transformCourseData'; +import { deduplicateInto } from '@/lib/deduplicateInto'; +import { loadMockData } from '@/mocks/loadMockData'; +import { + REFRESH_INTERVAL_MS, + CACHE_TTL_MINUTES, + CACHE_TTL_MS, + getLastRequestTime, + setLastRequestTime, + clearLastRequestTime, +} from '@/lib/cache'; export function useCourseData(courses: CourseBase[]) { const [vods, setVods] = useState([]); const [assigns, setAssigns] = useState([]); - const [quizes, setQuizes] = useState([]); + const [quizzes, setQuizzes] = useState([]); const [refreshTime, setRefreshTime] = useState(null); - const [isPending, setIsPending] = useState(false); + const [isPending, setIsPending] = useState(false); const [remainingTime, setRemainingTime] = useState(0); const [isError, setIsError] = useState(false); - // dev 모드: mock 데이터 로드 + const applyMock = useCallback(async () => { + const mock = await loadMockData(); + setVods(mock.vods); + setAssigns(mock.assigns); + setQuizzes(mock.quizzes); + }, []); + useEffect(() => { if (!import.meta.env.VITE_MOCK) return; let cancelled = false; - import('@/mocks/mockData').then(({ mockVods, mockAssigns, mockQuizes }) => { + applyMock().then(() => { if (cancelled) return; - setVods(mockVods); - setAssigns(mockAssigns); - setQuizes(mockQuizes); setRefreshTime(new Date().toLocaleTimeString()); setRemainingTime(5); }); return () => { cancelled = true; }; - }, []); + }, [applyMock]); - // dev 모드에서는 실제 fetch를 하지 않음 - const updateData = useCallback(async () => { + const refreshCourseData = useCallback(async () => { if (import.meta.env.VITE_MOCK) { - const { mockVods, mockAssigns, mockQuizes } = await import('@/mocks/mockData'); - setVods(mockVods); - setAssigns(mockAssigns); - setQuizes(mockQuizes); + await applyMock(); setRefreshTime(new Date().toLocaleTimeString()); setRemainingTime(0); return; } + try { setIsError(false); setIsPending(true); - const currentTime = new Date().getTime(); + const fetchedAt = Date.now(); - const tempVods: Vod[] = []; - const tempAssigns: Assign[] = []; - const tempQuizes: Quiz[] = []; - - const vodSet = new Set(tempVods.map((v) => makeVodKey(v.courseId, v.title, v.week))); - const assignSet = new Set(tempAssigns.map((a) => makeAssignKey(a.courseId, a.title, a.dueDate ? a.dueDate : ''))); - const quizSet = new Set(tempQuizes.map((q) => makeQuizKey(q.courseId, q.title, q.dueDate ? q.dueDate : ''))); + const vods: Vod[] = []; + const assigns: Assign[] = []; + const quizzes: Quiz[] = []; + const seenVods = new Set(); + const seenAssigns = new Set(); + const seenQuizzes = new Set(); await Promise.all( courses.map(async (course) => { - const result = await requestData(course.courseId); - - result.vodDataArray.forEach((vodData) => { - result.vodAttendanceArray.forEach((vodAttendanceData) => { - const vodKey = makeVodKey(course.courseId, vodData.title, vodData.week); - if ( - vodAttendanceData.title === vodData.title && - vodAttendanceData.week === vodData.week && - isCurrentDateInRange(vodData.range) - ) { - if (!vodSet.has(vodKey)) { - vodSet.add(vodKey); - tempVods.push({ - courseId: course.courseId, - prof: course.prof, - courseTitle: course.courseTitle, - week: vodAttendanceData.week, - title: vodData.title, - isAttendance: vodAttendanceData.isAttendance, - weeklyAttendance: vodAttendanceData.weeklyAttendance, - length: vodData.length, - range: vodData.range, - subject: vodData.subject, - url: vodData.url, - }); - } - } - }); - }); - - result.assignDataArray.forEach((assignData) => { - const assignKey = makeAssignKey( - course.courseId, - assignData.title, - assignData.dueDate ? assignData.dueDate : '' - ); - if (!assignSet.has(assignKey) && isCurrentDateByDate(assignData.dueDate)) { - console.info(assignKey); - assignSet.add(assignKey); - tempAssigns.push({ - courseId: course.courseId, - prof: course.prof, - courseTitle: course.courseTitle, - subject: assignData.subject, - title: assignData.title, - dueDate: assignData.dueDate, - isSubmit: assignData.isSubmit, - url: assignData.url, - }); - } - }); - - result.quizDataArray.forEach((quizData) => { - const quizKey = makeQuizKey(course.courseId, quizData.title, quizData.dueDate ? quizData.dueDate : ''); - if (!quizSet.has(quizKey) && isCurrentDateByDate(quizData.dueDate)) { - console.info(quizKey); - quizSet.add(quizKey); - tempQuizes.push({ - courseId: course.courseId, - prof: course.prof, - courseTitle: course.courseTitle, - subject: quizData.subject, - title: quizData.title, - dueDate: quizData.dueDate, - url: quizData.url, - }); - } - }); + const scraped = await scrapeCourseData(course.courseId); + + deduplicateInto( + vods, + mergeVodWithAttendance(course, scraped.vodDataArray, scraped.vodAttendanceArray), + seenVods, + (v) => makeVodKey(v.courseId, v.title, v.week), + ); + deduplicateInto( + assigns, + mergeDueDateItems(course, scraped.assignDataArray), + seenAssigns, + (a) => makeItemKey(a.courseId, a.title, a.dueDate ?? ''), + ); + deduplicateInto( + quizzes, + mergeDueDateItems(course, scraped.quizDataArray), + seenQuizzes, + (q) => makeItemKey(q.courseId, q.title, q.dueDate ?? ''), + ); }) ); - setVods(tempVods); - setAssigns(tempAssigns); - setQuizes(tempQuizes); + setVods(vods); + setAssigns(assigns); + setQuizzes(quizzes); - saveDataToStorage('vod', tempVods); - saveDataToStorage('assign', tempAssigns); - saveDataToStorage('quiz', tempQuizes); + saveDataToStorage('vod', vods); + saveDataToStorage('assign', assigns); + saveDataToStorage('quiz', quizzes); - setRefreshTime(new Date(currentTime).toLocaleTimeString()); + setRefreshTime(new Date(fetchedAt).toLocaleTimeString()); setRemainingTime(0); - localStorage.setItem('lastRequestTime', currentTime.toString()); - saveDataToStorage('lastRequestTime', currentTime.toString()); - - setIsPending(false); + setLastRequestTime(fetchedAt); + saveDataToStorage('lastRequestTime', fetchedAt.toString()); } catch (error) { console.warn(error); - localStorage.removeItem('lastRequestTime'); + clearLastRequestTime(); setIsError(true); + } finally { setIsPending(false); } - }, [courses]); + }, [courses, applyMock]); + // 캐시 TTL 기반 자동 갱신 타이머 useEffect(() => { - let timer: ReturnType; - if (remainingTime >= 1440) { - updateData(); - } else { - timer = setTimeout(() => { - setRemainingTime((prev) => prev + 1); - }, 60 * 1000); + if (remainingTime >= CACHE_TTL_MINUTES) { + refreshCourseData(); + return; } - return () => { - clearTimeout(timer); - }; - }, [remainingTime, updateData]); + const timer = setTimeout(() => setRemainingTime((prev) => prev + 1), REFRESH_INTERVAL_MS); + return () => clearTimeout(timer); + }, [remainingTime, refreshCourseData]); + // 초기 로드: 캐시 확인 후 fetch 또는 캐시 사용 useEffect(() => { if (!courses || courses.length === 0) return; - const lastRequestTime = localStorage.getItem('lastRequestTime'); - const currentTime = new Date().getTime(); - const oneDay = 60 * 60 * 1000 * 24; + const lastRequest = getLastRequestTime(); + const now = Date.now(); + + if (lastRequest) { + setRefreshTime(new Date(lastRequest).toLocaleTimeString()); + } - if (lastRequestTime) setRefreshTime(new Date(parseInt(lastRequestTime, 10)).toLocaleTimeString()); - if (!lastRequestTime || currentTime - parseInt(lastRequestTime, 10) >= oneDay) { + if (!lastRequest || now - lastRequest >= CACHE_TTL_MS) { setIsPending(true); - updateData(); + refreshCourseData(); } else { - const minutes = (currentTime - parseInt(lastRequestTime, 10)) / (60 * 1000); - setRemainingTime(minutes); - loadDataFromStorage('vod', (data) => { - if (!data) return; - setVods((data as Vod[]).filter((vod) => isCurrentDateInRange(vod.range))); + setRemainingTime((now - lastRequest) / REFRESH_INTERVAL_MS); + loadDataFromStorage('vod', (data) => { + if (data) setVods(data.filter((vod) => isCurrentDateInRange(vod.range))); }); - loadDataFromStorage('assign', (data) => { - if (!data) return; - setAssigns((data as Assign[]).filter((assign) => isCurrentDateByDate(assign.dueDate))); + loadDataFromStorage('assign', (data) => { + if (data) setAssigns(data.filter((assign) => isCurrentDateByDate(assign.dueDate))); }); - loadDataFromStorage('quiz', (data) => { - if (!data) return; - setQuizes((data as Quiz[]).filter((quiz) => isCurrentDateByDate(quiz.dueDate))); + loadDataFromStorage('quiz', (data) => { + if (data) setQuizzes(data.filter((quiz) => isCurrentDateByDate(quiz.dueDate))); }); } - }, [courses, updateData]); - return { vods, assigns, quizes, isPending, remainingTime, refreshTime, isError, updateData, setIsPending }; + }, [courses, refreshCourseData]); + + return { + vods, + assigns, + quizzes, + isPending, + remainingTime, + refreshTime, + isError, + refreshCourseData, + setIsPending, + }; } diff --git a/src/hooks/useDashboardFilters.ts b/src/hooks/useDashboardFilters.ts new file mode 100644 index 0000000..de3c4d3 --- /dev/null +++ b/src/hooks/useDashboardFilters.ts @@ -0,0 +1,99 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Assign, Filters, Quiz, TAB_TYPE, Vod } from '@/types'; +import { filterVods, filterAssigns, filterQuizzes } from '@/lib/filterData'; + +interface UseDashboardFiltersParams { + vods: Vod[]; + assigns: Assign[]; + quizzes: Quiz[]; + activeTab: TAB_TYPE; +} + +const INITIAL_FILTERS: Record = { + VIDEO: { courseTitles: [], attendanceStatuses: [] }, + ASSIGN: { courseTitles: [], submitStatuses: [] }, + QUIZ: { courseTitles: [] }, +}; + +function uniqueTitles(items: { courseTitle: string }[]): string[] { + return [...new Set(items.map((item) => item.courseTitle))]; +} + +export function useDashboardFilters({ vods, assigns, quizzes, activeTab }: UseDashboardFiltersParams) { + const [searchTerm, setSearchTerm] = useState(''); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [filters, setFilters] = useState(INITIAL_FILTERS); + + useEffect(() => { + setSearchTerm(''); + setIsFilterOpen(false); + }, [activeTab]); + + const courseTitlesMap = useMemo( + () => ({ + VIDEO: uniqueTitles(vods), + ASSIGN: uniqueTitles(assigns), + QUIZ: uniqueTitles(quizzes), + }), + [vods, assigns, quizzes] + ); + + const filteredVods = useMemo( + () => filterVods(vods, filters[activeTab], searchTerm, 'isAttendance'), + [vods, filters, activeTab, searchTerm] + ); + + const filteredAssigns = useMemo( + () => filterAssigns(assigns, filters[activeTab], searchTerm, 'isSubmit'), + [assigns, filters, activeTab, searchTerm] + ); + + const filteredQuizzes = useMemo( + () => filterQuizzes(quizzes, filters[activeTab], searchTerm, 'dueDate'), + [quizzes, filters, activeTab, searchTerm] + ); + + const handleFilterChange = useCallback( + (field: K, value: Filters[K] extends (infer T)[] | undefined ? T : never) => { + setFilters((prev) => { + const current = (prev[activeTab][field] as (typeof value)[]) || []; + const updated = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; + return { ...prev, [activeTab]: { ...prev[activeTab], [field]: updated } }; + }); + }, + [activeTab] + ); + + const handleCourseTitleChange = useCallback( + (title: string) => handleFilterChange('courseTitles', title), + [handleFilterChange] + ); + const handleAttendanceFilterChange = useCallback( + (status: string) => handleFilterChange('attendanceStatuses', status), + [handleFilterChange] + ); + const handleSubmitFilterChange = useCallback( + (isSubmit: boolean) => handleFilterChange('submitStatuses', isSubmit), + [handleFilterChange] + ); + + const clearFilters = useCallback(() => { + setFilters((prev) => ({ ...prev, [activeTab]: { ...INITIAL_FILTERS[activeTab] } })); + }, [activeTab]); + + return { + searchTerm, + setSearchTerm, + isFilterOpen, + setIsFilterOpen, + filters, + courseTitlesMap, + filteredVods, + filteredAssigns, + filteredQuizzes, + handleCourseTitleChange, + handleAttendanceFilterChange, + handleSubmitFilterChange, + clearFilters, + }; +} diff --git a/src/hooks/useGetCourse.ts b/src/hooks/useGetCourse.ts deleted file mode 100644 index 392be39..0000000 --- a/src/hooks/useGetCourse.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CourseBase } from '@/content/types'; -import { saveDataToStorage } from '@/lib/storage'; -import { removeSquareBrackets } from '@/lib/utils'; -import { useState, useEffect } from 'react'; - -export interface UseCourseResult { - courses: CourseBase[]; -} - -export const useGetCourses = (): UseCourseResult => { - const [courses, setCourses] = useState([]); - useEffect(() => { - if (import.meta.env.VITE_MOCK) { - import('@/mocks/mockData').then(({ mockCourses }) => { - setCourses(mockCourses); - saveDataToStorage('courses', JSON.stringify(mockCourses)); - console.info('[Dotbugi] DEV 모드: mock 강의 목록 사용'); - }); - return; - } - if (!document) return; - const courseData = Array.from(document.querySelectorAll('.course_box')); - const data = courseData - .map((div) => { - const label = div.querySelector('.course_link .course-name .course-label')?.textContent?.trim() || null; - if (!label || label === '커뮤니티') return null; - const a = div.querySelector('a'); - const url = new URL((a as HTMLAnchorElement).href); - const urlParams = new URLSearchParams(url.search); - const courseId = urlParams.get('id') || ''; - const titleSection = div.querySelector('.course_link .course-name .course-title'); - const prof = titleSection?.querySelector('p')?.textContent?.trim() || ''; - let courseTitle = titleSection?.querySelector('h1, h2, h3')?.textContent?.replace(/new/i, '').trim() || ''; - courseTitle = removeSquareBrackets(courseTitle); - return { courseId, courseTitle, prof }; - }) - .filter( - (item): item is { courseId: string; courseTitle: string; prof: string } => - item !== null && item.courseId !== '' && item.courseTitle !== '' && item.prof !== '' - ); - - setCourses(data); - saveDataToStorage('courses', JSON.stringify(data)); - console.info('[Dotbugi] 강의 목록:', data); - }, []); - - return { courses }; -}; diff --git a/src/hooks/useGetCourses.ts b/src/hooks/useGetCourses.ts new file mode 100644 index 0000000..cda0a54 --- /dev/null +++ b/src/hooks/useGetCourses.ts @@ -0,0 +1,24 @@ +import { CourseBase } from '@/types'; +import { saveDataToStorage } from '@/lib/storage'; +import { parseCoursesFromDOM } from '@/lib/parseCourses'; +import { useState, useEffect } from 'react'; + +export const useGetCourses = () => { + const [courses, setCourses] = useState([]); + + useEffect(() => { + if (import.meta.env.VITE_MOCK) { + import('@/mocks/mockData').then(({ mockCourses }) => { + setCourses(mockCourses); + saveDataToStorage('courses', JSON.stringify(mockCourses)); + }); + return; + } + + const parsed = parseCoursesFromDOM(); + setCourses(parsed); + saveDataToStorage('courses', JSON.stringify(parsed)); + }, []); + + return { courses }; +}; diff --git a/src/hooks/useRemainingTime.ts b/src/hooks/useRemainingTime.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/attendance.ts b/src/lib/attendance.ts new file mode 100644 index 0000000..789f4bb --- /dev/null +++ b/src/lib/attendance.ts @@ -0,0 +1,7 @@ +export function isAttended(value: string) { + return value.toLowerCase().trim() === 'o'; +} + +export function isAbsent(value: string) { + return value.toUpperCase().startsWith('X'); +} diff --git a/src/lib/cache.ts b/src/lib/cache.ts new file mode 100644 index 0000000..63142b1 --- /dev/null +++ b/src/lib/cache.ts @@ -0,0 +1,18 @@ +export const REFRESH_INTERVAL_MS = 60 * 1000; +export const CACHE_TTL_MINUTES = 1440; // 24시간 +export const CACHE_TTL_MS = CACHE_TTL_MINUTES * REFRESH_INTERVAL_MS; + +const CACHE_KEY = 'lastRequestTime'; + +export function getLastRequestTime(): number | null { + const raw = localStorage.getItem(CACHE_KEY); + return raw ? parseInt(raw, 10) : null; +} + +export function setLastRequestTime(time: number): void { + localStorage.setItem(CACHE_KEY, time.toString()); +} + +export function clearLastRequestTime(): void { + localStorage.removeItem(CACHE_KEY); +} diff --git a/src/lib/dateUtils.ts b/src/lib/dateUtils.ts new file mode 100644 index 0000000..f98d90b --- /dev/null +++ b/src/lib/dateUtils.ts @@ -0,0 +1,94 @@ +import { TimeDifferenceResult } from '@/types'; + +const MS_PER_MINUTE = 1000 * 60; +const MS_PER_HOUR = MS_PER_MINUTE * 60; +const MS_PER_DAY = MS_PER_HOUR * 24; + +/** 하이픈 구분 날짜 문자열을 Date로 변환 (Safari 호환) */ +export function parseDate(str: string): Date { + return new Date(str.replace(/-/g, '/')); +} + +export function isCurrentDateInRange(dateRange: string | null) { + if (!dateRange || !dateRange.includes(' ~ ')) return false; + + const [startStr, endStr] = dateRange.split(' ~ '); + if (!startStr || !endStr) return false; + + const startDate = parseDate(startStr); + const endDate = parseDate(endStr); + const now = new Date(); + + return now >= startDate && now <= endDate; +} + +export function isCurrentDateByDate(date: string | null) { + if (!date || date.length <= 1) return false; + return new Date() <= new Date(date); +} + +export function isWithinSevenDays(date: string) { + const diffDays = (new Date(date).getTime() - Date.now()) / MS_PER_DAY; + return diffDays <= 7 && diffDays >= 0; +} + +export function extractEndDate(range: string | null): string | null { + if (!range) return null; + return range.split(' ~ ')[1] ?? null; +} + +export function calculateDueDate(dueDate: string | null): TimeDifferenceResult { + if (!dueDate) { + return { message: '정보없음', borderColor: 'border-amber-500', borderLeftColor: 'border-l-amber-500', textColor: 'text-amber-500' }; + } + + const now = new Date(); + const endDate = new Date(dueDate); + + if (isNaN(endDate.getTime())) { + return { message: 'Invalid date format', borderColor: 'gray', borderLeftColor: 'gray', textColor: 'black' }; + } + + if (now >= endDate) { + return { message: '마감', borderColor: 'border-red-950', borderLeftColor: 'border-l-red-950', textColor: 'text-red-950' }; + } + + const timeDiff = endDate.getTime() - now.getTime(); + const days = Math.floor(timeDiff / MS_PER_DAY); + + if (days >= 1) { + return { message: `${days}일 후`, borderColor: 'border-amber-500', borderLeftColor: 'border-l-amber-500', textColor: 'text-amber-500' }; + } + + const hours = Math.floor(timeDiff / MS_PER_HOUR); + const minutes = Math.floor(timeDiff / MS_PER_MINUTE); + return { + message: hours !== 0 ? `${hours}시간 후` : `${minutes}분 후`, + borderColor: 'border-red-700', + borderLeftColor: 'border-l-red-700', + textColor: 'text-red-700', + }; +} + +export function calculateRemainingTime(endTime: string | null) { + if (!endTime) return '정보없음'; + + const timeDiff = new Date(endTime).getTime() - Date.now(); + const daysLeft = Math.floor(timeDiff / MS_PER_DAY); + const hoursLeft = Math.floor((timeDiff % MS_PER_DAY) / MS_PER_HOUR); + const minutesLeft = Math.floor((timeDiff % MS_PER_HOUR) / MS_PER_MINUTE); + + if (daysLeft < 0 || hoursLeft < 0 || minutesLeft < 0) return '마감'; + return `${daysLeft === 0 ? '' : daysLeft + '일'} ${hoursLeft === 0 ? '' : hoursLeft + '시간'} ${minutesLeft}분 남음`; +} + +export function timeAgo(givenTimestamp: number) { + const diffMs = Date.now() - givenTimestamp; + + const days = Math.floor(diffMs / MS_PER_DAY); + const hours = Math.floor((diffMs % MS_PER_DAY) / MS_PER_HOUR); + const minutes = Math.floor((diffMs % MS_PER_HOUR) / MS_PER_MINUTE); + + if (days === 0 && hours === 0 && minutes === 0) return '지금 막'; + return `${days !== 0 ? days + '일 ' : ''}${hours !== 0 ? hours + '시간 ' : ''}${minutes !== 0 ? minutes + '분 전' : '전'}`; +} diff --git a/src/lib/deduplicateInto.ts b/src/lib/deduplicateInto.ts new file mode 100644 index 0000000..8678e4a --- /dev/null +++ b/src/lib/deduplicateInto.ts @@ -0,0 +1,17 @@ +/** + * 키 생성 함수 기반으로 중복 제거하면서 배열에 추가 + */ +export function deduplicateInto( + target: T[], + source: T[], + seen: Set, + getKey: (item: T) => string, +): void { + for (const item of source) { + const key = getKey(item); + if (!seen.has(key)) { + seen.add(key); + target.push(item); + } + } +} diff --git a/src/lib/fetchAssign.ts b/src/lib/fetchAssign.ts index d3f902a..f70a654 100644 --- a/src/lib/fetchAssign.ts +++ b/src/lib/fetchAssign.ts @@ -1,55 +1,37 @@ -export const fetchAssign = async (link: string) => { - try { - const response = await fetch(link, { - method: 'GET', - credentials: 'include', - }); +import { fetchHtml, getText, getHref } from './fetchHtml'; - if (!response.ok) { - throw new Error('Network response was not ok'); - } +// 과제 테이블 컬럼 (generaltable) +// [주(c0)] [과제(c1)] [종료 일시(c2)] [제출(c3)] [성적(c4)] +const COL = { + WEEK: '.cell.c0', + TITLE_LINK: '.cell.c1 a', + DUE_DATE: '.cell.c2', + SUBMIT_STATUS: '.cell.c3', +} as const; - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); +const NOT_SUBMITTED = '미제출'; - const headerMap: Record = {}; - const headers = Array.from(doc.querySelectorAll('table.generaltable thead tr th')); - headers.forEach((header) => { - const text = header.textContent?.trim(); - const className = header.className.match(/c\d+/)?.[0]; - if (text && className) { - if (text.includes('주제')) headerMap['subject'] = '.cell.' + className; - else if (text.includes('과제')) { - headerMap['title'] = '.cell.' + className; - headerMap['url'] = headerMap['title'] + ' a'; - } else if (text.includes('종료 일시')) headerMap['dueDate'] = '.cell.' + className; - else if (text.includes('제출')) headerMap['isSubmit'] = '.cell.' + className; - } - }); +export const fetchAssign = async (link: string) => { + try { + const doc = await fetchHtml(link); + const rows = doc.querySelectorAll('table.generaltable tbody tr'); - let subject: string; - const rows = Array.from(doc.querySelectorAll('table.generaltable tbody tr')); - const assignments = rows - .map((row) => { - const title = - row.querySelector(headerMap.title)?.textContent?.trim() || - row.querySelector(headerMap.assign)?.textContent?.trim() || - null; - const sbj = row.querySelector(headerMap.subject)?.textContent?.trim() || ''; - const url = (row.querySelector(headerMap.url) as HTMLAnchorElement)?.href || null; - const dueDate = row.querySelector(headerMap.dueDate)?.textContent?.trim() || null; - const isSubmit = row.querySelector(headerMap.isSubmit)?.textContent?.trim() === '미제출' ? false : true; + let lastWeekLabel = ''; - if (sbj.length !== 0) subject = sbj; - if (!title || !url || !dueDate) return null; - return { subject, title, url, dueDate, isSubmit }; - }) - .filter((assign) => assign !== null); + return Array.from(rows).flatMap((row) => { + const weekLabel = getText(row, COL.WEEK); + if (weekLabel) lastWeekLabel = weekLabel; - return assignments; + const title = getText(row, COL.TITLE_LINK); + const url = getHref(row, COL.TITLE_LINK); + const dueDate = getText(row, COL.DUE_DATE); + if (!title || !url || !dueDate) return []; + + const isSubmit = getText(row, COL.SUBMIT_STATUS) !== NOT_SUBMITTED; + return { subject: lastWeekLabel, title, url, dueDate, isSubmit }; + }); } catch (error) { - console.error('Fetch error:', error); + console.error('[Dotbugi] 과제 조회 오류:', error); throw error; } }; diff --git a/src/lib/fetchCourseData.ts b/src/lib/fetchCourseData.ts index aa1e1b8..35355d6 100644 --- a/src/lib/fetchCourseData.ts +++ b/src/lib/fetchCourseData.ts @@ -1,27 +1,27 @@ import { fetchVodAttendance } from './fetchVodAttendance'; -import { fetchIndexPage } from './fetchIndexPage'; -import { getAssignPageLink, getIndexPageLink, getQuizPageLink, getVodPageLink } from '@/constants/constant'; +import { fetchVodList } from './fetchVodList'; +import { getAssignPageLink, getIndexPageLink, getQuizPageLink, getVodPageLink } from '@/constants/links'; import { fetchAssign } from './fetchAssign'; import { fetchQuiz } from './fetchQuiz'; -export const requestData = async (id: string) => { - const VOD_LINK = getVodPageLink(id); - const INDEX_LINK = getIndexPageLink(id); - const ASSIGN_LINK = getAssignPageLink(id); - const QUIZ_LINK = getQuizPageLink(id); - +export const scrapeCourseData = async (courseId: string) => { try { - const [vodAttendanceArray, vodDataArray, assignDataArray, quizDataArray] = await Promise.all([ - fetchVodAttendance(VOD_LINK), - fetchIndexPage(INDEX_LINK), - fetchAssign(ASSIGN_LINK), - fetchQuiz(QUIZ_LINK), + const [vodAttendance, vodList, assignList, quizList] = await Promise.all([ + fetchVodAttendance(getVodPageLink(courseId)), + fetchVodList(getIndexPageLink(courseId)), + fetchAssign(getAssignPageLink(courseId)), + fetchQuiz(getQuizPageLink(courseId)), ]); - console.info('[Dotbugi]', vodAttendanceArray, vodDataArray, assignDataArray, quizDataArray); - return { vodAttendanceArray, vodDataArray, assignDataArray, quizDataArray }; + console.info('[Dotbugi]', vodAttendance, vodList, assignList, quizList); + return { + vodAttendanceArray: vodAttendance, + vodDataArray: vodList, + assignDataArray: assignList, + quizDataArray: quizList, + }; } catch (error) { - console.error('Error while fetching data:', error); + console.error('[Dotbugi] 강의 데이터 스크래핑 오류:', error); throw error; } }; diff --git a/src/lib/fetchHtml.ts b/src/lib/fetchHtml.ts new file mode 100644 index 0000000..c4e7558 --- /dev/null +++ b/src/lib/fetchHtml.ts @@ -0,0 +1,26 @@ +/** + * LMS 페이지를 fetch하고 DOM으로 파싱하여 반환 + */ +export async function fetchHtml(link: string): Promise { + const response = await fetch(link, { + method: 'GET', + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${link}`); + } + + const html = await response.text(); + return new DOMParser().parseFromString(html, 'text/html'); +} + +/** 셀렉터로 요소의 텍스트 추출. 없으면 null */ +export function getText(parent: Element, selector: string): string | null { + return parent.querySelector(selector)?.textContent?.trim() || null; +} + +/** 셀렉터로 a 태그의 href 추출. 없으면 null */ +export function getHref(parent: Element, selector: string): string | null { + return parent.querySelector(selector)?.href || null; +} diff --git a/src/lib/fetchIndexPage.ts b/src/lib/fetchIndexPage.ts deleted file mode 100644 index 63a039b..0000000 --- a/src/lib/fetchIndexPage.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const fetchIndexPage = async (link: string) => { - try { - const response = await fetch(link, { - method: 'GET', - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const weeks = Array.from(doc.querySelectorAll('#region-main > div > div > div.total_sections > div > ul > li')); - - const vods = weeks - .map((week, index) => { - const subject = week.querySelector('.content .sectionname')?.textContent?.trim() || index + '주차'; - const contents = Array.from(week.querySelectorAll('.content .vod .activityinstance')); - const vodsWeekly = contents - .filter((item) => !item.closest('.dimmed')) - .map((item) => { - const week = index + 1; - const instancename = item.querySelector('.instancename'); - instancename?.querySelector('.accesshide')?.remove(); - const title = instancename?.textContent?.trim() || null; - const url = item.querySelector('a')?.getAttribute('href') || null; - const range = item.querySelector('.text-ubstrap')?.textContent?.trim() || ''; - const length = item.querySelector('.text-info')?.textContent?.replace(',', '').trim() || ''; - - if (!title || !url || !range) return null; - return { week, subject, title, url, range, length }; - }) - .filter((item) => item !== null); - - if (!vodsWeekly || vodsWeekly.length === 0) return null; - return vodsWeekly; - }) - .filter((week) => week !== null) - .flat(); - - return vods; - } catch (error) { - console.error('Fetch error:', error); - throw error; - } -}; diff --git a/src/lib/fetchQuiz.ts b/src/lib/fetchQuiz.ts index e5107ce..8363ba5 100644 --- a/src/lib/fetchQuiz.ts +++ b/src/lib/fetchQuiz.ts @@ -1,58 +1,47 @@ +import { fetchHtml, getText } from './fetchHtml'; +import { BASE_LINK } from '@/constants/links'; + +// 퀴즈 테이블 컬럼 (generaltable) +// [주(c0)] [제목(c1)] [종료 일시(c2)] [성적(c3)] +const COL = { + WEEK: '.cell.c0', + TITLE_LINK: '.cell.c1 a', + DUE_DATE: '.cell.c2', +} as const; + +/** + * 상대/절대 경로를 퀴즈 모듈 절대 URL로 변환 + * view.php?id=123 → https://learn.hansung.ac.kr/mod/quiz/view.php?id=123 + */ +function toQuizUrl(rawHref: string): string { + if (rawHref.startsWith('http')) return rawHref; + return `${BASE_LINK}/mod/quiz/${rawHref}`; +} + export const fetchQuiz = async (link: string) => { try { - const response = await fetch(link, { - method: 'GET', - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const headerMap: Record = {}; - const headers = Array.from(doc.querySelectorAll('table.generaltable thead tr th')); - headers.forEach((header) => { - const text = header.textContent?.trim(); - const className = header.className.match(/c\d+/)?.[0]; - if (text && className) { - if (text.includes('주제')) headerMap['subject'] = '.cell.' + className; - else if (text.includes('제목')) { - headerMap['title'] = '.cell.' + className; - headerMap['url'] = headerMap['title'] + ' a'; - } else if (text.includes('종료 일시')) headerMap['dueDate'] = '.cell.' + className; - } - }); - - let subject: string; - const rows = Array.from(doc.querySelectorAll('table.generaltable tbody tr')); - const quizzes = rows - .map((row) => { - const sbj = row.querySelector(headerMap.subject)?.textContent?.trim() || ''; - const title = row.querySelector(headerMap.title)?.textContent?.trim() || null; - let url = (row.querySelector(headerMap.url) as HTMLAnchorElement)?.href || null; - const dueDate = row.querySelector(headerMap.dueDate)?.textContent?.trim() || null; - - if (sbj.length !== 0) subject = sbj; - - if (url) { - const index = url.indexOf('view'); - url = url.slice(0, index) + 'mod/quiz/' + url.slice(index); - } - - if (title && url && dueDate) { - return { title, subject, url, dueDate }; - } - return null; - }) - .filter((quiz) => quiz !== null); - - return quizzes; + const doc = await fetchHtml(link); + const rows = doc.querySelectorAll('table.generaltable tbody tr'); + + let lastWeekLabel = ''; + + return Array.from(rows) + .flatMap((row) => { + const weekLabel = getText(row, COL.WEEK); + if (weekLabel) lastWeekLabel = weekLabel; + + const titleLink = row.querySelector(COL.TITLE_LINK); + const dueDate = getText(row, COL.DUE_DATE); + if (!titleLink || !dueDate) return []; + + const title = titleLink.textContent?.trim(); + const rawHref = titleLink.getAttribute('href'); + if (!title || !rawHref) return []; + + return { title, subject: lastWeekLabel, url: toQuizUrl(rawHref), dueDate }; + }); } catch (error) { - console.error('Fetch error:', error); + console.error('[Dotbugi] 퀴즈 조회 오류:', error); throw error; } }; diff --git a/src/lib/fetchVodAttendance.ts b/src/lib/fetchVodAttendance.ts index 601bdbb..37ee9fc 100644 --- a/src/lib/fetchVodAttendance.ts +++ b/src/lib/fetchVodAttendance.ts @@ -1,129 +1,114 @@ -import { VodAttendanceData } from '@/content/types'; +import { VodAttendanceData } from '@/types'; +import { fetchHtml } from './fetchHtml'; + +// 출석부 테이블 컬럼 인덱스 (user_progress_table) +// [주차(0)] [강의 자료(1)] [출석인정 요구시간(2)] [총 학습시간(3)] [출석(4)] [주차 출석(5)] +const COL = { + WEEK: 0, + TITLE: 1, + ATTENDANCE: 4, + WEEKLY_ATTENDANCE: 5, +} as const; + +const BULK_APPROVED = ['일괄출석인정', 'Batch attendance']; /** - * 온라인 출석부 정보 가져오기 - * - * @param link 강의 링크 - * @returns [{온라인 강의 제목, 출석 여부, 주차정보}...] + * rowspan/colspan이 있는 테이블 행을 평탄화된 셀 값 배열로 변환 */ -export const fetchVodAttendance = async (link: string) => { - try { - const response = await fetch(link, { - method: 'GET', - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Network response was not ok'); +function flattenRow( + row: Element, + pendingSpans: (number | undefined)[], + spanValues: (string | null)[], +): (string | null)[] { + const cells = Array.from(row.querySelectorAll('td')); + const rowData: (string | null)[] = []; + let col = 0; + + const consumePendingSpans = () => { + while (pendingSpans[col] && pendingSpans[col]! > 0) { + rowData.push(spanValues[col] || null); + pendingSpans[col]! -= 1; + col++; } + }; - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); + consumePendingSpans(); - const headerMap: Record = {}; + for (const cell of cells) { + consumePendingSpans(); - const headers = Array.from(doc.querySelectorAll('.user_progress_table > thead > tr > th')); + const rowspan = parseInt(cell.getAttribute('rowspan') || '1', 10); + const colspan = parseInt(cell.getAttribute('colspan') || '1', 10); + const content = cell.textContent?.trim() || ''; - headers.forEach((header, index) => { - const text = header.textContent?.trim() || ''; - if (text === '강의 자료') headerMap['title'] = index; - else if (text === '출석') headerMap['isAttendance'] = index; - else if (text === '주차 출석') headerMap['weeklyAttendance'] = index; - }); + for (let i = 0; i < colspan; i++) { + rowData.push(content); + if (rowspan > 1) { + pendingSpans[col] = rowspan - 1; + spanValues[col] = content; + } else { + spanValues[col] = null; + } + col++; + } + } + + while (col < pendingSpans.length) { + if (pendingSpans[col] && pendingSpans[col]! > 0) { + rowData.push(spanValues[col] || null); + pendingSpans[col]! -= 1; + } else { + rowData.push(null); + } + col++; + } - const rows = Array.from(doc.querySelectorAll('.user_progress_table > tbody > tr')); + return rowData; +} - const spanTable: (number | undefined)[] = []; - const cellValues: (string | null)[] = []; - const vods: VodAttendanceData[] = []; - let idx = 0; +/** + * 온라인 출석부 정보 가져오기 + */ +export const fetchVodAttendance = async (link: string) => { + try { + const doc = await fetchHtml(link); + const rows = doc.querySelectorAll('.user_progress_table > tbody > tr'); + + const pendingSpans: (number | undefined)[] = []; + const spanValues: (string | null)[] = []; + const results: VodAttendanceData[] = []; + let currentWeek = 0; let lastWeeklyAttendance = ''; + rows.forEach((row) => { try { - const cells = Array.from(row.querySelectorAll('td')); - let colIndex = 0; - const rowData: (string | null)[] = []; - - while (spanTable[colIndex] && spanTable[colIndex]! > 0) { - rowData.push(cellValues[colIndex] || null); - spanTable[colIndex]! -= 1; - colIndex++; - } + const cells = flattenRow(row, pendingSpans, spanValues); - cells.forEach((cell) => { - while (spanTable[colIndex] && spanTable[colIndex]! > 0) { - rowData.push(cellValues[colIndex] || null); - spanTable[colIndex]! -= 1; - colIndex++; - } - - const rowspan = parseInt(cell.getAttribute('rowspan') || '1', 10); - const colspan = parseInt(cell.getAttribute('colspan') || '1', 10); - const cellContent = cell.textContent?.trim() || ''; - - for (let i = 0; i < colspan; i++) { - rowData.push(cellContent); - - if (rowspan > 1) { - spanTable[colIndex] = rowspan - 1; - cellValues[colIndex] = cellContent; - } else { - cellValues[colIndex] = null; - } - colIndex++; - } - }); - - while (colIndex < spanTable.length) { - if (spanTable[colIndex] && spanTable[colIndex]! > 0) { - rowData.push(cellValues[colIndex] || null); - spanTable[colIndex]! -= 1; - } else { - rowData.push(null); - } - colIndex++; - } + const title = cells[COL.TITLE] || ''; + const isAttendance = cells[COL.ATTENDANCE] || ''; - let weeklyAttendance = - headerMap['weeklyAttendance'] !== undefined ? rowData[headerMap['weeklyAttendance']] || '' : ''; + let weeklyAttendance = cells[COL.WEEKLY_ATTENDANCE] || ''; if (weeklyAttendance) { lastWeeklyAttendance = weeklyAttendance; } else { weeklyAttendance = lastWeeklyAttendance; } + if (BULK_APPROVED.some((keyword) => weeklyAttendance.includes(keyword))) weeklyAttendance = 'o'; - if (weeklyAttendance.includes('일괄출석인정')) weeklyAttendance = 'o'; - - const title = headerMap['title'] !== undefined ? rowData[headerMap['title']] || '' : ''; - const isAttendance = headerMap['isAttendance'] !== undefined ? rowData[headerMap['isAttendance']] || '' : ''; + const parsedWeek = parseInt(cells[COL.WEEK] || ''); + if (!isNaN(parsedWeek)) currentWeek = parsedWeek; - let weekStr = rowData[0] || ''; - if (weekStr !== '' && !isNaN(parseInt(weekStr))) { - idx = parseInt(weekStr); - } else { - weekStr = idx.toString(); - } + if (!title || !isAttendance) return; - if (!title || !isAttendance) { - return; - } - const week = parseInt(weekStr); - - vods.push({ - title, - isAttendance, - weeklyAttendance, - week, - }); + results.push({ title, isAttendance, weeklyAttendance, week: currentWeek }); } catch (error) { - console.error(`[Dotbugi] 영상 강의 조회 오류: ${link} ${row}`, error); + console.error(`[Dotbugi] 출석부 행 파싱 오류: ${link}`, error); } }); - return vods; + return results; } catch (error) { - console.error('Fetch error:', error); + console.error('[Dotbugi] 출석부 조회 오류:', error); throw error; } }; diff --git a/src/lib/fetchVodList.ts b/src/lib/fetchVodList.ts new file mode 100644 index 0000000..eb53700 --- /dev/null +++ b/src/lib/fetchVodList.ts @@ -0,0 +1,40 @@ +import { fetchHtml, getText, getHref } from './fetchHtml'; + +function parseWeekNumber(section: Element): number { + const match = section.id?.match(/section-(\d+)/); + return match ? parseInt(match[1], 10) : 0; +} + +function parseVodTitle(activity: Element): string | undefined { + const nameEl = activity.querySelector('.instancename'); + if (!nameEl) return; + const clone = nameEl.cloneNode(true) as Element; + clone.querySelector('.accesshide')?.remove(); + return clone.textContent?.trim(); +} + +export const fetchVodList = async (link: string) => { + try { + const doc = await fetchHtml(link); + const sections = doc.querySelectorAll('li[id^="section-"]'); + + return Array.from(sections).flatMap((section) => { + const week = parseWeekNumber(section); + const subject = getText(section, '.sectionname') || `${week}`; + const vodActivities = section.querySelectorAll('li.modtype_vod:not(.dimmed) .activityinstance'); + + return Array.from(vodActivities).flatMap((activity) => { + const title = parseVodTitle(activity); + const url = getHref(activity, 'a'); + const range = getText(activity, '.text-ubstrap'); + if (!title || !url || !range) return []; + + const length = getText(activity, '.text-info')?.replace(',', '') ?? ''; + return { week, subject, title, url, range, length }; + }); + }); + } catch (error) { + console.error('[Dotbugi] VOD 목록 조회 오류:', error); + throw error; + } +}; diff --git a/src/lib/filterData.tsx b/src/lib/filterData.tsx index 0c789e3..a9ed85f 100644 --- a/src/lib/filterData.tsx +++ b/src/lib/filterData.tsx @@ -1,34 +1,35 @@ -import { Vod, Assign, Quiz, Filters } from '@/content/types'; +import { Vod, Assign, Quiz, CourseBase, Filters } from '@/types'; +import { isAttended } from './utils'; + +function matchesBase(item: CourseBase & { title: string }, courseTitles: string[], term: string): boolean { + if (courseTitles.length > 0 && !courseTitles.includes(item.courseTitle)) return false; + if ( + term && + !item.courseTitle.toLowerCase().includes(term) && + !item.title.toLowerCase().includes(term) && + !item.prof.toLowerCase().includes(term) + ) + return false; + return true; +} // 필터 적용 for VODs export function filterVods(vods: Vod[], filters: Filters, searchTerm: string, sortBy: keyof Vod): Vod[] { - let data = vods; - const { courseTitles, attendanceStatuses } = filters; + const term = searchTerm.toLowerCase(); - if (courseTitles.length > 0) { - data = data.filter((vod) => courseTitles.includes(vod.courseTitle)); - } - - if (attendanceStatuses && attendanceStatuses.length > 0) { - data = data.filter((vod) => { - const status = vod.isAttendance.toLowerCase().trim() === 'o' ? '출석' : '결석'; - return attendanceStatuses.includes(status); - }); - } - - if (searchTerm !== '') { - data = data.filter( - (item) => - item.courseTitle.toLowerCase().includes(searchTerm.toLowerCase()) || - item.title.toLowerCase().includes(searchTerm.toLowerCase()) || - item.prof.toLowerCase().includes(searchTerm.toLowerCase()) - ); - } + const data = vods.filter((vod) => { + if (!matchesBase(vod, courseTitles, term)) return false; + if (attendanceStatuses && attendanceStatuses.length > 0) { + const status = isAttended(vod.isAttendance) ? '출석' : '결석'; + if (!attendanceStatuses.includes(status)) return false; + } + return true; + }); return data.sort((a, b) => { - const attendanceA = a.isAttendance.toLowerCase().trim() === 'o'; - const attendanceB = b.isAttendance.toLowerCase().trim() === 'o'; + const attendanceA = isAttended(a.isAttendance); + const attendanceB = isAttended(b.isAttendance); if (attendanceA !== attendanceB) { return attendanceA ? -1 : 1; @@ -48,26 +49,14 @@ export function filterVods(vods: Vod[], filters: Filters, searchTerm: string, so // 필터 적용 for Assigns export function filterAssigns(assigns: Assign[], filters: Filters, searchTerm: string, sortBy: keyof Assign): Assign[] { - let data = assigns; - const { courseTitles, submitStatuses } = filters; + const term = searchTerm.toLowerCase(); - if (courseTitles.length > 0) { - data = data.filter((assign) => courseTitles.includes(assign.courseTitle)); - } - - if (submitStatuses && submitStatuses.length > 0) { - data = data.filter((assign) => submitStatuses.includes(assign.isSubmit)); - } - - if (searchTerm !== '') { - data = data.filter( - (item) => - item.courseTitle.toLowerCase().includes(searchTerm.toLowerCase()) || - item.title.toLowerCase().includes(searchTerm.toLowerCase()) || - item.prof.toLowerCase().includes(searchTerm.toLowerCase()) - ); - } + const data = assigns.filter((assign) => { + if (!matchesBase(assign, courseTitles, term)) return false; + if (submitStatuses && submitStatuses.length > 0 && !submitStatuses.includes(assign.isSubmit)) return false; + return true; + }); return data.sort((a, b) => { // 미제출 우선 배치 @@ -87,24 +76,12 @@ export function filterAssigns(assigns: Assign[], filters: Filters, searchTerm: s }); } -// 필터 적용 for Quizes -export function filterQuizes(quizes: Quiz[], filters: Filters, searchTerm: string, sortBy: keyof Quiz): Quiz[] { - let data = quizes; - +// 필터 적용 for Quizzes +export function filterQuizzes(quizzes: Quiz[], filters: Filters, searchTerm: string, sortBy: keyof Quiz): Quiz[] { const { courseTitles } = filters; + const term = searchTerm.toLowerCase(); - if (courseTitles.length > 0) { - data = data.filter((quiz) => courseTitles.includes(quiz.courseTitle)); - } - - if (searchTerm !== '') { - data = data.filter( - (item) => - item.courseTitle.toLowerCase().includes(searchTerm.toLowerCase()) || - item.title.toLowerCase().includes(searchTerm.toLowerCase()) || - item.prof.toLowerCase().includes(searchTerm.toLowerCase()) - ); - } + const data = quizzes.filter((quiz) => matchesBase(quiz, courseTitles, term)); return data.sort((a, b) => { switch (sortBy) { diff --git a/src/lib/generateKey.ts b/src/lib/generateKey.ts new file mode 100644 index 0000000..e10b26e --- /dev/null +++ b/src/lib/generateKey.ts @@ -0,0 +1,4 @@ +export const makeVodKey = (courseId: string, title: string, week: number) => `${courseId}-${title}-${week}`; +export const makeItemKey = (courseId: string, title: string, dueDate: string) => `${courseId}-${title}-${dueDate}`; +export const makeVodGroupKey = (courseId: string, subject: string, range: string | null) => + `${courseId}-${subject}-${range}`; diff --git a/src/lib/parseCourses.ts b/src/lib/parseCourses.ts new file mode 100644 index 0000000..6c15efa --- /dev/null +++ b/src/lib/parseCourses.ts @@ -0,0 +1,25 @@ +import { CourseBase } from '@/types'; +import { removeSquareBrackets } from '@/lib/utils'; + +// course_label_ec = 커뮤니티(비교과), 클래스 기반 필터링으로 다국어 대응 +const COMMUNITY_CLASS = 'course_label_ec'; + +function parseCourseFromElement(li: Element): CourseBase | null { + const link = li.querySelector('a.course_link') as HTMLAnchorElement | null; + if (!link) return null; + + const courseId = new URL(link.href).searchParams.get('id'); + const titleEl = link.querySelector('.course-title'); + const prof = titleEl?.querySelector('p')?.textContent?.trim(); + const rawTitle = titleEl?.querySelector('h1, h2, h3')?.textContent?.replace(/new/i, '').trim(); + const courseTitle = rawTitle ? removeSquareBrackets(rawTitle) : ''; + + if (!courseId || !courseTitle || !prof) return null; + return { courseId, courseTitle, prof }; +} + +export function parseCoursesFromDOM(): CourseBase[] { + return Array.from(document.querySelectorAll('.my-course-lists > li')) + .filter((li) => !li.classList.contains(COMMUNITY_CLASS)) + .flatMap((li) => parseCourseFromElement(li) ?? []); +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 595f1ba..99a36bf 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -21,3 +21,30 @@ export function loadDataFromStorage(key: string, callback: (data: T | null) = } }); } + +/** + * storage에서 데이터를 로드하고 JSON 파싱 후 변환 함수를 적용하는 유틸 + */ +export function loadAndTransform( + storageKey: string, + transform: (data: T[]) => R, + callback: (result: R) => void +): void { + loadDataFromStorage(storageKey, (data: string | null) => { + if (!data) return; + + let parsedData: T[]; + if (typeof data === 'string') { + try { + parsedData = JSON.parse(data); + } catch (error) { + console.error(`JSON 파싱 에러 (${storageKey}):`, error); + return; + } + } else { + parsedData = data; + } + + callback(transform(parsedData)); + }); +} diff --git a/src/lib/stringUtils.ts b/src/lib/stringUtils.ts new file mode 100644 index 0000000..82d954a --- /dev/null +++ b/src/lib/stringUtils.ts @@ -0,0 +1,14 @@ +export function removeSquareBrackets(str: string) { + return str.replace(/\[[^\]]*\]/g, ''); +} + +export function formatDateString(input: string | null) { + if (!input) return '기한없음'; + const regex = /(\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}):\d{2} ~ (\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}):\d{2}/; + + return input.replace( + regex, + (_, y1, m1, d1, t1, y2, m2, d2, t2) => + `${y1.slice(2)}.${m1}.${d1} ${t1} ~ ${y2.slice(2)}.${m2}.${d2} ${t2}` + ); +} diff --git a/src/lib/summarizeCourseData.ts b/src/lib/summarizeCourseData.ts new file mode 100644 index 0000000..de59461 --- /dev/null +++ b/src/lib/summarizeCourseData.ts @@ -0,0 +1,19 @@ +import { Vod } from '@/types'; +import { makeVodGroupKey } from '@/lib/generateKey'; +import { isAttended } from '@/lib/utils'; + +export type CardData = { + done: number; + total: number; +}; + +export function summarizeVods(vods: Vod[]): CardData { + const groups: Record = {}; + for (const vod of vods) { + const key = makeVodGroupKey(vod.courseId, vod.subject, vod.range); + (groups[key] ??= []).push(vod); + } + const groupList = Object.values(groups); + const done = groupList.filter((g) => isAttended(g[0].weeklyAttendance)).length; + return { done, total: groupList.length }; +} diff --git a/src/lib/transformCourseData.ts b/src/lib/transformCourseData.ts new file mode 100644 index 0000000..6630c9a --- /dev/null +++ b/src/lib/transformCourseData.ts @@ -0,0 +1,43 @@ +import { Vod, CourseBase } from '@/types'; +import { isCurrentDateByDate, isCurrentDateInRange } from '@/lib/utils'; + +/** + * VOD 데이터와 출석 데이터를 title+week 기준으로 조인하고, 현재 기간 내 항목만 반환 + */ +export function mergeVodWithAttendance( + course: CourseBase, + vodList: { week: number; subject: string; title: string; url: string; range: string | null; length: string }[], + attendanceList: { title: string; isAttendance: string; weeklyAttendance: string; week: number }[] +): Vod[] { + const attendanceByKey = new Map(); + for (const att of attendanceList) { + attendanceByKey.set(`${att.title}-${att.week}`, att); + } + + const results: Vod[] = []; + for (const vod of vodList) { + if (!isCurrentDateInRange(vod.range)) continue; + const attendance = attendanceByKey.get(`${vod.title}-${vod.week}`); + if (!attendance) continue; + results.push({ + ...course, + ...vod, + isAttendance: attendance.isAttendance, + weeklyAttendance: attendance.weeklyAttendance, + }); + } + return results; +} + +/** + * 마감일 기반 항목(과제/퀴즈)에 course 정보를 병합하고, 현재 유효한 항목만 반환 + */ +export function mergeDueDateItems( + course: CourseBase, + items: T[], +): (CourseBase & T)[] { + return items + .filter((item) => isCurrentDateByDate(item.dueDate)) + .map((item) => ({ ...course, ...item })); +} + diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0f2c101..4172fb0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,3 @@ -import { TimeDifferenceResult } from '@/content/types'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -6,199 +5,15 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function isCurrentDateInRange(dateRange: string | null) { - if (!dateRange || !dateRange.includes(' ~ ')) { - return false; - } - - const [startStr, endStr] = dateRange.split(' ~ '); - - if (!startStr || !endStr) { - return false; - } - - const startDate = new Date(startStr.replace(/-/g, '/')); - const endDate = new Date(endStr.replace(/-/g, '/')); - - const currentDate = new Date(); - - return currentDate >= startDate && currentDate <= endDate; -} - -export function isCurrentDateByDate(date: string | null) { - if (!date || date.length <= 1) return false; - const endDate = new Date(date); - const currentDate = new Date(); - return currentDate <= endDate; -} - -export function isWithinSevenDays(date: string) { - const dueDate = new Date(date); // 문자열을 Date 객체로 변환 - const now = new Date(); // 현재 날짜 - const diffTime = dueDate.getTime() - now.getTime(); // 두 날짜의 차이 (밀리초) - const diffDays = diffTime / (1000 * 60 * 60 * 24); // 차이를 일(day)로 변환 - return diffDays <= 7 && diffDays >= 0; -} - -export const calculateTimeDifference = (timeRange: string | null): TimeDifferenceResult => { - if (!timeRange) { - return { - message: `정보없음`, - borderColor: 'border-amber-500', - borderLeftColor: 'border-l-amber-500', - textColor: 'text-amber-500', - }; - } - - const now = new Date(); - const [startString, endString] = timeRange.split(' ~ '); - const startDate = new Date(startString); - const endDate = new Date(endString); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return { message: 'Invalid date format', borderColor: 'gray', borderLeftColor: 'gray', textColor: 'black' }; - } - - if (now < endDate) { - const timeDiff = endDate.getTime() - now.getTime(); - const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - - if (days >= 1) { - return { - message: `${days}일 후`, - borderColor: 'border-amber-500', - borderLeftColor: 'border-l-amber-500', - textColor: 'text-amber-500', - }; - } else { - const hours = Math.floor(timeDiff / (1000 * 60 * 60)); - const minutes = Math.floor(timeDiff / (1000 * 60)); - return { - message: `${hours !== 0 ? `${hours}시간 후` : `${minutes}분 후`}`, - borderColor: 'border-red-700', - borderLeftColor: 'border-l-red-700', - textColor: 'text-red-700', - }; - } - } else { - return { - message: '마감', - borderColor: 'border-red-950', - borderLeftColor: 'border-l-red-950', - textColor: 'text-red-950', - }; - } -}; - -export const calculateDueDate = (dueDate: string | null): TimeDifferenceResult => { - if (!dueDate) { - return { - message: `정보없음`, - borderColor: 'border-amber-500', - borderLeftColor: 'border-l-amber-500', - textColor: 'text-amber-500', - }; - } - - const now = new Date(); - const endDate = new Date(dueDate); - - if (isNaN(endDate.getTime())) { - return { message: 'Invalid date format', borderColor: 'gray', borderLeftColor: 'gray', textColor: 'black' }; - } - - if (now < endDate) { - const timeDiff = endDate.getTime() - now.getTime(); - const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - - if (days >= 1) { - return { - message: `${days}일 후`, - borderColor: 'border-amber-500', - borderLeftColor: 'border-l-amber-500', - textColor: 'text-amber-500', - }; - } else { - const hours = Math.floor(timeDiff / (1000 * 60 * 60)); - const minutes = Math.floor(timeDiff / (1000 * 60)); - return { - message: `${hours !== 0 ? `${hours}시간 후` : `${minutes}분 후`}`, - borderColor: 'border-red-700', - borderLeftColor: 'border-l-red-700', - textColor: 'text-red-700', - }; - } - } else { - return { - message: '마감', - borderColor: 'border-red-950', - borderLeftColor: 'border-l-red-950', - textColor: 'text-red-950', - }; - } -}; - -export const formatDateString = (input: string | null) => { - if (!input) return '기한없음'; - const regex = /(\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}):\d{2} ~ (\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}):\d{2}/; - - const formatted = input.replace( - regex, - (_, year1, month1, day1, time1, year2, month2, day2, time2) => - `${year1.slice(2)}.${month1}.${day1} ${time1} ~ ${year2.slice(2)}.${month2}.${day2} ${time2}` - ); - - return formatted; -}; - -export const calculateRemainingTimeByRange = (range: string | null) => { - if (!range) return '정보없음'; - const [, endDateStr] = range.split(' ~ '); - const endDate = new Date(endDateStr); - - const now = new Date(); - - const timeDiff = endDate.getTime() - now.getTime(); - const daysLeft = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hoursLeft = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutesLeft = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); - - if (daysLeft < 0 || hoursLeft < 0 || minutesLeft < 0) return `마감`; - return `${daysLeft === 0 ? '' : daysLeft + '일'} ${hoursLeft === 0 ? '' : hoursLeft + '시간'} ${minutesLeft}분 남음`; -}; - -export const calculateRemainingTime = (endTime: string | null) => { - if (!endTime) return '정보없음'; - const endDate = new Date(endTime); - - const now = new Date(); - - const timeDiff = endDate.getTime() - now.getTime(); - const daysLeft = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hoursLeft = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutesLeft = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); - - if (daysLeft < 0 || hoursLeft < 0 || minutesLeft < 0) return `마감`; - return `${daysLeft === 0 ? '' : daysLeft + '일'} ${hoursLeft === 0 ? '' : hoursLeft + '시간'} ${minutesLeft}분 남음`; -}; - -export const removeSquareBrackets = (str: string) => { - return str.replace(/\[[^\]]*\]/g, ''); -}; - -export const TimeAgo = (givenTimestamp: number) => { - const now = Date.now(); - const diffMs = now - givenTimestamp; - - // 각 단위별 밀리초 - const msPerMinute = 1000 * 60; - const msPerHour = msPerMinute * 60; - const msPerDay = msPerHour * 24; - - // 차이를 일, 시간, 분 단위로 계산 - const days = Math.floor(diffMs / msPerDay); - const hours = Math.floor((diffMs % msPerDay) / msPerHour); - const minutes = Math.floor((diffMs % msPerHour) / msPerMinute); - if (days === 0 && hours === 0 && minutes === 0) return '지금 막'; - return `${days !== 0 ? days + '일 ' : ''}${hours !== 0 ? hours + '시간 ' : ''}${minutes !== 0 ? minutes + '분 전' : '전'}`; -}; +// 하위 호환: 기존 import 경로 유지 +export { isAttended, isAbsent } from './attendance'; +export { + isCurrentDateInRange, + isCurrentDateByDate, + isWithinSevenDays, + extractEndDate, + calculateDueDate, + calculateRemainingTime, + timeAgo as TimeAgo, +} from './dateUtils'; +export { removeSquareBrackets, formatDateString } from './stringUtils'; diff --git a/src/mocks/loadMockData.ts b/src/mocks/loadMockData.ts new file mode 100644 index 0000000..f05ac61 --- /dev/null +++ b/src/mocks/loadMockData.ts @@ -0,0 +1,12 @@ +import { Vod, Assign, Quiz } from '@/types'; + +export type MockData = { + vods: Vod[]; + assigns: Assign[]; + quizzes: Quiz[]; +}; + +export async function loadMockData(): Promise { + const { mockVods, mockAssigns, mockQuizes } = await import('./mockData'); + return { vods: mockVods, assigns: mockAssigns, quizzes: mockQuizes }; +} diff --git a/src/mocks/mockData.ts b/src/mocks/mockData.ts index 16253f3..a5476bd 100644 --- a/src/mocks/mockData.ts +++ b/src/mocks/mockData.ts @@ -1,4 +1,4 @@ -import { CourseBase, Vod, Assign, Quiz } from '@/content/types'; +import { CourseBase, Vod, Assign, Quiz } from '@/types'; function offsetDate(days: number, hours = 0): string { const d = new Date(); diff --git a/src/option/App.tsx b/src/option/App.tsx index ebb41cc..b123d3d 100644 --- a/src/option/App.tsx +++ b/src/option/App.tsx @@ -3,10 +3,10 @@ import { AnimatePresence, motion } from 'framer-motion'; import Sidebar from './Sidebar'; import { Toaster } from '@/components/ui/toaster'; -import VodPage from 'src/pages/VodPage'; -import AssignmentPage from 'src/pages/AssignmentPage'; -import DashboardPage from '@/pages/DashboardPage'; -import QuizPage from '@/pages/QuizPage'; +import VodPage from '@/option/pages/VodPage'; +import AssignmentPage from '@/option/pages/AssignmentPage'; +import DashboardPage from '@/option/pages/DashboardPage'; +import QuizPage from '@/option/pages/QuizPage'; import Header from './Header'; import Labo from './Labo'; import ColorSetting from './ColorSetting'; diff --git a/src/option/ColorSetting.tsx b/src/option/ColorSetting.tsx index 5fdab51..094bdae 100644 --- a/src/option/ColorSetting.tsx +++ b/src/option/ColorSetting.tsx @@ -7,7 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { Input } from '@/components/ui/input'; import { toast } from '@/hooks/use-toast'; import { loadDataFromStorage, saveDataToStorage } from '@/lib/storage'; -import type { CourseBase } from '@/content/types'; +import type { CourseBase } from '@/types'; import { HexColorPicker } from 'react-colorful'; // Update the CourseColorSetting type to include opacity diff --git a/src/option/SummaryCard.tsx b/src/option/SummaryCard.tsx index 2162a84..732e7f8 100644 --- a/src/option/SummaryCard.tsx +++ b/src/option/SummaryCard.tsx @@ -1,6 +1,6 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; -import useCardData from '@/hooks/useCardData'; +import useCardData, { CardData } from '@/hooks/useCardData'; import { Zap, Video, NotebookTextIcon } from 'lucide-react'; import { ReactNode } from 'react'; import clsx from 'clsx'; @@ -9,7 +9,7 @@ import { Link } from 'react-router-dom'; interface CardItemProps { title: string; icon: ReactNode; - data: { done: number; total: number }[]; + data: CardData; color: string; link: string; } @@ -23,13 +23,12 @@ const colorMap: Record = { }; export default function SummaryCard() { - const { vodSummary, assignSummary, quizSummary } = useCardData(); + const { vod, assign, quiz } = useCardData(); const CardItem = ({ title, icon, data, color, link }: CardItemProps) => { - const done = data.length > 0 ? data[0].done : 0; - const total = data.length > 0 ? data[0].total : 1; - const percentage = Math.round((done / total) * 100); - const bgColorClass = colorMap[color] || 'bg-gray-500'; // 기본값 설정 + const { done, total } = data; + const percentage = total > 0 ? Math.round((done / total) * 100) : 0; + const bgColorClass = colorMap[color] || 'bg-gray-500'; return ( @@ -40,9 +39,9 @@ export default function SummaryCard() { {title.includes('퀴즈') ? ( -
{data.length > 0 ? `${total} 개` : '0 개'}
+
{total > 0 ? `${total} 개` : '0 개'}
) : ( -
{data.length > 0 ? `${done} / ${total}` : '0 개'}
+
{total > 0 ? `${done} / ${total}` : '0 개'}
)} {title.includes('퀴즈') ? (
@@ -50,7 +49,7 @@ export default function SummaryCard() { )}

- {title.includes('퀴즈') ? '직접 확인' : isNaN(percentage) ? 0 + '% 완료' : percentage + '% 완료'} + {title.includes('퀴즈') ? '직접 확인' : percentage + '% 완료'}

@@ -63,21 +62,21 @@ export default function SummaryCard() { } - data={vodSummary} + data={vod} color="blue" link={'vod'} /> } - data={assignSummary} + data={assign} color="violet" link={'assignment'} /> } - data={quizSummary} + data={quiz} color="amber" link={'quiz'} /> diff --git a/src/option/calendar.tsx b/src/option/calendar.tsx index 02513b2..ad5c0a5 100644 --- a/src/option/calendar.tsx +++ b/src/option/calendar.tsx @@ -25,7 +25,7 @@ import { getCalendarEvents, convertCalendarEventsToGoogleEvents, GoogleCalendarEvent, -} from '@/lib/calendarUtils'; +} from '@/option/lib/calendarUtils'; import { toast } from '@/hooks/use-toast'; import { loadDataFromStorage } from '@/lib/storage'; import GoogleCalendar from '@/assets/calendar.png'; diff --git a/src/option/components/AssignCard.tsx b/src/option/components/AssignCard.tsx index a20acec..f18169d 100644 --- a/src/option/components/AssignCard.tsx +++ b/src/option/components/AssignCard.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Assign } from '@/content/types'; +import { Assign } from '@/types'; import { calculateDueDate, calculateRemainingTime } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; diff --git a/src/option/components/AssignContent.tsx b/src/option/components/AssignContent.tsx index 2c741c0..db7463b 100644 --- a/src/option/components/AssignContent.tsx +++ b/src/option/components/AssignContent.tsx @@ -1,6 +1,6 @@ import { Card, CardContent } from '@/components/ui/card'; import { useEffect, useState } from 'react'; -import { Assign } from '@/content/types'; +import { Assign } from '@/types'; import { loadDataFromStorage } from '@/lib/storage'; import AssignCard from './AssignCard'; import { ScrollArea } from '@/components/ui/scroll-area'; diff --git a/src/option/components/CourseDetailModal.tsx b/src/option/components/CourseDetailModal.tsx index fdcb39a..53e0700 100644 --- a/src/option/components/CourseDetailModal.tsx +++ b/src/option/components/CourseDetailModal.tsx @@ -1,10 +1,10 @@ import ReactDOM from 'react-dom'; import { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; -import type { Vod } from '@/content/types'; +import type { Vod } from '@/types'; import { Card, CardContent } from '@/components/ui/card'; import { BadgeCheck, Siren, TriangleAlert, Video, X } from 'lucide-react'; -import { calculateRemainingTimeByRange, calculateTimeDifference, formatDateString } from '@/lib/utils'; +import { calculateDueDate, calculateRemainingTime, extractEndDate, formatDateString, isAttended } from '@/lib/utils'; import { ScrollArea } from '@/components/ui/scroll-area'; import type React from 'react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; @@ -43,7 +43,7 @@ const CourseDetailModal: React.FC = ({ vodList, onClose }: ModalProp }, 300); }; - const timeDifference = calculateTimeDifference(vodList[0].range); + const timeDifference = calculateDueDate(extractEndDate(vodList[0].range)); const modalContent = (
= ({ vodList, onClose }: ModalProp
{vodList.map((vod, index) => { - const isAttendance = vod.isAttendance.toLowerCase().trim() === 'o'; + const isAttendance = isAttended(vod.isAttendance); return ( = ({ vodList, onClose }: ModalProp paddingRight: '4px', }} > - {calculateRemainingTimeByRange(vodList[0].range)} + {calculateRemainingTime(extractEndDate(vodList[0].range))} diff --git a/src/option/components/CourseHideModal.tsx b/src/option/components/CourseHideModal.tsx index 8ca515a..ed4d69c 100644 --- a/src/option/components/CourseHideModal.tsx +++ b/src/option/components/CourseHideModal.tsx @@ -1,6 +1,6 @@ import ReactDOM from 'react-dom'; import { useEffect, useState } from 'react'; -import type { Vod } from '@/content/types'; +import type { Vod } from '@/types'; import type React from 'react'; import { Button } from '@/components/ui/button'; diff --git a/src/option/components/QuizCard.tsx b/src/option/components/QuizCard.tsx index 74d08c7..2cded30 100644 --- a/src/option/components/QuizCard.tsx +++ b/src/option/components/QuizCard.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Quiz } from '@/content/types'; +import { Quiz } from '@/types'; import { calculateDueDate, calculateRemainingTime } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; diff --git a/src/option/components/QuizContent.tsx b/src/option/components/QuizContent.tsx index 7724c9d..0c2f017 100644 --- a/src/option/components/QuizContent.tsx +++ b/src/option/components/QuizContent.tsx @@ -1,6 +1,6 @@ import { Card, CardContent } from '@/components/ui/card'; import { useEffect, useState } from 'react'; -import { Quiz } from '@/content/types'; +import { Quiz } from '@/types'; import { loadDataFromStorage } from '@/lib/storage'; import QuizCard from './QuizCard'; import { ScrollArea } from '@radix-ui/react-scroll-area'; diff --git a/src/option/components/Sidebar.tsx b/src/option/components/Sidebar.tsx index ad90465..6773f42 100644 --- a/src/option/components/Sidebar.tsx +++ b/src/option/components/Sidebar.tsx @@ -1,8 +1,8 @@ import { SidebarHeader } from './SidebarHeader'; -import { TYPES } from '@/content/types'; +import { TAB_LABEL } from './data'; import { data } from './data'; -export function Sidebar({ activeTab, setActiveTab }: { activeTab: TYPES; setActiveTab: (type: TYPES) => void }) { +export function Sidebar({ activeTab, setActiveTab }: { activeTab: TAB_LABEL; setActiveTab: (type: TAB_LABEL) => void }) { return (