Word 변환 WYSIWYG 개선 - 위치/크기/줄바꿈/가로배치 지원
This commit is contained in:
parent
31746e8a0b
commit
2e122b0703
|
|
@ -14,10 +14,12 @@
|
|||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"html-to-docx": "^1.8.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"imap": "^0.8.19",
|
||||
"joi": "^17.11.0",
|
||||
|
|
@ -2256,6 +2258,93 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/dom": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.6.tgz",
|
||||
"integrity": "sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "1.0.5",
|
||||
"@oozcitak/url": "1.0.0",
|
||||
"@oozcitak/util": "8.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/infra": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.5.tgz",
|
||||
"integrity": "sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/util": "8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/infra/node_modules/@oozcitak/util": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
|
||||
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/url": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.0.tgz",
|
||||
"integrity": "sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "1.0.3",
|
||||
"@oozcitak/util": "1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/url/node_modules/@oozcitak/infra": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.3.tgz",
|
||||
"integrity": "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/util": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/url/node_modules/@oozcitak/infra/node_modules/@oozcitak/util": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.1.tgz",
|
||||
"integrity": "sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/url/node_modules/@oozcitak/util": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.2.tgz",
|
||||
"integrity": "sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/util": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.4.tgz",
|
||||
"integrity": "sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@paralleldrive/cuid2": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
|
||||
|
|
@ -4326,6 +4415,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-split": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz",
|
||||
"integrity": "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
||||
|
|
@ -4521,6 +4616,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelize": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001745",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz",
|
||||
|
|
@ -5202,6 +5306,56 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
|
||||
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/@types/node": {
|
||||
"version": "24.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
|
|
@ -5216,6 +5370,11 @@
|
|||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
|
|
@ -5349,6 +5508,27 @@
|
|||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ent": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz",
|
||||
"integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.3",
|
||||
"es-errors": "^1.3.0",
|
||||
"punycode": "^1.4.1",
|
||||
"safe-regex-test": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ent/node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
|
|
@ -5361,6 +5541,16 @@
|
|||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz",
|
||||
"integrity": "sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.0",
|
||||
"string-template": "~0.2.0",
|
||||
"xtend": "~4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
|
|
@ -5643,6 +5833,14 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ev-store": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ev-store/-/ev-store-7.0.0.tgz",
|
||||
"integrity": "sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==",
|
||||
"dependencies": {
|
||||
"individual": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
|
|
@ -6279,6 +6477,16 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/global": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
|
|
@ -6413,6 +6621,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
|
|
@ -6443,6 +6661,22 @@
|
|||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mdevils"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://patreon.com/mdevils"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
|
|
@ -6450,6 +6684,27 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-to-docx": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/html-to-docx/-/html-to-docx-1.8.0.tgz",
|
||||
"integrity": "sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/dom": "1.15.6",
|
||||
"@oozcitak/util": "8.3.4",
|
||||
"color-name": "^1.1.4",
|
||||
"html-entities": "^2.3.3",
|
||||
"html-to-vdom": "^0.7.0",
|
||||
"image-size": "^1.0.0",
|
||||
"image-to-base64": "^2.2.0",
|
||||
"jszip": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^3.1.25",
|
||||
"virtual-dom": "^2.1.1",
|
||||
"xmlbuilder2": "2.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||
|
|
@ -6466,6 +6721,106 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/html-to-vdom/-/html-to-vdom-0.7.0.tgz",
|
||||
"integrity": "sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ent": "^2.0.0",
|
||||
"htmlparser2": "^3.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/dom-serializer": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
|
||||
"integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/dom-serializer/node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/dom-serializer/node_modules/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/domelementtype": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
|
||||
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/domhandler": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
|
||||
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/domutils": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
|
||||
"integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "0",
|
||||
"domelementtype": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/entities": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/htmlparser2": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^1.3.1",
|
||||
"domhandler": "^2.3.0",
|
||||
"domutils": "^1.5.1",
|
||||
"entities": "^1.1.1",
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-vdom/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
|
|
@ -6590,6 +6945,30 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/image-to-base64": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/image-to-base64/-/image-to-base64-2.2.0.tgz",
|
||||
"integrity": "sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/imap": {
|
||||
"version": "0.8.19",
|
||||
"resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz",
|
||||
|
|
@ -6626,6 +7005,12 @@
|
|||
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
@ -6673,6 +7058,11 @@
|
|||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/individual": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz",
|
||||
"integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g=="
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
|
|
@ -6854,6 +7244,15 @@
|
|||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
|
|
@ -7696,6 +8095,18 @@
|
|||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
|
|
@ -7812,6 +8223,15 @@
|
|||
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
|
|
@ -8177,6 +8597,21 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/min-document": {
|
||||
"version": "2.19.2",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
|
||||
"integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
|
|
@ -8300,6 +8735,24 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/native-duplexpair": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
|
||||
|
|
@ -8329,6 +8782,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next-tick": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz",
|
||||
"integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||
|
|
@ -8670,6 +9129,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parchment": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
|
||||
|
|
@ -9179,6 +9644,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
@ -9595,6 +10069,23 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"is-regex": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
|
|
@ -9610,6 +10101,12 @@
|
|||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
|
|
@ -9744,6 +10241,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
|
|
@ -10020,6 +10523,11 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/string-template": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
|
||||
"integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw=="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
|
|
@ -10685,6 +11193,22 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/virtual-dom": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz",
|
||||
"integrity": "sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browser-split": "0.0.1",
|
||||
"error": "^4.3.0",
|
||||
"ev-store": "^7.0.0",
|
||||
"global": "^4.3.0",
|
||||
"is-object": "^1.0.1",
|
||||
"next-tick": "^0.2.2",
|
||||
"x-is-array": "0.1.0",
|
||||
"x-is-string": "0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
|
|
@ -10862,6 +11386,80 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/x-is-array": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/x-is-array/-/x-is-array-0.1.0.tgz",
|
||||
"integrity": "sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA=="
|
||||
},
|
||||
"node_modules/x-is-string": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
|
||||
"integrity": "sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w=="
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-2.1.2.tgz",
|
||||
"integrity": "sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/dom": "1.15.5",
|
||||
"@oozcitak/infra": "1.0.5",
|
||||
"@oozcitak/util": "8.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/@oozcitak/dom": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.5.tgz",
|
||||
"integrity": "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "1.0.5",
|
||||
"@oozcitak/url": "1.0.0",
|
||||
"@oozcitak/util": "8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/@oozcitak/dom/node_modules/@oozcitak/util": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
|
||||
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/@oozcitak/util": {
|
||||
"version": "8.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.3.tgz",
|
||||
"integrity": "sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@
|
|||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"html-to-docx": "^1.8.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"imap": "^0.8.19",
|
||||
"joi": "^17.11.0",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,22 @@ import {
|
|||
} from "../types/report";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import {
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
ImageRun,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
WidthType,
|
||||
AlignmentType,
|
||||
VerticalAlign,
|
||||
BorderStyle,
|
||||
PageOrientation,
|
||||
convertMillimetersToTwip,
|
||||
} from "docx";
|
||||
|
||||
export class ReportController {
|
||||
/**
|
||||
|
|
@ -534,6 +550,739 @@ export class ReportController {
|
|||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 데이터를 WORD(DOCX)로 변환
|
||||
* POST /api/admin/reports/export-word
|
||||
*/
|
||||
async exportToWord(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { layoutConfig, queryResults, fileName = "리포트" } = req.body;
|
||||
|
||||
if (!layoutConfig || !layoutConfig.pages) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "레이아웃 데이터가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// mm를 twip으로 변환
|
||||
const mmToTwip = (mm: number) => convertMillimetersToTwip(mm);
|
||||
// px를 twip으로 변환 (1px = 15twip at 96DPI)
|
||||
const pxToTwip = (px: number) => Math.round(px * 15);
|
||||
|
||||
// 쿼리 결과 맵
|
||||
const queryResultsMap: Record<string, { fields: string[]; rows: Record<string, unknown>[] }> =
|
||||
queryResults || {};
|
||||
|
||||
// 컴포넌트 값 가져오기
|
||||
const getComponentValue = (component: any): string => {
|
||||
if (component.queryId && component.fieldName) {
|
||||
const queryResult = queryResultsMap[component.queryId];
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
const value = queryResult.rows[0][component.fieldName];
|
||||
if (value !== null && value !== undefined) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return `{${component.fieldName}}`;
|
||||
}
|
||||
return component.defaultValue || "";
|
||||
};
|
||||
|
||||
// px → half-point 변환 (1px = 0.75pt, Word는 half-pt 단위 사용)
|
||||
// px * 0.75 * 2 = px * 1.5
|
||||
const pxToHalfPt = (px: number) => Math.round(px * 1.5);
|
||||
|
||||
// 셀 내용 생성 헬퍼 함수 (가로 배치용)
|
||||
const createCellContent = (
|
||||
component: any,
|
||||
displayValue: string,
|
||||
pxToHalfPtFn: (px: number) => number,
|
||||
pxToTwipFn: (px: number) => number,
|
||||
queryResultsMapRef: Record<string, { fields: string[]; rows: Record<string, unknown>[] }>,
|
||||
AlignmentTypeRef: typeof AlignmentType,
|
||||
VerticalAlignRef: typeof VerticalAlign,
|
||||
BorderStyleRef: typeof BorderStyle,
|
||||
ParagraphRef: typeof Paragraph,
|
||||
TextRunRef: typeof TextRun,
|
||||
ImageRunRef: typeof ImageRun,
|
||||
TableRef: typeof Table,
|
||||
TableRowRef: typeof TableRow,
|
||||
TableCellRef: typeof TableCell
|
||||
): (Paragraph | Table)[] => {
|
||||
const result: (Paragraph | Table)[] = [];
|
||||
|
||||
// Text/Label
|
||||
if (component.type === "text" || component.type === "label") {
|
||||
const fontSizeHalfPt = pxToHalfPtFn(component.fontSize || 13);
|
||||
const alignment =
|
||||
component.textAlign === "center"
|
||||
? AlignmentTypeRef.CENTER
|
||||
: component.textAlign === "right"
|
||||
? AlignmentTypeRef.RIGHT
|
||||
: AlignmentTypeRef.LEFT;
|
||||
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
alignment,
|
||||
children: [
|
||||
new TextRunRef({
|
||||
text: displayValue,
|
||||
size: fontSizeHalfPt,
|
||||
color: (component.fontColor || "#000000").replace("#", ""),
|
||||
bold: component.fontWeight === "bold" || component.fontWeight === "600",
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Image
|
||||
else if (component.type === "image" && component.imageBase64) {
|
||||
try {
|
||||
const base64Data = component.imageBase64.split(",")[1] || component.imageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
new ImageRunRef({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: Math.round(component.width * 0.75),
|
||||
height: Math.round(component.height * 0.75),
|
||||
},
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
result.push(new ParagraphRef({ children: [] }));
|
||||
}
|
||||
}
|
||||
|
||||
// Signature
|
||||
else if (component.type === "signature") {
|
||||
const sigFontSize = pxToHalfPtFn(component.fontSize || 12);
|
||||
const textRuns: TextRun[] = [];
|
||||
if (component.showLabel !== false) {
|
||||
textRuns.push(new TextRunRef({ text: (component.labelText || "서명:") + " ", size: sigFontSize, font: "맑은 고딕" }));
|
||||
}
|
||||
if (component.imageBase64) {
|
||||
try {
|
||||
const base64Data = component.imageBase64.split(",")[1] || component.imageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
...textRuns,
|
||||
new ImageRunRef({
|
||||
data: imageBuffer,
|
||||
transformation: { width: Math.round(component.width * 0.75), height: Math.round(component.height * 0.75) },
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
textRuns.push(new TextRunRef({ text: "_".repeat(20), size: sigFontSize, font: "맑은 고딕" }));
|
||||
result.push(new ParagraphRef({ children: textRuns }));
|
||||
}
|
||||
} else {
|
||||
textRuns.push(new TextRunRef({ text: "_".repeat(20), size: sigFontSize, font: "맑은 고딕" }));
|
||||
result.push(new ParagraphRef({ children: textRuns }));
|
||||
}
|
||||
}
|
||||
|
||||
// Stamp
|
||||
else if (component.type === "stamp") {
|
||||
const stampFontSize = pxToHalfPtFn(component.fontSize || 12);
|
||||
const textRuns: TextRun[] = [];
|
||||
if (component.personName) {
|
||||
textRuns.push(new TextRunRef({ text: component.personName + " ", size: stampFontSize, font: "맑은 고딕" }));
|
||||
}
|
||||
if (component.imageBase64) {
|
||||
try {
|
||||
const base64Data = component.imageBase64.split(",")[1] || component.imageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
...textRuns,
|
||||
new ImageRunRef({
|
||||
data: imageBuffer,
|
||||
transformation: { width: Math.round(Math.min(component.width, component.height) * 0.75), height: Math.round(Math.min(component.width, component.height) * 0.75) },
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
textRuns.push(new TextRunRef({ text: "(인)", color: "DC2626", size: stampFontSize, font: "맑은 고딕" }));
|
||||
result.push(new ParagraphRef({ children: textRuns }));
|
||||
}
|
||||
} else {
|
||||
textRuns.push(new TextRunRef({ text: "(인)", color: "DC2626", size: stampFontSize, font: "맑은 고딕" }));
|
||||
result.push(new ParagraphRef({ children: textRuns }));
|
||||
}
|
||||
}
|
||||
|
||||
// Divider - 테이블 셀로 감싸서 정확한 너비 적용
|
||||
else if (component.type === "divider" && component.orientation === "horizontal") {
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
border: {
|
||||
bottom: {
|
||||
color: (component.lineColor || "#000000").replace("#", ""),
|
||||
space: 1,
|
||||
style: BorderStyleRef.SINGLE,
|
||||
size: (component.lineWidth || 1) * 8,
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 기타 (빈 paragraph)
|
||||
else {
|
||||
result.push(new ParagraphRef({ children: [] }));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 섹션 생성 (페이지별)
|
||||
const sections = layoutConfig.pages
|
||||
.sort((a: any, b: any) => a.page_order - b.page_order)
|
||||
.map((page: any) => {
|
||||
const pageWidthTwip = mmToTwip(page.width);
|
||||
const pageHeightTwip = mmToTwip(page.height);
|
||||
const marginTopMm = page.margins?.top || 10;
|
||||
const marginBottomMm = page.margins?.bottom || 10;
|
||||
const marginLeftMm = page.margins?.left || 10;
|
||||
const marginRightMm = page.margins?.right || 10;
|
||||
|
||||
const marginTop = mmToTwip(marginTopMm);
|
||||
const marginBottom = mmToTwip(marginBottomMm);
|
||||
const marginLeft = mmToTwip(marginLeftMm);
|
||||
const marginRight = mmToTwip(marginRightMm);
|
||||
|
||||
// 마진을 px로 변환 (1mm ≈ 3.78px at 96 DPI)
|
||||
const marginLeftPx = marginLeftMm * 3.78;
|
||||
const marginTopPx = marginTopMm * 3.78;
|
||||
|
||||
// 컴포넌트를 Y좌표순으로 정렬
|
||||
const sortedComponents = [...(page.components || [])].sort(
|
||||
(a: any, b: any) => a.y - b.y
|
||||
);
|
||||
|
||||
// 같은 Y좌표 범위(±30px)의 컴포넌트들을 그룹화
|
||||
const Y_GROUP_THRESHOLD = 30; // px
|
||||
const componentGroups: any[][] = [];
|
||||
let currentGroup: any[] = [];
|
||||
let groupBaseY = -Infinity;
|
||||
|
||||
for (const comp of sortedComponents) {
|
||||
const compY = comp.y - marginTopPx;
|
||||
if (currentGroup.length === 0) {
|
||||
currentGroup.push(comp);
|
||||
groupBaseY = compY;
|
||||
} else if (Math.abs(compY - groupBaseY) <= Y_GROUP_THRESHOLD) {
|
||||
currentGroup.push(comp);
|
||||
} else {
|
||||
componentGroups.push(currentGroup);
|
||||
currentGroup = [comp];
|
||||
groupBaseY = compY;
|
||||
}
|
||||
}
|
||||
if (currentGroup.length > 0) {
|
||||
componentGroups.push(currentGroup);
|
||||
}
|
||||
|
||||
// 컴포넌트를 Paragraph/Table로 변환
|
||||
const children: (Paragraph | Table)[] = [];
|
||||
|
||||
// Y좌표를 spacing으로 변환하기 위한 추적 변수
|
||||
let lastBottomY = 0;
|
||||
|
||||
// 각 그룹 처리
|
||||
for (const group of componentGroups) {
|
||||
// 그룹 내 컴포넌트들을 X좌표 순으로 정렬
|
||||
const sortedGroup = [...group].sort((a: any, b: any) => a.x - b.x);
|
||||
|
||||
// 그룹의 Y 좌표 (첫 번째 컴포넌트 기준)
|
||||
const groupY = Math.max(0, sortedGroup[0].y - marginTopPx);
|
||||
const groupHeight = Math.max(...sortedGroup.map((c: any) => c.height));
|
||||
|
||||
// spacing 계산
|
||||
const gapFromPrevious = Math.max(0, groupY - lastBottomY);
|
||||
const spacingBefore = pxToTwip(gapFromPrevious);
|
||||
|
||||
// 그룹에 컴포넌트가 여러 개면 하나의 테이블 행으로 배치
|
||||
if (sortedGroup.length > 1) {
|
||||
// spacing을 위한 빈 paragraph
|
||||
if (spacingBefore > 0) {
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, children: [] }));
|
||||
}
|
||||
|
||||
// 각 컴포넌트를 셀로 변환
|
||||
const cells: TableCell[] = [];
|
||||
let prevEndX = 0;
|
||||
|
||||
for (const component of sortedGroup) {
|
||||
const adjustedX = Math.max(0, component.x - marginLeftPx);
|
||||
const displayValue = getComponentValue(component);
|
||||
|
||||
// 이전 셀과의 간격을 위한 빈 셀 추가
|
||||
if (adjustedX > prevEndX + 5) {
|
||||
const gapWidth = adjustedX - prevEndX;
|
||||
cells.push(
|
||||
new TableCell({
|
||||
children: [new Paragraph({ children: [] })],
|
||||
width: { size: pxToTwip(gapWidth), type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 컴포넌트 셀 생성
|
||||
const cellContent = createCellContent(component, displayValue, pxToHalfPt, pxToTwip, queryResultsMap, AlignmentType, VerticalAlign, BorderStyle, Paragraph, TextRun, ImageRun, Table, TableRow, TableCell);
|
||||
cells.push(
|
||||
new TableCell({
|
||||
children: cellContent,
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
verticalAlign: VerticalAlign.TOP,
|
||||
})
|
||||
);
|
||||
prevEndX = adjustedX + component.width;
|
||||
}
|
||||
|
||||
// 테이블 행 생성
|
||||
const rowTable = new Table({
|
||||
rows: [new TableRow({ children: cells })],
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
children.push(rowTable);
|
||||
lastBottomY = groupY + groupHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 단일 컴포넌트 처리 (기존 로직)
|
||||
const component = sortedGroup[0];
|
||||
const displayValue = getComponentValue(component);
|
||||
const adjustedX = Math.max(0, component.x - marginLeftPx);
|
||||
const adjustedY = groupY;
|
||||
|
||||
// X좌표를 indent로 변환 (마진 제외한 순수 들여쓰기)
|
||||
const indentLeft = pxToTwip(adjustedX);
|
||||
|
||||
// Text/Label 컴포넌트 - 테이블 셀로 감싸서 width 내 줄바꿈 적용
|
||||
if (component.type === "text" || component.type === "label") {
|
||||
const fontSizeHalfPt = pxToHalfPt(component.fontSize || 13);
|
||||
const alignment =
|
||||
component.textAlign === "center"
|
||||
? AlignmentType.CENTER
|
||||
: component.textAlign === "right"
|
||||
? AlignmentType.RIGHT
|
||||
: AlignmentType.LEFT;
|
||||
|
||||
// 테이블 셀로 감싸서 width 제한 → 자동 줄바꿈
|
||||
const textCell = new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: displayValue,
|
||||
size: fontSizeHalfPt,
|
||||
color: (component.fontColor || "#000000").replace("#", ""),
|
||||
bold: component.fontWeight === "bold" || component.fontWeight === "600",
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
verticalAlign: VerticalAlign.TOP,
|
||||
});
|
||||
|
||||
const textTable = new Table({
|
||||
rows: [new TableRow({ children: [textCell] })],
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
indent: { size: indentLeft, type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
|
||||
// spacing을 위한 빈 paragraph
|
||||
if (spacingBefore > 0) {
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, children: [] }));
|
||||
}
|
||||
children.push(textTable);
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// Image 컴포넌트
|
||||
else if (component.type === "image" && component.imageBase64) {
|
||||
try {
|
||||
const base64Data = component.imageBase64.split(",")[1] || component.imageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
|
||||
const paragraph = new Paragraph({
|
||||
spacing: { before: spacingBefore, after: 0 },
|
||||
indent: { left: indentLeft },
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: Math.round(component.width * 0.75),
|
||||
height: Math.round(component.height * 0.75),
|
||||
},
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
});
|
||||
children.push(paragraph);
|
||||
lastBottomY = adjustedY + component.height;
|
||||
} catch (imgError) {
|
||||
console.error("이미지 처리 오류:", imgError);
|
||||
}
|
||||
}
|
||||
|
||||
// Divider 컴포넌트 - 테이블 셀로 감싸서 정확한 위치와 너비 적용
|
||||
else if (component.type === "divider") {
|
||||
if (component.orientation === "horizontal") {
|
||||
// spacing을 위한 빈 paragraph
|
||||
if (spacingBefore > 0) {
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, children: [] }));
|
||||
}
|
||||
|
||||
// 테이블 셀로 감싸서 너비 제한
|
||||
const dividerCell = new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
border: {
|
||||
bottom: {
|
||||
color: (component.lineColor || "#000000").replace("#", ""),
|
||||
space: 1,
|
||||
style: BorderStyle.SINGLE,
|
||||
size: (component.lineWidth || 1) * 8,
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
}),
|
||||
],
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
|
||||
const dividerTable = new Table({
|
||||
rows: [new TableRow({ children: [dividerCell] })],
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
indent: { size: indentLeft, type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
children.push(dividerTable);
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
}
|
||||
|
||||
// Signature 컴포넌트
|
||||
else if (component.type === "signature") {
|
||||
const labelText = component.labelText || "서명:";
|
||||
const showLabel = component.showLabel !== false;
|
||||
const sigFontSize = pxToHalfPt(component.fontSize || 12);
|
||||
const textRuns: TextRun[] = [];
|
||||
|
||||
if (showLabel) {
|
||||
textRuns.push(
|
||||
new TextRun({
|
||||
text: labelText + " ",
|
||||
size: sigFontSize,
|
||||
font: "맑은 고딕",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (component.imageBase64) {
|
||||
try {
|
||||
const base64Data = component.imageBase64.split(",")[1] || component.imageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
|
||||
const paragraph = new Paragraph({
|
||||
spacing: { before: spacingBefore, after: 0 },
|
||||
indent: { left: indentLeft },
|
||||
children: [
|
||||
...textRuns,
|
||||
new ImageRun({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: Math.round(component.width * 0.75),
|
||||
height: Math.round(component.height * 0.75),
|
||||
},
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
});
|
||||
children.push(paragraph);
|
||||
} catch (imgError) {
|
||||
console.error("서명 이미지 오류:", imgError);
|
||||
textRuns.push(new TextRun({ text: "_".repeat(20), size: sigFontSize, font: "맑은 고딕" }));
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, indent: { left: indentLeft }, children: textRuns }));
|
||||
}
|
||||
} else {
|
||||
textRuns.push(new TextRun({ text: "_".repeat(20), size: sigFontSize, font: "맑은 고딕" }));
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, indent: { left: indentLeft }, children: textRuns }));
|
||||
}
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// Stamp 컴포넌트
|
||||
else if (component.type === "stamp") {
|
||||
const personName = component.personName || "";
|
||||
const stampFontSize = pxToHalfPt(component.fontSize || 12);
|
||||
const textRuns: TextRun[] = [];
|
||||
|
||||
if (personName) {
|
||||
textRuns.push(
|
||||
new TextRun({
|
||||
text: personName + " ",
|
||||
size: stampFontSize,
|
||||
font: "맑은 고딕",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (component.imageBase64) {
|
||||
try {
|
||||
const base64Data = component.imageBase64.split(",")[1] || component.imageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
|
||||
const paragraph = new Paragraph({
|
||||
spacing: { before: spacingBefore, after: 0 },
|
||||
indent: { left: indentLeft },
|
||||
children: [
|
||||
...textRuns,
|
||||
new ImageRun({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: Math.round(Math.min(component.width, component.height) * 0.75),
|
||||
height: Math.round(Math.min(component.width, component.height) * 0.75),
|
||||
},
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
});
|
||||
children.push(paragraph);
|
||||
} catch (imgError) {
|
||||
console.error("도장 이미지 오류:", imgError);
|
||||
textRuns.push(new TextRun({ text: "(인)", color: "DC2626", size: stampFontSize, font: "맑은 고딕" }));
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, indent: { left: indentLeft }, children: textRuns }));
|
||||
}
|
||||
} else {
|
||||
textRuns.push(new TextRun({ text: "(인)", color: "DC2626", size: stampFontSize, font: "맑은 고딕" }));
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, indent: { left: indentLeft }, children: textRuns }));
|
||||
}
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// Table 컴포넌트
|
||||
else if (component.type === "table" && component.queryId) {
|
||||
const queryResult = queryResultsMap[component.queryId];
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
// 테이블 앞에 spacing과 indent를 위한 빈 paragraph 추가
|
||||
if (spacingBefore > 0 || indentLeft > 0) {
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, indent: { left: indentLeft }, children: [] }));
|
||||
}
|
||||
|
||||
const columns =
|
||||
component.tableColumns && component.tableColumns.length > 0
|
||||
? component.tableColumns
|
||||
: queryResult.fields.map((field: string) => ({
|
||||
field,
|
||||
header: field,
|
||||
align: "left",
|
||||
width: undefined,
|
||||
}));
|
||||
|
||||
// 테이블 폰트 사이즈 (기본 12px)
|
||||
const tableFontSize = pxToHalfPt(component.fontSize || 12);
|
||||
|
||||
// 헤더 행
|
||||
const headerCells = columns.map(
|
||||
(col: { header: string; align?: string }) =>
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment:
|
||||
col.align === "center"
|
||||
? AlignmentType.CENTER
|
||||
: col.align === "right"
|
||||
? AlignmentType.RIGHT
|
||||
: AlignmentType.LEFT,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: col.header,
|
||||
bold: true,
|
||||
size: tableFontSize,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
shading: {
|
||||
fill: (component.headerBackgroundColor || "#f3f4f6").replace("#", ""),
|
||||
},
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
})
|
||||
);
|
||||
const headerRow = new TableRow({ children: headerCells });
|
||||
|
||||
// 데이터 행
|
||||
const dataRows = queryResult.rows.map(
|
||||
(row: Record<string, unknown>) =>
|
||||
new TableRow({
|
||||
children: columns.map(
|
||||
(col: { field: string; align?: string }) =>
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment:
|
||||
col.align === "center"
|
||||
? AlignmentType.CENTER
|
||||
: col.align === "right"
|
||||
? AlignmentType.RIGHT
|
||||
: AlignmentType.LEFT,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: String(row[col.field] ?? ""),
|
||||
size: tableFontSize,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
indent: { size: indentLeft, type: WidthType.DXA },
|
||||
rows: [headerRow, ...dataRows],
|
||||
});
|
||||
children.push(table);
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 빈 페이지 방지
|
||||
if (children.length === 0) {
|
||||
children.push(new Paragraph({ children: [] }));
|
||||
}
|
||||
|
||||
return {
|
||||
properties: {
|
||||
page: {
|
||||
size: {
|
||||
width: pageWidthTwip,
|
||||
height: pageHeightTwip,
|
||||
orientation:
|
||||
page.width > page.height ? PageOrientation.LANDSCAPE : PageOrientation.PORTRAIT,
|
||||
},
|
||||
margin: {
|
||||
top: marginTop,
|
||||
bottom: marginBottom,
|
||||
left: marginLeft,
|
||||
right: marginRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
children,
|
||||
};
|
||||
});
|
||||
|
||||
// Document 생성
|
||||
const doc = new Document({
|
||||
sections,
|
||||
});
|
||||
|
||||
// Buffer로 변환
|
||||
const docxBuffer = await Packer.toBuffer(doc);
|
||||
|
||||
// 파일명 인코딩 (한글 지원)
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const safeFileName = encodeURIComponent(`${fileName}_${timestamp}.docx`);
|
||||
|
||||
// DOCX 파일로 응답
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename*=UTF-8''${safeFileName}`
|
||||
);
|
||||
res.setHeader("Content-Length", docxBuffer.length);
|
||||
|
||||
return res.send(docxBuffer);
|
||||
} catch (error: any) {
|
||||
console.error("WORD 변환 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "WORD 변환에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportController();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ router.post("/upload-image", upload.single("image"), (req, res, next) =>
|
|||
reportController.uploadImage(req, res, next)
|
||||
);
|
||||
|
||||
// WORD(DOCX) 내보내기
|
||||
router.post("/export-word", (req, res, next) =>
|
||||
reportController.exportToWord(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 목록
|
||||
router.get("/", (req, res, next) =>
|
||||
reportController.getReports(req, res, next)
|
||||
|
|
|
|||
|
|
@ -13,21 +13,6 @@ import { Printer, FileDown, FileText } from "lucide-react";
|
|||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
// @ts-ignore - docx 라이브러리 타입 이슈
|
||||
import {
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
WidthType,
|
||||
ImageRun,
|
||||
AlignmentType,
|
||||
VerticalAlign,
|
||||
convertInchesToTwip,
|
||||
} from "docx";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
|
||||
interface ReportPreviewModalProps {
|
||||
|
|
@ -282,270 +267,93 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
});
|
||||
};
|
||||
|
||||
// Base64를 Uint8Array로 변환
|
||||
const base64ToUint8Array = (base64: string): Uint8Array => {
|
||||
const base64Data = base64.split(",")[1] || base64;
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
// 이미지 URL을 Base64로 변환
|
||||
const imageUrlToBase64 = async (url: string): Promise<string> => {
|
||||
try {
|
||||
// 이미 Base64인 경우 그대로 반환
|
||||
if (url.startsWith("data:")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 컴포넌트를 TableCell로 변환
|
||||
const createTableCell = (component: any, pageWidth: number, widthPercent?: number): TableCell | null => {
|
||||
const cellWidth = widthPercent || 100;
|
||||
// 서버 이미지 URL을 fetch하여 Base64로 변환
|
||||
const fullUrl = getFullImageUrl(url);
|
||||
const response = await fetch(fullUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
if (component.type === "text" || component.type === "label") {
|
||||
const value = getComponentValue(component);
|
||||
return new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: value,
|
||||
size: (component.fontSize || 13) * 2,
|
||||
color: component.fontColor?.replace("#", "") || "000000",
|
||||
bold: component.fontWeight === "bold",
|
||||
}),
|
||||
],
|
||||
alignment:
|
||||
component.textAlign === "center"
|
||||
? AlignmentType.CENTER
|
||||
: component.textAlign === "right"
|
||||
? AlignmentType.RIGHT
|
||||
: AlignmentType.LEFT,
|
||||
}),
|
||||
],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
} else if (component.type === "signature" || component.type === "stamp") {
|
||||
if (component.imageUrl) {
|
||||
try {
|
||||
const imageData = base64ToUint8Array(component.imageUrl);
|
||||
return new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: imageData,
|
||||
transformation: {
|
||||
width: component.width || 150,
|
||||
height: component.height || 50,
|
||||
},
|
||||
}),
|
||||
],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `[${component.type === "signature" ? "서명" : "도장"}]`,
|
||||
size: 24,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (component.type === "table" && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows.length > 0) {
|
||||
const headerCells = queryResult.fields.map(
|
||||
(field) =>
|
||||
new TableCell({
|
||||
children: [new Paragraph({ text: field })],
|
||||
width: { size: 100 / queryResult.fields.length, type: WidthType.PERCENTAGE },
|
||||
}),
|
||||
);
|
||||
|
||||
const dataRows = queryResult.rows.map(
|
||||
(row) =>
|
||||
new TableRow({
|
||||
children: queryResult.fields.map(
|
||||
(field) =>
|
||||
new TableCell({
|
||||
children: [new Paragraph({ text: String(row[field] ?? "") })],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
rows: [new TableRow({ children: headerCells }), ...dataRows],
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
});
|
||||
|
||||
return new TableCell({
|
||||
children: [table],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("이미지 변환 실패:", error);
|
||||
return "";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// WORD 다운로드
|
||||
// WORD 다운로드 (백엔드 API 사용 - 컴포넌트 데이터 전송)
|
||||
const handleDownloadWord = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
// 페이지별로 섹션 생성
|
||||
const sections = layoutConfig.pages
|
||||
.sort((a, b) => a.page_order - b.page_order)
|
||||
.map((page) => {
|
||||
// 페이지 크기 설정 (A4 기준)
|
||||
const pageWidth = convertInchesToTwip(8.27); // A4 width in inches
|
||||
const pageHeight = convertInchesToTwip(11.69); // A4 height in inches
|
||||
const marginTop = convertInchesToTwip(page.margins.top / 96); // px to inches (96 DPI)
|
||||
const marginBottom = convertInchesToTwip(page.margins.bottom / 96);
|
||||
const marginLeft = convertInchesToTwip(page.margins.left / 96);
|
||||
const marginRight = convertInchesToTwip(page.margins.right / 96);
|
||||
|
||||
// 페이지 내 컴포넌트를 Y좌표 기준으로 정렬
|
||||
const sortedComponents = [...page.components].sort((a, b) => {
|
||||
// Y좌표 우선, 같으면 X좌표
|
||||
if (Math.abs(a.y - b.y) < 5) {
|
||||
return a.x - b.x;
|
||||
}
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
// 컴포넌트들을 행으로 그룹화 (같은 Y 범위 = 같은 행)
|
||||
const rows: Array<Array<(typeof sortedComponents)[0]>> = [];
|
||||
const rowTolerance = 20; // Y 좌표 허용 오차
|
||||
|
||||
for (const component of sortedComponents) {
|
||||
const existingRow = rows.find((row) => Math.abs(row[0].y - component.y) < rowTolerance);
|
||||
if (existingRow) {
|
||||
existingRow.push(component);
|
||||
} else {
|
||||
rows.push([component]);
|
||||
}
|
||||
}
|
||||
|
||||
// 각 행 내에서 X좌표로 정렬
|
||||
rows.forEach((row) => row.sort((a, b) => a.x - b.x));
|
||||
|
||||
// 페이지 전체를 큰 테이블로 구성 (레이아웃 제어용)
|
||||
const tableRows: TableRow[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.length === 1) {
|
||||
// 단일 컴포넌트 - 전체 너비 사용
|
||||
const component = row[0];
|
||||
const cell = createTableCell(component, pageWidth);
|
||||
if (cell) {
|
||||
tableRows.push(
|
||||
new TableRow({
|
||||
children: [cell],
|
||||
height: { value: component.height * 15, rule: 1 }, // 최소 높이 설정
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 여러 컴포넌트 - 가로 배치
|
||||
const cells: TableCell[] = [];
|
||||
const totalWidth = row.reduce((sum, c) => sum + c.width, 0);
|
||||
|
||||
for (const component of row) {
|
||||
const widthPercent = (component.width / totalWidth) * 100;
|
||||
const cell = createTableCell(component, pageWidth, widthPercent);
|
||||
if (cell) {
|
||||
cells.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
if (cells.length > 0) {
|
||||
const maxHeight = Math.max(...row.map((c) => c.height));
|
||||
tableRows.push(
|
||||
new TableRow({
|
||||
children: cells,
|
||||
height: { value: maxHeight * 15, rule: 1 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
properties: {
|
||||
page: {
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
margin: {
|
||||
top: marginTop,
|
||||
bottom: marginBottom,
|
||||
left: marginLeft,
|
||||
right: marginRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
children:
|
||||
tableRows.length > 0
|
||||
? [
|
||||
new Table({
|
||||
rows: tableRows,
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: 0, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [new Paragraph({ text: "" })],
|
||||
};
|
||||
});
|
||||
|
||||
// 문서 생성
|
||||
const doc = new Document({
|
||||
sections,
|
||||
toast({
|
||||
title: "처리 중",
|
||||
description: "WORD 파일을 생성하고 있습니다...",
|
||||
});
|
||||
|
||||
// Blob 생성 및 다운로드
|
||||
const blob = await Packer.toBlob(doc);
|
||||
const fileName = reportDetail?.report?.report_name_kor || "리포트";
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
// 이미지를 Base64로 변환하여 컴포넌트 데이터에 포함
|
||||
const pagesWithBase64 = await Promise.all(
|
||||
layoutConfig.pages.map(async (page) => {
|
||||
const componentsWithBase64 = await Promise.all(
|
||||
page.components.map(async (component) => {
|
||||
// 이미지가 있는 컴포넌트는 Base64로 변환
|
||||
if (component.imageUrl) {
|
||||
try {
|
||||
const base64 = await imageUrlToBase64(component.imageUrl);
|
||||
return { ...component, imageBase64: base64 };
|
||||
} catch {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
return { ...page, components: componentsWithBase64 };
|
||||
}),
|
||||
);
|
||||
|
||||
// 쿼리 결과 수집
|
||||
const queryResults: Record<string, { fields: string[]; rows: Record<string, unknown>[] }> = {};
|
||||
for (const page of layoutConfig.pages) {
|
||||
for (const component of page.components) {
|
||||
if (component.queryId) {
|
||||
const result = getQueryResult(component.queryId);
|
||||
if (result) {
|
||||
queryResults[component.queryId] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = reportDetail?.report?.report_name_kor || "리포트";
|
||||
|
||||
// 백엔드 API 호출 (컴포넌트 데이터 전송)
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const response = await apiClient.post(
|
||||
"/admin/reports/export-word",
|
||||
{
|
||||
layoutConfig: { ...layoutConfig, pages: pagesWithBase64 },
|
||||
queryResults,
|
||||
fileName,
|
||||
},
|
||||
{ responseType: "blob" },
|
||||
);
|
||||
|
||||
// Blob 다운로드
|
||||
const blob = new Blob([response.data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
});
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
|
|
@ -558,6 +366,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
description: "WORD 파일이 다운로드되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("WORD 변환 오류:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "WORD 생성에 실패했습니다.";
|
||||
toast({
|
||||
title: "오류",
|
||||
|
|
|
|||
Loading…
Reference in New Issue