feat: 프로젝트 드라이브 폴더 연동 + AI 검토 문서 전환
프로젝트 직접 업로드(project_attachments) 폐기, AI 검토 문서를 드라이브 폴더 연동으로 전환. 폴더 연동 CRUD·연동 폴더 문서에서 텍스트 지연 추출(NCP 다운로드, xlsx 포함)·AI 추천 주입. 폴더 선택 피커 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+707
-11
@@ -30,6 +30,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.4",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"exceljs": "^4.4.0",
|
||||
"express-rate-limit": "^8.3.2",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.11.1",
|
||||
@@ -1490,6 +1491,47 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fast-csv/format": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
|
||||
"integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^14.0.1",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isfunction": "^3.0.9",
|
||||
"lodash.isnil": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fast-csv/format/node_modules/@types/node": {
|
||||
"version": "14.18.63",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
|
||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fast-csv/parse": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz",
|
||||
"integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^14.0.1",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.isfunction": "^3.0.9",
|
||||
"lodash.isnil": "^4.0.0",
|
||||
"lodash.isundefined": "^3.0.1",
|
||||
"lodash.uniq": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fast-csv/parse/node_modules/@types/node": {
|
||||
"version": "14.18.63",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
|
||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
@@ -6110,6 +6152,118 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"archiver-utils": "^2.1.0",
|
||||
"async": "^3.2.4",
|
||||
"buffer-crc32": "^0.2.1",
|
||||
"readable-stream": "^3.6.0",
|
||||
"readdir-glob": "^1.1.2",
|
||||
"tar-stream": "^2.2.0",
|
||||
"zip-stream": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.4",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
@@ -6351,6 +6505,28 @@
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/binary": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
|
||||
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffers": "~0.1.1",
|
||||
"chainsaw": "~0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-version": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-version/-/binary-version-7.1.0.tgz",
|
||||
@@ -6521,7 +6697,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
@@ -6608,7 +6783,6 @@
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -6689,7 +6863,6 @@
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6714,7 +6887,6 @@
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -6732,6 +6904,23 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer-indexof-polyfill": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
|
||||
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/buffers": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -6947,6 +7136,18 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chainsaw": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
|
||||
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
|
||||
"license": "MIT/X11",
|
||||
"dependencies": {
|
||||
"traverse": ">=0.3.0 <0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -7307,11 +7508,25 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
|
||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"crc32-stream": "^4.0.2",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
@@ -7474,6 +7689,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/crc32-stream": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
|
||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^3.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -7739,6 +7979,51 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/duplexer2/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/duplexer2/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -7801,6 +8086,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
|
||||
@@ -8150,6 +8444,36 @@
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exceljs": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz",
|
||||
"integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"archiver": "^5.0.0",
|
||||
"dayjs": "^1.8.34",
|
||||
"fast-csv": "^4.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"readable-stream": "^3.6.0",
|
||||
"saxes": "^5.0.1",
|
||||
"tmp": "^0.2.0",
|
||||
"unzipper": "^0.10.11",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exceljs/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
|
||||
@@ -8293,6 +8617,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-csv": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
|
||||
"integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
"@fast-csv/parse": "4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -8741,6 +9078,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -8767,7 +9110,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
@@ -8785,6 +9127,22 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fstream": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
|
||||
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"inherits": "~2.0.0",
|
||||
"mkdirp": ">=0.5 0",
|
||||
"rimraf": "2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -9036,7 +9394,6 @@
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
@@ -9308,7 +9665,6 @@
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
@@ -10671,6 +11027,54 @@
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazystream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -10717,6 +11121,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/listenercount": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
|
||||
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/livekit-server-sdk": {
|
||||
"version": "2.15.5",
|
||||
"resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.15.5.tgz",
|
||||
@@ -10787,6 +11197,36 @@
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.groupby": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
|
||||
"integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -10799,12 +11239,31 @@
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isfunction": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
|
||||
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnil": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
|
||||
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
@@ -10823,6 +11282,12 @@
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isundefined": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
|
||||
"integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -10843,6 +11308,18 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
@@ -11213,7 +11690,6 @@
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -11226,7 +11702,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -11241,6 +11716,18 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -11406,7 +11893,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -12419,6 +12905,36 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob/node_modules/minimatch": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
@@ -12556,6 +13072,40 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -12616,6 +13166,18 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
|
||||
"integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
@@ -13610,6 +14172,15 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -13671,6 +14242,15 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
|
||||
"license": "MIT/X11",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
@@ -14317,6 +14897,60 @@
|
||||
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper": {
|
||||
"version": "0.10.14",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
|
||||
"integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.17",
|
||||
"binary": "~0.3.0",
|
||||
"bluebird": "~3.4.1",
|
||||
"buffer-indexof-polyfill": "~1.0.0",
|
||||
"duplexer2": "~0.1.4",
|
||||
"fstream": "^1.0.12",
|
||||
"graceful-fs": "^4.2.2",
|
||||
"listenercount": "~1.0.1",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "~1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unzipper/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unzipper/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
@@ -14832,6 +15466,12 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
@@ -14946,6 +15586,62 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
|
||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"archiver-utils": "^3.0.4",
|
||||
"compress-commons": "^4.1.2",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/archiver-utils": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
|
||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob": "^7.2.3",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.4",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"exceljs": "^4.4.0",
|
||||
"express-rate-limit": "^8.3.2",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.11.1",
|
||||
|
||||
@@ -123,6 +123,25 @@ export class NcpObjectStorageService extends StorageService {
|
||||
return getSignedUrl(client, command, { expiresIn });
|
||||
}
|
||||
|
||||
async getObjectBuffer(key: string): Promise<Buffer | null> {
|
||||
const client = this.ensureConfigured();
|
||||
try {
|
||||
const res = await client.send(
|
||||
new GetObjectCommand({ Bucket: this.bucketName, Key: key }),
|
||||
);
|
||||
const body = res.Body as
|
||||
| { transformToByteArray?: () => Promise<Uint8Array> }
|
||||
| undefined;
|
||||
// AWS SDK v3 의 스트림 → 바이트 배열(transformToByteArray)로 수집
|
||||
if (!body?.transformToByteArray) return null;
|
||||
const bytes = await body.transformToByteArray();
|
||||
return Buffer.from(bytes);
|
||||
} catch (err: unknown) {
|
||||
if (this.isNotFound(err)) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async headObject(key: string): Promise<ObjectHead | null> {
|
||||
const client = this.ensureConfigured();
|
||||
try {
|
||||
|
||||
@@ -21,6 +21,9 @@ export abstract class StorageService {
|
||||
// 객체 메타 조회 — 미존재 시 null. 업로드 완료(complete) 검증에 사용.
|
||||
abstract headObject(key: string): Promise<ObjectHead | null>;
|
||||
|
||||
// 객체 바이트 다운로드 — 서버 측 텍스트 추출(AI 검토) 등에 사용. 미존재 시 null.
|
||||
abstract getObjectBuffer(key: string): Promise<Buffer | null>;
|
||||
|
||||
// 객체 삭제(파일/미완료 업로드 정리). 미존재여도 에러 없이 통과.
|
||||
abstract deleteObject(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ import type { AttachmentKind } from '../../task/entities/attachment.entity';
|
||||
// pending: presign 발급됨(아직 실제 업로드/검증 전) / active: 업로드 완료·검증 통과
|
||||
export type DriveFileStatus = 'pending' | 'active';
|
||||
|
||||
// AI 텍스트 추출 상태(프로젝트 연동 문서의 AI 검토용 본문 캐시)
|
||||
// pending: 미추출 / done: 추출 완료 / unsupported: 추출 불가 형식 / failed: 추출 시도 실패
|
||||
export type DriveExtractStatus = 'pending' | 'done' | 'unsupported' | 'failed';
|
||||
|
||||
// 드라이브 파일 — 실제 바이너리는 오브젝트 스토리지에 저장하고 메타데이터만 보관.
|
||||
// 업로드는 presigned URL 직접 전송 후 complete 로 확정한다.
|
||||
@Entity('drive_files')
|
||||
@@ -67,6 +71,19 @@ export class DriveFile {
|
||||
@Column({ type: 'varchar', default: 'pending' })
|
||||
status!: DriveFileStatus;
|
||||
|
||||
// AI 검토용 추출 본문(프로젝트 연동 시 1회 추출·캐싱) — 본문은 응답에 노출하지 않음
|
||||
@Column({
|
||||
name: 'extracted_text',
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
select: false,
|
||||
})
|
||||
extractedText!: string | null;
|
||||
|
||||
// AI 추출 상태 — 연동 문서의 AI 반영 배지 산출 + 재추출 방지에 사용
|
||||
@Column({ name: 'extract_status', type: 'varchar', default: 'pending' })
|
||||
extractStatus!: DriveExtractStatus;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import type { DriveLinkPermission } from '../../drive/entities/project-folder-link.entity';
|
||||
|
||||
// 프로젝트에 드라이브 폴더를 연동하는 요청
|
||||
export class LinkDriveFolderDto {
|
||||
@ApiProperty({ description: '연동할 드라이브 폴더 ID', format: 'uuid' })
|
||||
@IsUUID('4', { message: '올바른 폴더 ID가 아닙니다.' })
|
||||
folderId!: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "연동 권한 ('read' 보기/다운로드 | 'read-write' 멤버 업로드 허용)",
|
||||
default: 'read',
|
||||
enum: ['read', 'read-write'],
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(['read', 'read-write'], { message: '올바른 권한 값이 아닙니다.' })
|
||||
permission?: DriveLinkPermission;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
type Relation,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { Project } from './project.entity';
|
||||
import type { AttachmentKind } from '../../task/entities/attachment.entity';
|
||||
|
||||
// 프로젝트 문서 첨부 — 실제 바이너리는 서버 업로드 폴더에 저장하고 메타데이터만 보관.
|
||||
// (업무 첨부 Attachment 와 동일 패턴 / 동일 업로드 설정 재사용)
|
||||
@Entity('project_attachments')
|
||||
export class ProjectAttachment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
// 소속 프로젝트 (프로젝트 삭제 시 함께 삭제)
|
||||
@Index()
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project!: Relation<Project>;
|
||||
|
||||
// 업로더 (사용자 삭제 시에도 첨부는 유지 — SET NULL)
|
||||
@ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true })
|
||||
@JoinColumn({ name: 'uploader_id' })
|
||||
uploader!: User | null;
|
||||
|
||||
// 원본 파일명(다운로드 시 표시)
|
||||
@Column()
|
||||
name!: string;
|
||||
|
||||
// 디스크 저장명(업로드 폴더 내 고유 파일명)
|
||||
@Column({ name: 'stored_name' })
|
||||
storedName!: string;
|
||||
|
||||
// 파일 크기(바이트)
|
||||
@Column({ type: 'int' })
|
||||
size!: number;
|
||||
|
||||
// MIME 타입
|
||||
@Column()
|
||||
mime!: string;
|
||||
|
||||
// 아이콘 종류(MIME/확장자에서 파생)
|
||||
@Column({ type: 'varchar' })
|
||||
kind!: AttachmentKind;
|
||||
|
||||
// AI 추천용 추출 본문 — 업로드 시 1회 추출·캐싱(매 호출 재파싱 방지).
|
||||
// 추출 불가 형식/실패 시 null. 용량이 크므로 기본 조회에서는 제외(select:false).
|
||||
@Column({
|
||||
name: 'extracted_text',
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
select: false,
|
||||
})
|
||||
extractedText!: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import { PDFParse } from 'pdf-parse';
|
||||
import { extractRawText } from 'mammoth';
|
||||
import { Workbook } from 'exceljs';
|
||||
|
||||
// 문서 1건당 추출 텍스트 상한(자) — DB 용량과 AI 토큰 사용량을 함께 보호한다.
|
||||
export const PER_DOC_TEXT_LIMIT = 8000;
|
||||
@@ -9,7 +10,7 @@ export const PER_DOC_TEXT_LIMIT = 8000;
|
||||
// ready: 추출 성공(반영 가능) / failed: 지원 형식이나 추출 실패 / unsupported: 미지원 형식
|
||||
export type AttachmentAiStatus = 'ready' | 'failed' | 'unsupported';
|
||||
|
||||
// 텍스트 추출 대상 형식인지 여부(txt·csv·pdf·docx). 미지원/추출실패 배지 구분에 사용한다.
|
||||
// 텍스트 추출 대상 형식인지 여부(txt·csv·pdf·docx·xlsx). 미지원/추출실패 배지 구분에 사용한다.
|
||||
export function isExtractableType(mime: string, name: string): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
return (
|
||||
@@ -21,34 +22,49 @@ export function isExtractableType(mime: string, name: string): boolean {
|
||||
lower.endsWith('.pdf') ||
|
||||
mime ===
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
lower.endsWith('.docx')
|
||||
lower.endsWith('.docx') ||
|
||||
mime ===
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
lower.endsWith('.xlsx')
|
||||
);
|
||||
}
|
||||
|
||||
// 첨부 문서에서 평문 텍스트를 추출한다(업로드 시 1회). 추출 불가 형식/실패/빈 결과는 null.
|
||||
// 지원 형식: txt·csv(직접 읽기), pdf(pdf-parse), docx(mammoth).
|
||||
// 그 외(이미지·xlsx·ppt·압축 등)는 추출하지 않고 null 을 반환한다.
|
||||
// 디스크 경로 버전 — 버퍼를 읽어 공용 추출기에 위임한다.
|
||||
export async function extractAttachmentText(
|
||||
absPath: string,
|
||||
mime: string,
|
||||
name: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const buf = await readFile(absPath);
|
||||
return extractTextFromBuffer(buf, mime, name);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 버퍼 버전 — 오브젝트 스토리지에서 받은 바이트에서 텍스트를 추출한다(드라이브 연동 문서용).
|
||||
// 지원 형식: txt·csv(직접 디코드), pdf(pdf-parse), docx(mammoth), xlsx(exceljs). 그 외는 null.
|
||||
export async function extractTextFromBuffer(
|
||||
buf: Buffer,
|
||||
mime: string,
|
||||
name: string,
|
||||
): Promise<string | null> {
|
||||
const lower = name.toLowerCase();
|
||||
try {
|
||||
// 1) 평문 텍스트 — 라이브러리 없이 그대로 읽는다
|
||||
// 1) 평문 텍스트
|
||||
if (
|
||||
mime === 'text/plain' ||
|
||||
mime === 'text/csv' ||
|
||||
lower.endsWith('.txt') ||
|
||||
lower.endsWith('.csv')
|
||||
) {
|
||||
const buf = await readFile(absPath);
|
||||
return finalize(buf.toString('utf8'));
|
||||
}
|
||||
|
||||
// 2) PDF — pdf-parse v2(PDFParse 클래스)로 페이지 텍스트 추출
|
||||
// 2) PDF — pdf-parse v2(PDFParse 클래스)
|
||||
if (mime === 'application/pdf' || lower.endsWith('.pdf')) {
|
||||
const buf = await readFile(absPath);
|
||||
const parser = new PDFParse({ data: buf });
|
||||
try {
|
||||
const result = await parser.getText();
|
||||
@@ -58,19 +74,46 @@ export async function extractAttachmentText(
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Word(.docx) — mammoth 로 서식 제거한 본문만 추출
|
||||
// 3) Word(.docx) — mammoth (버퍼 입력)
|
||||
if (
|
||||
mime ===
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
lower.endsWith('.docx')
|
||||
) {
|
||||
const result = await extractRawText({ path: absPath });
|
||||
const result = await extractRawText({ buffer: buf });
|
||||
return finalize(result.value);
|
||||
}
|
||||
|
||||
// 4) Excel(.xlsx) — exceljs 로 시트별 셀 텍스트 추출(행=탭 구분)
|
||||
if (
|
||||
mime ===
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
lower.endsWith('.xlsx')
|
||||
) {
|
||||
const wb = new Workbook();
|
||||
// exceljs 타입(Buffer)과 Node Buffer 제네릭(ArrayBufferLike) 마찰 회피 —
|
||||
// load 의 실제 파라미터 타입으로 캐스팅한다.
|
||||
await wb.xlsx.load(buf as unknown as Parameters<typeof wb.xlsx.load>[0]);
|
||||
const lines: string[] = [];
|
||||
wb.eachSheet((sheet) => {
|
||||
const sheetLines: string[] = [];
|
||||
sheet.eachRow({ includeEmpty: false }, (row) => {
|
||||
const cells: string[] = [];
|
||||
row.eachCell({ includeEmpty: false }, (cell) => {
|
||||
const t = (cell.text ?? '').toString().trim();
|
||||
if (t) cells.push(t);
|
||||
});
|
||||
if (cells.length) sheetLines.push(cells.join('\t'));
|
||||
});
|
||||
// 내용 있는 시트만 제목과 함께 포함
|
||||
if (sheetLines.length) lines.push(`# ${sheet.name}`, ...sheetLines);
|
||||
});
|
||||
return finalize(lines.join('\n'));
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
// 추출 실패는 무시한다 — 첨부 자체는 정상 저장되어야 한다.
|
||||
// 추출 실패는 무시 — 호출부가 상태만 'failed' 로 기록한다.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,37 +12,22 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiConsumes,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { BusinessException } from '../../common/exceptions/business.exception';
|
||||
import type { PublicUser } from '../user/user.service';
|
||||
import type { PaginatedResult } from '../../common/pagination/pagination';
|
||||
import {
|
||||
ProjectService,
|
||||
type ProjectResponse,
|
||||
type ProjectAttachmentResponse,
|
||||
type ProjectDriveLinkResponse,
|
||||
type ProjectDocumentResponse,
|
||||
} from './project.service';
|
||||
import {
|
||||
UPLOAD_DIR,
|
||||
MAX_FILE_SIZE,
|
||||
fileMimeFilter,
|
||||
type UploadedDiskFile,
|
||||
} from '../task/upload.config';
|
||||
import { CreateProjectDto } from './dto/create-project.dto';
|
||||
import { UpdateProjectDto } from './dto/update-project.dto';
|
||||
import { LinkDriveFolderDto } from './dto/link-drive-folder.dto';
|
||||
|
||||
// 프로젝트 컨트롤러 — 모든 엔드포인트 인증 필요
|
||||
@ApiTags('Project')
|
||||
@@ -102,7 +87,7 @@ export class ProjectController {
|
||||
|
||||
@Delete(':projectId')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '프로젝트 삭제 (소유자, 시스템 프로젝트 제외)' })
|
||||
@ApiOperation({ summary: '프로젝트 삭제 (소유자)' })
|
||||
@ApiResponse({ status: 200, description: '삭제 성공' })
|
||||
@ApiResponse({ status: 403, description: '권한 없음 (AUTH_003)' })
|
||||
async remove(
|
||||
@@ -113,65 +98,50 @@ export class ProjectController {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ===================== 문서 첨부 ===================== */
|
||||
/* ===================== 드라이브 연동 폴더 / AI 검토 문서 ===================== */
|
||||
|
||||
@Post(':projectId/files')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
dest: UPLOAD_DIR,
|
||||
limits: { fileSize: MAX_FILE_SIZE },
|
||||
fileFilter: fileMimeFilter,
|
||||
}),
|
||||
)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: '프로젝트 문서 업로드 (admin)' })
|
||||
@ApiResponse({ status: 201, description: '업로드 성공' })
|
||||
addFile(
|
||||
@Get(':projectId/drive-links')
|
||||
@ApiOperation({ summary: '연동된 드라이브 폴더 목록 (멤버)' })
|
||||
@ApiResponse({ status: 200, description: '조회 성공' })
|
||||
listDriveLinks(
|
||||
@Param('projectId', ParseUUIDPipe) projectId: string,
|
||||
@UploadedFile() file: UploadedDiskFile,
|
||||
@CurrentUser() user: PublicUser,
|
||||
): Promise<ProjectAttachmentResponse> {
|
||||
if (!file) {
|
||||
throw new BusinessException(
|
||||
'BIZ_001',
|
||||
'업로드할 수 없는 파일 형식이거나 파일이 없습니다.',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
return this.projectService.addFile(projectId, user.id, file);
|
||||
): Promise<ProjectDriveLinkResponse[]> {
|
||||
return this.projectService.listDriveLinks(projectId);
|
||||
}
|
||||
|
||||
@Delete(':projectId/files/:fileId')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '프로젝트 문서 삭제 (admin)' })
|
||||
@ApiResponse({ status: 200, description: '삭제 성공' })
|
||||
async removeFile(
|
||||
@Post(':projectId/drive-links')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: '드라이브 폴더 연동 (admin, 본인 소유 폴더만)' })
|
||||
@ApiResponse({ status: 201, description: '연동 성공' })
|
||||
@ApiResponse({ status: 403, description: '권한 없음 (AUTH_003)' })
|
||||
@ApiResponse({ status: 409, description: '이미 연동된 폴더 (BIZ_001)' })
|
||||
linkDriveFolder(
|
||||
@Param('projectId', ParseUUIDPipe) projectId: string,
|
||||
@Param('fileId', ParseUUIDPipe) fileId: string,
|
||||
@Body() dto: LinkDriveFolderDto,
|
||||
@CurrentUser() user: PublicUser,
|
||||
): Promise<ProjectDriveLinkResponse> {
|
||||
return this.projectService.linkDriveFolder(projectId, user.id, dto);
|
||||
}
|
||||
|
||||
@Delete(':projectId/drive-links/:linkId')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '드라이브 폴더 연동 해제 (admin)' })
|
||||
@ApiResponse({ status: 200, description: '해제 성공' })
|
||||
async unlinkDriveFolder(
|
||||
@Param('projectId', ParseUUIDPipe) projectId: string,
|
||||
@Param('linkId', ParseUUIDPipe) linkId: string,
|
||||
@CurrentUser() user: PublicUser,
|
||||
): Promise<null> {
|
||||
await this.projectService.removeFile(projectId, user.id, fileId);
|
||||
await this.projectService.unlinkDriveFolder(projectId, user.id, linkId);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Get(':projectId/files/:fileId/download')
|
||||
@ApiOperation({ summary: '프로젝트 문서 다운로드 (멤버)' })
|
||||
@ApiResponse({ status: 200, description: '파일 스트림' })
|
||||
async downloadFile(
|
||||
@Get(':projectId/documents')
|
||||
@ApiOperation({ summary: 'AI 검토 대상 문서 목록 (연동 폴더 내 파일)' })
|
||||
@ApiResponse({ status: 200, description: '조회 성공' })
|
||||
listDocuments(
|
||||
@Param('projectId', ParseUUIDPipe) projectId: string,
|
||||
@Param('fileId', ParseUUIDPipe) fileId: string,
|
||||
@CurrentUser() user: PublicUser,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const { attachment, absolutePath } =
|
||||
await this.projectService.getFileForDownload(projectId, user.id, fileId);
|
||||
res.setHeader('Content-Type', attachment.mime);
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename*=UTF-8''${encodeURIComponent(attachment.name)}`,
|
||||
);
|
||||
res.sendFile(absolutePath);
|
||||
): Promise<ProjectDocumentResponse[]> {
|
||||
return this.projectService.listDocuments(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { Project } from './entities/project.entity';
|
||||
import { ProjectMember } from './entities/project-member.entity';
|
||||
import { ProjectAttachment } from './entities/project-attachment.entity';
|
||||
import { Task } from '../task/entities/task.entity';
|
||||
import { DriveFolder } from '../drive/entities/drive-folder.entity';
|
||||
import { DriveFile } from '../drive/entities/drive-file.entity';
|
||||
import { ProjectFolderLink } from '../drive/entities/project-folder-link.entity';
|
||||
import { ActivityModule } from '../activity/activity.module';
|
||||
import { NotificationModule } from '../notification/notification.module';
|
||||
import { ProjectService } from './project.service';
|
||||
@@ -16,7 +18,14 @@ import { ProjectMemberController } from './project-member.controller';
|
||||
// ProjectService 의 권한 헬퍼(assertProjectMember/Admin)를 다른 모듈이 주입해 사용한다.
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Project, ProjectMember, ProjectAttachment, Task]),
|
||||
TypeOrmModule.forFeature([
|
||||
Project,
|
||||
ProjectMember,
|
||||
Task,
|
||||
DriveFolder,
|
||||
DriveFile,
|
||||
ProjectFolderLink,
|
||||
]),
|
||||
UserModule,
|
||||
ActivityModule,
|
||||
NotificationModule,
|
||||
|
||||
@@ -1,48 +1,58 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ILike, Repository, type FindOptionsOrder } from 'typeorm';
|
||||
import { unlink } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { ILike, In, Like, Repository, type FindOptionsOrder } from 'typeorm';
|
||||
import { UserService, type PublicUser } from '../user/user.service';
|
||||
import {
|
||||
paginated,
|
||||
resolvePage,
|
||||
type PaginatedResult,
|
||||
} from '../../common/pagination/pagination';
|
||||
import { BusinessException } from '../../common/exceptions/business.exception';
|
||||
import { StorageService } from '../../common/storage/storage.service';
|
||||
import { Project } from './entities/project.entity';
|
||||
import { ProjectMember } from './entities/project-member.entity';
|
||||
import { ProjectAttachment } from './entities/project-attachment.entity';
|
||||
import { Task } from '../task/entities/task.entity';
|
||||
import type { AttachmentKind } from '../task/entities/attachment.entity';
|
||||
import { DriveFolder } from '../drive/entities/drive-folder.entity';
|
||||
import { DriveFile } from '../drive/entities/drive-file.entity';
|
||||
import {
|
||||
UPLOAD_DIR,
|
||||
deriveKind,
|
||||
decodeOriginalName,
|
||||
type UploadedDiskFile,
|
||||
} from '../task/upload.config';
|
||||
ProjectFolderLink,
|
||||
type DriveLinkPermission,
|
||||
} from '../drive/entities/project-folder-link.entity';
|
||||
import {
|
||||
extractAttachmentText,
|
||||
extractTextFromBuffer,
|
||||
isExtractableType,
|
||||
type AttachmentAiStatus,
|
||||
} from './extract-text';
|
||||
import { CreateProjectDto } from './dto/create-project.dto';
|
||||
import { UpdateProjectDto } from './dto/update-project.dto';
|
||||
import { LinkDriveFolderDto } from './dto/link-drive-folder.dto';
|
||||
|
||||
// 프로젝트 첨부 응답
|
||||
export interface ProjectAttachmentResponse {
|
||||
id: string;
|
||||
// 문서의 AI 반영 상태 — 'pending'(추출 대기) 추가
|
||||
export type DocumentAiStatus = AttachmentAiStatus | 'pending';
|
||||
|
||||
// 프로젝트에 연동된 드라이브 폴더 응답
|
||||
export interface ProjectDriveLinkResponse {
|
||||
id: string; // 연동(link) ID
|
||||
folder: { id: string; name: string; path: string };
|
||||
permission: DriveLinkPermission;
|
||||
linkedBy: PublicUser | null;
|
||||
documentCount: number; // 연동 폴더(하위 포함) 내 활성 파일 수
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// AI 검토 대상 문서(연동 폴더 내 드라이브 파일) 응답 — 본문은 노출하지 않음
|
||||
export interface ProjectDocumentResponse {
|
||||
id: string; // 드라이브 파일 ID(다운로드는 드라이브 API 사용)
|
||||
name: string;
|
||||
size: number;
|
||||
kind: AttachmentKind;
|
||||
url: string;
|
||||
uploader: PublicUser | null;
|
||||
createdAt: string;
|
||||
// AI 추천 반영 상태 — 추출 본문은 노출하지 않고 상태 플래그만 제공
|
||||
aiStatus: AttachmentAiStatus;
|
||||
aiStatus: DocumentAiStatus;
|
||||
}
|
||||
|
||||
// 프로젝트 응답 형태 — 파생 라벨/진행률은 프론트에서 계산
|
||||
@@ -55,10 +65,11 @@ export interface ProjectResponse {
|
||||
doneCount: number; // 완료 업무 수
|
||||
totalCount: number; // 전체 업무 수
|
||||
updatedAt: string;
|
||||
// 단건(findOne)에서만 — 현재 사용자 권한 + 문서 첨부
|
||||
// 단건(findOne)에서만 — 현재 사용자 권한 + 연동 폴더/문서
|
||||
canManage?: boolean; // admin
|
||||
isOwner?: boolean; // 소유자
|
||||
attachments?: ProjectAttachmentResponse[];
|
||||
driveLinks?: ProjectDriveLinkResponse[];
|
||||
documents?: ProjectDocumentResponse[];
|
||||
}
|
||||
|
||||
// 프로젝트 비즈니스 로직 + 권한 헬퍼(다른 모듈이 주입해 사용)
|
||||
@@ -71,8 +82,13 @@ export class ProjectService {
|
||||
private readonly memberRepo: Repository<ProjectMember>,
|
||||
@InjectRepository(Task)
|
||||
private readonly taskRepo: Repository<Task>,
|
||||
@InjectRepository(ProjectAttachment)
|
||||
private readonly attachmentRepo: Repository<ProjectAttachment>,
|
||||
@InjectRepository(ProjectFolderLink)
|
||||
private readonly linkRepo: Repository<ProjectFolderLink>,
|
||||
@InjectRepository(DriveFolder)
|
||||
private readonly folderRepo: Repository<DriveFolder>,
|
||||
@InjectRepository(DriveFile)
|
||||
private readonly fileRepo: Repository<DriveFile>,
|
||||
private readonly storage: StorageService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@@ -125,7 +141,8 @@ export class ProjectService {
|
||||
...base,
|
||||
canManage: membership?.roleType === 'admin',
|
||||
isOwner: membership?.isOwner ?? false,
|
||||
attachments: await this.listFiles(project.id),
|
||||
driveLinks: await this.listDriveLinks(project.id),
|
||||
documents: await this.listDocuments(project.id),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,153 +230,226 @@ export class ProjectService {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 문서 첨부 ---
|
||||
// --- 드라이브 연동 폴더 / AI 검토 문서 ---
|
||||
|
||||
// 첨부 목록(최신순) — 프로젝트 상세에 포함
|
||||
async listFiles(projectId: string): Promise<ProjectAttachmentResponse[]> {
|
||||
// extractedText 는 select:false 이지만, AI 반영 상태 배지 산출을 위해 명시적으로 선택한다.
|
||||
// (본문은 응답에 담지 않고 상태 판별에만 사용)
|
||||
const rows = await this.attachmentRepo
|
||||
.createQueryBuilder('a')
|
||||
.leftJoinAndSelect('a.uploader', 'uploader')
|
||||
.addSelect('a.extractedText')
|
||||
.where('a.project_id = :projectId', { projectId })
|
||||
.orderBy('a.created_at', 'DESC')
|
||||
.getMany();
|
||||
return rows.map((a) => this.toAttachmentResponse(a, projectId));
|
||||
// 프로젝트에 연동된 드라이브 폴더 목록(최신순) — 프로젝트 상세에 포함
|
||||
async listDriveLinks(projectId: string): Promise<ProjectDriveLinkResponse[]> {
|
||||
const links = await this.linkRepo.find({
|
||||
where: { project: { id: projectId } },
|
||||
relations: { folder: true, linkedBy: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
const out: ProjectDriveLinkResponse[] = [];
|
||||
for (const l of links) {
|
||||
out.push(
|
||||
this.toDriveLinkResponse(l, await this.countFilesInFolderTree(l.folder)),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// AI 업무 추천용 — 프로젝트 문서들의 캐싱된 추출 본문을 문서명과 함께 합쳐 반환한다.
|
||||
// 총량 상한으로 잘라 토큰 사용을 보호하며, 추출 텍스트가 없으면 빈 문자열.
|
||||
// (extractedText 는 select:false 이므로 명시적으로 선택해 조회한다.)
|
||||
// 드라이브 폴더를 프로젝트에 연동(admin, 본인 소유 폴더만)
|
||||
async linkDriveFolder(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
dto: LinkDriveFolderDto,
|
||||
): Promise<ProjectDriveLinkResponse> {
|
||||
const project = await this.getProjectOrThrow(projectId);
|
||||
await this.assertProjectAdmin(project.id, userId);
|
||||
const folder = await this.folderRepo.findOne({
|
||||
where: { id: dto.folderId },
|
||||
relations: { owner: true },
|
||||
});
|
||||
if (!folder) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
// 타인 폴더 공유 방지 — 본인이 소유한 폴더만 연동 가능
|
||||
if (folder.owner?.id !== userId) {
|
||||
throw new BusinessException(
|
||||
'AUTH_003',
|
||||
'본인이 소유한 폴더만 연동할 수 있습니다.',
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
const existing = await this.linkRepo.findOne({
|
||||
where: { project: { id: project.id }, folder: { id: folder.id } },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BusinessException(
|
||||
'BIZ_001',
|
||||
'이미 연동된 폴더입니다.',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
const user = await this.userService.findById(userId);
|
||||
const saved = await this.linkRepo.save(
|
||||
this.linkRepo.create({
|
||||
project: { id: project.id } as Project,
|
||||
folder: { id: folder.id } as DriveFolder,
|
||||
linkedBy: user ?? null,
|
||||
permission: dto.permission ?? 'read',
|
||||
}),
|
||||
);
|
||||
return this.toDriveLinkResponse(
|
||||
{ ...saved, folder, linkedBy: user ?? null },
|
||||
await this.countFilesInFolderTree(folder),
|
||||
);
|
||||
}
|
||||
|
||||
// 연동 해제(admin)
|
||||
async unlinkDriveFolder(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
linkId: string,
|
||||
): Promise<void> {
|
||||
const project = await this.getProjectOrThrow(projectId);
|
||||
await this.assertProjectAdmin(project.id, userId);
|
||||
const link = await this.linkRepo.findOne({
|
||||
where: { id: linkId, project: { id: project.id } },
|
||||
});
|
||||
if (!link) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await this.linkRepo.remove(link);
|
||||
}
|
||||
|
||||
// AI 검토 대상 문서 목록 — 연동 폴더(하위 포함) 내 활성 파일 + 추출 상태
|
||||
async listDocuments(projectId: string): Promise<ProjectDocumentResponse[]> {
|
||||
const files = await this.collectLinkedFiles(projectId, false);
|
||||
return files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
kind: f.kind,
|
||||
aiStatus: this.driveAiStatus(f),
|
||||
}));
|
||||
}
|
||||
|
||||
// AI 업무 추천용 — 연동 폴더 내 문서들의 추출 본문을 문서명과 함께 합쳐 반환한다.
|
||||
// 미추출(pending) 문서는 이 시점에 NCP 에서 받아 추출·캐싱한다(지연 추출). 총량 상한 적용.
|
||||
async getAttachmentsTextForAi(projectId: string): Promise<string> {
|
||||
const TOTAL_LIMIT = 16000;
|
||||
const rows = await this.attachmentRepo
|
||||
.createQueryBuilder('a')
|
||||
.select(['a.name', 'a.extractedText'])
|
||||
.where('a.project_id = :projectId', { projectId })
|
||||
.orderBy('a.created_at', 'ASC')
|
||||
.getMany();
|
||||
|
||||
const files = await this.collectLinkedFiles(projectId, true);
|
||||
const parts: string[] = [];
|
||||
let used = 0;
|
||||
for (const r of rows) {
|
||||
const text = r.extractedText?.trim();
|
||||
for (const f of files) {
|
||||
let text = f.extractedText?.trim() ?? null;
|
||||
if (!text && f.extractStatus === 'pending') {
|
||||
text = await this.extractDriveFile(f);
|
||||
}
|
||||
if (!text) continue;
|
||||
const remaining = TOTAL_LIMIT - used;
|
||||
if (remaining <= 0) break;
|
||||
const slice = text.length > remaining ? text.slice(0, remaining) : text;
|
||||
parts.push(`### ${r.name}\n${slice}`);
|
||||
parts.push(`### ${f.name}\n${slice}`);
|
||||
used += slice.length;
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
// 첨부 업로드 — 관리자만. 디스크 저장은 multer 가 처리, 메타데이터만 영속
|
||||
async addFile(
|
||||
// 연동 폴더(자기+하위 트리) 내 활성 파일 수집. withText=true 면 추출 본문도 선택.
|
||||
private async collectLinkedFiles(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
file: UploadedDiskFile,
|
||||
): Promise<ProjectAttachmentResponse> {
|
||||
const project = await this.getProjectOrThrow(projectId);
|
||||
await this.assertProjectAdmin(project.id, userId);
|
||||
const uploader = await this.userService.findById(userId);
|
||||
|
||||
const name = decodeOriginalName(file.originalname);
|
||||
// 업로드 시점에 텍스트를 1회 추출해 캐싱 — AI 추천에서 재파싱 없이 꺼내 쓴다.
|
||||
const extractedText = await extractAttachmentText(
|
||||
file.path,
|
||||
file.mimetype,
|
||||
name,
|
||||
);
|
||||
const saved = await this.attachmentRepo.save(
|
||||
this.attachmentRepo.create({
|
||||
project: { id: project.id } as Project,
|
||||
uploader: uploader ?? null,
|
||||
name,
|
||||
storedName: file.filename,
|
||||
size: file.size,
|
||||
mime: file.mimetype,
|
||||
kind: deriveKind(file.mimetype, name),
|
||||
extractedText,
|
||||
}),
|
||||
);
|
||||
return this.toAttachmentResponse(
|
||||
{ ...saved, uploader: uploader ?? null },
|
||||
project.id,
|
||||
);
|
||||
withText: boolean,
|
||||
): Promise<DriveFile[]> {
|
||||
const folderIds = await this.collectLinkedFolderIds(projectId);
|
||||
if (folderIds.length === 0) return [];
|
||||
const qb = this.fileRepo
|
||||
.createQueryBuilder('f')
|
||||
.leftJoin('f.folder', 'fd')
|
||||
.where('fd.id IN (:...folderIds)', { folderIds })
|
||||
.andWhere("f.status = 'active'")
|
||||
.orderBy('f.name', 'ASC');
|
||||
if (withText) qb.addSelect('f.extractedText');
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
// 첨부 삭제 — 관리자만. 디스크 파일도 정리
|
||||
async removeFile(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
fileId: string,
|
||||
): Promise<void> {
|
||||
const project = await this.getProjectOrThrow(projectId);
|
||||
await this.assertProjectAdmin(project.id, userId);
|
||||
const attachment = await this.attachmentRepo.findOne({
|
||||
where: { id: fileId, project: { id: project.id } },
|
||||
// 프로젝트에 연동된 모든 폴더 + 그 하위 폴더 ID 집합(materialized path 기반)
|
||||
private async collectLinkedFolderIds(projectId: string): Promise<string[]> {
|
||||
const links = await this.linkRepo.find({
|
||||
where: { project: { id: projectId } },
|
||||
relations: { folder: true },
|
||||
});
|
||||
if (!attachment) {
|
||||
throw new NotFoundException();
|
||||
if (links.length === 0) return [];
|
||||
const ids = new Set<string>();
|
||||
for (const l of links) {
|
||||
const tree = await this.folderRepo.find({
|
||||
where: { path: Like(`${l.folder.path}%`) },
|
||||
select: { id: true },
|
||||
});
|
||||
tree.forEach((f) => ids.add(f.id));
|
||||
ids.add(l.folder.id);
|
||||
}
|
||||
await this.attachmentRepo.remove(attachment);
|
||||
await this.deleteFileFromDisk(attachment.storedName);
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
// 다운로드용 첨부 메타 + 디스크 절대경로 — 멤버면 조회 가능. 컨트롤러가 스트리밍
|
||||
async getFileForDownload(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
fileId: string,
|
||||
): Promise<{ attachment: ProjectAttachment; absolutePath: string }> {
|
||||
const project = await this.getProjectOrThrow(projectId);
|
||||
await this.assertProjectMember(project.id, userId);
|
||||
const attachment = await this.attachmentRepo.findOne({
|
||||
where: { id: fileId, project: { id: project.id } },
|
||||
// 폴더(자기+하위) 내 활성 파일 수
|
||||
private async countFilesInFolderTree(folder: DriveFolder): Promise<number> {
|
||||
const tree = await this.folderRepo.find({
|
||||
where: { path: Like(`${folder.path}%`) },
|
||||
select: { id: true },
|
||||
});
|
||||
const ids = tree.map((f) => f.id);
|
||||
if (folder.id && !ids.includes(folder.id)) ids.push(folder.id);
|
||||
if (ids.length === 0) return 0;
|
||||
return this.fileRepo.count({
|
||||
where: { folder: { id: In(ids) }, status: 'active' },
|
||||
});
|
||||
if (!attachment) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return {
|
||||
attachment,
|
||||
absolutePath: join(UPLOAD_DIR, attachment.storedName),
|
||||
};
|
||||
}
|
||||
|
||||
// 디스크 파일 삭제(베스트 에포트 — 없어도 무시)
|
||||
private async deleteFileFromDisk(storedName: string): Promise<void> {
|
||||
// 드라이브 파일 1건 텍스트 추출(NCP 다운로드 → 추출 → 캐싱). 결과 상태를 함께 기록.
|
||||
private async extractDriveFile(f: DriveFile): Promise<string | null> {
|
||||
if (!isExtractableType(f.mime, f.name)) {
|
||||
await this.fileRepo.update(f.id, { extractStatus: 'unsupported' });
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await unlink(join(UPLOAD_DIR, storedName));
|
||||
const buf = await this.storage.getObjectBuffer(f.objectKey);
|
||||
if (!buf) {
|
||||
await this.fileRepo.update(f.id, { extractStatus: 'failed' });
|
||||
return null;
|
||||
}
|
||||
const text = await extractTextFromBuffer(buf, f.mime, f.name);
|
||||
if (text) {
|
||||
await this.fileRepo.update(f.id, {
|
||||
extractedText: text,
|
||||
extractStatus: 'done',
|
||||
});
|
||||
return text;
|
||||
}
|
||||
await this.fileRepo.update(f.id, { extractStatus: 'failed' });
|
||||
return null;
|
||||
} catch {
|
||||
// 이미 없거나 권한 문제 — 본 흐름을 막지 않는다
|
||||
await this.fileRepo.update(f.id, { extractStatus: 'failed' });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private toAttachmentResponse(
|
||||
att: ProjectAttachment,
|
||||
projectId: string,
|
||||
): ProjectAttachmentResponse {
|
||||
private toDriveLinkResponse(
|
||||
link: ProjectFolderLink,
|
||||
documentCount: number,
|
||||
): ProjectDriveLinkResponse {
|
||||
return {
|
||||
id: att.id,
|
||||
name: att.name,
|
||||
size: att.size,
|
||||
kind: att.kind,
|
||||
url: `/api/projects/${projectId}/files/${att.id}/download`,
|
||||
uploader: att.uploader ? UserService.toPublic(att.uploader) : null,
|
||||
createdAt: att.createdAt.toISOString(),
|
||||
aiStatus: this.deriveAiStatus(att),
|
||||
id: link.id,
|
||||
folder: {
|
||||
id: link.folder.id,
|
||||
name: link.folder.name,
|
||||
path: link.folder.path,
|
||||
},
|
||||
permission: link.permission,
|
||||
linkedBy: link.linkedBy ? UserService.toPublic(link.linkedBy) : null,
|
||||
documentCount,
|
||||
createdAt: link.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// 첨부의 AI 반영 상태 산출 — 추출 본문 유무 + 형식으로 판별
|
||||
private deriveAiStatus(att: ProjectAttachment): AttachmentAiStatus {
|
||||
if (att.extractedText && att.extractedText.trim().length > 0) {
|
||||
return 'ready';
|
||||
}
|
||||
// 본문이 비어 있음 — 지원 형식이면 추출 실패, 아니면 미지원 형식
|
||||
return isExtractableType(att.mime, att.name) ? 'failed' : 'unsupported';
|
||||
// 드라이브 문서의 AI 반영 상태 — 추출 상태/형식으로 판별
|
||||
private driveAiStatus(f: DriveFile): DocumentAiStatus {
|
||||
if (f.extractStatus === 'done') return 'ready';
|
||||
if (f.extractStatus === 'unsupported') return 'unsupported';
|
||||
if (f.extractStatus === 'failed') return 'failed';
|
||||
// pending — 추출 가능 형식이면 '대기', 아니면 미지원
|
||||
return isExtractableType(f.mime, f.name) ? 'pending' : 'unsupported';
|
||||
}
|
||||
|
||||
// 프로젝트별 업무 완료/전체 수 — 진행률 집계용
|
||||
|
||||
Reference in New Issue
Block a user