From 5390f4ec76af190738a26eabc63eec7058f404b0 Mon Sep 17 00:00:00 2001 From: Hanyi SEO <122385460+hanyiseo2@users.noreply.github.com> Date: Sun, 30 Jul 2023 01:55:16 +0900 Subject: [PATCH] =?UTF-8?q?admin-imgUrl=20=EC=9E=91=EC=97=85=EC=A4=91=20(#?= =?UTF-8?q?528)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * admin-imgUrl * POST admin/ingredient - create ingredient --- package-lock.json | 219 ++++++++++++++++++++++++++----- package.json | 4 +- src/app.ts | 8 +- src/config/config.js | 3 + src/config/multerConfig.ts | 15 +++ src/config/s3Config.ts | 10 ++ src/controllers/Admin.ts | 147 +++++++++++++++++++-- src/dao/IngredientDao.ts | 11 ++ src/dao/PerfumeDao.ts | 15 ++- src/service/ImageService.ts | 44 ++++++- src/service/IngredientService.ts | 19 +++ src/service/PerfumeService.ts | 17 ++- 12 files changed, 457 insertions(+), 55 deletions(-) create mode 100644 src/config/multerConfig.ts create mode 100644 src/config/s3Config.ts diff --git a/package-lock.json b/package-lock.json index 1b0ab305..f43fb667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-typescript": "^7.16.7", "@opensearch-project/opensearch": "^1.2.0", - "aws-sdk": "^2.1101.0", + "aws-sdk": "^2.1418.0", "babel-plugin-module-resolver": "^4.1.0", "body-parser": "^1.20.1", "chai": "^4.3.6", @@ -34,6 +34,7 @@ "lodash": "^4.17.21", "mongoose": "^6.4.6", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "mysql2": "^2.3.3", "node-cron": "^3.0.2", "parseurl": "^1.3.3", @@ -59,6 +60,7 @@ "@types/lodash": "^4.14.180", "@types/mocha": "^9.1.0", "@types/morgan": "^1.9.3", + "@types/multer": "^1.4.7", "@types/node": "^17.0.45", "@types/node-cron": "^3.0.2", "@types/sinon": "^10.0.15", @@ -2252,6 +2254,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, + "node_modules/@types/multer": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/mysql": { "version": "2.15.21", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.21.tgz", @@ -2693,6 +2704,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2782,9 +2798,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1207.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1207.0.tgz", - "integrity": "sha512-UDNYNeWw9ATbz+pH4lI3AUQgnmK3RwowCrXmW+lVV0bZYo+efiB/LEWQKe0nZK9K2h1LxZYihIih9dOvaGme/w==", + "version": "2.1418.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1418.0.tgz", + "integrity": "sha512-6WDMJQAWKwVt+44+61c/SAXKpUSwToqBMeaqizhEe3GN8TWfxMc9RfCnsYIIwS+L+5hedmKC5oc6Fg2ujs8KUQ==", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -2795,7 +2811,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" }, "engines": { "node": ">= 10.0.0" @@ -3064,8 +3080,18 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } }, "node_modules/bytes": { "version": "3.1.2", @@ -3320,6 +3346,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -5968,11 +6008,21 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -6260,6 +6310,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/mysql": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", @@ -7826,6 +7893,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8326,6 +8401,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -8763,22 +8843,33 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dependencies": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" } }, "node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "engines": { "node": ">=4.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -10460,6 +10551,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, + "@types/multer": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/mysql": { "version": "2.15.21", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.21.tgz", @@ -10773,6 +10873,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -10841,9 +10946,9 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, "aws-sdk": { - "version": "2.1207.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1207.0.tgz", - "integrity": "sha512-UDNYNeWw9ATbz+pH4lI3AUQgnmK3RwowCrXmW+lVV0bZYo+efiB/LEWQKe0nZK9K2h1LxZYihIih9dOvaGme/w==", + "version": "2.1418.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1418.0.tgz", + "integrity": "sha512-6WDMJQAWKwVt+44+61c/SAXKpUSwToqBMeaqizhEe3GN8TWfxMc9RfCnsYIIwS+L+5hedmKC5oc6Fg2ujs8KUQ==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -10854,7 +10959,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" } }, "aws4": { @@ -11049,8 +11154,15 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } }, "bytes": { "version": "3.1.2", @@ -11245,6 +11357,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -13256,8 +13379,15 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } }, "mocha": { "version": "10.2.0", @@ -13482,6 +13612,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "mysql": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", @@ -14650,6 +14794,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -15011,6 +15160,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -15337,18 +15491,23 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==" + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index 983048ca..6f5c5315 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-typescript": "^7.16.7", "@opensearch-project/opensearch": "^1.2.0", - "aws-sdk": "^2.1101.0", + "aws-sdk": "^2.1418.0", "babel-plugin-module-resolver": "^4.1.0", "body-parser": "^1.20.1", "chai": "^4.3.6", @@ -48,6 +48,7 @@ "lodash": "^4.17.21", "mongoose": "^6.4.6", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "mysql2": "^2.3.3", "node-cron": "^3.0.2", "parseurl": "^1.3.3", @@ -73,6 +74,7 @@ "@types/lodash": "^4.14.180", "@types/mocha": "^9.1.0", "@types/morgan": "^1.9.3", + "@types/multer": "^1.4.7", "@types/node": "^17.0.45", "@types/node-cron": "^3.0.2", "@types/sinon": "^10.0.15", diff --git a/src/app.ts b/src/app.ts index ca7ccc37..3d5c2b66 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,7 +10,6 @@ import properties from '@properties'; import { logger } from '@modules/winston'; import makeMorgan from '@modules/morgan'; - import { HttpError } from '@errors'; import statusCode from '@utils/statusCode'; import { verifyTokenMiddleware, encryptPassword } from '@middleware/auth'; @@ -23,8 +22,10 @@ const { specs, swaggerMetadataHandler, } = require('@modules/swagger'); - import { sequelize } from './models'; +import { multerConfig } from './config/multerConfig'; +import multer from 'multer'; + sequelize.sync(); require('@utils/db/mongoose.js'); @@ -51,6 +52,7 @@ const corsOptionsDelegate: CorsOptionsDelegate = function ( app.use(cors(corsOptionsDelegate)); app.use(bodyParser.json({ limit: '5mb' })); +const upload = multer({ storage: multerConfig.storage }); app.use( makeMorgan((message: string) => { @@ -58,6 +60,8 @@ app.use( }) ); +app.use(upload.single('file')); + app.use('/docs', swaggerUi.serve, swaggerUi.setup(specs)); app.use(swaggerMetadataHandler); diff --git a/src/config/config.js b/src/config/config.js index 0982ef47..5265bd5d 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -8,6 +8,9 @@ module.exports = { dialect: process.env.MYSQL_DEV_DIALECT, timezone: '+09:00', logging: false, + s3AccessKey: process.env.AWS_ACCESS_KEY_ID, + s3SecretKey: process.env.AWS_SECRET_ACCESS_KEY, + bucketName: process.env.AWS_BUCKET_NAME, }, test: { username: process.env.MYSQL_TST_USERNAME, diff --git a/src/config/multerConfig.ts b/src/config/multerConfig.ts new file mode 100644 index 00000000..b2c384ae --- /dev/null +++ b/src/config/multerConfig.ts @@ -0,0 +1,15 @@ +import multer from 'multer'; +type FileNameCallback = (error: Error | null, filename: string) => void; + +export const multerConfig = { + storage: multer.diskStorage({ + destination: 'perfumes/', + filename: function ( + _req: any, + file: Express.Multer.File, + cb: FileNameCallback + ) { + cb(null, file.originalname); + }, + }), +}; diff --git a/src/config/s3Config.ts b/src/config/s3Config.ts new file mode 100644 index 00000000..fc819b38 --- /dev/null +++ b/src/config/s3Config.ts @@ -0,0 +1,10 @@ +import AWS from 'aws-sdk'; +import config from './config'; + +const storage: AWS.S3 = new AWS.S3({ + accessKeyId: config.development.s3AccessKey, + secretAccessKey: config.development.s3SecretKey, + region: 'ap-northeast-2', +}); + +export default storage; diff --git a/src/controllers/Admin.ts b/src/controllers/Admin.ts index 03c1e8c4..290b4c89 100644 --- a/src/controllers/Admin.ts +++ b/src/controllers/Admin.ts @@ -26,6 +26,7 @@ import { ListAndCountDTO } from '@src/data/dto'; import IngredientCategoryService from '@src/service/IngredientCategoryService'; import { DuplicatedEntryError } from '@src/utils/errors/errors'; import * as Hangul from 'hangul-js'; +import ImageService from '@src/service/ImageService'; let Admin: AdminService = new AdminService(); let Perfume: PerfumeService = new PerfumeService(); let Ingredient: IngredientService = new IngredientService(); @@ -441,9 +442,16 @@ export const createIngredientCategory: RequestHandler = async ( } }; +export async function createImageUrl( + file: Express.Multer.File +): Promise { + const fileLocation: string = await ImageService.uploadImagefileToS3(file); + return fileLocation; +} + /** * @swagger - * /admin/perfumes: + * /admin/perfume: * post: * tags: * - admin @@ -452,14 +460,64 @@ export const createIngredientCategory: RequestHandler = async ( * operationId: createPerfume * produces: * - application/json - * consumes: - * - multipart/form-data * parameters: * - name: body * in: body * required: true * schema: * $ref: '#/definitions/PerfumeInput' + * responses: + * 200: + * description: success + * schema: + * type: object + * properties: + * message: + * type: string + * 400: + * description: 요청 실패 + * 409: + * description: 같은 이름의 카테고리가 존재할 때 + * schema: + * type: object + * x-swagger-router-controller: Admin + */ + +export const createPerfume: RequestHandler = async ( + req: Request, + res: Response +) => { + const { name, englishName, brandIdx, abundanceRate, Notes } = req.body; + try { + await Perfume.create(name, englishName, brandIdx, abundanceRate, Notes); + res.status(StatusCode.OK).json({ + message: '성공', + }); + } catch (e: any) { + if (e instanceof DuplicatedEntryError) { + res.status(StatusCode.CONFLICT).json( + new ResponseDTO(MSG_EXIST_DUPLICATE_ENTRY, false) + ); + } else { + res.status(StatusCode.BAD_REQUEST).json( + new SimpleResponseDTO(e.message) + ); + } + } +}; + +/** + * @swagger + * /admin/perfume/img: + * post: + * tags: + * - admin + * summary: 향수 추가 + * description: 향수 추가 + * operationId: createPerfumeImg + * consumes: + * - multipart/form-data + * parameters: * - name: file * in: formData * type: file @@ -480,21 +538,19 @@ export const createIngredientCategory: RequestHandler = async ( * type: object * x-swagger-router-controller: Admin */ -export const createPerfume: RequestHandler = async ( + +export const createPerfumeImg: RequestHandler = async ( req: Request, res: Response ) => { - const { name, englishName, brandIdx, abundanceRate, Notes, imageUrl } = - req.body; + console.log(req.file); + if (!req.file) + return res + .status(400) + .json({ error: 'cannot find file from the request' }); + const imageUrl = await createImageUrl(req.file); try { - await Perfume.create( - name, - englishName, - brandIdx, - abundanceRate, - Notes, - imageUrl - ); + await Perfume.createImg(imageUrl); res.status(StatusCode.OK).json({ message: '성공', }); @@ -655,3 +711,66 @@ export const createBrand: RequestHandler = async ( } } }; + +/** + * @swagger + * /admin/Ingredient: + * post: + * tags: + * - admin + * summary: 향료 추가 + * description: 향료 추가 + * operationId: createIngredient + * produces: + * - application/json + * parameters: + * - name: body + * in: body + * required: true + * schema: + * type: object + * properties: + * name: + * type: string + * seriesIdx: + * type: number + * categoryIdx: + * type: number + * responses: + * 200: + * description: success + * schema: + * type: object + * properties: + * message: + * type: string + * 400: + * description: 요청 실패 + * 409: + * description: 같은 이름의 카테고리가 존재할 때 + * schema: + * type: object + * x-swagger-router-controller: Admin + */ +export const createIngredient: RequestHandler = async ( + req: Request, + res: Response +) => { + const { name, seriesIdx, categoryIdx } = req.body; + try { + await Ingredient.create(name, seriesIdx, categoryIdx); + res.status(StatusCode.OK).json({ + message: '성공', + }); + } catch (e: any) { + if (e instanceof DuplicatedEntryError) { + res.status(StatusCode.CONFLICT).json( + new ResponseDTO(MSG_EXIST_DUPLICATE_ENTRY, false) + ); + } else { + res.status(StatusCode.BAD_REQUEST).json( + new SimpleResponseDTO(e.message) + ); + } + } +}; diff --git a/src/dao/IngredientDao.ts b/src/dao/IngredientDao.ts index cde4dd23..6e00f5bb 100644 --- a/src/dao/IngredientDao.ts +++ b/src/dao/IngredientDao.ts @@ -105,6 +105,17 @@ class IngredientDao { order: [['createdAt', 'desc']], }); } + + async create(name: string, seriesIdx: number, categoryIdx: number) { + return Ingredient.create({ + name, + seriesIdx, + categoryIdx, + englishName: '', + description: '', + imageUrl: '', + }); + } } export default IngredientDao; diff --git a/src/dao/PerfumeDao.ts b/src/dao/PerfumeDao.ts index 64cc894e..11120bfa 100644 --- a/src/dao/PerfumeDao.ts +++ b/src/dao/PerfumeDao.ts @@ -446,8 +446,7 @@ class PerfumeDao { englishName: string, brandIdx: number, abundanceRate: number, - Notes: Array, - imageUrl: string + Notes: Array ) { await sequelize.transaction(async (transaction) => { const created = await Perfume.create( @@ -456,7 +455,7 @@ class PerfumeDao { englishName, brandIdx, abundanceRate, - imageUrl, + imageUrl: '', story: '', volumeAndPrice: '', }, @@ -470,6 +469,16 @@ class PerfumeDao { return created; }); } + + async createImg(imageUrl: string | any) { + const created = await Perfume.update( + { + imageUrl, + }, + { where: {} } + ); + return created; + } } export default PerfumeDao; diff --git a/src/service/ImageService.ts b/src/service/ImageService.ts index 4bcbcc76..075e4bf0 100644 --- a/src/service/ImageService.ts +++ b/src/service/ImageService.ts @@ -1,4 +1,11 @@ import S3FileDao from '@src/dao/S3FileDao'; +import config from '../config/config'; +import fs from 'fs'; +import AWS from 'aws-sdk'; +import { + DuplicatedEntryError, + FailedToCreateError, +} from '@src/utils/errors/errors'; class ImageService { s3FileDao: S3FileDao; @@ -21,6 +28,41 @@ class ImageService { return [defaultImage]; } -} + static async uploadImagefileToS3( + fileData: Express.Multer.File + ): Promise { + try { + console.log('Inside the Service'); + const fileContent: Buffer = fs.readFileSync(fileData.path); + + const storage: AWS.S3 = new AWS.S3({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: 'ap-northeast-2', + }); + console.log('Inside the Service2'); + + const params: { + Bucket: string; + Key: string; + Body: Buffer; + } = { + Bucket: config.development.bucketName as string, + Key: fileData.originalname, + Body: fileContent, + }; + console.log('Inside the Service3'); + + const result = await storage.upload(params).promise(); + console.log(result.Location); + return result.Location; + } catch (err: Error | any) { + if (err.parent?.errno === 1062) { + throw new DuplicatedEntryError(); + } + throw new FailedToCreateError(); + } + } +} export default ImageService; diff --git a/src/service/IngredientService.ts b/src/service/IngredientService.ts index 3fa56af7..e4d975ad 100644 --- a/src/service/IngredientService.ts +++ b/src/service/IngredientService.ts @@ -9,6 +9,10 @@ import { PagingDTO, } from '@dto/index'; import { Op } from 'sequelize'; +import { + DuplicatedEntryError, + FailedToCreateError, +} from '@src/utils/errors/errors'; const LOG_TAG: string = '[Ingredient/Service]'; @@ -79,7 +83,22 @@ class IngredientService { }; }); return new ListAndCountDTO(count, perfumesWithCategory); + } + async create(name: string, seriesIdx: number, categoryIdx: number) { + try { + return await this.ingredientDao.create( + name, + seriesIdx, + categoryIdx + ); + } catch (err: Error | any) { + if (err.parent.errno === 1062) { + throw new DuplicatedEntryError(); + } + + throw new FailedToCreateError(); + } } } diff --git a/src/service/PerfumeService.ts b/src/service/PerfumeService.ts index 98ce6e38..116156bd 100644 --- a/src/service/PerfumeService.ts +++ b/src/service/PerfumeService.ts @@ -479,8 +479,7 @@ class PerfumeService { englishName: string, brandIdx: number, abundanceRate: number, - Notes: Array, - imageUrl: string + Notes: Array ) { try { return await perfumeDao.create( @@ -488,8 +487,7 @@ class PerfumeService { englishName, brandIdx, abundanceRate, - Notes, - imageUrl + Notes ); } catch (err: Error | any) { if (err.parent?.errno === 1062) { @@ -498,6 +496,17 @@ class PerfumeService { throw new FailedToCreateError(); } } + + async createImg(imageUrl: string) { + try { + return await perfumeDao.createImg(imageUrl); + } catch (err: Error | any) { + if (err.parent?.errno === 1062) { + throw new DuplicatedEntryError(); + } + throw new FailedToCreateError(); + } + } } export default PerfumeService;