Przeglądaj źródła

update 2024-05-14

junggilpark 1 rok temu
rodzic
commit
11b7357745
5 zmienionych plików z 1603 dodań i 246 usunięć
  1. 724 1
      package-lock.json
  2. 6 1
      package.json
  3. 427 92
      src/app.js
  4. 1 1
      src/static/styles/custom.css
  5. 445 151
      src/views/hello.html

+ 724 - 1
package-lock.json

@@ -17,6 +17,7 @@
                 "axios": "^1.6.8",
                 "cookie-parser": "^1.4.6",
                 "cors": "^2.8.5",
+                "docx": "^8.5.0",
                 "dote": "^1.1.0",
                 "dotenv": "^16.4.5",
                 "env": "^0.0.2",
@@ -28,7 +29,11 @@
                 "path": "^0.12.7",
                 "restify": "^11.1.0",
                 "send": "^0.18.0",
-                "unzipper": "^0.11.4"
+                "unzipper": "^0.11.4",
+                "websocket": "^1.0.35",
+                "write-excel-file": "^2.0.1",
+                "wss": "^3.3.4",
+                "xlsx": "^0.18.5"
             },
             "devDependencies": {
                 "env-cmd": "^10.1.0",
@@ -423,6 +428,14 @@
                 "node": ">=14"
             }
         },
+        "node_modules/@types/node": {
+            "version": "20.12.12",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
+            "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
+            "dependencies": {
+                "undici-types": "~5.26.4"
+            }
+        },
         "node_modules/abbrev": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -452,6 +465,14 @@
                 "node": ">= 0.6"
             }
         },
+        "node_modules/adler-32": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+            "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
         "node_modules/agent-base": {
             "version": "7.1.1",
             "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
@@ -484,6 +505,15 @@
             "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
             "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         },
+        "node_modules/amdefine": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+            "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.4.2"
+            }
+        },
         "node_modules/ansi-regex": {
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@@ -558,6 +588,15 @@
                 "node": ">= 14"
             }
         },
+        "node_modules/argparse": {
+            "version": "1.0.10",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+            "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+            "optional": true,
+            "dependencies": {
+                "sprintf-js": "~1.0.2"
+            }
+        },
         "node_modules/array-flatten": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -787,6 +826,18 @@
             "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
             "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
         },
+        "node_modules/bufferutil": {
+            "version": "4.0.8",
+            "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
+            "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "node-gyp-build": "^4.3.0"
+            },
+            "engines": {
+                "node": ">=6.14.2"
+            }
+        },
         "node_modules/busboy": {
             "version": "1.6.0",
             "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -824,6 +875,18 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/cfb": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+            "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+            "dependencies": {
+                "adler-32": "~1.3.0",
+                "crc-32": "~1.2.0"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
         "node_modules/chokidar": {
             "version": "3.6.0",
             "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -848,6 +911,14 @@
                 "fsevents": "~2.3.2"
             }
         },
+        "node_modules/codepage": {
+            "version": "1.15.0",
+            "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+            "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
         "node_modules/color-convert": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1071,6 +1142,18 @@
             "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.6.tgz",
             "integrity": "sha512-h2V2XZ3uOTLilF5dPIptgUfN/o2ia/80Ie0Lly18LAnw5s8Eb7kt8rfxSUy24AztJZas9f6DPZpVlzDUtFt/ag=="
         },
+        "node_modules/d": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+            "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+            "dependencies": {
+                "es5-ext": "^0.10.64",
+                "type": "^2.7.2"
+            },
+            "engines": {
+                "node": ">=0.12"
+            }
+        },
         "node_modules/dashdash": {
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -1091,6 +1174,12 @@
                 "ms": "^2.1.1"
             }
         },
+        "node_modules/deep-is": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+            "optional": true
+        },
         "node_modules/define-data-property": {
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -1145,6 +1234,21 @@
             "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
             "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
         },
+        "node_modules/docx": {
+            "version": "8.5.0",
+            "resolved": "https://registry.npmjs.org/docx/-/docx-8.5.0.tgz",
+            "integrity": "sha512-4SbcbedPXTciySXiSnNNLuJXpvxFe5nqivbiEHXyL8P/w0wx2uW7YXNjnYgjW0e2e6vy+L/tMISU/oAiXCl57Q==",
+            "dependencies": {
+                "@types/node": "^20.3.1",
+                "jszip": "^3.10.1",
+                "nanoid": "^5.0.4",
+                "xml": "^1.0.1",
+                "xml-js": "^1.6.8"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/dote": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/dote/-/dote-1.1.0.tgz",
@@ -1298,6 +1402,43 @@
                 "node": ">= 0.4"
             }
         },
+        "node_modules/es5-ext": {
+            "version": "0.10.64",
+            "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+            "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "es6-iterator": "^2.0.3",
+                "es6-symbol": "^3.1.3",
+                "esniff": "^2.0.1",
+                "next-tick": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/es6-iterator": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+            "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+            "dependencies": {
+                "d": "1",
+                "es5-ext": "^0.10.35",
+                "es6-symbol": "^3.1.1"
+            }
+        },
+        "node_modules/es6-symbol": {
+            "version": "3.1.4",
+            "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+            "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+            "dependencies": {
+                "d": "^1.0.2",
+                "ext": "^1.7.0"
+            },
+            "engines": {
+                "node": ">=0.12"
+            }
+        },
         "node_modules/escape-html": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1308,6 +1449,73 @@
             "resolved": "https://registry.npmjs.org/escape-regexp-component/-/escape-regexp-component-1.0.2.tgz",
             "integrity": "sha512-B0yxafj1D1ZTNEHkFoQxz4iboZSfaZHhaNhIug7GcUCL4ZUrVSJZTmWUAkPOFaYDfi3RNT9XM082TuGE6jpmiQ=="
         },
+        "node_modules/escodegen": {
+            "version": "1.8.1",
+            "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
+            "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==",
+            "optional": true,
+            "dependencies": {
+                "esprima": "^2.7.1",
+                "estraverse": "^1.9.1",
+                "esutils": "^2.0.2",
+                "optionator": "^0.8.1"
+            },
+            "bin": {
+                "escodegen": "bin/escodegen.js",
+                "esgenerate": "bin/esgenerate.js"
+            },
+            "engines": {
+                "node": ">=0.12.0"
+            },
+            "optionalDependencies": {
+                "source-map": "~0.2.0"
+            }
+        },
+        "node_modules/esniff": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+            "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+            "dependencies": {
+                "d": "^1.0.1",
+                "es5-ext": "^0.10.62",
+                "event-emitter": "^0.3.5",
+                "type": "^2.7.2"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/esprima": {
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+            "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==",
+            "optional": true,
+            "bin": {
+                "esparse": "bin/esparse.js",
+                "esvalidate": "bin/esvalidate.js"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/estraverse": {
+            "version": "1.9.3",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+            "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/esutils": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/etag": {
             "version": "1.8.1",
             "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1316,6 +1524,15 @@
                 "node": ">= 0.6"
             }
         },
+        "node_modules/event-emitter": {
+            "version": "0.3.5",
+            "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+            "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+            "dependencies": {
+                "d": "1",
+                "es5-ext": "~0.10.14"
+            }
+        },
         "node_modules/event-target-shim": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@@ -1460,6 +1677,14 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/ext": {
+            "version": "1.7.0",
+            "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+            "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+            "dependencies": {
+                "type": "^2.7.2"
+            }
+        },
         "node_modules/extsprintf": {
             "version": "1.3.0",
             "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -1483,6 +1708,12 @@
             "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
             "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
         },
+        "node_modules/fast-levenshtein": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+            "optional": true
+        },
         "node_modules/fast-querystring": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
@@ -1499,6 +1730,11 @@
                 "node": ">=6"
             }
         },
+        "node_modules/file-saver": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+            "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+        },
         "node_modules/fill-range": {
             "version": "7.0.1",
             "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1618,6 +1854,14 @@
                 "node": ">= 0.6"
             }
         },
+        "node_modules/frac": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+            "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
         "node_modules/fresh": {
             "version": "0.5.2",
             "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -1774,6 +2018,36 @@
             "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
             "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="
         },
+        "node_modules/handlebars": {
+            "version": "4.7.8",
+            "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+            "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+            "optional": true,
+            "dependencies": {
+                "minimist": "^1.2.5",
+                "neo-async": "^2.6.2",
+                "source-map": "^0.6.1",
+                "wordwrap": "^1.0.0"
+            },
+            "bin": {
+                "handlebars": "bin/handlebars"
+            },
+            "engines": {
+                "node": ">=0.4.7"
+            },
+            "optionalDependencies": {
+                "uglify-js": "^3.1.4"
+            }
+        },
+        "node_modules/handlebars/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==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/has-flag": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -2102,6 +2376,11 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/is-typedarray": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+            "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
+        },
         "node_modules/is-wsl": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -2123,6 +2402,105 @@
             "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
             "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
         },
+        "node_modules/istanbul": {
+            "version": "0.4.5",
+            "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz",
+            "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==",
+            "deprecated": "This module is no longer maintained, try this instead:\n  npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.",
+            "optional": true,
+            "dependencies": {
+                "abbrev": "1.0.x",
+                "async": "1.x",
+                "escodegen": "1.8.x",
+                "esprima": "2.7.x",
+                "glob": "^5.0.15",
+                "handlebars": "^4.0.1",
+                "js-yaml": "3.x",
+                "mkdirp": "0.5.x",
+                "nopt": "3.x",
+                "once": "1.x",
+                "resolve": "1.1.x",
+                "supports-color": "^3.1.0",
+                "which": "^1.1.1",
+                "wordwrap": "^1.0.0"
+            },
+            "bin": {
+                "istanbul": "lib/cli.js"
+            }
+        },
+        "node_modules/istanbul/node_modules/abbrev": {
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
+            "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==",
+            "optional": true
+        },
+        "node_modules/istanbul/node_modules/async": {
+            "version": "1.5.2",
+            "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+            "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==",
+            "optional": true
+        },
+        "node_modules/istanbul/node_modules/glob": {
+            "version": "5.0.15",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+            "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==",
+            "optional": true,
+            "dependencies": {
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "2 || 3",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/istanbul/node_modules/has-flag": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+            "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/istanbul/node_modules/nopt": {
+            "version": "3.0.6",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+            "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==",
+            "optional": true,
+            "dependencies": {
+                "abbrev": "1"
+            },
+            "bin": {
+                "nopt": "bin/nopt.js"
+            }
+        },
+        "node_modules/istanbul/node_modules/supports-color": {
+            "version": "3.2.3",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+            "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==",
+            "optional": true,
+            "dependencies": {
+                "has-flag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
+        "node_modules/istanbul/node_modules/which": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+            "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+            "optional": true,
+            "dependencies": {
+                "isexe": "^2.0.0"
+            },
+            "bin": {
+                "which": "bin/which"
+            }
+        },
         "node_modules/jackspeak": {
             "version": "2.3.6",
             "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -2140,6 +2518,32 @@
                 "@pkgjs/parseargs": "^0.11.0"
             }
         },
+        "node_modules/js-yaml": {
+            "version": "3.14.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+            "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+            "optional": true,
+            "dependencies": {
+                "argparse": "^1.0.7",
+                "esprima": "^4.0.0"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/js-yaml/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==",
+            "optional": true,
+            "bin": {
+                "esparse": "bin/esparse.js",
+                "esvalidate": "bin/esvalidate.js"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/jsbn": {
             "version": "0.1.1",
             "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
@@ -2332,6 +2736,19 @@
                 "safe-buffer": "~5.1.0"
             }
         },
+        "node_modules/levn": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+            "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+            "optional": true,
+            "dependencies": {
+                "prelude-ls": "~1.1.2",
+                "type-check": "~0.3.2"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/lie": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
@@ -2510,6 +2927,23 @@
             "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
             "optional": true
         },
+        "node_modules/nanoid": {
+            "version": "5.0.7",
+            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
+            "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "bin": {
+                "nanoid": "bin/nanoid.js"
+            },
+            "engines": {
+                "node": "^18 || >=20"
+            }
+        },
         "node_modules/negotiator": {
             "version": "0.6.3",
             "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -2518,6 +2952,27 @@
                 "node": ">= 0.6"
             }
         },
+        "node_modules/neo-async": {
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+            "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+            "optional": true
+        },
+        "node_modules/next-tick": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+            "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+        },
+        "node_modules/node-gyp-build": {
+            "version": "4.8.1",
+            "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
+            "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
+            "bin": {
+                "node-gyp-build": "bin.js",
+                "node-gyp-build-optional": "optional.js",
+                "node-gyp-build-test": "build-test.js"
+            }
+        },
         "node_modules/nodemon": {
             "version": "2.0.22",
             "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
@@ -2641,6 +3096,23 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/optionator": {
+            "version": "0.8.3",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+            "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+            "optional": true,
+            "dependencies": {
+                "deep-is": "~0.1.3",
+                "fast-levenshtein": "~2.0.6",
+                "levn": "~0.3.0",
+                "prelude-ls": "~1.1.2",
+                "type-check": "~0.3.2",
+                "word-wrap": "~1.2.3"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/pako": {
             "version": "1.0.11",
             "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -2765,6 +3237,15 @@
             "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz",
             "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="
         },
+        "node_modules/prelude-ls": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+            "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+            "optional": true,
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/process": {
             "version": "0.11.10",
             "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -2935,6 +3416,12 @@
             "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
             "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
         },
+        "node_modules/resolve": {
+            "version": "1.1.7",
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+            "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==",
+            "optional": true
+        },
         "node_modules/restify": {
             "version": "11.1.0",
             "resolved": "https://registry.npmjs.org/restify/-/restify-11.1.0.tgz",
@@ -3094,6 +3581,11 @@
             "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
             "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
         },
+        "node_modules/sax": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
+            "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
+        },
         "node_modules/select-hose": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -3271,6 +3763,18 @@
                 "atomic-sleep": "^1.0.0"
             }
         },
+        "node_modules/source-map": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+            "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==",
+            "optional": true,
+            "dependencies": {
+                "amdefine": ">=0.0.4"
+            },
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
         "node_modules/spdy": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
@@ -3362,6 +3866,23 @@
                 "node": ">= 10.x"
             }
         },
+        "node_modules/sprintf-js": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+            "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+            "optional": true
+        },
+        "node_modules/ssf": {
+            "version": "0.11.2",
+            "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+            "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+            "dependencies": {
+                "frac": "~1.1.2"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
         "node_modules/sshpk": {
             "version": "1.18.0",
             "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -3596,6 +4117,23 @@
             "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
             "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
         },
+        "node_modules/type": {
+            "version": "2.7.2",
+            "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+            "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
+        },
+        "node_modules/type-check": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+            "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+            "optional": true,
+            "dependencies": {
+                "prelude-ls": "~1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/type-is": {
             "version": "1.6.18",
             "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -3613,6 +4151,26 @@
             "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
             "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
         },
+        "node_modules/typedarray-to-buffer": {
+            "version": "3.1.5",
+            "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+            "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+            "dependencies": {
+                "is-typedarray": "^1.0.0"
+            }
+        },
+        "node_modules/uglify-js": {
+            "version": "3.17.4",
+            "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+            "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+            "optional": true,
+            "bin": {
+                "uglifyjs": "bin/uglifyjs"
+            },
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
         "node_modules/uid-safe": {
             "version": "2.1.5",
             "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -3624,12 +4182,22 @@
                 "node": ">= 0.8"
             }
         },
+        "node_modules/ultron": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+            "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
+        },
         "node_modules/undefsafe": {
             "version": "2.0.5",
             "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
             "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
             "dev": true
         },
+        "node_modules/undici-types": {
+            "version": "5.26.5",
+            "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+            "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+        },
         "node_modules/unpipe": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3650,6 +4218,18 @@
                 "graceful-fs": "^4.2.2"
             }
         },
+        "node_modules/utf-8-validate": {
+            "version": "5.0.10",
+            "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
+            "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "node-gyp-build": "^4.3.0"
+            },
+            "engines": {
+                "node": ">=6.14.2"
+            }
+        },
         "node_modules/util": {
             "version": "0.10.4",
             "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
@@ -3728,6 +4308,35 @@
                 "minimalistic-assert": "^1.0.0"
             }
         },
+        "node_modules/websocket": {
+            "version": "1.0.35",
+            "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
+            "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
+            "dependencies": {
+                "bufferutil": "^4.0.1",
+                "debug": "^2.2.0",
+                "es5-ext": "^0.10.63",
+                "typedarray-to-buffer": "^3.1.5",
+                "utf-8-validate": "^5.0.2",
+                "yaeti": "^0.0.6"
+            },
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
+        "node_modules/websocket/node_modules/debug": {
+            "version": "2.6.9",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+            "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+            "dependencies": {
+                "ms": "2.0.0"
+            }
+        },
+        "node_modules/websocket/node_modules/ms": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+            "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+        },
         "node_modules/which": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3742,6 +4351,37 @@
                 "node": ">= 8"
             }
         },
+        "node_modules/wmf": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+            "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/word": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+            "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/word-wrap": {
+            "version": "1.2.5",
+            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+            "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/wordwrap": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+            "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+            "optional": true
+        },
         "node_modules/wrap-ansi": {
             "version": "8.1.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -3831,6 +4471,81 @@
             "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
             "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
         },
+        "node_modules/write-excel-file": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/write-excel-file/-/write-excel-file-2.0.1.tgz",
+            "integrity": "sha512-qKDIRuCvhvPg0uKjX1bmzTgUGZfr+cJX7+ULpBhVp983jJ3JYUfg91RCJ5C8tfMxlNIziJgZrs7B9KjBOCkmsQ==",
+            "dependencies": {
+                "@babel/runtime": "^7.17.9",
+                "archiver": "^7.0.1",
+                "file-saver": "^2.0.5",
+                "jszip": "^3.9.1"
+            }
+        },
+        "node_modules/ws": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
+            "integrity": "sha512-61a+9LgtYZxTq1hAonhX8Xwpo2riK4IOR/BIVxioFbCfc3QFKmpE4x9dLExfLHKtUfVZigYa36tThVhO57erEw==",
+            "dependencies": {
+                "safe-buffer": "~5.0.1",
+                "ultron": "~1.1.0"
+            }
+        },
+        "node_modules/ws/node_modules/safe-buffer": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz",
+            "integrity": "sha512-cr7dZWLwOeaFBLTIuZeYdkfO7UzGIKhjYENJFAxUOMKWGaWDm2nJM2rzxNRm5Owu0DH3ApwNo6kx5idXZfb/Iw=="
+        },
+        "node_modules/wss": {
+            "version": "3.3.4",
+            "resolved": "https://registry.npmjs.org/wss/-/wss-3.3.4.tgz",
+            "integrity": "sha512-WgVZercD6pVUwEUjFSUpTE7UoOGYAQUwye5VoSuYyPH58xZKfvCEYPTbdGdqdILMnmhSCPoRJXhgsSKY3CmbtA==",
+            "dependencies": {
+                "ws": "^2.3.1"
+            },
+            "engines": {
+                "node": ">=6"
+            },
+            "optionalDependencies": {
+                "istanbul": "^0.4.5"
+            }
+        },
+        "node_modules/xlsx": {
+            "version": "0.18.5",
+            "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+            "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+            "dependencies": {
+                "adler-32": "~1.3.0",
+                "cfb": "~1.2.1",
+                "codepage": "~1.15.0",
+                "crc-32": "~1.2.1",
+                "ssf": "~0.11.2",
+                "wmf": "~1.0.1",
+                "word": "~0.3.0"
+            },
+            "bin": {
+                "xlsx": "bin/xlsx.njs"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/xml": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
+            "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="
+        },
+        "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==",
+            "dependencies": {
+                "sax": "^1.2.4"
+            },
+            "bin": {
+                "xml-js": "bin/cli.js"
+            }
+        },
         "node_modules/xtend": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -3839,6 +4554,14 @@
                 "node": ">=0.4"
             }
         },
+        "node_modules/yaeti": {
+            "version": "0.0.6",
+            "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
+            "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
+            "engines": {
+                "node": ">=0.10.32"
+            }
+        },
         "node_modules/yallist": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

+ 6 - 1
package.json

@@ -15,6 +15,7 @@
         "axios": "^1.6.8",
         "cookie-parser": "^1.4.6",
         "cors": "^2.8.5",
+        "docx": "^8.5.0",
         "dote": "^1.1.0",
         "dotenv": "^16.4.5",
         "env": "^0.0.2",
@@ -26,7 +27,11 @@
         "path": "^0.12.7",
         "restify": "^11.1.0",
         "send": "^0.18.0",
-        "unzipper": "^0.11.4"
+        "unzipper": "^0.11.4",
+        "websocket": "^1.0.35",
+        "write-excel-file": "^2.0.1",
+        "wss": "^3.3.4",
+        "xlsx": "^0.18.5"
     },
     "devDependencies": {
         "env-cmd": "^10.1.0",

+ 427 - 92
src/app.js

@@ -13,23 +13,28 @@ const JSZIP = require('jszip');
 require('dotenv').config({ path: './env/.env.test' });
 const session = require('express-session');
 const multer  = require('multer');
-const {DeviceCodeCredential} = require('@azure/identity');
-const {TokenCredentialAuthenticationProvider} = require('@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials')
-const {Client} = require('@microsoft/microsoft-graph-client')
-
-const credential = new DeviceCodeCredential({
-  tenantId : process.env.TENANT_ID,
-  clientId : process.env.CLIENT_ID,
-  userPromptCallback: (info) => {
-    console.log(info.message);
-  },
-});
+const XLSX = require('xlsx'); 
 
-const authPr = new TokenCredentialAuthenticationProvider(credential, {
-  scopes: ['.default'],
-});
+const {Document, Packer, Paragraph, TextRun} = require('docx');
+let WebSocketServer = require('websocket').server;
+const redirectUri = 'https://localhost:53000/redirect';
+// const {DeviceCodeCredential} = require('@azure/identity');
+// const {TokenCredentialAuthenticationProvider} = require('@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials')
+// const {Client} = require('@microsoft/microsoft-graph-client')
+
+// const credential = new DeviceCodeCredential({
+//   tenantId : process.env.TENANT_ID,
+//   clientId : process.env.CLIENT_ID,
+//   userPromptCallback: (info) => {
+//     console.log(info.message);
+//   },
+// });
+
+// const authPr = new TokenCredentialAuthenticationProvider(credential, {
+//   scopes: ['.default'],
+// });
 
-const graphClient = Client.initWithMiddleware({ authProvider: authPr });
+// const graphClient = Client.initWithMiddleware({ authProvider: authPr });
 
 const storage =  multer.diskStorage({
   destination: function (req, file, cb) { 
@@ -76,6 +81,45 @@ const options = {
 };
 
 const server = https.createServer(options, serverApp);
+let wsServer = new WebSocketServer({
+  httpServer: server,
+  ssl : true,
+  key : process.env.SSL_KEY_FILE ? fs.readFileSync(process.env.SSL_KEY_FILE) : undefined,
+  cert: process.env.SSL_CRT_FILE ? fs.readFileSync(process.env.SSL_CRT_FILE) : undefined,
+});
+
+wsServer.on('request', function(req) {
+  var connection = req.accept();
+  connection.on('message', function(message) {
+    // console.log(message);
+    if (message && message.type === 'utf8') {
+      const {id, siteId, name, originSiteId, originId, totalCount, value, isFolder} = JSON.parse(message.utf8Data);
+      if (value && !isFolder) {
+        try {
+          const interval = setInterval(async ()=>{
+
+            const result = await axios.get(`${value}`);
+            if (result && result.data && result.data.percentageComplete >= 0) {
+              if (result.data.percentageComplete === 100) {
+                clearInterval(interval);
+              }
+              const percent = result.data.percentageComplete;
+              connection.sendUTF(percent);
+            }
+          }, 2000);
+        }
+        catch (error) {
+          // console.log(error);
+          console.log(error.message);
+          console.log(error.name);
+          console.log(error.errors);
+          connection.sendUTF(error);
+          // return res.json(error);
+        }
+      }
+    }
+  })
+});
 
 const corsOption = {
   origin: "*",
@@ -92,11 +136,8 @@ server.listen(SERVER_PORT, function () {
 serverApp.get("/tab", 
   isAuthenticated, 
   async function (req, res, next) {
-
-    // const result = await graphClient.api('/me').get();
-    // console.log(result);
     res.sendFile(path.join(__dirname, "/views/hello.html"), 
-      { idTokenClaims: req.session.account.idTokenClaims }
+      // { idTokenClaims: req.session.account.idTokenClaims }
     );
   }
 );
@@ -112,7 +153,7 @@ function isAccessToken(req, res, next) {
   if (!req.session.accessToken) {
     return authProvider.acquireToken({
       scopes: ['.default'],
-      redirectUri: 'https://localhost:53000/redirect',
+      redirectUri: redirectUri,
       successRedirect: '/api-redirect'
     })(req, res, next);
   }
@@ -121,7 +162,7 @@ function isAccessToken(req, res, next) {
 
 serverApp.get("/auth/signin", authProvider.login({
   scopes: ['.default'],
-  redirectUri: 'https://localhost:53000/redirect',
+  redirectUri: redirectUri,
   successRedirect: '/tab'
 }))
 
@@ -179,19 +220,19 @@ serverApp.get("/post-redirect",
 
 serverApp.post("/api-update", authProvider.acquireToken({
   scopes: [],
-  redirectUri: 'https://localhost:53000/redirect',
+  redirectUri: redirectUri,
   successRedirect: '/post-redirect'
 }));
 
 serverApp.post("/api-post", authProvider.acquireToken({
   scopes: ['.default'],
-  redirectUri: 'https://localhost:53000/redirect',
+  redirectUri: redirectUri,
   successRedirect: '/post-redirect'
 }));
 
 serverApp.post("/getGroupList", authProvider.acquireToken({
         scopes: ['.default'],
-        redirectUri: 'https://localhost:53000/redirect',
+        redirectUri: redirectUri,
         successRedirect: '/group-redirect'
 }));
   
@@ -254,18 +295,20 @@ serverApp.get("/group-redirect",
   }
 )
 
-serverApp.post('/makeFolder', 
+function isAccessTokens(req, res, next) {
+  if (!req.session.accessToken) {
+    return authProvider.acquireToken({
+      scopes: ['.default'],
+      redirectUri: redirectUri,
+      successRedirect: req.url
+    })(req, res, next);
+  }
+  next();
+}
+
+serverApp.post('/api/makeFolder', 
     isAuthenticated,
-    (req, res, next)=>{
-      if (!req.session.accessToken) {
-        return authProvider.acquireToken({
-          scopes: ['.default'],
-          redirectUri: 'https://localhost:53000/redirect',
-          successRedirect: '/makeFolder'
-        })(req, res, next);
-      }
-      next();
-    },
+    isAccessTokens,
     async (req, res, next)=>{
 
       const options = {
@@ -291,18 +334,135 @@ serverApp.post('/makeFolder',
       }
 })
 
+serverApp.post('/api/makeWord', 
+    isAuthenticated,
+    isAccessTokens,
+    async (req, res, next)=>{
+
+      const fileOptions = { headers: {
+        Authorization: `Bearer ${req.session.accessToken}`,
+        "Content-Type" : file.mimeType
+      }}
+      const {siteId, itemId, name} = req.body;
+      const doc = new Document({
+        sections : [
+          {
+            properties: {},
+            children : [
+              new Paragraph({
+                children: [
+                ]
+              })
+            ]
+          }
+        ]
+      })
+      try{
+        await axios.put(endPoint + "/sites/"+ siteId +"/drive/items/"+itemId+':/'+name+':/content', file.buffer, fileOptions);
+      }
+      catch(error) {
+        console.log(error.response.data.error); 
+        next(error);
+        // resultObj.success = 'F';
+        // resultObj.message = '선택하신 파일 정보 삭제중 오류가 발생하였습니다.\n' + error.response.data.error.message;
+        // return res.json(resultObj);
+      }
+})
+
+serverApp.post('/api/makeExcel', 
+    isAuthenticated,
+    isAccessTokens,
+    async (req, res, next)=>{
+
+      const fileOptions = { headers: {
+        Authorization: `Bearer ${req.session.accessToken}`,
+        "Content-Type" : 'application/ms-excel'
+      }}
+
+      const options = { headers: {
+        Authorization: `Bearer ${req.session.accessToken}`,
+      }}
+      const {siteId, path, name} = req.body;
+      const wb = XLSX.utils.book_new();
+      XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([]), 'Sheet1');
+      XLSX.writeFile(wb, name);
+      const data = fs.readFileSync(name);
+      try{
+        const parentData = await axios.get(endPoint + "/sites/"+ siteId +path, options);
+        if (parentData.data) {
+          const result = await axios.put(endPoint + "/sites/"+ siteId +"/drive/items/"+parentData.data.id+':/'+name+':/content', data, fileOptions);
+          fs.unlinkSync(name);
+          if (result.data) {
+            return res.json({message:'요청하신 Excel 파일이 생성 되었습니다.', success: 'S'});
+          }
+          return res.json({message:'요청하신 Excel 파일이 생성 되지 않았습니다.', success: 'F'});
+        }
+      }
+      catch(error) {
+        console.log(error.response.data.error); 
+        return res.json({message:'요청하신 Excel 파일 생성 중 오류가 발생하였습니다.', success: 'F', error: error});
+        // next(error);
+        // resultObj.success = 'F';
+        // resultObj.message = '선택하신 파일 정보 삭제중 오류가 발생하였습니다.\n' + error.response.data.error.message;
+        // return res.json(resultObj);
+      }
+})
+
+serverApp.post('/api/check-name', 
+    isAuthenticated,
+    isAccessTokens,
+    async (req, res, next)=>{
+
+      const options = {
+        headers: {
+          Authorization: `Bearer ${req.session.accessToken}`,
+        },
+      };
+      const {siteId, path, name} = req.body;
+      try{
+        const sitesInfo = await axios.get(endPoint + "/sites/"+ siteId + path, options);
+        if (sitesInfo.data) {
+          const itemId = sitesInfo.data.id;
+          const result = await axios.get(endPoint + "/sites/"+ siteId +"/drive/items/" + itemId +"/children", options);
+          if (result && result.data && result.data.value) {
+            let idx = result.data.value.findIndex(obj=>obj.name === name);
+            console.log(name);
+            console.log(idx);
+            console.log((idx > -1));
+            console.log(result.data.value);
+            res.json({hasName: (idx > -1)});
+          }
+        }
+      }
+      catch(error) {
+        if (error.response) {
+          console.log("error.response", error.response);
+          const statusCode = err.response.status; // 400
+          const statusText = err.response.statusText; // Bad Request
+          const message = err.response.data.message[0]; // id should not be empty
+          console.log(`${statusCode} - ${statusText} - ${message}`);
+          res.json(error.response);
+        }
+        else {
+          console.log(error);
+          res.json(error);
+        }
+        // if (error.response) {
+        //   console.log(error.response.data.error); 
+        // }
+        // else {
+        //   console.log(error.AxiosError);
+        // }
+        // res.json({hasName: false});
+        // resultObj.success = 'F';
+        // resultObj.message = '선택하신 파일 정보 삭제중 오류가 발생하였습니다.\n' + error.response.data.error.message;
+        // return res.json(resultObj);
+      }
+})
+
 serverApp.post('/api/upload', upload.array('file'),
   isAuthenticated,
-  (req, res, next)=>{
-    if (!req.session.accessToken) {
-      return authProvider.acquireToken({
-        scopes: ['.default'],
-        redirectUri: 'https://localhost:53000/redirect',
-        successRedirect: '/api/upload'
-      })(req, res, next);
-    }
-    next();
-  },
+  isAccessTokens,
   async (req, res, next)=>{
     const startTime = new Date();
     const folderParam = {
@@ -332,7 +492,10 @@ serverApp.post('/api/upload', upload.array('file'),
             param.name = fileInfo.name;
             let folderPath = '';
             if (fileInfo.path) {
-              folderPath = ':' + fileInfo.path; 
+              folderPath = fileInfo.path; 
+              if (!path.includes(':')) {
+                folderPath = ":" + folderPath;
+              }
             }
             let uri = endPoint + "/sites/"+ siteId + path + folderPath;
             try {
@@ -352,9 +515,14 @@ serverApp.post('/api/upload', upload.array('file'),
               // await axios.post(endPoint + "/sites/"+ siteId +"/drive/items/" + itemId +"/children", param, options);
             }
             catch(error) {
+              if (error.response) {
+                console.log(error.response.data);
+              } 
+              else {
                 console.log(error);
+              }
                 // next(error);
-                return res.json({success:'F', message: '요청하신 파일 업로드 중 오류가 발생하였습니다.', error : error});
+              return res.json({success:'F', message: '요청하신 파일 업로드 중 오류가 발생하였습니다.', error : error});
             }
           }
         }
@@ -372,17 +540,22 @@ serverApp.post('/api/upload', upload.array('file'),
               let formatPath = '';
               if (filePath) {
                 if (Array.isArray(filePath) && filePath.length > 0) {
-                  formatPath = ":" + filePath[0];
+                  formatPath = filePath[0];
                   if (filePath.length > 1) {
                     req.body[ fileName + "_path"] = filePath.splice(1); 
                   }
                 }
                 else if (filePath.trim()){
-                  formatPath = ":" + filePath;
+                  formatPath = filePath;
+                }
+
+                if (!path.includes(":")) {
+                  formatPath = ":" + formatPath;
                 }
               }
               let itemId = '';
               const uri = endPoint + "/sites/"+ siteId + path + formatPath;
+              
               if (beforeUri === uri) {
                 itemId = beforeItemId;
               }
@@ -421,16 +594,7 @@ serverApp.post('/api/upload', upload.array('file'),
 
 serverApp.post('/api/download',
   isAuthenticated,
-  (req, res, next)=>{
-    if (!req.session.accessToken) {
-      return authProvider.acquireToken({
-        scopes: ['.default'],
-        redirectUri: 'https://localhost:53000/redirect',
-        successRedirect: '/api/download'
-      })(req, res, next);
-    }
-    next();
-  },
+  isAccessTokens,
   async (req, res, next)=>{
     if (req.body) {
       const {siteId, path, fileIds, zipName} = req.body;
@@ -594,16 +758,7 @@ async function getFolderItems(url, array, options) {
 
 serverApp.post('/api/delete', 
   isAuthenticated,
-  (req, res, next)=>{
-    if (!req.session.accessToken) {
-      return authProvider.acquireToken({
-        scopes: ['.default'],
-        redirectUri: 'https://localhost:53000/redirect',
-        successRedirect: '/api/delete'
-      })(req, res, next);
-    }
-    next();
-  },
+  isAccessTokens,
   async (req, res, next)=>{
     if (req.body) {
       const {siteId, itemIds} = req.body;
@@ -646,16 +801,7 @@ serverApp.post('/api/delete',
 
 serverApp.post('/api/update-name', 
   isAuthenticated,
-  (req, res, next)=>{
-    if (!req.session.accessToken) {
-      return authProvider.acquireToken({
-        scopes: ['.default'],
-        redirectUri: 'https://localhost:53000/redirect',
-        successRedirect: '/api/update-name'
-      })(req, res, next);
-    }
-    next();
-  },
+  isAccessTokens,
   async (req, res, next)=>{
     if (req.body) {
       const {siteId, itemId, name} = req.body;
@@ -688,18 +834,57 @@ serverApp.post('/api/update-name',
       }
   });
   
+serverApp.post('/api/move-item', 
+  isAuthenticated,
+  isAccessTokens,
+  async (req, res, next)=>{
+    if (req.body) {
+      const {id, name, siteId, text} = req.body;
+      const resultObj = {message:'', successItems : [], failItems : [], locations: []};
+      if (name && isNaN(name)) {
+        const nameArray = JSON.parse(name);
+        const options = {
+          headers: {
+            Authorization: `Bearer ${req.session.accessToken}`,
+            ContentType: "application/json",
+          },
+        };
+
+        if (nameArray && nameArray.length > 0) {
+          for (let moveItem of nameArray) {
+            if (moveItem) {
+              const param = {
+                parentReference: {
+                  id: id,
+                },
+                name: moveItem.name
+              };
+              try {
+                const result = await axios.patch(`${endPoint}/sites/${siteId}/drive/items/${moveItem.id}`, param, options);
+                if (result) {
+                  resultObj.successItems.push(moveItem);
+                  // resultObj.locations.push(result.headers.location);
+                }
+              }
+              catch (error) {
+                // console.log(error);
+                console.log(error.message);
+                console.log(error.name);
+                console.log(error.errors);
+                resultObj.failItems.push(moveItem);
+              }
+            }
+          }
+          resultObj.message = `요청 하신 ${nameArray.length} 개 파일 중 ${ resultObj.successItems.length} 개 파일이 ${text} 되었습니다.`;
+        }
+      }
+      return res.json(resultObj);
+    }
+});
+  
 serverApp.post('/api/copy-item', 
 isAuthenticated,
-(req, res, next)=>{
-  if (!req.session.accessToken) {
-    return authProvider.acquireToken({
-      scopes: ['.default'],
-      redirectUri: 'https://localhost:53000/redirect',
-      successRedirect: '/api/copy-item'
-    })(req, res, next);
-  }
-  next();
-},
+isAccessTokens,
 async (req, res, next)=>{
   if (req.body) {
     const {id, name, siteId, driveId, text} = req.body;
@@ -726,8 +911,7 @@ async (req, res, next)=>{
             try {
               const result = await axios.post(`${endPoint}/sites/${siteId}/drive/items/${moveItem.id}/copy`, param, options);
               if (result) {
-                console.log(result);
-                resultObj.successItems.push(moveItem.name);
+                resultObj.successItems.push(moveItem);
                 resultObj.locations.push(result.headers.location);
               }
             }
@@ -736,7 +920,7 @@ async (req, res, next)=>{
               console.log(error.message);
               console.log(error.name);
               console.log(error.errors);
-              resultObj.failItems.push(moveItem.name);
+              resultObj.failItems.push(moveItem);
             }
           }
         }
@@ -769,3 +953,154 @@ serverApp.post('/api/loading',
       }
   }
 });
+
+
+serverApp.post('/api/folder-count',
+  isAuthenticated,
+  isAccessTokens,
+  async (req, res, next)=>{
+    if (req.body) {
+      const {id, siteId, originSiteId, originId, name, totalCount} = req.body;
+      const options = { headers: {
+        Authorization: `Bearer ${req.session.accessToken}`,
+      }}
+      let totCount = 0;
+      let successCount = 0;
+      if (id, siteId) {
+        try {
+          if (!totalCount) {
+            const originResult = await axios.get(endPoint + "/sites/"+ originSiteId +"/drive/items/"+originId, options);
+            if (originResult && originResult.data) {
+              totCount = await getFolderItemsCount(originSiteId, originResult.data.id, options, 0);
+              console.log('totCount', totCount);
+            }
+          }
+          else {
+            totCount = totalCount;
+          }
+          const result = await axios.get(endPoint + "/sites/"+ siteId +"/drive/items/"+id + "/children", options);
+          if (result && result.data && result.data.value) {
+            const value = result.data.value;
+            const idx = value.findIndex(obj=> obj.name === name);
+            successCount = await getFolderItemsCount(siteId, value[idx].id, options, 0);
+            console.log('successCount', totCount);
+          }
+          return res.json({totalCount: totCount, copyCount: successCount});
+          // const result = await axios.get(`${url}`);
+        }
+        catch (error) {
+          console.log(error.message);
+          console.log(error.name);
+          console.log(error.errors);
+          return res.json(error);
+        }
+      }
+  }
+});
+
+async function getFolderItemsCount(siteId, id, options, count) {
+  try {
+    const result = await axios.get(endPoint + "/sites/"+ siteId +"/drive/items/"+id +"/children", options);
+    // console.log("실행 URL :", endPoint + "/sites/"+ siteId +"/drive/items/"+id +"/children");
+  
+    if (result.data && result.data.value) {
+      const value = result.data.value;
+      console.log('value.length', value.length);
+      count += value.length;
+      for (let ii = 0; ii < value.length; ii++) {
+        if (value[ii].folder && value[ii].folder.childCount) {
+          count += await getFolderItemsCount(siteId, value[ii].id, options, 0);
+        }
+      }
+    }
+  }
+  catch(error) {
+    console.log(error.message);
+    console.log(error.name);
+    console.log(error.errors);
+  }
+  // console.log('개수', count);
+  return count;
+}
+
+
+serverApp.post('/api/add-tab', 
+isAuthenticated,
+isAccessTokens,
+async (req, res, next)=>{
+  if (req.body) {
+    const {name, siteId, path, teamId, teamName} = req.body;
+    const resultObj = {message:'', successItems : [], failItems : [], locations: []};
+    if (name && siteId && path && teamId && teamName) {
+      const options = {
+        headers: {
+          Authorization: `Bearer ${req.session.accessToken}`,
+          ContentType: "application/json",
+        },
+      };
+
+      try {
+        const teamInfo = await axios.get(`${endPoint}/teams/${teamId}/channels`, options);
+        const sharePoint = await axios.get(`${endPoint}/sites/${siteId}/drive`, options);
+        // const siteIndo = await axios.get(`${endPoint}/sites/${siteId}`, options);
+        console.log();
+        if (teamInfo.data && teamInfo.data.value && sharePoint.data) {
+          const sharePointUrl = sharePoint.data.webUrl;
+          if (path.includes('/drive/root:/') && sharePointUrl) {
+            let channelName = path.replace('/drive/root:/', '');
+            console.log(channelName);
+            console.log(sharePointUrl + '/' + channelName);
+            const contentUrl = sharePointUrl + '/' + channelName;
+            channelName = channelName.substring(0, channelName.indexOf('/'));
+            console.log(channelName);
+            const idx = teamInfo.data.value.findIndex(obj => obj.displayName === channelName);
+            if (idx >= 0 && contentUrl) {
+              const channelId = teamInfo.data.value[idx].id;
+              console.log(teamInfo.data);
+              console.log(`${endPoint}/sites/${siteId}${path}`);
+              console.log(channelId);
+              if (channelId) {
+                const param = {
+                  displayName: name,
+                  // "teamsApp@odata.bind" : "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/06805b9e-77e3-4b93-ac81-525eb87513b8",
+                  // "teamsApp@odata.bind" : "https://graph.microsoft.com/v1.0/$metadata#teams('a677e58b-0812-4abb-887f-4c49f5adfa3d')/channels('19%3Abec3c8a2cd164db0b001942f087b5a4a%40thread.tacv2')/tabs",
+                  configuration: {
+                    entityId: null,
+                    contentUrl: contentUrl,
+                    websiteUrl: "",
+                    removeUrl: null
+                  }
+                }
+                //sites/root/lists
+                const channelInfo = await axios.post(`${endPoint}/teams/${teamId}/channels/${channelId}/tabs`, param, options);
+                console.log(channelInfo.data);
+              }
+              // const result = await axios.get(`${endPoint}/sites/${siteId}${path}`, options);
+              // console.log(result.data);
+              // if (result) {
+              //   const param = {
+              //     displayName: name,
+              //     configuration: {
+              //       entityId: null,
+              //       contentUrl: "",
+              //       websiteUrl: "",
+              //       removeUrl: null
+              //     }
+              //   }
+              // }
+            }
+          }
+        }
+      }
+      catch (error) {
+        // console.log(error);
+        console.log(error.message);
+        console.log(error.name);
+        console.log(error.errors);
+        console.log(error.response.data);
+      }
+        // resultObj.message = `요청 하신 ${nameArray.length} 개 파일 중 ${ resultObj.successItems.length} 개 파일이 ${text} 되었습니다.`;
+    }
+    return res.json(resultObj);
+  }
+});

+ 1 - 1
src/static/styles/custom.css

@@ -735,7 +735,7 @@ html, body {
 }
 .modal .modal-move-box .right-box .content .loading-box > div > div img {
   animation-name: spinCircle; 
-  animation-duration: .8s; 
+  animation-duration: 1.2s; 
   animation-iteration-count: infinite;
 }
 .modal .modal-move-box .right-box .content .loading-box > div > div:nth-child(1) > div:nth-child(1) {

+ 445 - 151
src/views/hello.html

@@ -4,13 +4,13 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <link rel="icon" href="/favicon.ico" />
     <link rel="stylesheet" type="text/css" href="/static/styles/custom.css?v=06" />
-    <script
+    <!-- <script
       src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
       integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
       crossorigin="anonymous"
     ></script>
-    <!-- <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.2.1/js/msal.js"></script> -->
-    <script src="/static/scripts/teamsapp.js"></script>
+    <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.2.1/js/msal.js"></script>
+    <script src="/static/scripts/teamsapp.js"></script> -->
     <script src="https://code.jquery.com/jquery-latest.min.js"></script>
     <title>Microsoft Teams Tab</title>
     <style>
@@ -39,8 +39,8 @@
                       <div>새로만들기</div>
                       <div class="mk-items click-menu">
                         <div onclick="mkdir()"><img src="/static/images/folder.png" width="20" height="20" alt="문서 이미지">&nbsp;폴더</div>
-                        <div><img src="/static/images/docx.svg" width="20" height="20" alt="문서 이미지">&nbsp;Word 문서</div>
-                        <div><img src="/static/images/xlsx.svg" width="20" height="20" alt="문서 이미지">&nbsp;Excel 통합 문서</div>
+                        <div onclick="mkWord()"><img src="/static/images/docx.svg" width="20" height="20" alt="문서 이미지">&nbsp;Word 문서</div>
+                        <div onclick="mkExcel()"><img src="/static/images/xlsx.svg" width="20" height="20" alt="문서 이미지">&nbsp;Excel 통합 문서</div>
                         <div><img src="/static/images/pptx.svg" width="20" height="20" alt="문서 이미지">&nbsp;PowerPoint 프레젠테이션</div>
                       </div> 
                     </div>
@@ -87,7 +87,7 @@
   });
 
   $(()=>{
-    microsoftTeams.appInitialization.notifySuccess();
+    // microsoftTeams.appInitialization.notifySuccess();
     getGroupList();
     window.oncontextmenu = function () {
         return false;
@@ -101,7 +101,8 @@
         $('.file-content input[type="checkbox"]').prop('checked', false);
         $('.file-content > div.on').removeClass('on');
       }
-    })
+    });
+
   });
 
 
@@ -235,7 +236,8 @@
             // let method = `findOneDriveChildrenItems('${obj.id}')`;(siteId, path, name, value)
             let method = `siteDriveChildrenItems('${obj.parentReference.siteId}','${obj.parentReference.path + '/' + obj.name}','${name}', null, event)`;
             if (obj.file) {
-              method = `downloadItems('${obj['@microsoft.graph.downloadUrl']}', '${obj.name}', event)`;
+              // method = `downloadItems('${obj['@microsoft.graph.downloadUrl']}', '${obj.name}', event)`;
+              method = `openWebUrl('${obj.webUrl}', '${obj.name}', event)`;
             }
             str += `
               <div id="row_${idx}">
@@ -295,11 +297,23 @@
                   }
                 }
               }
-              
-              let str = `<div>편집</div>
-                <div>탭으로 설정</div>
-                <div onclick="download()">다운로드</div>
-                <div onclick="deleteItem()">삭제</div>`;
+
+              const teamId = $('section .panel .panel-item').eq(0).attr('id');
+              const teamList = document.querySelector('#my-team').nextElementSibling.nextElementSibling;
+              let isTeam = false;
+              if (teamList && $(teamList).find('#'+teamId+'_li').length > 0) {
+                isTeam = true;
+              } 
+              let str = `<div>편집</div>`;
+              const selectImage = $('section .file-content div.on').find('img');
+              if (checkCnt === 1 && 
+                  selectImage.length && 
+                  isTeam === true &&
+                  selectImage.attr('src') !== '/static/images/folder.png') {
+                str += `<div onclick="addTabs()">탭으로 설정</div>`;
+              }
+              str += `<div onclick="download()">다운로드</div>
+                      <div onclick="deleteItem()">삭제</div>`;
 
               if (!isChannel) {
                 str +=`<div onclick="moveItem('이동')">이동</div>`;
@@ -343,6 +357,82 @@
         panel.html(panelStr);
     }
 
+    function openWebUrl(url, name, event) {
+      // console.log(url, name, event);
+      window.open(url);
+      // let iframe = `<iframe allowfullscreen="true" src="${url}" style="position:absolute; width:100%; height:100%"></iframe>`;
+      // $('body').append(iframe);
+    }
+
+    function addTabs() {
+      const fileName = $('section .file-content div.on .file_name').text();
+      const siteId = $('section .panel .panel-item').eq(0).attr('id');
+      const path = getDrivePath() + '/' + fileName;
+      let teamId = null;
+      const teamData = _listData.joinedTeams.teams;
+      const teamName = $('section .panel .panel-item').eq(0).text().trim();
+      const channelName = $('section .panel .panel-item').eq(1).text().trim();
+      if (teamData && teamData.length > 0) {
+        let idx = teamData.findIndex(obj=> obj.displayName === teamName);
+        if (idx >= 0) {
+          teamId = teamData[idx].id;
+        }
+      }
+      console.log(fileName , siteId , teamId , path, teamName, channelName);
+      if (fileName && siteId && teamId && path && teamName && channelName) {
+        //teams/a677e58b-0812-4abb-887f-4c49f5adfa3d/channels
+        $.ajax({
+          method : 'post',
+          url : '/api/add-tab',
+          data : {
+            siteId : siteId,
+            path : path,
+            name : fileName,
+            teamId : teamId,
+            teamName : teamName,
+            channelName : channelName,
+          },
+          success : (res) => {
+            console.log(res);
+          },
+          error : (error) => {
+            console.log(error);
+          }
+        })
+        // callApi('get', '/sites/' + siteId + path + '/'+ fileName, (jsonData)=>{
+        //   console.log(jsonData);
+        // });
+        // const param = {
+        //   displayName: fileName,
+        //   webUrl : "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/06805b9e-77e3-4b93-ac81-525eb87513b8",
+        //   "configuration": {
+        //     "entityId": "2DCA2E6C7A10415CAF6B8AB6661B3154",
+        //     "contentUrl": "https://www.contoso.com/Orders/2DCA2E6C7A10415CAF6B8AB6661B3154/tabView",
+        //     "websiteUrl": "https://www.contoso.com/Orders/2DCA2E6C7A10415CAF6B8AB6661B3154",
+        //     "removeUrl": "https://www.contoso.com/Orders/2DCA2E6C7A10415CAF6B8AB6661B3154/uninstallTab"
+        //   }
+        // }
+      }
+      else {
+        alertMessage('탭으로 설정', '탭으로 설정 할 파일 정보를 확인 해주세요.');
+      }
+      // const param = {
+      //   "displayName": "My Contoso Tab",
+      //   "teamsApp@odata.bind" : "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/06805b9e-77e3-4b93-ac81-525eb87513b8",
+      //   "configuration": {
+      //     "entityId": "2DCA2E6C7A10415CAF6B8AB6661B3154",
+      //     "contentUrl": "https://www.contoso.com/Orders/2DCA2E6C7A10415CAF6B8AB6661B3154/tabView",
+      //     "websiteUrl": "https://www.contoso.com/Orders/2DCA2E6C7A10415CAF6B8AB6661B3154",
+      //     "removeUrl": "https://www.contoso.com/Orders/2DCA2E6C7A10415CAF6B8AB6661B3154/uninstallTab"
+      //   }
+      // }
+      // $.ajax({
+      //   url : '/api/add-tabs'
+
+      // })
+    }
+
+    //파일별 이미지 세팅
     function getFileImage(obj) {
       let images = "/static/images/";
       let imageName = "";
@@ -388,7 +478,7 @@
           ext = 'zip';
         }
         else if (['jsp', 'js', 'css', 'php', 'ini', 'yml', 'json', 'java', 'c', 'cpp', 'h', 'm', 'mm', 
-                  'pl', 'o', 'class', 'as', 'asp', 'cs', 'md'].includes(ext)) {
+                  'pl', 'o', 'class', 'as', 'asp', 'cs', 'md', 'ps1', 'ps2'].includes(ext)) {
           ext = 'code';
         }
         else if (['html'].includes(ext)) {
@@ -517,7 +607,18 @@
       const selectedDrive = $('.modal-move-box .left-box div.on');
       const siteId = selectedDrive.attr('id');
       const pathArr = $('.modal-move-box .right-box .panel .panel-item');
-      
+      const fileArr = $('.modal-move-box .list .file-name');
+      const moveArr = $('section .file-content div.on .sp-name');
+      if (moveArr && moveArr.length && fileArr && fileArr.length) {
+        for (let ii = 0; ii < moveArr.length; ii++) {
+          for (let jj = 0; jj < fileArr.length; jj++) {
+              if (moveArr.eq(ii).text() === fileArr.eq(jj).text()) {
+                return alertMessage(alertTitle, '대상 경로에 이미 같은 이름의 파일이 있습니다.<br>파일을 '+text+' 하려면 해당 파일의 명칭을 변경해주세요.<br>파일명 : ' + moveArr.eq(ii).text());
+              }
+          }
+        }
+      }
+
       if (selectedDrive[0] && siteId) {
         let path = getDrivePath(true);
         const currentId = $('section .panel-item').eq(0).attr('id');
@@ -529,18 +630,20 @@
         callApi('get', '/sites/'+ currentId + getDrivePath(), (itemData)=>{
           if (itemData && itemData.id){
             const itemId = itemData.id;
-            callApi('get', '/sites/'+ selectId + path, (jsonData)=>{
+            callApi('get', '/sites/'+ selectId + path, async (jsonData)=>{
               const $selectedItems = $('.file-content div.on');
               const name = [];
               let str = `<div class="loading-box"><div>파일 생성중입니다...</div>`;
               for (let ii = 0; ii < $selectedItems.length; ii++) {
                 const selectCheck =  $selectedItems.eq(ii).find('input[type="checkbox"]');
                 const selectName = $selectedItems.eq(ii).find('.sp-name');
+                const isFolder = ($selectedItems.eq(ii).find('img').attr('src') === '/static/images/folder.png');
                 const idArr = [];
                 if ($selectedItems.eq(ii) && selectCheck.length && selectName.length) {
                     name.push({
                       id : selectCheck.val(),
                       name : selectName.text(),
+                      isFolder : isFolder,
                     });
                     str += `<div>
                               <div>
@@ -559,8 +662,15 @@
                             </div>`
                 }
               }
+
               if (jsonData && jsonData.id) {
                 let uri = '/api/copy-item';
+                if (selectId === currentId) {
+                  uri = '/api/move-item';
+                }
+                else {
+                  $('.modal-move-box .content').append(str);
+                }
                 let param = {
                   siteId : currentId,
                   itemId : itemId,
@@ -569,8 +679,7 @@
                   driveId : jsonData.parentReference.driveId,
                   text : text,
                 }
-                
-                $('.modal-move-box .content').append(str);
+              
                 $.ajax({
                   method : 'post',
                   url : uri,
@@ -582,89 +691,113 @@
                       let failIndexes = [];
                       if (res.failItems && res.failItems.length) {
                         res.failItems.forEach((failItem)=>{
-                          let idx = name.findIndex(obj=> obj.name === failItem);
+                          let idx = name.findIndex(obj=> obj.name === failItem.name);
                           failIndexes.push(idx);
                           $('.loading_image_' + idx).remove();
                           $('.loading_image_' + idx +'_box').html('실패');
-                        })
+                        });
                       }
+
                       if (res.locations && res.locations.length > 0) {
-                        function getLoadingState(arr){
-                          Promise.all(res.locations.map((val, idx) => {
-                            if (failIndexes.includes(idx)) {
-                              return null;
-                            }
-                            return $.ajax({url: '/api/loading', global: false, method:'post', data: {url: val}});
-                          }))
-                          .then((values)=>{
-                            if (values && values.length > 0) {
-                              const array = [];
-                              console.log(values);
-                              for (let idx in values) {
-                                const value = values[idx];
-                                console.log(value);
-                                if (value && value.percentageComplete) {
-                                  if (value.percentageComplete !== 100) {
-                                    array.push(res.locations[idx]);
-                                  }
-                                  else {
-                                    $('.loading_image_' + idx).remove();
-                                    $('.loading_image_' + idx +'_box').html('완료됨');
-                                  }
-                                  const percent = value.percentageComplete;
-                                  $('.gauge_'+ idx).css('width', percent+'%');
-                                  $('.gauge_'+ idx + '_value').html(percent);
-                                }
-                              }
-                              if (array.length) {
-                                setTimeout(()=>{
-                                  getLoadingState(array);
-                                }, 2000)
-                              }
-                              else {
-                                if (text === '이동') {
-                                  const checkedArr = [];
-                                  name.forEach((obj)=>{
-                                    checkedArr.push(obj.id);
-                                  });
-                                  $.ajax({
-                                    url: '/api/delete', 
-                                    global: false, 
-                                    method:'post', 
-                                    data: {
-                                      siteId : currentId,
-                                      itemIds : JSON.stringify(checkedArr),
-                                    },
-                                    success: (res)=>{
-                                      console.log();
-                                      if (res && res.success === 'S') {
-                                        alertMessage(alertTitle, message);
-                                        const selected = $('section .panel-item.on');
-                                        selected.removeClass('on');
-                                        selected.click();
-                                        modalClose('move');
-                                      }
-                                    },
-                                    error : (error)=>{
-                                      console.log(error);
-                                    }
-                                  });
-                                }
-                                else {
-                                  alertMessage(alertTitle, message);
-                                  const selected = $('section .panel-item.on');
-                                  selected.removeClass('on');
-                                  selected.click();
-                                  modalClose('move');
-                                }
-                              }
-                            }
-                          })
-                          .catch((error)=>{
-                            console.log(error);
-                          })
+                        // function getLoadingState(arr, totalCount){
+                        //   Promise.all(res.locations.map((val, idx) => {
+                        //     if (failIndexes.includes(idx)) {
+                        //       return null;
+                        //     }
+                        //     if (name[idx].isFolder) {
+                        //       return $.ajax({ url: '/api/folder-count', global : false, method: 'post', 
+                        //                       data: { id: jsonData.id, siteId: selectId, name: name[idx].name, 
+                        //                             originSiteId: currentId, originId: name[idx].id, totalCount: totalCount }
+                        //                     });
+                        //     }
+                        //     return $.ajax({url: '/api/loading', global: false, method:'post', data: {url: val}});
+                        //   }))
+                        //   .then((values)=>{
+                        //     console.log(values);
+                        //     if (values) {
+                        //       const array = [];
+                        //       for (let idx in values) {
+                        //         const value = values[idx];
+                        //         if (value && value.percentageComplete) {
+                        //           if (value.percentageComplete !== 100) {
+                        //             array.push(res.locations[idx]);
+                        //           }
+                        //           else {
+                        //             $('.loading_image_' + idx).remove();
+                        //             $('.loading_image_' + idx +'_box').html('완료됨');
+                        //           }
+                        //           const percent = value.percentageComplete;
+                        //           $('.gauge_'+ idx).css('width', percent+'%');
+                        //           $('.gauge_'+ idx + '_value').html(percent);
+                        //         }
+                        //         else if(value && value.totalCount){
+                        //           console.log(value);
+                        //         }
+                        //       }
+                        //       if (array.length) {
+                        //         setTimeout(()=>{
+                        //           getLoadingState(array);
+                        //         }, 2000)
+                        //       }
+                        //       else {
+                        //         if (text === '이동') {
+                        //           const checkedArr = [];
+                        //           name.forEach((obj)=>{
+                        //             checkedArr.push(obj.id);
+                        //           });
+                        //           $.ajax({
+                        //             url: '/api/delete', 
+                        //             global: false, 
+                        //             method:'post', 
+                        //             data: {
+                        //               siteId : currentId,
+                        //               itemIds : JSON.stringify(checkedArr),
+                        //             },
+                        //             success: (res)=>{
+                        //               console.log();
+                        //               if (res && res.success === 'S') {
+                        //                 alertMessage(alertTitle, message);
+                        //                 const selected = $('section .panel-item.on');
+                        //                 selected.removeClass('on');
+                        //                 selected.click();
+                        //                 modalClose('move');
+                        //               }
+                        //             },
+                        //             error : (error)=>{
+                        //               console.log(error);
+                        //             }
+                        //           });
+                        //         }
+                        //         else {
+                        //           alertMessage(alertTitle, message);
+                        //           const selected = $('section .panel-item.on');
+                        //           selected.removeClass('on');
+                        //           selected.click();
+                        //           modalClose('move');
+                        //         }
+                        //       }
+                        //     }
+                        //   })
+                        //   .catch((error)=>{
+                        //     console.log(error);
+                        //   })
+                        // }
+                        // getLoadingState(res.locations);
+                        // jsonData.id, siteId: selectId, name: name[idx].name, 
+                        //                             originSiteId: currentId, originId: name[idx].id, totalCount: totalCount 
+                        for (let idx in res.locations) {
+                          if (!failIndexes.includes(idx)) {
+                              getFileState(jsonData.id, selectId, name[idx].name, currentId, name[idx].id, 0, 
+                              res.locations[idx], name[idx].isFolder, idx, alertTitle, message);
+                          }
                         }
-                        getLoadingState(res.locations);
+                      }
+                      else {
+                        alertMessage(alertTitle, message);
+                        const selected = $('section .panel-item.on');
+                        selected.removeClass('on');
+                        selected.click();
+                        modalClose('move');
                       }
                     }
                   },
@@ -688,6 +821,57 @@
       }
     }
 
+    function getFileState(id, siteId, name, originSiteId, originId, totalCount, value, isFolder, idx, alertTitle, message) {
+      const wss = new WebSocket('wss://localhost:53000/');
+      wss.onopen = (event)=>{
+        console.log('WebSocket Connection');
+        wss.send(JSON.stringify({id: id, siteId : siteId, name: name, originSiteId : originSiteId, 
+                                originId : originId, totalCount: totalCount, value : value, isFolder : isFolder}));
+      };
+      
+      wss.onclose = (event)=>{
+        console.log('WebSocket Close')
+      };
+      
+      wss.onmessage = async (event)=>{
+        if (event.data) {
+          const percent = event.data;
+          $('.gauge_'+ idx).css('width', percent+'%');
+          $('.gauge_'+ idx + '_value').html(percent);
+          if (percent === '100') {
+            $('.loading_image_' + idx).remove();
+            $('.loading_image_' + idx +'_box').html('완료됨');
+            try {
+              const deleteData = await $.ajax({
+                url: '/api/delete', 
+                global: false, 
+                method:'post', 
+                data: {
+                  siteId : originSiteId,
+                  itemIds : JSON.stringify([originId]),
+                },});
+                console.log(deleteData);
+            }
+            catch(error) {
+              console.log(error);
+            }
+
+            if ($('.modal.move').length && $('img[src="/static/images/refresh.png"]').length === 0) {
+              alertMessage(alertTitle, message);
+              const selected = $('section .panel-item.on');
+              selected.removeClass('on');
+              selected.click();
+              modalClose('move');
+            }
+            wss.close();
+          }
+        }
+      };
+      
+      wss.onerror = (event)=>{
+        console.log('에러 :',event.data)
+      };
+    }
 
     function setContent(siteId, event) {
       const $selectEl = $('#move_' + siteId);
@@ -843,7 +1027,7 @@
         const imgData = getFileImage(obj);
         return  `<div class="move_row row_${idx} ${className}">
                       <div><img src="${imgData[0]}" alt="${imgData[1]}" width="25" height="25"></div>
-                      <div><span onclick="${method}">${name}</span></div>
+                      <div><span onclick="${method}" class="file-name">${name}</span></div>
                       <div>${date}</div>
                       <div>${modifyName}</div>
                     </div>`;
@@ -1072,65 +1256,65 @@
           $(this).css('background-color', '#f5f5f5');
           const items = transfer.items;
           _formData = new FormData($('<form enctype="multipart/form-data"></form>')[0]);
-          await getFilesDataTransferItems(items);
-          
+          const result = await getFilesDataTransferItems(items);
+          console.log(result);
           upload(_formData);
       });
     }
 
-    function upload(formData) {
-          const folders = formData.getAll('folder');
-          const files = formData.getAll('file');
-          const alertTitle = '파일 업로드';
-          if (folders.length === 0 && files.length === 0) return;
-          if (folders && folders.length > 100) {
-            return alertMessage(alertTitle, '업로드 폴더는 최대 100개 까지 가능합니다.<br>업로드 폴더 수 : '+ folders.length, null);
-          }
+  async function upload(formData) {
+        const folders = formData.getAll('folder');
+        const files = formData.getAll('file');
+        const alertTitle = '파일 업로드';
+        if (folders.length === 0 && files.length === 0) return;
+        
+        if (folders && folders.length > 100) {
+          return alertMessage(alertTitle, '업로드 폴더는 최대 100개 까지 가능합니다.<br>업로드 폴더 수 : '+ folders.length, null);
+        }
 
-          if (files && files.length > 100) {
-            return alertMessage(alertTitle, '업로드 파일은 최대 100개 까지 가능합니다.\n업로드 파일 수 : '+ files.length, null);
-          }
-          let siteId = getSitesId();
-          if (siteId === null) {
-            return alertMessage(alertTitle, '선택된 그룹 정보를 찾을 수 없습니다. 생성할 그룹을 선택해주세요.', null);
-          }
-          
-          const groupIndex = _listData.joinedTeams.teams.findIndex(obj => obj.sharePoint.siteId === siteId);
-          const pathArr = $('.panel').children();
+        if (files && files.length > 100) {
+          return alertMessage(alertTitle, '업로드 파일은 최대 100개 까지 가능합니다.\n업로드 파일 수 : '+ files.length, null);
+        }
+        let siteId = getSitesId();
+        if (siteId === null) {
+          return alertMessage(alertTitle, '선택된 그룹 정보를 찾을 수 없습니다. 생성할 그룹을 선택해주세요.', null);
+        }
+        
+        const groupIndex = _listData.joinedTeams.teams.findIndex(obj => obj.sharePoint.siteId === siteId);
+        const pathArr = $('.panel').children();
 
-          if (groupIndex >= 0 && $('.panel').children().length === 1) {
-            return alertMessage(alertTitle,'채널에는 업로드 할 수 없습니다. 채널 리스트를 먼저 선택해 주세요.');
-          }
+        if (groupIndex >= 0 && $('.panel').children().length === 1) {
+          return alertMessage(alertTitle,'채널에는 업로드 할 수 없습니다. 채널 리스트를 먼저 선택해 주세요.');
+        }
 
-          let sitePath = getDrivePath();
+        let sitePath = getDrivePath();
 
-          formData.append('siteId', siteId);
-          formData.append('path', sitePath);
+        formData.append('siteId', siteId);
+        formData.append('path', sitePath);
 
-          $.ajax({
-            method: 'post',
-            processData : false,
-            contentType : false,
-            data : formData,
-            url : '/api/upload',
-            success: (res) => {
-              let str = res.message;
-              console.log(res.error);
-              if (res.error) {
-                str += '<br>오류 : ' + res.error.message;
-              }
-              alertMessage(alertTitle, str, null);
-              const selectDrive = $('.panel-item.on');
-              selectDrive.removeClass('on');
-              selectDrive.click();
-              
-            },
-            error: (error) => {
-              console.log(error);
-              alertMessage(alertTitle, error, null);
-            },
+        $.ajax({
+          method: 'post',
+          processData : false,
+          contentType : false,
+          data : formData,
+          url : '/api/upload',
+          success: (res) => {
+            let str = res.message;
+            if (res.error) {
+              str += '<br>오류 : ' + res.error.message;
+            }
+            alertMessage(alertTitle, str, null);
+            const selectDrive = $('.panel-item.on');
+            selectDrive.removeClass('on');
+            selectDrive.click();
             
-          });
+          },
+          error: (error) => {
+            console.log(error);
+            alertMessage(alertTitle, error, null);
+          },
+          
+        });
     }
 
     function alertMessage(title, message, color) {
@@ -1202,7 +1386,70 @@
         }
     }
     
-    function getFilesDataTransferItems(dataTransferItems) {
+  //   function getFilesDataTransferItems(dataTransferItems) {
+  //     function traverseFileTreePromise(item, folder) {
+  //       return new Promise(resolve => {
+  //         let subfolder = [];
+  //         let files = [];
+  //         let fileObj = {};
+
+  //         if (item.isFile) {
+  //           item.file(file => {
+  //             let fileName = file.name;
+  //             // if (file.name.indexOf('.') > 0) {
+  //             //   fileName = file.name.substring(0, file.name.lastIndexOf('.'));
+  //             // }
+  //             let path = item.fullPath.substring(0, item.fullPath.lastIndexOf('/'));
+  //             if (!path) {
+  //               path = '';
+  //             }
+  //             _formData.append('file', file);
+  //             _formData.append(fileName + '_path', path);
+  //             resolve(fileObj);
+  //           });
+  //         } else if (item.isDirectory) {
+  //           let dirReader = item.createReader();
+  //           const path = item.fullPath.substring(0, item.fullPath.lastIndexOf('/'));
+  //           _formData.append('folder', JSON.stringify({name : item.name, path : path}));
+  //           dirReader.readEntries(entries => {
+  //             let entriesPromises = [];
+  //             fileObj = {
+  //               path : item.fullPath,
+  //               param : {
+  //                 name: item.name,
+  //                 folder: {},
+  //                 '@microsoft.graph.conflictBehavior': 'rename'
+  //               },
+  //               subfolder : subfolder,
+  //             };
+  //             folder.push(fileObj);
+                
+  //             for (let entr of entries) {
+                
+  //               entriesPromises.push(
+  //                 traverseFileTreePromise(entr, subfolder)
+  //               );
+  //             }
+  //             resolve(Promise.all(entriesPromises));
+  //           });
+  //         }
+  //       });
+  //     }
+
+  //     let files = [];
+  //     return new Promise((resolve, reject) => {
+  //       let entriesPromises = [];
+  //       for (let it of dataTransferItems)
+  //         entriesPromises.push(
+  //           traverseFileTreePromise(it.webkitGetAsEntry(), files)
+  //         );
+  //       Promise.all(entriesPromises).then(entries => {
+  //         resolve(files);
+  //       });
+  //     });
+  // }
+
+  function getFilesDataTransferItems(dataTransferItems) {
       function traverseFileTreePromise(item, folder) {
         return new Promise(resolve => {
           let subfolder = [];
@@ -1421,7 +1668,6 @@
           let siteId = '';
           let str = "";
         
-          
           if (joinedTeams && joinedTeams.teams.length > 0) {
             str += `<li>
                       <input id="my-team" type="checkbox">
@@ -1555,7 +1801,7 @@
           '@microsoft.graph.conflictBehavior': 'rename'
         })
       },
-      url : '/makeFolder',
+      url : '/api/makeFolder',
       success: (res) => {
         $('.modal').remove();
         alertMessage("폴더 생성", '폴더가 생성되었습니다.<br>폴더명 : '+ res.name);
@@ -1596,6 +1842,54 @@
       $('body').append(modalContainer);
     
   }
+  function mkExcel() {
+    const siteId = getSitesId();
+    const modalContainer = $(
+      `<div class="modal excel" style="display: flex;">
+          <div class="modal-box">
+              <div class="header">
+                <div class="title">Excel 통합 문서</div>
+                <div class="x-button">
+                  <span><img src="/static/images/x-button.png" width="15" height="15" alt="닫기 버튼" onclick="modalClose('excel')"></span>
+                </div>
+              </div>
+              <div class="modal-content">
+                <div>이름</div>
+                <div style="display:flex; align-items:flex-end; user-select:none;">
+                  <input type="text" placeholder="새 이름을 입력하세요." style="width:'calc(100% - 35px)';" name="change_name" id="excel_input" value="">&nbsp;.xlsx
+                </div>
+              </div>
+              <div class="button-box">
+                  <div class="name-btn" onclick="makeExcel('${siteId}')">만들기</div>
+                  <div class="cancel-btn" onclick="modalClose('excel')">취소</div>
+              </div>
+          </div>
+      </div>`);
+      $('body').append(modalContainer);
+  }
+  function makeExcel(siteId) {
+    const name = $('#excel_input').val();
+    if (!name && !name.trim()) {
+      return alertMessage('Excel 통합 문서', '생성할 Excel 이름을 입력해주세요.');
+    }
+
+    const path = getDrivePath();
+    $.ajax({
+      url: '/api/makeExcel',
+      method : 'post',
+      data : {
+        siteId : siteId,
+        name : name + '.xlsx',
+        path : path,
+      },
+      success: (res)=> {
+        console.log(res)
+      },
+      error: (error)=>{
+        console.log(error);
+      }
+    })
+  }
 
   function modalClose(className){
       $('.modal.' + className).remove();
@@ -1629,12 +1923,12 @@
     // })
   
     const modalContainer = $(
-      `<div class="modal" style="display: flex;">
+      `<div class="modal name" style="display: flex;">
           <div class="modal-box">
               <div class="header">
                 <div class="title">이름 바꾸기</div>
                 <div class="x-button">
-                  <span><img src="/static/images/x-button.png" width="15" height="15" alt="닫기 버튼" onclick="$('.modal').remove()"></span>
+                  <span><img src="/static/images/x-button.png" width="15" height="15" alt="닫기 버튼" onclick="modalClose('name')"></span>
                 </div>
               </div>
               <div class="modal-content">
@@ -1645,7 +1939,7 @@
               </div>
               <div class="button-box">
                   <div class="name-btn" onclick="updateDriveItems('${siteId}','${itemId}')">이름 바꾸기</div>
-                  <div class="cancel-btn" onclick="$('.modal').remove()">취소</div>
+                  <div class="cancel-btn" onclick="modalClose('name')">취소</div>
               </div>
           </div>
       </div>`);