diff --git a/package-lock.json b/package-lock.json index ceac1fa..b3058f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,10 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.2", + "aws-sdk": "^2.1574.0", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", + "multer-s3": "^2.10.0", "mysql2": "^3.9.1", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -27,7 +29,8 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "typeorm": "^0.3.20", - "typeorm-naming-strategies": "^4.1.0" + "typeorm-naming-strategies": "^4.1.0", + "uuid": "^9.0.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -36,9 +39,11 @@ "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.11", "@types/node": "^20.3.1", "@types/passport": "^1.0.16", "@types/passport-google-oauth20": "^2.0.14", + "@types/passport-jwt": "^4.0.1", "@types/passport-kakao": "^1.0.3", "@types/passport-naver": "^1.0.4", "@types/supertest": "^6.0.0", @@ -2347,6 +2352,15 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", @@ -2384,6 +2398,16 @@ "@types/passport-oauth2": "*" } }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, "node_modules/@types/passport-kakao": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/passport-kakao/-/passport-kakao-1.0.3.tgz", @@ -2415,6 +2439,16 @@ "@types/passport": "*" } }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", @@ -3130,6 +3164,72 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1574.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1574.0.tgz", + "integrity": "sha512-AC3VptGeggp7AFGj66PIt9GrnLyDBo7Owb5TmbGDe2OIvgeHH3nBD4S3wRZQ9u3Ea6iREnalXxQ2n5rflCeG9g==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4903,6 +5003,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5038,6 +5146,14 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -5457,6 +5573,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -5490,6 +5620,11 @@ "node": "*" } }, + "node_modules/html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5672,6 +5807,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5690,6 +5840,17 @@ "node": ">=8" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -5728,6 +5889,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5784,6 +5959,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -6576,6 +6765,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7102,6 +7299,16 @@ "node": ">= 6.0.0" } }, + "node_modules/multer-s3": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/multer-s3/-/multer-s3-2.10.0.tgz", + "integrity": "sha512-RZsiqG19C9gE82lB7v8duJ+TMIf70fWYHlIwuNcsanOH1ePBoPXZvboEQxEow9jUkk7WQsuyVA2TgriOuDrVrw==", + "dependencies": { + "file-type": "^3.3.0", + "html-comment-regex": "^1.1.2", + "run-parallel": "^1.1.6" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -7805,6 +8012,14 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7936,11 +8151,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -8237,7 +8460,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -8288,6 +8510,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -9613,6 +9840,32 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9820,6 +10073,24 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "dependencies": { + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -9883,6 +10154,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "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", diff --git a/package.json b/package.json index 6e3b377..40656fe 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.2", + "aws-sdk": "^2.1574.0", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", + "multer-s3": "^2.10.0", "mysql2": "^3.9.1", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -38,7 +40,8 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "typeorm": "^0.3.20", - "typeorm-naming-strategies": "^4.1.0" + "typeorm-naming-strategies": "^4.1.0", + "uuid": "^9.0.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -47,9 +50,11 @@ "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.11", "@types/node": "^20.3.1", "@types/passport": "^1.0.16", "@types/passport-google-oauth20": "^2.0.14", + "@types/passport-jwt": "^4.0.1", "@types/passport-kakao": "^1.0.3", "@types/passport-naver": "^1.0.4", "@types/supertest": "^6.0.0", diff --git a/src/application/domain/user/controller/user.controller.ts b/src/application/domain/user/controller/user.controller.ts index 7101f64..b52b4ac 100644 --- a/src/application/domain/user/controller/user.controller.ts +++ b/src/application/domain/user/controller/user.controller.ts @@ -1,9 +1,23 @@ -import { Controller, Get, Param, Post, UseFilters, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, + UploadedFile, + UseFilters, + UseGuards, + UseInterceptors +} from "@nestjs/common"; import { GlobalExceptionFilter } from "../../../../infrastructure/global/filter/global.exception.filter"; import { UserService } from "../service/user.service"; import { AuthGuard } from "@nestjs/passport"; import { CurrentUser } from "../../../../infrastructure/global/decorator/current-user"; import { UserEntity } from "../../../../infrastructure/domain/user/persistence/user.entity"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { UpdateUserRequest } from "../dto/user.dto"; @UseFilters(GlobalExceptionFilter) @Controller('user') @@ -14,7 +28,7 @@ export class UserController { } @UseGuards(AuthGuard()) - @Post('apply/:id') + @Patch('apply/:id') async applyFriend(@Param() id, @CurrentUser() user: UserEntity) { await this.userService.applyFriend(id, user) } @@ -24,4 +38,46 @@ export class UserController { async queryApplyFriend(@CurrentUser() user: UserEntity) { return await this.userService.queryApplyFriend(user) } + + @UseGuards(AuthGuard()) + @Get('friend') + async queryFriend(@CurrentUser() user: UserEntity) { + return await this.userService.queryFriend(user) + } + + @Get('search') + async searchUser(@Query('keyword') keyword: string) { + return await this.userService.searchUser(keyword) + } + + @UseGuards(AuthGuard()) + @Post('status/:id') + async updateApplyStatus(@Param() id, @Query('status') status, @CurrentUser() user: UserEntity) { + await this.userService.updateApplyStatus(id, status, user) + } + + @UseGuards(AuthGuard()) + @Patch('notification') + async updateIsTurnOn(@Query('is-turn-on') isTurnOn: boolean, @CurrentUser() user: UserEntity) { + await this.userService.notificationOnOff(isTurnOn, user) + } + + @UseInterceptors(FileInterceptor('file')) + @UseGuards(AuthGuard()) + @Patch('upload') + async uploadProfile(@UploadedFile('file') file: Express.Multer.File, @CurrentUser() user: UserEntity) { + await this.userService.uploadProfile(file, user) + } + + @UseGuards(AuthGuard()) + @Get('info') + async queryUserInfo(@CurrentUser() user: UserEntity) { + return await this.userService.queryUserInfo(user) + } + + @UseGuards(AuthGuard()) + @Patch('update') + async updateUserInfo(@CurrentUser() user: UserEntity, @Body() request: UpdateUserRequest) { + return await this.userService.updateUser(user, request) + } } \ No newline at end of file diff --git a/src/application/domain/user/dto/user.dto.ts b/src/application/domain/user/dto/user.dto.ts index e30e69c..34f9409 100644 --- a/src/application/domain/user/dto/user.dto.ts +++ b/src/application/domain/user/dto/user.dto.ts @@ -1,9 +1,31 @@ +import { int } from "aws-sdk/clients/datapipeline"; +import { IsNotEmpty, IsNumber, IsString, Matches } from "class-validator"; + export class QueryApplyFriendListResponse { users: FriendListElement[] } export class FriendListElement { + id: string nickname: string profile: string isTurnOn: boolean +} + +export class QueryUserInfoResponse { + id: string + nickname: string + email: string + point: number + profile: string +} + +export class UpdateUserRequest { + @Matches(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/, { + message: '유효한 이메일 주소를 입력해주세요.', + }) + email: string; + @IsNotEmpty() + @IsString() + nickname: string; } \ No newline at end of file diff --git a/src/application/domain/user/service/user.service.ts b/src/application/domain/user/service/user.service.ts index 9f2b49b..7af4dab 100644 --- a/src/application/domain/user/service/user.service.ts +++ b/src/application/domain/user/service/user.service.ts @@ -2,8 +2,11 @@ import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { FriendApplyEntity } from "../../../../infrastructure/domain/user/persistence/friend.apply.entity"; -import { FriendListElement, QueryApplyFriendListResponse } from "../dto/user.dto"; +import { FriendListElement, QueryApplyFriendListResponse, QueryUserInfoResponse } from "../dto/user.dto"; import { UserEntity } from "../../../../infrastructure/domain/user/persistence/user.entity"; +import { FriendEntity } from "../../../../infrastructure/domain/user/persistence/friend.entity"; +import { AwsService } from "../../../../infrastructure/global/utils/s3/aws.service"; +import { randomUUID } from "crypto"; @Injectable() export class UserService { @@ -12,6 +15,9 @@ export class UserService { private friendApplyRepository: Repository, @InjectRepository(UserEntity) private userRepository: Repository, + @InjectRepository(FriendEntity) + private friendRepository: Repository, + private awsService: AwsService ) { } @@ -35,14 +41,51 @@ export class UserService { } async queryApplyFriend(req) { + let applyList = await this.friendApplyRepository.findBy({ receiveUserId: req }) + let friendListResponse = new QueryApplyFriendListResponse(); + + friendListResponse.users = await Promise.all(applyList.map(async (friend) => { + let requestUser = await this.userRepository.findOneBy(friend.requestUserId); + let element = new FriendListElement(); + + element.id = friend.id + element.nickname = requestUser.nickname + element.profile = requestUser.profile + element.isTurnOn = requestUser.isTurnOn - let friendList = await this.friendApplyRepository.findBy({ receiveUserId: req }) + return element + })) + + return friendListResponse + } + + async queryFriend(req) { + let friendList = await this.friendRepository.findBy([ { userId1: req }, { userId2: req } ]) let friendListResponse = new QueryApplyFriendListResponse(); friendListResponse.users = await Promise.all(friendList.map(async (friend) => { - let requestUser = await this.userRepository.findOneBy(friend.requestUserId); + let requestUser = await this.userRepository.findOneBy({ id: friend.userId2.id }); let element = new FriendListElement(); + element.id = friend.id + element.nickname = requestUser.nickname + element.profile = requestUser.profile + element.isTurnOn = requestUser.isTurnOn + + return element + })) + return friendListResponse + } + + async searchUser(keyword) { + let users = await this.userRepository.findBy({ nickname: keyword }) + + let friendListResponse = new QueryApplyFriendListResponse(); + friendListResponse.users = await Promise.all(users.map(async (user) => { + let requestUser = await this.userRepository.findOneBy({ id: user.id }); + let element = new FriendListElement(); + + element.id = user.id element.nickname = requestUser.nickname element.profile = requestUser.profile element.isTurnOn = requestUser.isTurnOn @@ -52,4 +95,78 @@ export class UserService { return friendListResponse } + + async updateApplyStatus(id, status, req) { + let apply = await this.friendApplyRepository.findOne({ where: id }) + + if (!apply) { + throw new HttpException('Apply Not Found', HttpStatus.NOT_FOUND) + } + + if (status === 'ACCEPT') { + let user = await apply.requestUserId + + let friend = new FriendEntity(); + + friend.userId1 = req + friend.userId2 = user + + await this.friendApplyRepository.delete(apply) + await this.friendRepository.save(friend) + } else if (status === 'REJECT') { + await this.friendApplyRepository.delete(apply) + } + } + + async notificationOnOff(isTurnOn, req) { + let user = await this.userRepository.findOne({ where: req }) + + if (!user) { + throw new HttpException('User Not Found', 404) + } + + user.isTurnOn = isTurnOn == 'true' ? true : false + await this.userRepository.save(user) + } + + async uploadProfile(file, req) { + let user = await this.userRepository.findOne({ where: req }) + + if (!user) { + throw new HttpException('User Not Found', 404) + } + + user.profile = await this.awsService.upload(randomUUID(), file) + await this.userRepository.save(user) + } + + async queryUserInfo(req) { + let user = await this.userRepository.findOne({ where: req }) + let response = new QueryUserInfoResponse() + + if (!user) { + throw new HttpException('User Not Found', 404) + } + + response.id = user.id + response.email = user.email + response.nickname = user.nickname + response.point = user.point + response.profile = user.profile + + return response + } + + async updateUser(req, request) { + let user = await this.userRepository.findOne({ where: req }) + + if (!user) { + throw new HttpException('User Not Found', 404) + } + + user.email = request.email + user.nickname = request.nickname + + await this.userRepository.save(user) + } } \ No newline at end of file diff --git a/src/infrastructure/domain/user/persistence/friend.entity.ts b/src/infrastructure/domain/user/persistence/friend.entity.ts index 37d9b5e..dd08aab 100644 --- a/src/infrastructure/domain/user/persistence/friend.entity.ts +++ b/src/infrastructure/domain/user/persistence/friend.entity.ts @@ -8,9 +8,9 @@ export class FriendEntity { @ManyToOne(() => UserEntity) @JoinColumn({ name: 'user_id1' }) - userId1: string; + userId1: UserEntity; @ManyToOne(() => UserEntity) @JoinColumn({ name: 'user_id2' }) - user_id2: string; + userId2: UserEntity; } \ No newline at end of file diff --git a/src/infrastructure/domain/user/persistence/user.entity.ts b/src/infrastructure/domain/user/persistence/user.entity.ts index e9e5cfa..eb0e3cc 100644 --- a/src/infrastructure/domain/user/persistence/user.entity.ts +++ b/src/infrastructure/domain/user/persistence/user.entity.ts @@ -20,6 +20,9 @@ export class UserEntity { @Column('varchar', { nullable: false, length: 3000 }) profile: string; + @Column('int', {nullable: false, default: 0}) + point: number; + @Column('boolean', { nullable: false, default: true }) isTurnOn: boolean; diff --git a/src/infrastructure/global/jwt/jwt.guard.ts b/src/infrastructure/global/jwt/jwt.guard.ts new file mode 100644 index 0000000..6becd2b --- /dev/null +++ b/src/infrastructure/global/jwt/jwt.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from "@nestjs/passport"; + +export class JwtGuard extends AuthGuard('jwt') {} \ No newline at end of file diff --git a/src/infrastructure/global/module/user.module.ts b/src/infrastructure/global/module/user.module.ts index 96e3f71..9c473eb 100644 --- a/src/infrastructure/global/module/user.module.ts +++ b/src/infrastructure/global/module/user.module.ts @@ -5,6 +5,7 @@ import { FriendApplyEntity } from "../../domain/user/persistence/friend.apply.en import { FriendEntity } from "../../domain/user/persistence/friend.entity"; import { UserController } from "../../../application/domain/user/controller/user.controller"; import { UserService } from "../../../application/domain/user/service/user.service"; +import { AwsService } from "../utils/s3/aws.service"; const USER_REPOSITORY = TypeOrmModule.forFeature([ UserEntity, FriendApplyEntity, FriendEntity ]); @@ -12,7 +13,7 @@ const USER_REPOSITORY = TypeOrmModule.forFeature([ UserEntity, FriendApplyEntity @Module({ imports: [ USER_REPOSITORY ], controllers: [ UserController ], - providers: [ UserService ], + providers: [ UserService, AwsService ], exports: [ USER_REPOSITORY ] }) export class UserModule { diff --git a/src/infrastructure/global/utils/s3/aws.service.ts b/src/infrastructure/global/utils/s3/aws.service.ts new file mode 100644 index 0000000..b680e1a --- /dev/null +++ b/src/infrastructure/global/utils/s3/aws.service.ts @@ -0,0 +1,35 @@ +import { HttpException, Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import * as AWS from 'aws-sdk'; + +@Injectable() +export class AwsService { + private s3Client + constructor(private config: ConfigService) { + this.s3Client = new AWS.S3({ + accessKeyId: config.get('AWS_S3_ACCESS_KEY'), + secretAccessKey: config.get('AWS_S3_SECRET_KEY'), + region: config.get('AWS_S3_REGION'), + }); + } + async upload(fileName: string, file) { + const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif']; + + const extension = file.originalname.split('.').pop() + + if (!allowedExtensions.includes(extension)) { + throw new HttpException('Unsupported file extension', 400); + } + const command = { + Bucket: this.config.get('AWS_S3_BUCKET_NAME'), + Key: fileName, + Body: file.buffer, + ACL: 'public-read', + ContentType: file.mimeType, + } + + await this.s3Client.upload(command).promise() + + return `https://s3.${this.config.get('AWS_S3_REGION')}.amazonaws.com/${this.config.get('AWS_S3_BUCKET_NAME')}/${fileName}`; + } +} \ No newline at end of file