From afd7bda269bcfaa6c2008d54a888adb3bd5687ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Tue, 27 Jan 2026 19:49:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EA=B2=AC=EC=A0=81=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EA=B0=9C=EC=84=A0,=20=EC=97=91=EC=85=80?= =?UTF-8?q?=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C,=20PDF=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 견적 시스템: - QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가 - DiscountModal: 할인율/할인금액 상호 계산 모달 - QuoteTransactionModal: 거래명세서 미리보기 모달 - LocationDetailPanel, LocationListPanel 개선 템플릿 기능: - UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드) - DocumentViewer: PDF 생성 기능 개선 신규 API: - /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트 UI 개선: - 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%) - 각종 리스트 컴포넌트 정렬/필터링 개선 패키지 추가: - html2canvas, jspdf, puppeteer, dom-to-image-more Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 1174 ++++++++++++++++- package.json | 4 + src/app/api/pdf/generate/route.ts | 180 +++ .../document-system/presets/index.ts | 12 +- src/components/document-system/types.ts | 17 + .../document-system/viewer/DocumentViewer.tsx | 121 +- .../hr/AttendanceManagement/index.tsx | 36 +- .../DynamicItemForm/components/FormHeader.tsx | 56 +- .../items/DynamicItemForm/index.tsx | 33 +- src/components/items/ItemDetailClient.tsx | 59 +- src/components/items/ItemListClient.tsx | 129 +- .../material/StockStatus/StockStatusList.tsx | 71 +- .../production/WorkResults/WorkResultList.tsx | 83 +- src/components/quotes/DiscountModal.tsx | 231 ++++ src/components/quotes/LocationDetailPanel.tsx | 504 +++---- src/components/quotes/LocationListPanel.tsx | 243 ++-- src/components/quotes/QuoteFooterBar.tsx | 94 +- .../quotes/QuoteManagementClient.tsx | 146 +- src/components/quotes/QuotePreviewContent.tsx | 477 ++++--- src/components/quotes/QuotePreviewModal.tsx | 95 +- src/components/quotes/QuoteRegistrationV2.tsx | 214 ++- .../quotes/QuoteTransactionModal.tsx | 221 ++++ .../templates/UniversalListPage/index.tsx | 173 ++- .../templates/UniversalListPage/types.ts | 44 + src/components/ui/account-number-input.tsx | 2 +- src/components/ui/business-number-input.tsx | 2 +- src/components/ui/card-number-input.tsx | 2 +- src/components/ui/command.tsx | 2 +- src/components/ui/currency-input.tsx | 2 +- src/components/ui/input.tsx | 2 +- src/components/ui/number-input.tsx | 2 +- src/components/ui/personal-number-input.tsx | 2 +- src/components/ui/phone-input.tsx | 2 +- src/components/ui/quantity-input.tsx | 2 +- src/components/ui/textarea.tsx | 2 +- 35 files changed, 3493 insertions(+), 946 deletions(-) create mode 100644 src/app/api/pdf/generate/route.ts create mode 100644 src/components/quotes/DiscountModal.tsx create mode 100644 src/components/quotes/QuoteTransactionModal.tsx diff --git a/package-lock.json b/package-lock.json index 8d685752..8285e15f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,10 +40,14 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dom-to-image-more": "^3.7.2", + "html2canvas": "^1.4.1", "immer": "^11.0.1", + "jspdf": "^4.0.0", "lucide-react": "^0.552.0", "next": "^15.5.9", "next-intl": "^4.4.0", + "puppeteer": "^24.36.0", "react": "^19.2.3", "react-day-picker": "^9.11.1", "react-dom": "^19.2.3", @@ -82,6 +86,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@capacitor/app": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.0.0.tgz", @@ -1498,6 +1534,27 @@ "node": ">=18" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.1.tgz", + "integrity": "sha512-YmhAxs7XPuxN0j7LJloHpfD1ylhDuFmmwMvfy/+6nBSrETT2ycL53LrhgPtR+f+GcPSybQVuQ5inWWu5MrWCpA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3712,6 +3769,12 @@ "url": "https://github.com/sponsors/ueberdosis" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3840,12 +3903,25 @@ "version": "20.19.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -3864,12 +3940,29 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", @@ -4427,6 +4520,15 @@ "node": ">=0.8" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4444,11 +4546,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4648,6 +4758,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4701,6 +4823,20 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4708,6 +4844,115 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4731,6 +4976,15 @@ "node": ">=8" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4785,7 +5039,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4811,6 +5064,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -4841,6 +5114,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chromium-bidi": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.0.1.tgz", + "integrity": "sha512-c+RLxH0Vg2x2syS9wPw378oJgiJNXtYXUvnVAldUlt5uaHekn0CCU7gPksNgHjrH1qFhmjVXQj4esvuthuC7OQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4859,6 +5154,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4897,7 +5206,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4910,7 +5218,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -4920,6 +5227,44 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -4953,6 +5298,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -5087,6 +5441,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5161,7 +5524,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5230,6 +5592,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5246,6 +5622,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devtools-protocol": { + "version": "0.0.1551306", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1551306.tgz", + "integrity": "sha512-CFx8QdSim8iIv+2ZcEOclBKTQY6BI1IEDa7Tm9YkwAXzEWFndTEzpTo5jAUhSnq24IC7xaDw0wvGcm96+Y3PEg==", + "license": "BSD-3-Clause" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5259,6 +5641,22 @@ "node": ">=0.10.0" } }, + "node_modules/dom-to-image-more": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.7.2.tgz", + "integrity": "sha512-uQf+pHv6eQhgfI8t2bFuinV0KsPyT8TZgCLwcSU8uBVgN9v6leb0mMpvp6HQAlAcplP3NCcGjxbdqef6pTzvmw==", + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5281,6 +5679,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -5307,6 +5714,24 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -5494,6 +5919,15 @@ "benchmarks" ] }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5506,6 +5940,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.39.2", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", @@ -5885,6 +6340,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -5915,7 +6383,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -5925,7 +6392,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -5937,6 +6403,35 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5953,6 +6448,12 @@ "node": ">=6.0.0" } }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -5997,6 +6498,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -6007,6 +6519,21 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6161,6 +6688,15 @@ "node": ">= 0.4" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6209,6 +6745,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -6240,6 +6791,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6397,6 +6962,45 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6421,7 +7025,6 @@ "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": { "parent-module": "^1.0.0", @@ -6480,6 +7083,21 @@ "tslib": "^2.8.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6498,6 +7116,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -6650,6 +7274,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -6925,14 +7558,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "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": { "argparse": "^2.0.1" @@ -6948,6 +7579,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6975,6 +7612,23 @@ "json5": "lib/cli.js" } }, + "node_modules/jspdf": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7296,6 +7950,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -7347,6 +8007,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/lucide-react": { "version": "0.552.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz", @@ -7445,11 +8114,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -7502,6 +8176,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next": { "version": "15.5.9", "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", @@ -7797,6 +8480,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7871,11 +8563,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7884,6 +8613,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7911,6 +8658,19 @@ "dev": true, "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8016,6 +8776,15 @@ "node": ">= 0.8.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8230,6 +8999,41 @@ "prosemirror-transform": "^1.1.0" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "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/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8249,6 +9053,45 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.36.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.36.0.tgz", + "integrity": "sha512-BD/VCyV/Uezvd6o7Fd1DmEJSxTzofAKplzDy6T9d4WbLTQ5A+06zY7VwO91ZlNU22vYE8sidVEsTpTrKc+EEnQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.11.1", + "chromium-bidi": "13.0.1", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1551306", + "puppeteer-core": "24.36.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.36.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.36.0.tgz", + "integrity": "sha512-P3Ou0MAFDCQ0dK1d9F9+8jTrg6JvXjUacgG0YkJQP4kbEnUOGokSDEMmMId5ZhXD5HwsHM202E9VwEpEjWfwxg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.11.1", + "chromium-bidi": "13.0.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1551306", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8270,6 +9113,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -8505,6 +9358,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -8526,6 +9386,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -8557,7 +9426,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8584,6 +9452,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rope-sequence": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", @@ -8679,7 +9557,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8881,6 +9758,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -8891,6 +9806,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8919,6 +9844,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8933,6 +9868,37 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -9046,6 +10012,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -9118,6 +10096,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -9149,6 +10137,49 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -9338,6 +10369,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -9381,7 +10418,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -9495,6 +10532,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -9536,6 +10582,12 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", + "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9669,6 +10721,50 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", @@ -9690,6 +10786,52 @@ "node": ">=0.8" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b5c62fef..6f873c77 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,14 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dom-to-image-more": "^3.7.2", + "html2canvas": "^1.4.1", "immer": "^11.0.1", + "jspdf": "^4.0.0", "lucide-react": "^0.552.0", "next": "^15.5.9", "next-intl": "^4.4.0", + "puppeteer": "^24.36.0", "react": "^19.2.3", "react-day-picker": "^9.11.1", "react-dom": "^19.2.3", diff --git a/src/app/api/pdf/generate/route.ts b/src/app/api/pdf/generate/route.ts new file mode 100644 index 00000000..a70abf4e --- /dev/null +++ b/src/app/api/pdf/generate/route.ts @@ -0,0 +1,180 @@ +import { NextRequest, NextResponse } from 'next/server'; +import puppeteer from 'puppeteer'; + +/** + * PDF 생성 API + * POST /api/pdf/generate + * + * Body: { + * html: string, + * styles: string, + * title?: string, + * orientation?: 'portrait' | 'landscape', + * documentNumber?: string, + * createdDate?: string, + * showHeaderFooter?: boolean + * } + * Response: PDF blob + */ +export async function POST(request: NextRequest) { + try { + const { + html, + styles = '', + title = '문서', + orientation = 'portrait', + documentNumber = '', + createdDate = '', + showHeaderFooter = true, + } = await request.json(); + + if (!html) { + return NextResponse.json( + { error: 'HTML content is required' }, + { status: 400 } + ); + } + + // Puppeteer 브라우저 실행 + const browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + ], + }); + + const page = await browser.newPage(); + + // 전체 HTML 문서 구성 (인라인 스타일 포함) + const fullHtml = ` + + + + + + ${title} + + + +
+ ${html} +
+ + + `; + + // 뷰포트 설정 (문서 전체가 보이도록 넓게) + await page.setViewport({ + width: 1200, + height: 1600, + deviceScaleFactor: 2, + }); + + // HTML 설정 + await page.setContent(fullHtml, { + waitUntil: 'networkidle0', + }); + + // 헤더 템플릿 (문서번호, 생성일) + const headerTemplate = showHeaderFooter + ? ` +
+
+ ${documentNumber ? `문서번호: ${documentNumber}` : ''} + ${createdDate ? `생성일: ${createdDate}` : ''} +
+
+ ` + : ''; + + // 푸터 템플릿 (라인 + 페이지번호) + const footerTemplate = showHeaderFooter + ? ` +
+
+ ${title} + Page / +
+
+ ` + : ''; + + // PDF 생성 (자동 스케일로 A4에 맞춤) + const pdfBuffer = await page.pdf({ + format: 'A4', + landscape: orientation === 'landscape', + printBackground: true, + preferCSSPageSize: false, + scale: 0.75, // 문서를 75%로 축소하여 A4에 맞춤 + displayHeaderFooter: showHeaderFooter, + headerTemplate: headerTemplate, + footerTemplate: footerTemplate, + margin: { + top: showHeaderFooter ? '20mm' : '10mm', + right: '10mm', + bottom: showHeaderFooter ? '20mm' : '10mm', + left: '10mm', + }, + }); + + // 브라우저 종료 + await browser.close(); + + // PDF 응답 + return new NextResponse(pdfBuffer, { + status: 200, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(title)}.pdf"`, + }, + }); + } catch (error) { + console.error('PDF 생성 오류:', error); + return NextResponse.json( + { error: 'PDF 생성 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} diff --git a/src/components/document-system/presets/index.ts b/src/components/document-system/presets/index.ts index cbd3db73..b0543307 100644 --- a/src/components/document-system/presets/index.ts +++ b/src/components/document-system/presets/index.ts @@ -16,7 +16,7 @@ export const DOCUMENT_PRESETS: Record = { print: true, download: true, }, - actions: ['print', 'download'], + actions: ['pdf', 'print', 'download'], }, // 건설 프로젝트용 (CRUD) @@ -27,7 +27,7 @@ export const DOCUMENT_PRESETS: Record = { print: true, download: false, }, - actions: ['edit', 'delete', 'print'], + actions: ['edit', 'delete', 'pdf', 'print'], }, // 결재 문서용 (기본) @@ -38,7 +38,7 @@ export const DOCUMENT_PRESETS: Record = { print: true, download: false, }, - actions: ['edit', 'submit', 'print'], + actions: ['edit', 'submit', 'pdf', 'print'], }, // 결재 문서용 - 기안함 모드 (임시저장 상태: 복제, 상신, 인쇄) @@ -49,7 +49,7 @@ export const DOCUMENT_PRESETS: Record = { print: true, download: false, }, - actions: ['copy', 'submit', 'print'], + actions: ['copy', 'submit', 'pdf', 'print'], }, // 결재 문서용 - 결재함 모드 (수정, 반려, 승인, 인쇄) @@ -60,7 +60,7 @@ export const DOCUMENT_PRESETS: Record = { print: true, download: false, }, - actions: ['edit', 'reject', 'approve', 'print'], + actions: ['edit', 'reject', 'approve', 'pdf', 'print'], }, // 조회 전용 @@ -71,7 +71,7 @@ export const DOCUMENT_PRESETS: Record = { print: true, download: false, }, - actions: ['print'], + actions: ['pdf', 'print'], }, // 견적서/문서 전송용 (PDF, 이메일, 팩스, 카카오톡, 인쇄) diff --git a/src/components/document-system/types.ts b/src/components/document-system/types.ts index 92c39667..ef5cf358 100644 --- a/src/components/document-system/types.ts +++ b/src/components/document-system/types.ts @@ -144,6 +144,20 @@ export interface CustomBlock { // DocumentViewer Props // ============================================================ +// ============================================================ +// PDF Meta Types +// ============================================================ + +export interface PdfMeta { + documentNumber?: string; + createdDate?: string; + showHeaderFooter?: boolean; +} + +// ============================================================ +// DocumentViewer Props +// ============================================================ + export interface DocumentViewerProps { // Config 기반 (권장) config?: DocumentConfig; @@ -158,6 +172,9 @@ export interface DocumentViewerProps { // 데이터 data?: any; + // PDF 메타 정보 (헤더/푸터용) + pdfMeta?: PdfMeta; + // 액션 핸들러 onPrint?: () => void; onDownload?: () => void; diff --git a/src/components/document-system/viewer/DocumentViewer.tsx b/src/components/document-system/viewer/DocumentViewer.tsx index 2b8706e0..acb7b9ad 100644 --- a/src/components/document-system/viewer/DocumentViewer.tsx +++ b/src/components/document-system/viewer/DocumentViewer.tsx @@ -1,12 +1,13 @@ 'use client'; -import React, { useEffect, ReactNode } from 'react'; +import React, { useEffect, ReactNode, useCallback } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; +import { toast } from 'sonner'; import { printArea } from '@/lib/print-utils'; import { DocumentToolbar } from './DocumentToolbar'; import { DocumentContent } from './DocumentContent'; @@ -17,6 +18,7 @@ import { DocumentViewerProps, ActionType, DocumentFeatures, + PdfMeta, } from '../types'; /** @@ -59,6 +61,9 @@ export function DocumentViewer({ // 데이터 data, + // PDF 메타 정보 + pdfMeta, + // 액션 핸들러 onPrint: propOnPrint, onDownload, @@ -120,6 +125,118 @@ export function DocumentViewer({ } }; + // 인라인 스타일 적용된 HTML 복제 생성 + const cloneWithInlineStyles = (element: HTMLElement): HTMLElement => { + const clone = element.cloneNode(true) as HTMLElement; + + // 원본 요소들과 복제 요소들 매칭 + const originalElements = element.querySelectorAll('*'); + const clonedElements = clone.querySelectorAll('*'); + + // 루트 요소 스타일 적용 + const rootStyle = window.getComputedStyle(element); + applyStyles(clone, rootStyle); + + // 모든 하위 요소 스타일 적용 + originalElements.forEach((orig, index) => { + const cloned = clonedElements[index] as HTMLElement; + if (cloned) { + const computedStyle = window.getComputedStyle(orig); + applyStyles(cloned, computedStyle); + } + }); + + return clone; + }; + + // 계산된 스타일을 인라인으로 적용 + const applyStyles = (element: HTMLElement, computedStyle: CSSStyleDeclaration) => { + const importantStyles = [ + 'display', 'position', 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height', + 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'border', 'border-width', 'border-style', 'border-color', + 'border-top', 'border-right', 'border-bottom', 'border-left', + 'border-collapse', 'border-spacing', + 'background', 'background-color', + 'color', 'font-family', 'font-size', 'font-weight', 'font-style', + 'text-align', 'text-decoration', 'vertical-align', 'line-height', 'white-space', + 'flex', 'flex-direction', 'flex-wrap', 'justify-content', 'align-items', 'gap', + 'grid-template-columns', 'grid-template-rows', 'grid-gap', + 'table-layout', 'overflow', 'visibility', 'opacity', + ]; + + importantStyles.forEach((prop) => { + const value = computedStyle.getPropertyValue(prop); + if (value && value !== 'none' && value !== 'normal' && value !== 'auto') { + element.style.setProperty(prop, value); + } + }); + }; + + // PDF 핸들러 (서버사이드 Puppeteer로 생성) + const handlePdf = useCallback(async () => { + if (onPdf) { + onPdf(); + return; + } + + try { + toast.loading('PDF 생성 중...', { id: 'pdf-generating' }); + + // print-area 영역 찾기 + const printAreaEl = document.querySelector('.print-area') as HTMLElement; + if (!printAreaEl) { + toast.error('PDF 생성 실패: 문서 영역을 찾을 수 없습니다.', { id: 'pdf-generating' }); + return; + } + + // 인라인 스타일 적용된 HTML 복제 + const clonedElement = cloneWithInlineStyles(printAreaEl); + const html = clonedElement.outerHTML; + + // 서버 API 호출 + const response = await fetch('/api/pdf/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + html, + title, + orientation: 'portrait', + documentNumber: pdfMeta?.documentNumber || '', + createdDate: pdfMeta?.createdDate || new Date().toISOString().slice(0, 10), + showHeaderFooter: pdfMeta?.showHeaderFooter !== false, + }), + }); + + if (!response.ok) { + throw new Error('PDF 생성 API 오류'); + } + + // PDF Blob 다운로드 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // 파일명 생성 (날짜 포함) + const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + link.download = `${title}_${date}.pdf`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + toast.success('PDF가 생성되었습니다.', { id: 'pdf-generating' }); + } catch (error) { + console.error('PDF 생성 오류:', error); + toast.error('PDF 생성 중 오류가 발생했습니다.', { id: 'pdf-generating' }); + } + }, [onPdf, title, pdfMeta]); + // 콘텐츠 렌더링 const renderContent = (): ReactNode => { // 1. children이 있으면 children 사용 (정적 모드) @@ -179,7 +296,7 @@ export function DocumentViewer({ onApprove={onApprove} onReject={onReject} onCopy={onCopy} - onPdf={onPdf} + onPdf={handlePdf} onEmail={onEmail} onFax={onFax} onKakao={onKakao} diff --git a/src/components/hr/AttendanceManagement/index.tsx b/src/components/hr/AttendanceManagement/index.tsx index 12aa8ea2..ff7a6ad2 100644 --- a/src/components/hr/AttendanceManagement/index.tsx +++ b/src/components/hr/AttendanceManagement/index.tsx @@ -7,12 +7,12 @@ import { UserCheck, AlertCircle, Calendar, - Download, Plus, FileText, Edit, Search, } from 'lucide-react'; +import type { ExcelColumn } from '@/lib/utils/excel-download'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -351,10 +351,23 @@ export function AttendanceManagement() { router.push(`/ko/hr/documents/new?type=${data.reasonType}`); }, [router]); - const handleExcelDownload = useCallback(() => { - console.log('Excel download'); - // TODO: 엑셀 다운로드 기능 구현 - }, []); + // ===== 엑셀 컬럼 정의 ===== + const excelColumns: ExcelColumn[] = useMemo(() => [ + { header: '부서', key: 'department' }, + { header: '직책', key: 'position' }, + { header: '이름', key: 'employeeName' }, + { header: '직급', key: 'rank' }, + { header: '기준일', key: 'baseDate' }, + { header: '출근', key: 'checkIn', transform: (value) => value ? String(value).substring(0, 5) : '-' }, + { header: '퇴근', key: 'checkOut', transform: (value) => value ? String(value).substring(0, 5) : '-' }, + { header: '휴게', key: 'breakTime', transform: (value) => value ? String(value) : '-' }, + { header: '연장근무', key: 'overtimeHours', transform: (value) => value ? String(value) : '-' }, + { header: '상태', key: 'status', transform: (value) => ATTENDANCE_STATUS_LABELS[value as AttendanceStatus] }, + { header: '사유', key: 'reason', transform: (value) => { + const reason = value as AttendanceRecord['reason']; + return reason?.label || '-'; + }}, + ], []); const handleReasonClick = useCallback((record: AttendanceRecord) => { if (record.reason?.documentId) { @@ -458,12 +471,15 @@ export function AttendanceManagement() { searchPlaceholder: '이름, 부서 검색...', + // 엑셀 다운로드 설정 (클라이언트 사이드 필터링이므로 filteredData 사용) + excelDownload: { + columns: excelColumns, + filename: '근태현황', + sheetName: '근태', + }, + extraFilters: (
- - +
+

+ {mode === 'create' ? '품목 등록' : '품목 수정'} +

+

+ 품목 정보를 입력하세요 +

); diff --git a/src/components/items/DynamicItemForm/index.tsx b/src/components/items/DynamicItemForm/index.tsx index 6a200038..30a54ddd 100644 --- a/src/components/items/DynamicItemForm/index.tsx +++ b/src/components/items/DynamicItemForm/index.tsx @@ -9,6 +9,9 @@ import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { cn } from '@/lib/utils'; +import { useMenuStore } from '@/store/menuStore'; +import { Save, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { FormSectionSkeleton } from '@/components/ui/skeleton'; @@ -45,6 +48,7 @@ export default function DynamicItemForm({ onSubmit, }: DynamicItemFormProps) { const router = useRouter(); + const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); // 품목 유형 상태 (변경 가능) const [selectedItemType, setSelectedItemType] = useState(initialItemType || ''); @@ -658,17 +662,12 @@ export default function DynamicItemForm({ : []; return ( -
+ {/* Validation 에러 Alert */} {/* 헤더 */} - router.back()} - /> + {/* 기본 정보 - 목업과 동일한 레이아웃 (모든 필드 한 줄씩) */} @@ -1045,6 +1044,26 @@ export default function DynamicItemForm({ onCancel={handleCancelDuplicate} onGoToEdit={handleGoToEditDuplicate} /> + + {/* 하단 액션 버튼 (sticky) */} +
+ + +
); } diff --git a/src/components/items/ItemDetailClient.tsx b/src/components/items/ItemDetailClient.tsx index 5211303c..7f6c52ba 100644 --- a/src/components/items/ItemDetailClient.tsx +++ b/src/components/items/ItemDetailClient.tsx @@ -29,6 +29,7 @@ import { import { ArrowLeft, Edit, Package, FileImage, Download, FileText, Check, Calendar } from 'lucide-react'; import { downloadFileById } from '@/lib/utils/fileDownload'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { useMenuStore } from '@/store/menuStore'; interface ItemDetailClientProps { item: ItemMaster; @@ -94,39 +95,20 @@ function getStorageUrl(path: string | undefined): string | null { export default function ItemDetailClient({ item }: ItemDetailClientProps) { const router = useRouter(); + const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); return ( -
+
{/* 헤더 */} -
-
-
- -
-
-

품목 상세 정보

-

- 등록된 품목 정보를 확인합니다 -

-
+
+
+
- -
- - +
+

품목 상세 정보

+

+ 등록된 품목 정보를 확인합니다 +

@@ -632,6 +614,25 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) { )} + + {/* 하단 액션 버튼 (sticky) */} +
+ + +
); } \ No newline at end of file diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index b9726559..ccd136df 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -17,8 +17,8 @@ import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; -import { Search, Plus, Edit, Trash2, Package, Download, FileDown, Upload } from 'lucide-react'; -import { downloadExcel, downloadSelectedExcel, downloadExcelTemplate, parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download'; +import { Search, Plus, Edit, Trash2, Package, FileDown, Upload } from 'lucide-react'; +import { downloadExcelTemplate, parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download'; import { useItemList } from '@/hooks/useItemList'; import { handleApiError } from '@/lib/api/error-handler'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -253,34 +253,29 @@ export default function ItemListClient() { { header: '활성상태', key: 'isActive', width: 10, transform: (v) => v ? '활성' : '비활성' }, ]; - // 전체 엑셀 다운로드 - const handleExcelDownload = () => { - if (items.length === 0) { - alert('다운로드할 데이터가 없습니다.'); - return; - } - downloadExcel({ - data: items, - columns: excelColumns, - filename: '품목목록', - sheetName: '품목', - }); - }; - - // 선택 항목 엑셀 다운로드 - const handleSelectedExcelDownload = (selectedIds: string[]) => { - if (selectedIds.length === 0) { - alert('선택된 항목이 없습니다.'); - return; - } - downloadSelectedExcel({ - data: items, - columns: excelColumns, - selectedIds, - idField: 'id', - filename: '품목목록_선택', - sheetName: '품목', - }); + // API 응답을 ItemMaster 타입으로 변환 (엑셀 다운로드용) + const mapItemResponse = (result: unknown): ItemMaster[] => { + const data = result as { data?: { data?: Record[] }; }; + const rawItems = data.data?.data ?? []; + return rawItems.map((item: Record) => ({ + id: String(item.id ?? item.item_id ?? ''), + itemCode: (item.code ?? item.item_code ?? '') as string, + itemName: (item.name ?? item.item_name ?? '') as string, + itemType: (item.type_code ?? item.item_type ?? '') as ItemMaster['itemType'], + partType: item.part_type as ItemMaster['partType'], + unit: (item.unit ?? '') as string, + specification: (item.specification ?? '') as string, + isActive: item.is_active != null ? Boolean(item.is_active) : !item.deleted_at, + category1: (item.category1 ?? '') as string, + category2: (item.category2 ?? '') as string, + category3: (item.category3 ?? '') as string, + salesPrice: (item.sales_price ?? 0) as number, + purchasePrice: (item.purchase_price ?? 0) as number, + currentRevision: (item.current_revision ?? 0) as number, + isFinal: Boolean(item.is_final ?? false), + createdAt: (item.created_at ?? '') as string, + updatedAt: (item.updated_at ?? '') as string, + })); }; // 업로드용 템플릿 컬럼 정의 @@ -416,55 +411,31 @@ export default function ItemListClient() { icon: Plus, }, - // 헤더 액션 (엑셀 다운로드) - headerActions: ({ selectedItems }) => ( -
- {/* 양식 다운로드 버튼 - 추후 활성화 - - */} - {/* 양식 업로드 버튼 - 추후 활성화 - - */} - {/* 엑셀 데이터 다운로드 버튼 */} - {selectedItems.size > 0 ? ( - - ) : ( - - )} -
- ), + // 엑셀 다운로드 설정 (공통 기능) + excelDownload: { + columns: excelColumns, + filename: '품목목록', + sheetName: '품목', + fetchAllUrl: '/api/proxy/items', + fetchAllParams: ({ activeTab }): Record => { + // 현재 선택된 타입 필터 적용 + if (activeTab && activeTab !== 'all') { + return { type: activeTab }; + } + return { group_id: '1' }; // 품목관리 그룹 + }, + mapResponse: mapItemResponse, + }, + + // 헤더 액션 (양식 다운로드/업로드 - 추후 활성화) + // headerActions: () => ( + //
+ // + //
+ // ), // API 액션 (일괄 삭제 포함) actions: { diff --git a/src/components/material/StockStatus/StockStatusList.tsx b/src/components/material/StockStatus/StockStatusList.tsx index b9038c53..b9d1ef9a 100644 --- a/src/components/material/StockStatus/StockStatusList.tsx +++ b/src/components/material/StockStatus/StockStatusList.tsx @@ -17,9 +17,9 @@ import { CheckCircle2, Clock, AlertCircle, - Download, Eye, } from 'lucide-react'; +import type { ExcelColumn } from '@/lib/utils/excel-download'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { TableCell, TableRow } from '@/components/ui/table'; @@ -37,7 +37,7 @@ import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { getStocks, getStockStats, getStockStatsByType } from './actions'; import { ITEM_TYPE_LABELS, ITEM_TYPE_STYLES, STOCK_STATUS_LABELS } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import type { StockItem, StockStats, ItemType } from './types'; +import type { StockItem, StockStats, ItemType, StockStatusType } from './types'; // 페이지당 항목 수 const ITEMS_PER_PAGE = 20; @@ -80,10 +80,42 @@ export function StockStatusList() { [router] ); - // ===== 엑셀 다운로드 ===== - const handleExcelDownload = useCallback(() => { - console.log('엑셀 다운로드'); - // TODO: 엑셀 다운로드 기능 구현 + // ===== 엑셀 컬럼 정의 ===== + const excelColumns: ExcelColumn[] = useMemo(() => [ + { header: '품목코드', key: 'itemCode' }, + { header: '품목명', key: 'itemName' }, + { header: '품목유형', key: 'itemType', transform: (value) => ITEM_TYPE_LABELS[value as ItemType] || String(value) }, + { header: '단위', key: 'unit' }, + { header: '재고량', key: 'stockQty' }, + { header: '안전재고', key: 'safetyStock' }, + { header: 'LOT수', key: 'lotCount' }, + { header: 'LOT경과일', key: 'lotDaysElapsed' }, + { header: '상태', key: 'status', transform: (value) => value ? STOCK_STATUS_LABELS[value as StockStatusType] : '-' }, + { header: '위치', key: 'location' }, + ], []); + + // ===== API 응답 매핑 함수 ===== + const mapStockResponse = useCallback((result: unknown): StockItem[] => { + const data = result as { data?: { data?: Record[] } }; + const rawItems = data.data?.data ?? []; + return rawItems.map((item: Record) => { + const stock = item.stock as Record | null; + const hasStock = !!stock; + return { + id: String(item.id ?? ''), + itemCode: (item.code ?? '') as string, + itemName: (item.name ?? '') as string, + itemType: (item.item_type ?? 'RM') as ItemType, + unit: (item.unit ?? 'EA') as string, + stockQty: hasStock ? (parseFloat(String(stock?.stock_qty)) || 0) : 0, + safetyStock: hasStock ? (parseFloat(String(stock?.safety_stock)) || 0) : 0, + lotCount: hasStock ? (Number(stock?.lot_count) || 0) : 0, + lotDaysElapsed: hasStock ? (Number(stock?.days_elapsed) || 0) : 0, + status: hasStock ? (stock?.status as StockStatusType | null) : null, + location: hasStock ? ((stock?.location as string) || '-') : '-', + hasStock, + }; + }); }, []); // ===== 통계 카드 ===== @@ -236,13 +268,24 @@ export function StockStatusList() { // 테이블 푸터 tableFooter, - // 헤더 액션 (엑셀 다운로드) - headerActions: () => ( - - ), + // 엑셀 다운로드 설정 + excelDownload: { + columns: excelColumns, + filename: '재고현황', + sheetName: '재고', + fetchAllUrl: '/api/proxy/stocks', + fetchAllParams: ({ activeTab, searchValue }) => { + const params: Record = {}; + if (activeTab && activeTab !== 'all') { + params.item_type = activeTab; + } + if (searchValue) { + params.search = searchValue; + } + return params; + }, + mapResponse: mapStockResponse, + }, // 테이블 행 렌더링 renderTableRow: ( @@ -363,7 +406,7 @@ export function StockStatusList() { ); }, }), - [tabs, stats, tableFooter, handleRowClick, handleExcelDownload] + [tabs, stats, tableFooter, handleRowClick, excelColumns, mapStockResponse] ); return ; diff --git a/src/components/production/WorkResults/WorkResultList.tsx b/src/components/production/WorkResults/WorkResultList.tsx index 6924cc60..0b32d830 100644 --- a/src/components/production/WorkResults/WorkResultList.tsx +++ b/src/components/production/WorkResults/WorkResultList.tsx @@ -17,10 +17,9 @@ import { CheckCircle2, XCircle, Percent, - Download, CheckCircle, } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import type { ExcelColumn } from '@/lib/utils/excel-download'; import { Badge } from '@/components/ui/badge'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -73,10 +72,58 @@ export function WorkResultList() { // TODO: 상세 보기 기능 구현 }, []); - // ===== 엑셀 다운로드 ===== - const handleExcelDownload = useCallback(() => { - console.log('엑셀 다운로드'); - // TODO: 엑셀 다운로드 기능 구현 + // ===== 엑셀 컬럼 정의 ===== + const excelColumns: ExcelColumn[] = useMemo(() => [ + { header: '로트번호', key: 'lotNo' }, + { header: '작업일', key: 'workDate' }, + { header: '작업지시번호', key: 'workOrderNo' }, + { header: '공정', key: 'processName' }, + { header: '품목명', key: 'productName' }, + { header: '규격', key: 'specification' }, + { header: '생산수량', key: 'productionQty' }, + { header: '양품수량', key: 'goodQty' }, + { header: '불량수량', key: 'defectQty' }, + { header: '불량률(%)', key: 'defectRate' }, + { header: '검사', key: 'inspection', transform: (value): string => value ? '완료' : '대기' }, + { header: '포장', key: 'packaging', transform: (value): string => value ? '완료' : '대기' }, + { header: '작업자', key: 'workerName', transform: (value): string => (value as string) || '-' }, + ], []); + + // ===== API 응답 매핑 함수 ===== + const mapWorkResultResponse = useCallback((result: unknown): WorkResult[] => { + const data = result as { data?: { data?: Record[] } }; + const rawItems = data.data?.data ?? []; + return rawItems.map((api: Record) => { + const options = api.options as { result?: Record } | null; + const apiResult = options?.result; + const goodQty = Number(apiResult?.good_qty ?? 0); + const defectQty = Number(apiResult?.defect_qty ?? 0); + const workOrder = api.work_order as Record | undefined; + const process = workOrder?.process as Record | undefined; + + return { + id: String(api.id ?? ''), + workOrderId: Number(api.work_order_id ?? 0), + lotNo: (apiResult?.lot_no as string) || '-', + workDate: (apiResult?.completed_at as string) || (api.updated_at as string) || '', + workOrderNo: (workOrder?.work_order_no as string) || '-', + projectName: (workOrder?.project_name as string) || '-', + processName: (process?.process_name as string) || '-', + processCode: (process?.process_code as string) || '-', + productName: (api.item_name as string) || '', + specification: (api.specification as string) || '-', + quantity: parseFloat(String(api.quantity)) || 0, + productionQty: goodQty + defectQty, + goodQty, + defectQty, + defectRate: Number(apiResult?.defect_rate ?? 0), + inspection: Boolean(apiResult?.is_inspected), + packaging: Boolean(apiResult?.is_packaged), + workerId: (apiResult?.worker_id as number) ?? null, + workerName: (api.worker_name as string) ?? null, + memo: (apiResult?.memo as string) ?? null, + }; + }); }, []); // ===== 통계 카드 ===== @@ -182,13 +229,21 @@ export function WorkResultList() { // 통계 카드 stats, - // 헤더 액션 (엑셀 다운로드) - headerActions: () => ( - - ), + // 엑셀 다운로드 설정 + excelDownload: { + columns: excelColumns, + filename: '작업실적', + sheetName: '작업실적', + fetchAllUrl: '/api/proxy/work-results', + fetchAllParams: ({ searchValue }) => { + const params: Record = {}; + if (searchValue) { + params.q = searchValue; + } + return params; + }, + mapResponse: mapWorkResultResponse, + }, // 테이블 행 렌더링 renderTableRow: ( @@ -305,7 +360,7 @@ export function WorkResultList() { ); }, }), - [stats, handleView, handleExcelDownload] + [stats, handleView, excelColumns, mapWorkResultResponse] ); return ; diff --git a/src/components/quotes/DiscountModal.tsx b/src/components/quotes/DiscountModal.tsx new file mode 100644 index 00000000..4b618025 --- /dev/null +++ b/src/components/quotes/DiscountModal.tsx @@ -0,0 +1,231 @@ +/** + * 할인하기 모달 + * + * - 공급가액 표시 (읽기 전용) + * - 할인율 입력 (% 단위, 소수점 첫째자리까지) + * - 할인금액 입력 (원 단위) + * - 할인 후 공급가액 자동 계산 + * - 할인율 ↔ 할인금액 상호 계산 + */ + +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Percent } from "lucide-react"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "../ui/dialog"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; + +// ============================================================================= +// Props +// ============================================================================= + +interface DiscountModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** 공급가액 (할인 전 금액) */ + supplyAmount: number; + /** 기존 할인율 (%) */ + initialDiscountRate?: number; + /** 기존 할인금액 (원) */ + initialDiscountAmount?: number; + /** 적용 콜백 */ + onApply: (discountRate: number, discountAmount: number) => void; +} + +// ============================================================================= +// 컴포넌트 +// ============================================================================= + +export function DiscountModal({ + open, + onOpenChange, + supplyAmount, + initialDiscountRate = 0, + initialDiscountAmount = 0, + onApply, +}: DiscountModalProps) { + // --------------------------------------------------------------------------- + // 상태 + // --------------------------------------------------------------------------- + + const [discountRate, setDiscountRate] = useState(""); + const [discountAmount, setDiscountAmount] = useState(""); + + // --------------------------------------------------------------------------- + // 초기화 + // --------------------------------------------------------------------------- + + useEffect(() => { + if (open) { + // 모달이 열릴 때 초기값 설정 + if (initialDiscountRate > 0) { + setDiscountRate(initialDiscountRate.toString()); + setDiscountAmount(initialDiscountAmount.toString()); + } else if (initialDiscountAmount > 0) { + setDiscountAmount(initialDiscountAmount.toString()); + const rate = supplyAmount > 0 ? (initialDiscountAmount / supplyAmount) * 100 : 0; + setDiscountRate(rate.toFixed(1)); + } else { + setDiscountRate(""); + setDiscountAmount(""); + } + } + }, [open, initialDiscountRate, initialDiscountAmount, supplyAmount]); + + // --------------------------------------------------------------------------- + // 핸들러 + // --------------------------------------------------------------------------- + + // 할인율 변경 → 할인금액 자동 계산 + const handleDiscountRateChange = useCallback( + (value: string) => { + // 숫자와 소수점만 허용 + const sanitized = value.replace(/[^0-9.]/g, ""); + + // 소수점이 여러 개인 경우 첫 번째만 유지 + const parts = sanitized.split("."); + const formatted = parts.length > 2 + ? parts[0] + "." + parts.slice(1).join("") + : sanitized; + + setDiscountRate(formatted); + + const rate = parseFloat(formatted) || 0; + if (rate >= 0 && rate <= 100) { + const amount = Math.round(supplyAmount * (rate / 100)); + setDiscountAmount(amount.toString()); + } + }, + [supplyAmount] + ); + + // 할인금액 변경 → 할인율 자동 계산 + const handleDiscountAmountChange = useCallback( + (value: string) => { + // 숫자만 허용 + const sanitized = value.replace(/[^0-9]/g, ""); + setDiscountAmount(sanitized); + + const amount = parseInt(sanitized) || 0; + if (supplyAmount > 0 && amount >= 0 && amount <= supplyAmount) { + const rate = (amount / supplyAmount) * 100; + setDiscountRate(rate.toFixed(1)); + } + }, + [supplyAmount] + ); + + // 적용 + const handleApply = useCallback(() => { + const rate = parseFloat(discountRate) || 0; + const amount = parseInt(discountAmount) || 0; + onApply(rate, amount); + onOpenChange(false); + }, [discountRate, discountAmount, onApply, onOpenChange]); + + // 취소 + const handleCancel = useCallback(() => { + onOpenChange(false); + }, [onOpenChange]); + + // --------------------------------------------------------------------------- + // 계산 + // --------------------------------------------------------------------------- + + const discountedAmount = supplyAmount - (parseInt(discountAmount) || 0); + + // --------------------------------------------------------------------------- + // 렌더링 + // --------------------------------------------------------------------------- + + return ( + + + + 할인하기 + + +
+ {/* 공급가액 (읽기 전용) */} +
+ + +
+ + {/* 할인율 */} +
+ +
+ handleDiscountRateChange(e.target.value)} + className="pr-8 text-right" + /> + + % + +
+
+ + {/* 할인금액 */} +
+ +
+ handleDiscountAmountChange(e.target.value.replace(/,/g, ""))} + className="pr-8 text-right" + /> + + 원 + +
+
+ + {/* 할인 후 공급가액 (읽기 전용) */} +
+ + +
+
+ + + + + +
+
+ ); +} diff --git a/src/components/quotes/LocationDetailPanel.tsx b/src/components/quotes/LocationDetailPanel.tsx index 1761bc7e..1cdd71af 100644 --- a/src/components/quotes/LocationDetailPanel.tsx +++ b/src/components/quotes/LocationDetailPanel.tsx @@ -10,7 +10,7 @@ "use client"; import { useState, useMemo, useEffect } from "react"; -import { Package, Settings, Plus, Trash2, Loader2 } from "lucide-react"; +import { Package, Settings, Plus, Trash2, Loader2, Calculator, Save } from "lucide-react"; import { getItemCategoryTree, type ItemCategoryNode } from "./actions"; import { Badge } from "../ui/badge"; @@ -35,7 +35,6 @@ import { } from "../ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { ItemSearchModal } from "./ItemSearchModal"; -import { LocationEditModal } from "./LocationEditModal"; import type { LocationItem } from "./QuoteRegistrationV2"; import type { FinishedGoods } from "./actions"; @@ -138,8 +137,12 @@ const DEFAULT_TABS: TabDefinition[] = [ interface LocationDetailPanelProps { location: LocationItem | null; onUpdateLocation: (locationId: string, updates: Partial) => void; + onDeleteLocation?: (locationId: string) => void; + onCalculateLocation?: (locationId: string) => Promise; + onSaveItems?: () => void; finishedGoods: FinishedGoods[]; disabled?: boolean; + isCalculating?: boolean; } // ============================================================================= @@ -149,8 +152,12 @@ interface LocationDetailPanelProps { export function LocationDetailPanel({ location, onUpdateLocation, + onDeleteLocation, + onCalculateLocation, + onSaveItems, finishedGoods, disabled = false, + isCalculating = false, }: LocationDetailPanelProps) { // --------------------------------------------------------------------------- // 상태 @@ -158,7 +165,6 @@ export function LocationDetailPanel({ const [activeTab, setActiveTab] = useState("BODY"); const [itemSearchOpen, setItemSearchOpen] = useState(false); - const [editModalOpen, setEditModalOpen] = useState(false); const [itemCategories, setItemCategories] = useState([]); const [categoriesLoading, setCategoriesLoading] = useState(true); @@ -336,125 +342,184 @@ export function LocationDetailPanel({ return (
- {/* 헤더 */} -
-
-

- {location.floor} / {location.code} -

-
- 제품명: - - {location.productCode} - - {location.bomResult && ( - - 산출완료 - - )} -
-
-
- - {/* 제품 정보 */} -
- {/* 오픈사이즈 */} -
- 오픈사이즈 -
- handleFieldChange("openWidth", value ?? 0)} - disabled={disabled} - className="w-24 h-8 text-center font-bold" - /> - × - handleFieldChange("openHeight", value ?? 0)} - disabled={disabled} - className="w-24 h-8 text-center font-bold" - /> - {!disabled && ( - - )} -
-
- - {/* 제작사이즈, 산출중량, 산출면적, 수량 */} -
-
- 제작사이즈 -

- {location.manufactureWidth || location.openWidth + 280} × {location.manufactureHeight || location.openHeight + 280} -

-
-
- 산출중량 -

{location.weight?.toFixed(1) || "-"} kg

-
-
- 산출면적 -

{location.area?.toFixed(1) || "-"}

-
-
- 수량 - handleFieldChange("quantity", value ?? 1)} - disabled={disabled} - className="w-24 h-7 text-center font-semibold" - min={1} - /> +
- {/* 필수 설정 (읽기 전용) */} -
-

- - 필수 설정 -

-
-
- - - {GUIDE_RAIL_TYPES.find(t => t.value === location.guideRailType)?.label || location.guideRailType} - -
-
- - - {MOTOR_POWERS.find(p => p.value === location.motorPower)?.label || location.motorPower} - -
-
- - - {CONTROLLERS.find(c => c.value === location.controller)?.label || location.controller} - -
-
-
- - {/* 탭 및 품목 테이블 */} + {/* ②-2 품목 상세 영역 */}
- {/* 탭 목록 - 스크롤 가능 */} + {/* 탭 목록 */}
{categoriesLoading ? (
@@ -467,7 +532,7 @@ export function LocationDetailPanel({ {tab.label} @@ -476,122 +541,95 @@ export function LocationDetailPanel({ )}
- {/* 동적 탭 콘텐츠 렌더링 */} + {/* 탭 콘텐츠 */} {detailTabs.map((tab) => { const items = bomItemsByTab[tab.value] || []; const isBendingTab = tab.parentCode === "BENDING"; - const isMotorTab = tab.value === "MOTOR_CTRL"; - const isAccessoryTab = tab.value === "ACCESSORY"; return ( -
+
품목명 - {/* 본체: 제작사이즈 */} - {!isBendingTab && !isMotorTab && !isAccessoryTab && ( - 제작사이즈 - )} - {/* 절곡품: 재질, 규격, 납품길이 */} + 규격 {isBendingTab && ( - <> - 재질 - 규격 - 납품길이 - + 납품길이 )} - {/* 모터: 유형, 사양 */} - {isMotorTab && ( - <> - 유형 - 사양 - - )} - {/* 부자재: 규격, 납품길이 */} - {isAccessoryTab && ( - <> - 규격 - 납품길이 - - )} - 수량 - 작업 + 수량 + - {items.map((item: any, index: number) => ( - - {item.item_name} - {/* 본체: 제작사이즈 */} - {!isBendingTab && !isMotorTab && !isAccessoryTab && ( - {item.manufacture_size || "-"} - )} - {/* 절곡품: 재질, 규격, 납품길이 */} - {isBendingTab && ( - <> - {item.material || "-"} - {item.spec || "-"} - - - - - )} - {/* 모터: 유형, 사양 */} - {isMotorTab && ( - <> - {item.type || "-"} - {item.spec || "-"} - - )} - {/* 부자재: 규격, 납품길이 */} - {isAccessoryTab && ( - <> - {item.spec || "-"} - - - - - )} - - {}} - className="w-16 h-8 text-center" - min={1} - disabled={disabled} - /> - - - + {items.length === 0 ? ( + + + 산출된 품목이 없습니다 - ))} + ) : ( + items.map((item: any, index: number) => ( + + {item.item_name} + {item.spec || item.specification || "-"} + {isBendingTab && ( + + + + )} + + {}} + className="w-14 h-7 text-center text-xs" + min={1} + disabled={disabled} + /> + + + + + + )) + )}
- {/* 품목 추가 버튼 + 안내 */} + {/* 품목 추가 + 저장 버튼 */}
- - 💡 금액은 아래 견적금액요약에서 확인하세요 - +
@@ -614,13 +658,6 @@ export function LocationDetailPanel({
- {/* 금액 안내 */} - {!location.bomResult && ( -
- 💡 금액은 아래 견적금액요약에서 확인하세요 -
- )} - {/* 품목 검색 모달 */} { if (!location) return; - // 현재 탭 정보 가져오기 const currentTab = detailTabs.find((t) => t.value === activeTab); - const categoryCode = activeTab; // 카테고리 코드를 직접 사용 + const categoryCode = activeTab; const categoryLabel = currentTab?.label || activeTab; - // 새 품목 생성 (카테고리 코드 포함) const newItem: BomCalculationResultItem & { category_code?: string; is_manual?: boolean } = { item_code: item.code, item_name: item.name, @@ -643,11 +678,10 @@ export function LocationDetailPanel({ unit_price: 0, total_price: 0, process_group: categoryLabel, - category_code: categoryCode, // 새 카테고리 코드 사용 - is_manual: true, // 수동 추가 품목 표시 + category_code: categoryCode, + is_manual: true, }; - // 기존 bomResult 가져오기 const existingBomResult = location.bomResult || { finished_goods: { code: location.productCode || "", name: location.productName || "" }, subtotals: {}, @@ -656,11 +690,9 @@ export function LocationDetailPanel({ items: [], }; - // 기존 items에 새 아이템 추가 const existingItems = existingBomResult.items || []; const updatedItems = [...existingItems, newItem]; - // subtotals 업데이트 (해당 카테고리의 count, subtotal 증가) const existingSubtotals = existingBomResult.subtotals || {}; const rawCategorySubtotal = existingSubtotals[categoryCode]; const categorySubtotal = (typeof rawCategorySubtotal === 'object' && rawCategorySubtotal !== null) @@ -675,7 +707,6 @@ export function LocationDetailPanel({ }, }; - // grouped_items 업데이트 (해당 카테고리의 items 배열에 추가) const existingGroupedItems = existingBomResult.grouped_items || {}; const categoryGroupedItems = existingGroupedItems[categoryCode] || { items: [] }; const updatedGroupedItems = { @@ -686,7 +717,6 @@ export function LocationDetailPanel({ }, }; - // grand_total 업데이트 const updatedGrandTotal = (existingBomResult.grand_total || 0) + (newItem.total_price || 0); const updatedBomResult = { @@ -697,23 +727,11 @@ export function LocationDetailPanel({ grand_total: updatedGrandTotal, }; - // location 업데이트 onUpdateLocation(location.id, { bomResult: updatedBomResult }); - console.log(`[품목 추가] ${item.code} - ${item.name} → ${categoryLabel} (${categoryCode})`); }} tabLabel={detailTabs.find((t) => t.value === activeTab)?.label} /> - - {/* 개소 정보 수정 모달 */} - { - onUpdateLocation(locationId, updates); - }} - />
); } \ No newline at end of file diff --git a/src/components/quotes/LocationListPanel.tsx b/src/components/quotes/LocationListPanel.tsx index 79f793a7..2e57802b 100644 --- a/src/components/quotes/LocationListPanel.tsx +++ b/src/components/quotes/LocationListPanel.tsx @@ -9,7 +9,7 @@ "use client"; import { useState, useCallback } from "react"; -import { Plus, Upload, Download, Trash2, Pencil } from "lucide-react"; +import { Plus, Upload, Download } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../ui/button"; @@ -32,7 +32,6 @@ import { TableRow, } from "../ui/table"; import { DeleteConfirmDialog } from "../ui/confirm-dialog"; -import { LocationEditModal } from "./LocationEditModal"; import type { LocationItem } from "./QuoteRegistrationV2"; import type { FinishedGoods } from "./actions"; @@ -112,9 +111,6 @@ export function LocationListPanel({ // 삭제 확인 다이얼로그 const [deleteTarget, setDeleteTarget] = useState(null); - // 수정 모달 - const [editTarget, setEditTarget] = useState(null); - // --------------------------------------------------------------------------- // 핸들러 // --------------------------------------------------------------------------- @@ -275,162 +271,49 @@ export function LocationListPanel({ return (
- {/* 헤더 */} -
-
-

- 📋 발주 개소 목록 ({locations.length}) -

-
- - -
-
-
- - {/* 개소 목록 테이블 */} -
- - - - - 부호 - 사이즈 - 제품 - 수량 - - - - - {locations.length === 0 ? ( - - - 개소를 추가해주세요 - - - ) : ( - locations.map((loc) => ( - onSelectLocation(loc.id)} - > - {loc.floor} - {loc.code} - - {loc.openWidth}×{loc.openHeight} - - {loc.productCode} - {loc.quantity} - - {!disabled && ( -
- - -
- )} -
-
- )) - )} -
-
-
- - {/* 추가 폼 */} + {/* ① 입력 영역 (상단으로 이동) */} {!disabled && ( -
- {/* 1행: 층, 부호, 가로, 세로, 제품명, 수량 */} +
+ {/* 1행: 층, 부호, 가로, 세로, 제품코드, 수량 */}
handleFormChange("floor", e.target.value)} - className="h-8 text-sm" + className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("code", e.target.value)} - className="h-8 text-sm" + className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("openWidth", value?.toString() ?? "")} - className="h-8 text-sm" + className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("openHeight", value?.toString() ?? "")} - className="h-8 text-sm" + className="h-8 text-sm placeholder:text-gray-300" />
- + + + +
+
+
+ + {/* 개소 목록 테이블 (간소화: 부호, 사이즈만) */} +
+ + + + 부호 + 사이즈 + + + + {locations.length === 0 ? ( + + + 개소를 추가해주세요 + + + ) : ( + locations.map((loc) => ( + onSelectLocation(loc.id)} + > + +
{loc.code}
+
{loc.productCode}
+
+ +
{loc.openWidth}X{loc.openHeight}
+
{loc.floor} · {loc.quantity}개
+
+
+ )) + )} +
+
+
+ {/* 삭제 확인 다이얼로그 */} - - {/* 개소 정보 수정 모달 */} - !open && setEditTarget(null)} - location={editTarget} - onSave={(locationId, updates) => { - onUpdateLocation(locationId, updates); - setEditTarget(null); - toast.success("개소 정보가 수정되었습니다."); - }} - />
); } \ No newline at end of file diff --git a/src/components/quotes/QuoteFooterBar.tsx b/src/components/quotes/QuoteFooterBar.tsx index 1ba5f1cb..e272aadc 100644 --- a/src/components/quotes/QuoteFooterBar.tsx +++ b/src/components/quotes/QuoteFooterBar.tsx @@ -2,13 +2,13 @@ * 견적 푸터 바 * * - 예상 전체 견적금액 표시 - * - 버튼: 견적서 산출, 저장, 견적 확정 + * - 버튼: 견적서 보기, 거래명세서 보기, 할인하기, 최종확정 * - 뒤로가기 버튼 */ "use client"; -import { Download, Save, Check, ArrowLeft, Loader2, Calculator, Eye, Pencil, ClipboardList } from "lucide-react"; +import { Save, Check, ArrowLeft, Loader2, FileText, Pencil, ClipboardList, Percent } from "lucide-react"; import { Button } from "../ui/button"; @@ -20,17 +20,25 @@ interface QuoteFooterBarProps { totalLocations: number; totalAmount: number; status: "draft" | "temporary" | "final"; - onCalculate: () => void; - onPreview: () => void; - onSaveTemporary: () => void; - onSaveFinal: () => void; + /** 견적서 보기 */ + onQuoteView: () => void; + /** 거래명세서 보기 */ + onTransactionView: () => void; + /** 저장 (임시저장) */ + onSave: () => void; + /** 최종확정 */ + onFinalize: () => void; + /** 뒤로가기 */ onBack: () => void; + /** 수정 */ onEdit?: () => void; + /** 수주등록 */ onOrderRegister?: () => void; - isCalculating?: boolean; + /** 할인하기 */ + onDiscount?: () => void; isSaving?: boolean; disabled?: boolean; - /** view 모드 여부 (view: 수정+최종저장, edit: 임시저장+최종저장) */ + /** view 모드 여부 (view: 수정+최종확정, edit: 저장+최종확정) */ isViewMode?: boolean; } @@ -42,14 +50,14 @@ export function QuoteFooterBar({ totalLocations, totalAmount, status, - onCalculate, - onPreview, - onSaveTemporary, - onSaveFinal, + onQuoteView, + onTransactionView, + onSave, + onFinalize, onBack, onEdit, onOrderRegister, - isCalculating = false, + onDiscount, isSaving = false, disabled = false, isViewMode = false, @@ -79,36 +87,26 @@ export function QuoteFooterBar({ {/* 오른쪽: 버튼들 */}
- {/* 견적서 산출 - edit 모드에서만 활성화 */} - {!isViewMode && ( - - )} - - {/* 미리보기 */} + {/* 견적서 보기 */} + + {/* 거래명세서 보기 */} + {/* 수정 - view 모드에서만 표시 */} @@ -123,10 +121,22 @@ export function QuoteFooterBar({ )} + {/* 할인하기 */} + {onDiscount && ( + + )} + {/* 저장 - edit 모드에서만 표시 */} {!isViewMode && ( )} - {/* 견적 확정 - 양쪽 모드에서 표시 (final 상태가 아닐 때만) */} + {/* 최종확정 - 양쪽 모드에서 표시 (final 상태가 아닐 때만) */} {status !== "final" && ( )} diff --git a/src/components/quotes/QuoteManagementClient.tsx b/src/components/quotes/QuoteManagementClient.tsx index 5a8eeeba..e8d4eb97 100644 --- a/src/components/quotes/QuoteManagementClient.tsx +++ b/src/components/quotes/QuoteManagementClient.tsx @@ -12,6 +12,7 @@ import { useState, useMemo, useCallback, useTransition } from 'react'; import { useRouter } from 'next/navigation'; +import { format, startOfMonth, endOfMonth } from 'date-fns'; import { FileText, Edit, @@ -23,6 +24,13 @@ import { import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { getQuoteStatusBadge } from '@/components/atoms/BadgeSm'; import { TableRow, TableCell } from '@/components/ui/table'; import { @@ -37,7 +45,6 @@ import { type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, - type TabOption, type StatCard, type ListParams, } from '@/components/templates/UniversalListPage'; @@ -64,6 +71,15 @@ export function QuoteManagementClient({ const router = useRouter(); const [isPending, startTransition] = useTransition(); + // ===== 날짜 필터 상태 ===== + const today = new Date(); + const [startDate, setStartDate] = useState(format(startOfMonth(today), 'yyyy-MM-dd')); + const [endDate, setEndDate] = useState(format(endOfMonth(today), 'yyyy-MM-dd')); + + // ===== 필터 상태 ===== + const [productCategoryFilter, setProductCategoryFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); + // ===== 산출내역서 다이얼로그 상태 ===== const [isCalculationDialogOpen, setIsCalculationDialogOpen] = useState(false); const [calculationQuote, setCalculationQuote] = useState(null); @@ -156,35 +172,6 @@ export function QuoteManagementClient({ return getQuoteStatusBadge(legacyQuote as any); }, []); - // ===== 탭 옵션 ===== - const tabs: TabOption[] = useMemo(() => [ - { value: 'all', label: '전체', count: allQuotes.length, color: 'blue' }, - { - value: 'initial', - label: '최초작성', - count: allQuotes.filter((q) => q.currentRevision === 0 && !q.isFinal && q.status !== 'converted').length, - color: 'gray', - }, - { - value: 'revising', - label: '수정중', - count: allQuotes.filter((q) => q.currentRevision > 0 && !q.isFinal && q.status !== 'converted').length, - color: 'orange', - }, - { - value: 'final', - label: '최종확정', - count: allQuotes.filter((q) => q.isFinal && q.status !== 'converted').length, - color: 'green', - }, - { - value: 'converted', - label: '수주전환', - count: allQuotes.filter((q) => q.status === 'converted').length, - color: 'purple', - }, - ], [allQuotes]); - // ===== 통계 카드 계산 ===== const computeStats = useCallback((data: Quote[]): StatCard[] => { const now = new Date(); @@ -307,22 +294,41 @@ export function QuoteManagementClient({ clientSideFiltering: true, itemsPerPage: 20, - // 탭 필터 함수 - tabFilter: (item: Quote, activeTab: string) => { - if (activeTab === 'all') return true; - if (activeTab === 'initial') { - return item.currentRevision === 0 && !item.isFinal && item.status !== 'converted'; - } - if (activeTab === 'revising') { - return item.currentRevision > 0 && !item.isFinal && item.status !== 'converted'; - } - if (activeTab === 'final') { - return item.isFinal && item.status !== 'converted'; - } - if (activeTab === 'converted') { - return item.status === 'converted'; - } - return true; + // 필터링 함수 (날짜 + 제품분류 + 상태) + customFilterFn: (items: Quote[]) => { + return items.filter((item) => { + // 날짜 필터 + const itemDate = item.registrationDate; + if (itemDate) { + if (startDate && itemDate < startDate) return false; + if (endDate && itemDate > endDate) return false; + } + + // 제품분류 필터 + if (productCategoryFilter !== 'all') { + if (productCategoryFilter === 'STEEL' && item.productCategory !== 'STEEL') return false; + if (productCategoryFilter === 'SCREEN' && item.productCategory !== 'SCREEN') return false; + if (productCategoryFilter === 'MIXED' && item.productCategory !== 'MIXED') return false; + } + + // 상태 필터 + if (statusFilter !== 'all') { + if (statusFilter === 'initial') { + // 최초작성: currentRevision === 0 && !isFinal + if (!(item.currentRevision === 0 && !item.isFinal)) return false; + } + if (statusFilter === 'revising') { + // N차수정: currentRevision > 0 && !isFinal + if (!(item.currentRevision > 0 && !item.isFinal)) return false; + } + if (statusFilter === 'final') { + // 최종확정: isFinal === true + if (!item.isFinal) return false; + } + } + + return true; + }); }, // 검색 필터 함수 @@ -344,13 +350,53 @@ export function QuoteManagementClient({ ); }, - // 탭 설정 - tabs, - defaultTab: 'all', + // 탭 비활성화 (필터로 대체) + tabs: [], // 통계 카드 computeStats, + // 테이블 우측 필터 (탭 영역에 표시) + tableHeaderActions: ( +
+ {/* 제품분류 필터 */} + + + {/* 상태 필터 */} + +
+ ), + + // 날짜 필터 + dateRangeSelector: { + enabled: true, + showPresets: true, + startDate, + endDate, + onStartDateChange: setStartDate, + onEndDateChange: setEndDate, + }, + // 검색 searchPlaceholder: '견적번호, 발주처, 담당자, 현장코드, 현장명 검색...', @@ -553,7 +599,7 @@ export function QuoteManagementClient({ ); }, }), - [tabs, computeStats, router, handleView, handleEdit, handleDeleteClick, handleViewHistory, handleBulkDelete, getRevisionBadge, isPending] + [computeStats, router, handleView, handleEdit, handleDeleteClick, handleViewHistory, handleBulkDelete, getRevisionBadge, isPending, startDate, endDate, productCategoryFilter, statusFilter] ); return ( diff --git a/src/components/quotes/QuotePreviewContent.tsx b/src/components/quotes/QuotePreviewContent.tsx index dee9f720..63e062ee 100644 --- a/src/components/quotes/QuotePreviewContent.tsx +++ b/src/components/quotes/QuotePreviewContent.tsx @@ -3,215 +3,368 @@ /** * 견적서 문서 콘텐츠 * - * 공통 컴포넌트 사용: - * - DocumentHeader: simple 레이아웃 (결재란 없음) - * - SectionHeader: 섹션 제목 + * 양식 타입: + * - vendor: 업체발송용 (부가세 별도/포함) + * - calculation: 산출내역서 */ import type { QuoteFormDataV2 } from './QuoteRegistrationV2'; -import { DocumentHeader, SectionHeader } from '@/components/document-system'; + +// 양식 타입 +type TemplateType = 'vendor' | 'calculation'; interface QuotePreviewContentProps { data: QuoteFormDataV2; + /** 양식 타입 (기본: vendor) */ + templateType?: TemplateType; + /** 부가세 포함 여부 (업체발송용에서만 사용) */ + vatIncluded?: boolean; + /** 할인율 (%) */ + discountRate?: number; + /** 할인금액 (원) */ + discountAmount?: number; } -export function QuotePreviewContent({ data: quoteData }: QuotePreviewContentProps) { - // 총 금액 계산 - const totalAmount = quoteData.locations.reduce( +export function QuotePreviewContent({ + data: quoteData, + templateType = 'vendor', + vatIncluded = false, + discountRate = 0, + discountAmount = 0, +}: QuotePreviewContentProps) { + // 소계 (할인 전 금액) + const subtotal = quoteData.locations.reduce( (sum, loc) => sum + (loc.totalPrice || 0), 0 ); + // 할인 적용 후 금액 + const afterDiscount = subtotal - discountAmount; + // 부가세 - const vat = Math.round(totalAmount * 0.1); - const grandTotal = totalAmount + vat; + const vat = Math.round(afterDiscount * 0.1); + + // 총 견적금액 (부가세 포함 여부에 따라) + const grandTotal = vatIncluded ? afterDiscount + vat : afterDiscount; + + // 할인 적용 여부 + const hasDiscount = discountAmount > 0; + + // 산출내역서 여부 + const isCalculation = templateType === 'calculation'; return ( -
- {/* 제목 (공통 컴포넌트) */} - - - {/* 수요자 정보 */} -
-
- 수 요 자 +
+ {/* 헤더: 제목 + 결재란 */} +
+ {/* 왼쪽: 제목 */} +
+

+ 견 적 서 +

+

+ 문서번호: {quoteData.id || 'ABC123'} | 작성일자: {quoteData.registrationDate || '-'} +

-
-
- 업체명 - {quoteData.clientName || "-"} -
-
- 담당자 - {quoteData.manager || "-"} -
-
- 프로젝트명 - {quoteData.siteName || "-"} -
-
- 연락처 - {quoteData.contact || "-"} -
-
- 견적일자 - {quoteData.registrationDate || "-"} -
-
- 유효기간 - {quoteData.dueDate || "-"} -
+ + {/* 오른쪽: 결재란 */} +
+ + + + + + + + + + + + + + + + + + + + + + + +
작성승인승인승인
홍길동이름이름이름
부서명부서명부서명부서명
- {/* 공급자 정보 */} -
-
- 공 급 자 + {/* 수요자 / 공급자 정보 (좌우 배치) */} +
+ {/* 수요자 */} +
+
+ 수 요 자 +
+ + + + + + + + + + + + + + + + + + + + + + + +
업체명{quoteData.clientName || '-'}
제품명{quoteData.locations[0]?.productCode || '-'}
현장명{quoteData.siteName || '-'}
담당자{quoteData.manager || '-'}
연락처{quoteData.contact || '-'}
-
-
- 상호 - 프론트_테스트회사 -
-
- 사업자등록번호 - 123-45-67890 -
-
- 대표자 - 프론트 -
-
- 업태 - 업태명 -
-
- 종목 - 김종명 -
-
- 사업장주소 - 07547 서울 강서구 양천로 583 B-1602 -
-
- 전화 - 01048209104 -
-
- 이메일 - codebridgex@codebridge-x.com + + {/* 공급자 */} +
+
+ 공 급 자
+ + + + + + + + + + + + + + + + + + + + + + + +
상호회사명
등록번호123-12-12345
사업장주소주소명
업태제조업
TEL031-123-1234
- {/* 총 견적금액 */} -
-

총 견적금액

-

- ₩ {grandTotal.toLocaleString()} -

-

※ 부가가치세 포함

-
- - {/* 제품 구성정보 */} -
- 제 품 구 성 정 보 -
-
- 모델 - - {quoteData.locations[0]?.productCode || "-"} - -
-
- 총 수량 - {quoteData.locations.length}개소 -
-
- 오픈사이즈 - - {quoteData.locations[0]?.openWidth || "-"} × {quoteData.locations[0]?.openHeight || "-"} - -
-
- 설치유형 - - -
+ {/* 내역 테이블 */} +
+
+ 내 역
-
- - {/* 품목 내역 */} -
- 품 목 내 역 - +
- - - - - - - - + + + + + + + + + + + + + + + + + + + + + + {quoteData.locations.map((loc, index) => ( - - - - - - - + + + + + + + + + - ))} - - - - + + + + + + {/* 할인율 */} + + + + - - - - - - - - - + + + + + {/* 부가세 포함일 때 추가 행들 */} + {vatIncluded && ( + <> + + + + + + + + + + + + + + + + + )}
No.품목명규격수량단위단가금액
No.품종부호제품명오픈사이즈수량단위단가합계금액
가로세로
{index + 1}{loc.productCode} - {loc.openWidth}×{loc.openHeight} - {loc.quantity}EA +
{index + 1}{loc.floor || '-'}{loc.symbol || '-'}{loc.productCode}{loc.openWidth}{loc.openHeight}{loc.quantity}SET {(loc.unitPrice || 0).toLocaleString()} + {(loc.totalPrice || 0).toLocaleString()}
공급가액 합계 - {totalAmount.toLocaleString()} + {/* 소계 */} +
+ {vatIncluded ? '공급가액 합계' : '소계'} + {subtotal.toLocaleString()}
+ 할인율 + + {hasDiscount ? `${discountRate.toFixed(1)}%` : '0.0%'}
부가가치세 (10%) - {vat.toLocaleString()} -
총 견적금액 - {grandTotal.toLocaleString()} + + {/* 할인금액 */} +
+ 할인금액 + + {hasDiscount ? `-${discountAmount.toLocaleString()}` : '0'}
+ 할인 후 공급가액 + {afterDiscount.toLocaleString()}
+ 부가가치세 합계 + {vat.toLocaleString()}
+ 총 견적금액 + {grandTotal.toLocaleString()}
- {/* 비고사항 */} -
- 비 고 사 항 -
- {quoteData.remarks || "비고 테스트"} + {/* 합계금액 박스 */} +
+ + 합계금액 ({vatIncluded ? '부가세 포함' : '부가세 별도'}) + + + ₩ {grandTotal.toLocaleString()} + +
+ + {/* 산출내역서일 경우 세부 산출내역서 테이블 추가 */} + {isCalculation && ( +
+
+ 세 부 산 출 내 역 서 +
+ + + + + + + + + + + + + + {quoteData.locations.map((loc) => ( + <> + {/* 각 개소별 품목 상세 */} + + + + + + + + + + + ))} + {/* 소계 */} + + + + + + +
부호항목규격수량단위단가금액
{loc.symbol || '-'}항목명규격명{loc.quantity}SET + {(loc.unitPrice || 0).toLocaleString()} + + {(loc.totalPrice || 0).toLocaleString()} +
+ 소계 + {subtotal.toLocaleString()}
+ )} + + {/* 비고 */} +
+
+
+ 비고 +
+
+

※ 해당 견적서의 유효기간은 발행일 기준 1개월 입니다.

+

※ 견적금액의 50%를 입금하시면 생산가 진행합니다.

+
+
+
+ + {/* 결제방법 / 담당자 */} +
+ + + + + + + + + + + + + + + +
결제방법계좌이체계좌정보국민은행 12312132132
담당자홍길동 과장연락처010-1234-1234
); diff --git a/src/components/quotes/QuotePreviewModal.tsx b/src/components/quotes/QuotePreviewModal.tsx index 8ac015c9..2549ae51 100644 --- a/src/components/quotes/QuotePreviewModal.tsx +++ b/src/components/quotes/QuotePreviewModal.tsx @@ -3,68 +3,131 @@ /** * 견적서 미리보기 모달 * - * document-system 통합 버전 (2026-01-22) + * 양식 선택: + * - 업체발송용 (부가세 포함/별도는 기본정보에서 선택한 값 사용) + * - 산출내역서 */ -import { Download, Mail } from 'lucide-react'; +import { useState } from 'react'; +import { Copy, Pencil, FileText } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { DocumentViewer } from '@/components/document-system'; import type { QuoteFormDataV2 } from './QuoteRegistrationV2'; import { QuotePreviewContent } from './QuotePreviewContent'; +// 양식 타입: 업체발송용 / 산출내역서 +type TemplateType = 'vendor' | 'calculation'; + interface QuotePreviewModalProps { open: boolean; onOpenChange: (open: boolean) => void; quoteData: QuoteFormDataV2 | null; + /** 할인율 (%) */ + discountRate?: number; + /** 할인금액 (원) */ + discountAmount?: number; + /** 복제 핸들러 */ + onDuplicate?: () => void; + /** 수정 핸들러 */ + onEdit?: () => void; } export function QuotePreviewModal({ open, onOpenChange, quoteData, + discountRate = 0, + discountAmount = 0, + onDuplicate, + onEdit, }: QuotePreviewModalProps) { + // 양식 타입 상태 (기본: 업체발송용) + const [templateType, setTemplateType] = useState('vendor'); + if (!quoteData) return null; - const handlePdfDownload = () => { - console.log('[테스트] PDF 다운로드'); + // 부가세 포함 여부는 기본정보에서 선택한 값 사용 + const vatIncluded = quoteData.vatType === 'included'; + + const handleDuplicate = () => { + console.log('[테스트] 복제'); + onDuplicate?.(); }; - const handleEmailSend = () => { - console.log('[테스트] 이메일 전송'); + const handleEdit = () => { + console.log('[테스트] 수정'); + onEdit?.(); }; const toolbarExtra = ( <> + {/* 복제 버튼 */} + + {/* 수정 버튼 */} + + {/* 양식 선택 드롭다운 */} + + + + + + setTemplateType('vendor')} + className={templateType === 'vendor' ? 'bg-blue-50' : ''} + > + 업체발송용 + + setTemplateType('calculation')} + className={templateType === 'calculation' ? 'bg-blue-50' : ''} + > + 산출내역서 + + + ); return ( - + ); } diff --git a/src/components/quotes/QuoteRegistrationV2.tsx b/src/components/quotes/QuoteRegistrationV2.tsx index ddfb51cf..a67a85fb 100644 --- a/src/components/quotes/QuoteRegistrationV2.tsx +++ b/src/components/quotes/QuoteRegistrationV2.tsx @@ -33,6 +33,8 @@ import { LocationDetailPanel } from "./LocationDetailPanel"; import { QuoteSummaryPanel } from "./QuoteSummaryPanel"; import { QuoteFooterBar } from "./QuoteFooterBar"; import { QuotePreviewModal } from "./QuotePreviewModal"; +import { QuoteTransactionModal } from "./QuoteTransactionModal"; +import { DiscountModal } from "./DiscountModal"; import { getFinishedGoods, @@ -82,15 +84,16 @@ export interface LocationItem { // 견적 폼 데이터 V2 export interface QuoteFormDataV2 { id?: string; - registrationDate: string; - writer: string; - clientId: string; - clientName: string; - siteName: string; - manager: string; - contact: string; - dueDate: string; - remarks: string; + quoteNumber: string; // 견적번호 + registrationDate: string; // 접수일 + writer: string; // 작성자 + clientId: string; // 수주처 ID + clientName: string; // 수주처명 + siteName: string; // 현장명 + manager: string; // 담당자 + contact: string; // 연락처 + vatType: "included" | "excluded"; // 부가세 (포함/별도) + remarks: string; // 비고 status: "draft" | "temporary" | "final"; // 작성중, 임시저장, 최종저장 locations: LocationItem[]; } @@ -118,6 +121,7 @@ const createNewLocation = (): LocationItem => ({ // 초기 폼 데이터 const INITIAL_FORM_DATA: QuoteFormDataV2 = { + quoteNumber: "", // 자동생성 또는 서버에서 부여 registrationDate: getLocalDateString(new Date()), writer: "", // useAuth()에서 currentUser.name으로 설정됨 clientId: "", @@ -125,7 +129,7 @@ const INITIAL_FORM_DATA: QuoteFormDataV2 = { siteName: "", manager: "", contact: "", - dueDate: "", + vatType: "included", // 기본값: 부가세 포함 remarks: "", status: "draft", locations: [], @@ -172,7 +176,11 @@ export function QuoteRegistrationV2({ const [selectedLocationId, setSelectedLocationId] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isCalculating, setIsCalculating] = useState(false); - const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [quotePreviewOpen, setQuotePreviewOpen] = useState(false); + const [transactionPreviewOpen, setTransactionPreviewOpen] = useState(false); + const [discountModalOpen, setDiscountModalOpen] = useState(false); + const [discountRate, setDiscountRate] = useState(0); + const [discountAmount, setDiscountAmount] = useState(0); const pendingAutoCalculateRef = useRef(false); // API 데이터 @@ -286,11 +294,23 @@ export function QuoteRegistrationV2({ return formData.locations.find((loc) => loc.id === selectedLocationId) || null; }, [formData.locations, selectedLocationId]); - // 총 금액 + // 총 금액 (할인 전) const totalAmount = useMemo(() => { return formData.locations.reduce((sum, loc) => sum + (loc.totalPrice || 0), 0); }, [formData.locations]); + // 할인 적용 후 총 금액 + const discountedTotalAmount = useMemo(() => { + return totalAmount - discountAmount; + }, [totalAmount, discountAmount]); + + // 할인 적용 핸들러 + const handleApplyDiscount = useCallback((rate: number, amount: number) => { + setDiscountRate(rate); + setDiscountAmount(amount); + toast.success(`할인이 적용되었습니다. (${rate.toFixed(1)}%, ${amount.toLocaleString()}원)`); + }, []); + // 개소별 합계 const locationTotals = useMemo(() => { return formData.locations.map((loc) => ({ @@ -661,33 +681,34 @@ export function QuoteRegistrationV2({ -
+ {/* 1행: 견적번호 | 접수일 | 수주처 | 현장명 */} +
- + + +
+
+ handleFieldChange("registrationDate", e.target.value)} + disabled={isViewMode} />
- - -
-
- +
-
- -
+
+ + {/* 2행: 담당자 | 연락처 | 작성자 | 부가세 */} +
- +
-
- -
- + handleFieldChange("dueDate", e.target.value)} - disabled={isViewMode} + value={formData.writer} + disabled + className="bg-gray-50" />
-
+
+ + +
+
+ + {/* 3행: 상태 | 비고 */} +
+
+ + +
+
-