diff --git a/.eslintrc.json b/.eslintrc.json index e95dff8..500183f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,6 +16,8 @@ "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-var-requires": "warn", - "@typescript-eslint/no-unsafe-call": "warn" + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-return": "warn" } } diff --git a/package-lock.json b/package-lock.json index 7632ee0..33c4454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "tiimmate-backend", + "name": "teammate-backend", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tiimmate-backend", + "name": "teammate-backend", "version": "0.0.1", "license": "ISC", "dependencies": { @@ -14,13 +14,18 @@ "express": "^4.18.2", "express-async-handler": "^1.2.0", "http-status-codes": "^2.3.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.7.0", + "redaxios": "^0.5.1", "sequelize": "^6.35.2", - "sequelize-cli": "^6.6.2" + "sequelize-cli": "^6.6.2", + "uuid": "^9.0.1", + "zod": "^3.22.4" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.11.0", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", @@ -418,6 +423,15 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -981,6 +995,11 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1011,6 +1030,58 @@ "node": ">=6" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1347,6 +1418,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", @@ -1543,21 +1622,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1568,22 +1632,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/eslint/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1641,15 +1689,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1683,21 +1722,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -1713,15 +1737,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1734,18 +1749,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2509,6 +2512,51 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2536,12 +2584,47 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "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==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -2919,6 +3002,21 @@ "node": ">= 0.8.0" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2939,6 +3037,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3139,6 +3246,11 @@ "node": ">=8.10.0" } }, + "node_modules/redaxios": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/redaxios/-/redaxios-0.5.1.tgz", + "integrity": "sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3460,6 +3572,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/sequelize/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==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -3925,9 +4045,13 @@ } }, "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -4161,6 +4285,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 83b09ee..fa7f733 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "tiimmate-backend", + "name": "teammate-backend", "version": "0.0.1", "description": "", "main": "app.ts", @@ -15,13 +15,18 @@ "express": "^4.18.2", "express-async-handler": "^1.2.0", "http-status-codes": "^2.3.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.7.0", + "redaxios": "^0.5.1", "sequelize": "^6.35.2", - "sequelize-cli": "^6.6.2" + "sequelize-cli": "^6.6.2", + "uuid": "^9.0.1", + "zod": "^3.22.4" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.11.0", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", diff --git a/src/app.ts b/src/app.ts index 4848efe..579e5ad 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,7 +9,15 @@ import { response } from "./config/response"; import { BaseError } from "./config/error"; import { status } from "./config/response.status"; -// import { authRouter } from "./routes/auth.route"; +import { authRouter } from "./routes/auth.route"; +import { teamsRouter } from "./routes/teams.route"; +import { membersRouter } from "./routes/members.route"; +import { gamesRouter } from "./routes/games.route"; +import { guestsRouter } from "./routes/guests.route"; +import { postsRouter } from "./routes/posts.route"; +import { usersRouter } from "./routes/users.route"; +import { matchingsRouter } from "./routes/matchings.route"; +import { reviewsRouter } from "./routes/reviews.route"; const app = express(); @@ -27,7 +35,15 @@ app.use(express.static("public")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); -// app.use("/auth", authRouter); +app.use("/auth", authRouter); +app.use("/teams", teamsRouter); +app.use("/members", membersRouter); +app.use("/games", gamesRouter); +app.use("/guests", guestsRouter); +app.use("/posts", postsRouter); +app.use("/users", usersRouter); +app.use("/matchings", matchingsRouter); +app.use("/reviews", reviewsRouter); app.use((req: Request, res: Response, next: NextFunction) => { const err = new BaseError(status.NOT_FOUND); @@ -37,6 +53,7 @@ app.use((req: Request, res: Response, next: NextFunction) => { app.use((err, req: Request, res: Response, next: NextFunction) => { res.locals.message = err.message; res.locals.err = process.env.NODE_ENV !== "production" ? err : {}; + console.log(err); const error = err instanceof BaseError ? err : new BaseError(status.INTERNAL_SERVER_ERROR); res.status(error.data.status).send(response(error.data)); }); diff --git a/src/config/error.ts b/src/config/error.ts index 6e45fb0..44e5076 100644 --- a/src/config/error.ts +++ b/src/config/error.ts @@ -3,8 +3,8 @@ import { Status } from "./response.status"; export class BaseError extends Error { public data: Status; - constructor(data: Status) { + constructor(data: Status, detail?: any) { super(data.message); - this.data = data; + this.data = detail ? { ...data, detail } : data; } } diff --git a/src/config/response.status.ts b/src/config/response.status.ts index 9fb6894..9631a6c 100644 --- a/src/config/response.status.ts +++ b/src/config/response.status.ts @@ -5,6 +5,7 @@ export interface Status { isSuccess: boolean; code: number | string; message: string; + detail?: any; } export const status: { [key: string]: Status } = { @@ -19,4 +20,197 @@ export const status: { [key: string]: Status } = { code: "COMMON000", message: "서버 에러, 관리자에게 문의 바랍니다.", }, + NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "COMMON001", + message: "요청한 페이지를 찾을 수 없습니다. 관리자에게 문의 바랍니다.", + }, + MISSING_REQUIRED_FIELDS: { + status: StatusCodes.BAD_REQUEST, + isSuccess: false, + code: "COMMON002", + message: "요청에 필요한 정보가 누락되었습니다.", + }, + REQUEST_VALIDATION_ERROR: { + status: StatusCodes.BAD_REQUEST, + isSuccess: false, + code: "COMMON003", + message: "요청이 유효하지 않습니다.", + }, + + //auth err + MISSING_ACCESS_TOKEN: { + status: StatusCodes.UNAUTHORIZED, + isSuccess: false, + code: "AUTH001", + message: "Access Token이 없습니다.", + }, + ACCESS_TOKEN_EXPIRED: { + status: StatusCodes.UNAUTHORIZED, + isSuccess: false, + code: "AUTH002", + message: "Access Token이 만료되었습니다.", + }, + ACCESS_TOKEN_NOT_EXPIRED: { + status: StatusCodes.BAD_REQUEST, + isSuccess: false, + code: "AUTH003", + message: "Access Token이 아직 만료되지 않았습니다.", + }, + ACCESS_TOKEN_VERIFICATION_FAILED: { + status: StatusCodes.UNAUTHORIZED, + isSuccess: false, + code: "AUTH004", + message: "Access Token 검증에 실패했습니다.", + }, + MISSING_REFRESH_TOKEN: { + status: StatusCodes.UNAUTHORIZED, + isSuccess: false, + code: "AUTH005", + message: "Refresh Token이 없습니다.", + }, + REFRESH_TOKEN_VERIFICATION_FAILED: { + status: StatusCodes.UNAUTHORIZED, + isSuccess: false, + code: "AUTH006", + message: "Refresh Token 검증에 실패했습니다. 다시 로그인해 주세요.", + }, + + //team err + NO_JOINABLE_TEAM: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "TEAM001", + message: "해당 초대 코드로 가입할 수 있는 팀이 없습니다.", + }, + TEAM_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "TEAM002", + message: "요청한 팀을 찾을 수 없습니다.", + }, + TEAM_LEADER_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "TEAM003", + message: "요청한 유저가 팀장인 팀을 찾을 수 없습니다.", + }, + TEAM_INFO_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "TEAM004", + message: "팀 정보 입력이 필요합니다.", + }, + + //member err + ALREADY_JOINED: { + status: StatusCodes.CONFLICT, + isSuccess: false, + code: "MEMBER001", + message: "해당 팀에 이미 가입되어 있습니다.", + }, + MEMBER_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "MEMBER002", + message: "멤버를 찾을 수 없습니다.", + }, + + // guest error + GUEST_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "GUESTER001", + message: "게스팅을 찾을 수 없습니다.", + }, + GUESTUSER_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "GUESTER002", + message: "게스트 신청 유저를 찾을 수 없습니다.", + }, + GUESTUSER_ALREADY_EXIST: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "GUESTER003", + message: "이미 신청한 게스트 모집글입니다.", + }, + ACCESS_DENIED_FOR_GUEST: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "GUESTER004", + message: "게스트 수정 권한이 없습니다.", + }, + CLOSED_GUEST: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "GUESTER005", + message: "마감된 게스트 모집글입니다.", + }, + + // game error + GAME_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "GAME001", + message: "게임을 찾을 수 없습니다.", + }, + + //post err + POST_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "POST001", + message: "요청한 글을 찾을 수 없습니다.", + }, + + //user err + USER_NOT_FOUND: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "USER001", + message: "요청한 유저를 찾을 수 없습니다.", + }, + + NOT_FILL_USER_PROFILE: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "USER002", + message: "유저 정보가 필요합니다.", + }, + + //review err + MATCH_ID_REQUIRED: { + status: StatusCodes.BAD_REQUEST, + isSuccess: false, + code: "REVIEW001", + message: "TeamMatchId 또는 GuestMatchId 중 하나가 필요합니다.", + }, + NO_REVIEW_TARGET: { + status: StatusCodes.NOT_FOUND, + isSuccess: false, + code: "REVIEW002", + message: "리뷰할 대상이 없습니다.", + }, + REVIEW_NOT_CURRENTLY_WRITABLE: { + status: StatusCodes.BAD_REQUEST, + isSuccess: false, + code: "REVIEW003", + message: "리뷰 작성 가능한 시간이 아닙니다.", + }, + REVIEW_ALREADY_WRITTEN: { + status: StatusCodes.CONFLICT, + isSuccess: false, + code: "REVIEW004", + message: "이미 리뷰를 작성했습니다.", + }, + + // game-apply err + GAME_APPLICATION_ALREADY_EXIST: { + status: StatusCodes.CONFLICT, + isSuccess: false, + code: "GAMEAPPLY001", + message: "이미 신청한 연습경기 모집글입니다.", + }, }; diff --git a/src/config/response.ts b/src/config/response.ts index 6e794f0..d93dcf0 100644 --- a/src/config/response.ts +++ b/src/config/response.ts @@ -4,14 +4,16 @@ export interface Response { isSuccess: boolean; code: number | string; message: string; + detail?: any; result?: any; } -export const response = ({ isSuccess, code, message }: Status, result?: any): Response => { +export const response = ({ isSuccess, code, message, detail }: Status, result?: any): Response => { return { isSuccess: isSuccess, code: code, message: message, + detail: detail, result: result, }; }; diff --git a/src/constants/age-group.constant.ts b/src/constants/age-group.constant.ts new file mode 100644 index 0000000..7bf35ce --- /dev/null +++ b/src/constants/age-group.constant.ts @@ -0,0 +1,13 @@ +import { AgeGroup } from "../types/age-group.enum"; + +const AgeGroups: Record = { + [AgeGroup.Teenagers]: "10대", + [AgeGroup.Twenties]: "20대", + [AgeGroup.Thirties]: "30대", + [AgeGroup.Forties]: "40대", + [AgeGroup.FiftiesAndAbove]: "50대 이상~", +}; + +export const getAgeGroup = (key: AgeGroup): string | undefined => { + return AgeGroups[key]; +}; \ No newline at end of file diff --git a/src/constants/category.constant.ts b/src/constants/category.constant.ts new file mode 100644 index 0000000..6605651 --- /dev/null +++ b/src/constants/category.constant.ts @@ -0,0 +1,17 @@ +import { Category } from "../types/category.enum"; + +const Categories: Record = { + [Category.Basketball]: "농구", + [Category.Baseball]: "야구", + [Category.Tennis]: "테니스", + [Category.Soccer]: "축구", + [Category.Futsal]: "풋살", + [Category.Volleyball]: "배구", + [Category.Bowling]: "볼링", + [Category.Badminton]: "배드민턴", + [Category.TableTennis]: "탁구", +}; + +export const getCategory = (key: Category): string | undefined => { + return Categories[key]; +}; diff --git a/src/constants/gender.constant.ts b/src/constants/gender.constant.ts new file mode 100644 index 0000000..7803951 --- /dev/null +++ b/src/constants/gender.constant.ts @@ -0,0 +1,19 @@ +import { Gender } from "../types/gender.enum"; + +const Genders: Record, string> = { + [Gender.Female]: "여성", + [Gender.Male]: "남성", +}; + +const TeamGenders: Record = { + ...Genders, + [Gender.Mixed]: "혼성", +}; + +export const getGender = (key: Exclude): string | undefined => { + return Genders[key]; +}; + +export const getTeamGender = (key: Gender): string | undefined => { + return TeamGenders[key]; +}; diff --git a/src/constants/guest-status.constant.ts b/src/constants/guest-status.constant.ts new file mode 100644 index 0000000..3653e5b --- /dev/null +++ b/src/constants/guest-status.constant.ts @@ -0,0 +1,10 @@ +const guestStatus: Record = { + 0: "미승인", + 1: "승인", +}; + +export const defaultStatus = 0; + +export const getGuestUserStatus = (key: number): string | undefined => { + return guestStatus[key]; +}; diff --git a/src/constants/status.constant.ts b/src/constants/status.constant.ts new file mode 100644 index 0000000..cf32418 --- /dev/null +++ b/src/constants/status.constant.ts @@ -0,0 +1,10 @@ +const Statuses: Record = { + 0: "모집 중", + 1: "모집 완료", +}; + +export const defaultStatus = 0; + +export const getStatus = (key: number): string | undefined => { + return Statuses[key]; +}; diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..3e3b8ad --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,24 @@ +import { Request, Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { googleLogin, kakaoLogin, naverLogin, generateNewAccessToken, logoutUser } from "../services/auth.service"; + +export const authGoogle = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await googleLogin(req.body))); +}; + +export const authKakao = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await kakaoLogin(req.body))); +}; + +export const authNaver = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await naverLogin(req.body))); +}; + +export const refreshAccessToken = async (req: Request, res: Response, next) => { + res.send(response(status.SUCCESS, await generateNewAccessToken(req))); +}; + +export const logout = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await logoutUser(req.user.id))); +}; diff --git a/src/controllers/games.controller.ts b/src/controllers/games.controller.ts new file mode 100644 index 0000000..18143a9 --- /dev/null +++ b/src/controllers/games.controller.ts @@ -0,0 +1,45 @@ +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { + readGamesByDate, + readGamesByGender, + readGamesByLevel, + readGamesByRegion, + readGameDetail, + createGame, + updateGame, + addGameApplication, +} from "../services/games.service"; +import { readTeamAvailPreviewById } from "../services/teams.service"; + +export const fetchGamesByDate = async (req, res, next) => { + res.send(response(status.SUCCESS, await readGamesByDate(req.query))); +}; + +export const fetchGamesByGender = async (req, res, next) => { + res.send(response(status.SUCCESS, await readGamesByGender(req.query))); +}; + +export const fetchGamesByLevel = async (req, res, next) => { + res.send(response(status.SUCCESS, await readGamesByLevel(req.query))); +}; + +export const fetchGamesByRegion = async (req, res, next) => { + res.send(response(status.SUCCESS, await readGamesByRegion(req.query))); +}; + +export const fetchGameDetail = async (req, res, next) => { + res.send(response(status.SUCCESS, await readGameDetail(req.params))); +}; + +export const addGame = async (req, res, next) => { + return res.send(response(status.SUCCESS, await createGame(req.user.id, req.body))); +}; + +export const modifyGame = async (req, res, next) => { + return res.send(response(status.SUCCESS, await updateGame(req.user.id, req.params, req.body))); +}; + +export const applyGame = async (req, res, next) => { + return res.send(response(status.SUCCESS, await addGameApplication(req.user.id, req.params, req.body))); +}; diff --git a/src/controllers/guests.controller.ts b/src/controllers/guests.controller.ts new file mode 100644 index 0000000..b936fd2 --- /dev/null +++ b/src/controllers/guests.controller.ts @@ -0,0 +1,45 @@ +import { Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { + readGuesting, + readGuestingByLevel, + readGuestingByGender, + readGuestingByRegion, + readDetailedGuesting, + createGuesting, + addGuestUser, + updateGuesting, +} from "../services/guests.service"; + +export const addGuesting = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createGuesting(req.user.id, req.body))); +}; + +export const modifyGuesting = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await updateGuesting(req.user.id, req.params, req.body))); +}; + +export const GuestingPreview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readGuesting(req.query))); +}; + +export const GuestingPreviewByLevel = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readGuestingByLevel(req.query))); +}; + +export const GuestingPreviewByGender = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readGuestingByGender(req.query))); +}; + +export const GuestingPreviewByRegion = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readGuestingByRegion(req.query))); +}; + +export const DetailedGuestingPreview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readDetailedGuesting(req.params))); +}; + +export const applicationGuesting = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await addGuestUser(req.user.id, req.params))); +}; diff --git a/src/controllers/matchings.controller.ts b/src/controllers/matchings.controller.ts new file mode 100644 index 0000000..e9e4a29 --- /dev/null +++ b/src/controllers/matchings.controller.ts @@ -0,0 +1,35 @@ +import { Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { + readApplyGuestingUser, + readMatchingGuesting, + readMatchingHosting, + readHostingApplicantsTeamList, +} from "../services/matchings.service"; +import { addOpposingTeam } from "../services/teams.service"; +import { updateGuestUserStatus } from "../services/guests.service"; + +export const matchingGuestingPreview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readMatchingGuesting(req.user.id, req.query))); +}; + +export const matchingHostingPreview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readMatchingHosting(req.user.id, req.query))); +}; + +export const ApplyGuestingUserPreview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readApplyGuestingUser(req.params))); +}; + +export const modifyGuestStatus = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await updateGuestUserStatus(req.params))); +}; + +export const fetchHostingApplicantsTeamList = async (req, res, next) => { + return res.send(response(status.SUCCESS, await readHostingApplicantsTeamList(req.user.id, req.query))); +}; + +export const gameApplicationApproval = async (req, res, next) => { + return res.send(response(status.SUCCESS, await addOpposingTeam(req.user.id, req.params, req.body))); +}; diff --git a/src/controllers/members.controller.ts b/src/controllers/members.controller.ts new file mode 100644 index 0000000..201ea28 --- /dev/null +++ b/src/controllers/members.controller.ts @@ -0,0 +1,7 @@ +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { createMember } from "../services/members.service"; + +export const addMember = async (req, res, next) => { + res.send(response(status.SUCCESS, await createMember(req.user.id, req.body))); +}; diff --git a/src/controllers/posts.controller.ts b/src/controllers/posts.controller.ts new file mode 100644 index 0000000..bc13a99 --- /dev/null +++ b/src/controllers/posts.controller.ts @@ -0,0 +1,55 @@ +import { Request, Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { + createOrDeleteBookmark, + readBookmarkedPosts, + readPost, + readCommunityPosts, + readMyPosts, + createCommunityPost, + createComment, + readComments, + createRentPost, + readRentPosts, +} from "../services/posts.service"; + +export const fetchCommunityPosts = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readCommunityPosts(req.user?.id, req.query))); +}; + +export const fetchMyPosts = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readMyPosts(req.user.id, req.query))); +}; + +export const fetchBookmarkedPosts = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readBookmarkedPosts(req.user.id, req.query))); +}; + +export const addOrRemoveBookmark = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createOrDeleteBookmark(req.user.id, req.params))); +}; + +export const fetchPost = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readPost(req.user.id, req.params))); +}; + +export const addCommunityPost = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createCommunityPost(req.user.id, req.body))); +}; + +export const addComment = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createComment(req.user.id, req.params, req.body))); +}; + +export const fetchComments = async (req: Request, res: Response, next) => { + res.send(response(status.SUCCESS, await readComments(req.params, req.query))); +}; + +export const addRentPost = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createRentPost(req.user.id, req.body))); +}; + +export const fetchRentPosts = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readRentPosts(req.user?.id, req.query))); +}; diff --git a/src/controllers/reviews.controller.ts b/src/controllers/reviews.controller.ts new file mode 100644 index 0000000..bc9dbc7 --- /dev/null +++ b/src/controllers/reviews.controller.ts @@ -0,0 +1,12 @@ +import { Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { createTeamReview, createUserReview } from "../services/reviews.service"; + +export const addTeamReview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createTeamReview(req.user.id, req.body))); +}; + +export const addUserReview = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createUserReview(req.user.id, req.body))); +}; diff --git a/src/controllers/teams.controller.ts b/src/controllers/teams.controller.ts new file mode 100644 index 0000000..e650a11 --- /dev/null +++ b/src/controllers/teams.controller.ts @@ -0,0 +1,30 @@ +import { Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { + readTeamAvailPreviewById, + readTeamPreviews, + readTeamDetail, + createTeam, + updateTeam, +} from "../services/teams.service"; + +export const fetchTeamPreviews = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readTeamPreviews(req.user.id, req.query))); +}; + +export const addTeam = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await createTeam(req.user.id, req.body))); +}; + +export const modifyTeam = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await updateTeam(req.user.id, req.params, req.body))); +}; + +export const fetchTeamDetail = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readTeamDetail(req.user.id, req.params))); +}; + +export const fetchTeamsAvailById = async (req, res, next) => { + res.send(response(status.SUCCESS, await readTeamAvailPreviewById(req.user.id, req.query))); +}; diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index a9203d0..44cce66 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -1,8 +1,12 @@ -import { response } from "../../config/response"; -import { status } from "../../config/response.status"; +import { Response } from "express"; +import { response } from "../config/response"; +import { status } from "../config/response.status"; +import { readUserProfile, updateUserProfile } from "../services/users.service"; -// import { ... } from "../services/users.service"; +export const fetchUserProfile = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await readUserProfile(req.params))); +}; -// export const ... = async (req, res, next) => { -// res.send(response(status.SUCCESS, await ...)); -// }; +export const modifyUserProfile = async (req, res: Response, next) => { + res.send(response(status.SUCCESS, await updateUserProfile(req.user.id, req.params, req.body))); +}; diff --git a/src/daos/bookmark.dao.ts b/src/daos/bookmark.dao.ts new file mode 100644 index 0000000..b92e503 --- /dev/null +++ b/src/daos/bookmark.dao.ts @@ -0,0 +1,22 @@ +import db from "../models"; + +export const insertOrDeleteBookmark = async (userId: number, postId: number) => { + const bookmark = await getBookmark(userId, postId); + if (bookmark) { + await bookmark.destroy(); + } else { + await db.Bookmark.create({ + postId, + userId, + }); + } +}; + +export const getBookmark = async (userId: number, postId: number) => { + return await db.Bookmark.findOne({ + where: { + postId, + userId, + }, + }); +}; diff --git a/src/daos/comment.dao.ts b/src/daos/comment.dao.ts new file mode 100644 index 0000000..dbf76ad --- /dev/null +++ b/src/daos/comment.dao.ts @@ -0,0 +1,42 @@ +import db from "../models"; +import { CreateCommentBody } from "../schemas/comment.schema"; +import { calculateHasNext, generateCursorCondition } from "../utils/paging.util"; + +const defaultLimit = 20; + +export const findComment = async (postId: number, cursorId: number | undefined) => { + const commentsBeforeCursorForPost = { postId, ...generateCursorCondition(cursorId) }; + + const comments = await db.Comment.findAll({ + raw: true, + where: commentsBeforeCursorForPost, + order: [["createdAt", "DESC"]], + limit: defaultLimit, + include: [ + { + model: db.User, + attributes: ["nickname"], + }, + ], + attributes: ["id", "content", "createdAt"], + }); + + const ascendingComments = comments.sort((a, b) => a.id - b.id); + return { ascendingComments, hasNext: calculateHasNext(ascendingComments, defaultLimit) }; +}; + +export const getCommentCount = async (postId: number) => { + return await db.Comment.count({ + where: { + postId, + }, + }); +}; + +export const insertComment = async (userId: number, postId: number, body: CreateCommentBody) => { + await db.Comment.create({ + postId, + authorId: userId, + content: body.content, + }); +}; diff --git a/src/daos/game-apply.dao.ts b/src/daos/game-apply.dao.ts new file mode 100644 index 0000000..e63e802 --- /dev/null +++ b/src/daos/game-apply.dao.ts @@ -0,0 +1,11 @@ +import db from "../models"; + +export const checkApplicationExisting = async (gameId: number, teamId: number) => { + return await db.GameApply.findOne({ + raw: true, + where: { + gameId, + teamId, + }, + }); +}; diff --git a/src/daos/game.dao.ts b/src/daos/game.dao.ts new file mode 100644 index 0000000..ebe3999 --- /dev/null +++ b/src/daos/game.dao.ts @@ -0,0 +1,162 @@ +import db from "../models"; +import { Op, Sequelize } from "sequelize"; +import { CreateGameBody } from "../schemas/game.schema"; +import { ApplyGameBody } from "../schemas/game-apply.schema"; +import { getTeamIdByLeaderId } from "./team.dao"; +import { MatchType } from "../types/match-type.enum"; +import { Category } from "../types/category.enum"; +import { Gender } from "../types/gender.enum"; +import { calculateHasNext, generateCursorCondition } from "../utils/paging.util"; + +const defaultLimit = 20; + +export const findGamesByDate = async (date: string, category: Category, cursorId: number | undefined) => { + const gamesBeforeCursor = generateCursorCondition(cursorId); + const teamFilter = { category }; + return findGames(date, gamesBeforeCursor, teamFilter); +}; + +export const findGamesByGender = async ( + date: string, + category: Category, + gender: Gender, + cursorId: number | undefined, +) => { + const gamesBeforeCursor = generateCursorCondition(cursorId); + const teamFilter = { category, gender }; + return findGames(date, gamesBeforeCursor, teamFilter); +}; + +export const findGamesByLevel = async (date: string, category: Category, skillLevel, cursorId: number | undefined) => { + const gamesBeforeCursor = generateCursorCondition(cursorId); + const teamFilter = { category, skillLevel }; + return findGames(date, gamesBeforeCursor, teamFilter); +}; + +export const findGamesByRegion = async (date: string, category: Category, region, cursorId: number | undefined) => { + const gamesBeforeCursor = generateCursorCondition(cursorId); + const teamFilter = { category, region }; + return findGames(date, gamesBeforeCursor, teamFilter); +}; + +export const findGames = async (date: string, gameFilter: object, teamFilter: object) => { + const games = await db.Game.findAll({ + raw: true, + where: { + ...gameFilter, + [Op.and]: Sequelize.literal(`DATE_FORMAT(game_time, '%Y-%m-%d') = DATE_FORMAT('${date}', '%Y-%m-%d')`), + }, + include: [ + { + model: db.Team, + as: "HostTeam", + attributes: ["id", "name", "region", "gender", "ageGroup", "skillLevel"], + where: teamFilter, + }, + ], + attributes: ["id", "gameTime", "status"], + order: [["created_at", "DESC"]], + limit: defaultLimit, + }); + return { games, hasNext: calculateHasNext(games, defaultLimit) }; +}; + +export const getGameDetail = async (gameId) => { + return await db.Game.findOne({ + raw: true, + where: { + id: gameId, + }, + attributes: ["hostTeamId", "gameTime", "gameDuration", "description"], + }); +}; + +export const insertGame = async (hostTeamId, data: CreateGameBody, category) => { + await db.Game.create({ + hostTeamId: hostTeamId, + gameTime: data.gameTime, + gameDuration: data.gameDuration, + category: category, + description: data.description, + }); +}; + +export const setGame = async (game, body) => { + Object.keys(body).forEach((field) => { + game[field] = body[field]; + }); + await game.save(); +}; + +export const getGameByUserId = async (gameId, userId) => { + const hostTeamId = await getTeamIdByLeaderId(userId); + return await db.Game.findOne({ + where: { + id: gameId, + hostTeamId: hostTeamId, + }, + }); +}; + +export const insertGameApplication = async (gameId: number, body: ApplyGameBody) => { + const teamId = body.teamId; + + await db.GameApply.create({ + gameId: gameId, + teamId: teamId, + }); +}; + +export const getGame = async (gameId: number) => { + return await db.Game.findOne({ + raw: true, + where: { + id: gameId, + }, + attributes: ["hostTeamId", "opposingTeamId", "gameTime"], + }); +}; + +export const updateOpposingTeam = async (gameId: number, opposingTeamId: number) => { + await db.Game.update({ opposingTeamId: opposingTeamId, status: 1 }, { where: { id: gameId } }); +}; + +export const findGameByHostTeamsAndGameTime = async (teamIds: number[], gameTime) => { + const hostTeamFilter = { + hostTeamId: { + [Op.in]: teamIds, + }, + }; + return findGameByTeamsAndGameTime(hostTeamFilter, gameTime); +}; + +export const findGameByOpposingTeamsAndGameTime = async (teamIds: number[], gameTime) => { + const opposingTeamFilter = { + opposingTeamId: { + [Op.in]: teamIds, + }, + }; + return findGameByTeamsAndGameTime(opposingTeamFilter, gameTime); +}; + +const findGameByTeamsAndGameTime = async (teamFilter, gameTime: string) => { + const gameResults = await db.Game.findAll({ + raw: true, + where: { + ...teamFilter, + [Op.and]: Sequelize.literal(`DATE_FORMAT(game_time, '%Y-%m-%d') = DATE_FORMAT('${gameTime}', '%Y-%m-%d')`), + }, + include: [ + { + model: db.Team, + as: "HostTeam", + attributes: ["id", "name", "region", "gender", "ageGroup", "skillLevel"], + }, + ], + attributes: ["id", "gameTime"], + }); + for (const gameResult of gameResults) { + gameResult.type = MatchType.game; + } + return gameResults; +}; diff --git a/src/daos/guest-user.dao.ts b/src/daos/guest-user.dao.ts new file mode 100644 index 0000000..abea246 --- /dev/null +++ b/src/daos/guest-user.dao.ts @@ -0,0 +1,86 @@ +import db from "../models"; + +export const insertGuestUser = async (guestingId: number, userId: number) => { + await db.GuestUser.create({ + guestId: guestingId, + userId: userId, + status: 0, + }); +}; + +export const getApplyGuestingUser = async (guestingId: number) => { + return await db.GuestUser.findAll({ + raw: true, + where: { + guestId: guestingId, + }, + include: [ + { + model: db.User, + include: [ + { + model: db.Profile, + attributes: ["position"], + }, + ], + attributes: ["nickname", "height", "avatarUrl"], + }, + ], + attributes: ["status"], + }); +}; + +export const getGuestUserById = async (guestUserId: number) => { + return await db.GuestUser.findOne({ + where: { + id: guestUserId, + }, + }); +}; + +export const getGuestIdById = async (guestUserId: number) => { + const guest = await db.GuestUser.findOne({ + where: { + id: guestUserId, + }, + attributes: ["guestId"], + }); + return guest?.guestId; +}; + +export const checkClosedGuest = async (guestId: number) => { + const count = await db.GuestUser.count({ + where: { + guestId: guestId, + status: 1, + }, + }); + const recruitCount = await db.Guest.findOne({ + where: { + id: guestId, + }, + attributes: ["recruitCount"], + }); + console.log(count, recruitCount.recruitCount, count >= recruitCount.recruitCount); + return count >= recruitCount.recruitCount; +}; + +export const setGuestUserStatus = async (guestUser, guest) => { + guestUser.status = 1; + await guestUser.save(); + const check: boolean = await checkClosedGuest(guest.id); + if (check) { + guest.status = 1; + await guest.save(); + } +}; + +export const checkForDuplicateGuestUser = async (userId, guestId) => { + return await db.GuestUser.findOne({ + raw: true, + where: { + userId, + guestId, + }, + }); +}; diff --git a/src/daos/guest.dao.ts b/src/daos/guest.dao.ts new file mode 100644 index 0000000..6c884ae --- /dev/null +++ b/src/daos/guest.dao.ts @@ -0,0 +1,200 @@ +import db from "../models"; +import { CreateGuestingBody, UpdateGuestingBody } from "../schemas/guest.schema"; +import { Op, Sequelize } from "sequelize"; +import { Category } from "../types/category.enum"; +import { Gender } from "../types/gender.enum"; +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { MatchType } from "../types/match-type.enum"; +import { calculateHasNext, generateCursorCondition } from "../utils/paging.util"; + +const defaultLimit = 20; + +export const insertGuesting = async (teamId: number, data: CreateGuestingBody) => { + await db.Guest.create({ + teamId: teamId, + gameTime: data.gameTime, + description: data.description, + recruitCount: data.recruitCount, + gameDuration: data.gameDuration, + status: 0, + }); +}; + +export const setGuesting = async (guesting, body: UpdateGuestingBody) => { + Object.keys(body).forEach((field) => { + guesting[field] = body[field]; + }); + await guesting.save(); +}; + +export const findGuestAll = (date: string, category: Category, cursorId: number | undefined) => { + const guestsBeforeCursor = generateCursorCondition(cursorId); + const TeamFilter = { category }; + return findGuests(date, guestsBeforeCursor, TeamFilter); +}; + +export const findGuestByGender = (date: string, category: Category, gender: Gender, cursorId: number | undefined) => { + const guestsBeforeCursor = generateCursorCondition(cursorId); + const TeamFilter = { gender, category }; + return findGuests(date, guestsBeforeCursor, TeamFilter); +}; + +export const findGuestByLevel = (date: string, category: Category, level: string, cursorId: number | undefined) => { + const guestsBeforeCursor = generateCursorCondition(cursorId); + const minLevel = Math.floor(parseInt(level) / 10) * 10; + const TeamFilter = { skillLevel: { [Op.between]: [minLevel, minLevel + 9] }, category }; + return findGuests(date, guestsBeforeCursor, TeamFilter); +}; + +export const findGuestByRegion = (date: string, category: Category, region: string, cursorId: number | undefined) => { + const guestsBeforeCursor = generateCursorCondition(cursorId); + const TeamFilter = { region, category }; + return findGuests(date, guestsBeforeCursor, TeamFilter); +}; + +export const findGuests = async (date: string, guestFilter: object, TeamFilter: object) => { + const guests = await db.Guest.findAll({ + raw: true, + where: { + ...guestFilter, + [Op.and]: Sequelize.literal(`DATE_FORMAT(game_time, '%Y-%m-%d') = DATE_FORMAT('${date}', '%Y-%m-%d')`), + }, + order: [["gameTime", "DESC"]], + limit: defaultLimit, + include: [ + { + model: db.Team, + where: TeamFilter, + attributes: ["id", "name", "region", "gender", "ageGroup", "skillLevel"], + }, + ], + attributes: ["id", "gameTime", "recruitCount", "gameDuration", "status"], + }); + return { guests, hasNext: calculateHasNext(guests, defaultLimit) }; +}; + +export const getDetailedGuesting = async (guestingId: number) => { + return await db.Guest.findOne({ + raw: true, + where: { + id: guestingId, + }, + attributes: ["teamId", "gameTime", "description", "recruitCount", "gameDuration", "status"], + }); +}; + +export const getTeamByGuestingId = async (guestingId: number, userId: number) => { + return await db.Guest.findOne({ + where: { + id: guestingId, + }, + include: { + model: db.Team, + where: { + leaderId: userId, + }, + attributes: [], + }, + attributes: ["teamId"], + }); +}; + +export const getGuestingById = async (guestingId: number) => { + return await db.Guest.findOne({ + where: { + id: guestingId, + }, + }); +}; + +export const getGuestingByAcceptedUserId = async (guestingId: number, userId: number) => { + return await db.Guest.findOne({ + raw: true, + where: { + id: guestingId, + }, + include: [ + { + model: db.GuestUser, + where: { + userId, + status: 1, + }, + attributes: [], + }, + ], + attributes: ["teamId", "gameTime"], + }); +}; + +export const getCategoryThroughTeamJoin = async (guestingId: number) => { + const guest = await db.Guest.findOne({ + raw: true, + where: { + id: guestingId, + }, + include: [ + { + model: db.Team, + attributes: ["category"], + }, + ], + attributes: [], + }); + if (!guest) { + throw new BaseError(status.GUEST_NOT_FOUND); + } + return guest["Team.category"]; +}; + +export const findGuestingByTeamsAndGameTime = async (teamIds: number[], gameTime: string) => { + const guestResults = await db.Guest.findAll({ + raw: true, + where: { + teamId: { + [Op.in]: teamIds, + }, + [Op.and]: Sequelize.literal(`DATE_FORMAT(game_time, '%Y-%m-%d') = DATE_FORMAT('${gameTime}', '%Y-%m-%d')`), + }, + include: [ + { + model: db.Team, + attributes: ["id", "name", "region", "gender", "ageGroup", "skillLevel"], + }, + ], + attributes: ["id", "gameTime", "gameDuration"], + }); + for (const guestResult of guestResults) { + guestResult.type = MatchType.guest; + } + return guestResults; +}; + +export const findGuestingByUserAndGameTime = async (userId: number, date: string) => { + const guestResults = await db.Guest.findAll({ + raw: true, + where: { + [Op.and]: Sequelize.literal(`DATE_FORMAT(game_time, '%Y-%m-%d') = DATE_FORMAT('${date}', '%Y-%m-%d')`), + }, + include: [ + { + model: db.Team, + attributes: ["id", "name", "region", "gender", "ageGroup", "skillLevel"], + }, + { + model: db.GuestUser, + where: { + userId, + status: 1, + }, + attributes: [], + }, + ], + attributes: ["id", "gameTime", "gameDuration"], + }); + for (const guestResult of guestResults) { + guestResult.type = MatchType.guest; + } + return guestResults; +}; diff --git a/src/daos/image.dao.ts b/src/daos/image.dao.ts new file mode 100644 index 0000000..03abdff --- /dev/null +++ b/src/daos/image.dao.ts @@ -0,0 +1,10 @@ +import db from "../models"; + +export const findImage = async (postId: number) => { + return await db.Image.findAll({ + where: { + postId, + }, + attributes: ["url"], + }); +}; diff --git a/src/daos/matching.dao.ts b/src/daos/matching.dao.ts new file mode 100644 index 0000000..755190e --- /dev/null +++ b/src/daos/matching.dao.ts @@ -0,0 +1,14 @@ +import db from "../models"; + +export const getTeamsAppliedById = async (gameId: number) => { + return await db.GameApply.findAll({ + raw: true, + where: { gameId: gameId }, + include: [ + { + model: db.Team, + attributes: ["id", "name", "logo"], + }, + ], + }); +}; diff --git a/src/daos/member.dao.ts b/src/daos/member.dao.ts new file mode 100644 index 0000000..003da5a --- /dev/null +++ b/src/daos/member.dao.ts @@ -0,0 +1,106 @@ +import db from "../models"; +import { Op } from "sequelize"; +import { Category } from "../types/category.enum"; + +export const findMemberInfoByCategory = async (teamId: number, category: Category) => { + return await db.Member.findAll({ + raw: true, + where: { + teamId, + }, + include: [ + { + model: db.User, + attributes: ["id", "nickname", "height", "avatarUrl"], + include: [ + { + model: db.Profile, + where: { + category: category, + }, + required: false, + attributes: ["position"], + }, + ], + }, + ], + attributes: [], + }); +}; + +export const findMemberInfoWithoutLeaderByTeamId = async (teamId, userInfoAttributes) => { + const team = await db.Team.findByPk(teamId); + if (!team) { + throw new Error("Team not found"); + } + const leaderId = team.leaderId; + + // 리더 제외한 멤버들 조회 + return await db.Member.findAll({ + raw: true, + where: { + teamId: teamId, + userId: { + [Op.ne]: leaderId, + }, + }, + include: [ + { + model: db.User, + attributes: userInfoAttributes(), + }, + ], + attributes: [], + }); +}; + +export const insertMember = async (teamId: number, userId: number) => { + await db.Member.create({ + teamId, + userId, + }); +}; + +export const isMemberExist = async (teamId: number, userId: number) => { + const member = await db.Member.findOne({ + where: { + teamId, + userId, + }, + }); + return member !== null; +}; + +export const findMemberToDelete = async (memberIdsToDelete: number[], teamId: number) => { + return await db.Member.findAll({ + where: { + teamId, + userId: { + [Op.in]: memberIdsToDelete, + }, + }, + }); +}; + +export const deleteMembers = async (members) => { + for (const member of members) { + await member.destroy(); + } +}; + +export const getMemberCountByTeamId = async (teamId) => { + const count = await db.Member.count({ + where: { + teamId, + }, + }); + return count; +}; + +export const addMemberCount = async (lists) => { + console.log("addMemberCount 함수는 제거될 함수입니다"); + for (const list of lists) { + const teamId = list["Team.id"] ?? list["HostTeam.id"]; + list.memberCount = (await getMemberCountByTeamId(teamId)) + 1; + } +}; diff --git a/src/daos/post.dao.ts b/src/daos/post.dao.ts new file mode 100644 index 0000000..2b2da0a --- /dev/null +++ b/src/daos/post.dao.ts @@ -0,0 +1,108 @@ +import db from "../models"; +import { Op, Sequelize } from "sequelize"; +import { CreatePostBody } from "../schemas/post.schema"; +import { PostType } from "../types/post-type.enum"; +import { calculateHasNext, generateCursorCondition } from "../utils/paging.util"; + +const defaultLimit = 20; + +export const findPostByType = async ( + userId: number | undefined, + date: string | undefined, + cursorId: number | undefined, + type: PostType, +) => { + const postsBeforeCursorByType = { type, ...generateCursorCondition(cursorId) }; + + if (type === PostType.RentalInfo) { + return findRentPostByDate(userId, date, postsBeforeCursorByType); + } else { + return findPostByFilter(userId, postsBeforeCursorByType); + } +}; + +export const findPostByAuthorId = async (userId: number, cursorId?: number) => { + const postsBeforeCursorForAuthor = { authorId: userId, ...generateCursorCondition(cursorId) }; + return findPostByFilter(userId, postsBeforeCursorForAuthor); +}; + +export const findBookmarkedPost = async (userId: number, cursorId?: number) => { + const postsBeforeCursor = generateCursorCondition(cursorId); + const includeBookmarkedPosts = [ + { + model: db.Bookmark, + where: { + userId, + }, + attributes: ["id"], + }, + ]; + return findPost(postsBeforeCursor, includeBookmarkedPosts); +}; + +export const getPost = async (postId: number) => { + return await db.Post.findOne({ + raw: true, + where: { + id: postId, + }, + attributes: ["title", "content", "link"], + }); +}; + +const findPostByFilter = async (userId: number | undefined, postFilter: object) => { + const includeAllPosts: any[] = []; + if (userId) { + includeAllPosts.push({ + model: db.Bookmark, + where: { + userId, + }, + required: false, + attributes: ["id"], + }); + } + return findPost(postFilter, includeAllPosts); +}; + +const findRentPostByDate = async (userId: number | undefined, date, postFilter: object) => { + const posts = await db.Post.findAll({ + raw: true, + where: { + [Op.and]: [ + postFilter, + Sequelize.literal(`DATE_FORMAT(rent_date, '%Y-%m-%d') = DATE_FORMAT('${date}', '%Y-%m-%d')`), + ], + }, + + order: [["createdAt", "DESC"]], + limit: defaultLimit, + attributes: ["id", "title", "content", "rentStatus"], + }); + return { posts, hasNext: calculateHasNext(posts, defaultLimit) }; +}; + +const findPost = async (postFilter: object, bookmarkInclude: Array) => { + const posts = await db.Post.findAll({ + raw: true, + where: postFilter, + order: [["createdAt", "DESC"]], + limit: defaultLimit, + attributes: ["id", "title", "createdAt"], + include: bookmarkInclude, + }); + return { posts, hasNext: calculateHasNext(posts, defaultLimit) }; +}; + +export const insertPost = async (userId: number, data: CreatePostBody, type: PostType) => { + await db.Post.create({ + title: data.title, + content: data.content, + link: data.link, + authorId: userId, + rentDate: data.rentDate, + rentPlace: data.rentPlace, + rentStatus: 0, + type, + }); +}; diff --git a/src/daos/profile.dao.ts b/src/daos/profile.dao.ts new file mode 100644 index 0000000..d08eae0 --- /dev/null +++ b/src/daos/profile.dao.ts @@ -0,0 +1,37 @@ +import db from "../models"; +import { CategoryProfile } from "../schemas/user-profile.schema"; +import { Category } from "../types/category.enum"; + +const defaultLevel = 0; + +export const getUserProfile = async (userId: number, category: Category) => { + return await db.Profile.findOne({ + raw: true, + where: { + userId, + category, + }, + attributes: ["id"], + }); +}; + +export const insertCategoryProfile = async (userId: number, category: Category, categoryProfile: CategoryProfile) => { + await db.Profile.create({ + userId, + category, + skillLevel: defaultLevel, + mannerLevel: defaultLevel, + ...categoryProfile, + }); +}; + +export const setCategoryProfile = async (id: number, categoryProfile: CategoryProfile) => { + await db.Profile.update( + { ...categoryProfile }, + { + where: { + id, + }, + }, + ); +}; diff --git a/src/daos/team-review.dao.ts b/src/daos/team-review.dao.ts new file mode 100644 index 0000000..37405ec --- /dev/null +++ b/src/daos/team-review.dao.ts @@ -0,0 +1,28 @@ +import db from "../models"; +import { CreateTeamReviewBody } from "../schemas/team-review.schema"; + +export const getExistingTeamReview = async ( + userId: number, + reviewedTeamId: number, + teamMatchId?: number, + guestMatchId?: number, +) => { + return await db.TeamReview.findOne({ + raw: true, + where: { + reviewerId: userId, + reviewedTeamId, + teamMatchId: teamMatchId ?? null, + guestMatchId: guestMatchId ?? null, + }, + attributes: ["id"], + }); +}; + +export const insertTeamReview = async (userId: number, reviewedTeamId: number, body: CreateTeamReviewBody) => { + await db.TeamReview.create({ + reviewerId: userId, + reviewedTeamId, + ...body, + }); +}; diff --git a/src/daos/team.dao.ts b/src/daos/team.dao.ts new file mode 100644 index 0000000..a572dc8 --- /dev/null +++ b/src/daos/team.dao.ts @@ -0,0 +1,222 @@ +import db from "../models"; +import { CreateTeamBody, UpdateTeamBodyWithoutMemberIdsToDelete } from "../schemas/team.schema"; +import { Category } from "../types/category.enum"; +import { Op } from "sequelize"; + +const defaultLevel = 0; + +export const findTeamPreviewByCategory = async (userId: number, category: Category) => { + const teamsAsLeader = await db.Team.findAll({ + raw: true, + where: { + category, + leaderId: userId, + }, + attributes: teamPreviewAttributes(), + }); + const teamsAsMember = await db.Team.findAll({ + raw: true, + where: { + category, + }, + include: [ + { + model: db.Member, + where: { + userId, + }, + attributes: [], + }, + ], + attributes: teamPreviewAttributes(), + }); + const previews = [...teamsAsLeader, ...teamsAsMember].sort((a, b) => a.name.localeCompare(b.name)); + return previews; +}; + +const teamPreviewAttributes = () => { + return ["id", "name", "logo"]; +}; + +export const insertTeam = async (data: CreateTeamBody, userId: number, inviteCode: string) => { + await db.Team.create({ + logo: data.logo, + name: data.name, + description: data.description, + gender: data.gender, + ageGroup: data.ageGroup, + region: data.region, + gymName: data.gymName, + leaderId: userId, + inviteCode, + skillLevel: defaultLevel, + mannerLevel: defaultLevel, + category: data.category, + }); +}; + +export const getTeam = async (teamId: number) => { + return await db.Team.findOne({ + raw: true, + where: { + id: teamId, + }, + attributes: ["id"], + }); +}; + +export const getTeamDetail = async (teamId: number) => { + return await db.Team.findOne({ + raw: true, + where: { + id: teamId, + }, + attributes: [ + "name", + "logo", + "skillLevel", + "mannerLevel", + "description", + "inviteCode", + "gender", + "ageGroup", + "region", + "gymName", + "leaderId", + "category", + ], + }); +}; + +export const getTeamDetailForGuesting = async (teamId: number) => { + return await db.Team.findOne({ + raw: true, + where: { + id: teamId, + }, + attributes: [ + "name", + "description", + "gender", + "ageGroup", + "gymName", + "skillLevel", + "mannerLevel", + "leaderId", + "category", + ], + }); +}; + +export const getTeamIdByInviteCode = async (inviteCode: string): Promise => { + const team = await db.Team.findOne({ + raw: true, + where: { + inviteCode, + }, + attributes: ["id"], + }); + return team?.id; +}; + +export const getTeamByLeaderId = async (teamId: number, userId: number) => { + return await db.Team.findOne({ + where: { + id: teamId, + leaderId: userId, + }, + }); +}; + +export const getTeamIdByLeaderId = async (userId: number) => { + const team = await db.Team.findOne({ + raw: true, + where: { + leaderId: userId, + }, + attributes: ["id"], + }); + return team?.id; +}; + +export const getTeamIdsByLeaderId = async (userId: number) => { + return await db.Team.findAll({ + raw: true, + where: { + leaderId: userId, + }, + attributes: ["id"], + }); +}; + +export const findTeamIdByLeaderId = async (userId: number) => { + const teams = await db.Team.findAll({ + raw: true, + where: { + leaderId: userId, + }, + attributes: ["id"], + }); + return teams.map((team) => team.id); +}; + +export const getTeamCategoryByLeaderId = async (userId: number) => { + const team = await db.Team.findOne({ + raw: true, + where: { + leaderId: userId, + }, + attributes: ["category"], + }); + return team?.category; +}; + +export const getTeamNameByTeamId = async (teamId: number) => { + const team = await db.Team.findOne({ + raw: true, + where: { + id: teamId, + }, + attributes: ["name"], + }); + return team?.name; +}; + +export const setTeam = async (team, body: UpdateTeamBodyWithoutMemberIdsToDelete) => { + Object.keys(body).forEach((field) => { + team[field] = body[field]; + }); + await team.save(); +}; + +export const findTeamPreviewByCategoryForLeader = async (userId: number, category: Category) => { + return await db.Team.findAll({ + raw: true, + where: { + category, + leaderId: userId, + }, + attributes: ["id", "name"], + }); +}; + +export const findLeaderId = async (hostingTeamId: number, opposingTeamId: number) => { + return await db.Team.findAll({ + raw: true, + where: { + [Op.or]: [{ id: hostingTeamId }, { id: opposingTeamId }], + }, + attributes: ["id", "leaderId"], + }); +}; + +export const getLeaderId = async (teamId: number) => { + const team = await db.Team.findOne({ + raw: true, + where: { + id: teamId, + }, + attributes: ["leaderId"], + }); + return team.leaderId; +}; diff --git a/src/daos/user-review.dao.ts b/src/daos/user-review.dao.ts new file mode 100644 index 0000000..e532f66 --- /dev/null +++ b/src/daos/user-review.dao.ts @@ -0,0 +1,21 @@ +import db from "../models"; +import { CreateUserReviewBody } from "../schemas/user-review.schema"; + +export const getExistingUserReview = async (userId: number, revieweeId: number, guestMatchId: number) => { + return await db.UserReview.findOne({ + rew: true, + where: { + reviewerId: userId, + revieweeId, + guestMatchId, + }, + attributes: ["id"], + }); +}; + +export const insertUserReview = async (userId: number, body: CreateUserReviewBody) => { + await db.UserReview.create({ + reviewerId: userId, + ...body, + }); +}; diff --git a/src/daos/user.dao.ts b/src/daos/user.dao.ts new file mode 100644 index 0000000..d2a4ad5 --- /dev/null +++ b/src/daos/user.dao.ts @@ -0,0 +1,119 @@ +import db from "../models"; +import { CommonProfile } from "../schemas/user-profile.schema"; +import { Category } from "../types/category.enum"; +import { Provider } from "../types/provider.enum"; + +export const getUserByProviderId = async (provider: Provider, providerId: string) => { + return await db.User.findOne({ + raw: true, + where: { + provider, + providerId, + }, + }); +}; + +export const insertUser = async (provider: Provider, providerId: string, nickname: string) => { + return await db.User.create({ + nickname, + provider, + providerId, + }); +}; + +export const setRefreshToken = async (refreshToken: string | null, userId: number) => { + await db.User.update( + { refreshToken }, + { + where: { + id: userId, + }, + }, + ); +}; + +export const getRefreshToken = async (userId: number) => { + const user = await db.User.findOne({ + raw: true, + where: { + id: userId, + }, + attributes: ["refreshToken"], + }); + return user.refreshToken; +}; + +export const getUser = async (userId: number) => { + return await db.User.findOne({ + raw: true, + where: { + id: userId, + }, + }); +}; + +export const getUserInfoByCategory = async (userId: number, category: Category) => { + return await db.User.findOne({ + raw: true, + where: { + id: userId, + }, + attributes: ["id", "nickname", "height", "avatarUrl"], + include: [ + { + model: db.Profile, + where: { + category, + }, + required: false, + attributes: ["position"], + }, + ], + }); +}; + +export const getUserInfoById = async (userId: number) => { + throw new Error("더 이상 사용되지 않는 함수"); + return await db.User.findOne({ + raw: true, + where: { + id: userId, + }, + attributes: userInfoAttributes(), + }); +}; + +export const userInfoAttributes = () => { + return ["nickname", "height"]; +}; + +export const getUserProfileByCategory = async (userId: number, category: Category) => { + return await db.User.findOne({ + raw: true, + where: { + id: userId, + }, + include: [ + { + model: db.Profile, + where: { + category, + }, + required: false, + attributes: ["skillLevel", "mannerLevel", "region", "position", "description"], + }, + ], + attributes: ["id", "nickname", "gender", "ageGroup", "height", "avatarUrl"], + }); +}; + +export const setCommonProfile = async (userId: number, commonProfile: CommonProfile) => { + await db.User.update( + { ...commonProfile }, + { + where: { + id: userId, + }, + }, + ); +}; diff --git a/src/dtos/games.dto.ts b/src/dtos/games.dto.ts new file mode 100644 index 0000000..9668b3f --- /dev/null +++ b/src/dtos/games.dto.ts @@ -0,0 +1,47 @@ +import { getGender } from "../constants/gender.constant"; +import { getAgeGroup } from "../constants/age-group.constant"; +import { getStatus } from "../constants/status.constant"; + +export const readGameResponseDTO = (result) => { + return { + games: result.games.map((game) => ({ + gameId: game.id, + gameTime: game.gameTime, + teamName: game["HostTeam.name"], + teamRegion: game["HostTeam.region"], + teamGender: getGender(game["HostTeam.gender"]), + memberCount: game.memberCount, + teamAgeGroup: getAgeGroup(game["HostTeam.ageGroup"]), + teamSkillLevel: game["HostTeam.skillLevel"], + status: getStatus(game.status), + })), + hasNext: result.hasNext, + }; +}; + +export const readGameDetailResponseDTO = (gameDetail, teamDetail, leaderInfo, memberInfo) => { + const member = memberInfo.map((info) => ({ + avatarUrl: info["User.avatarUrl"], + nickname: info["User.nickname"], + height: info["User.height"], + position: info["User.Profiles.position"], + })); + return { + id: gameDetail.id, + name: teamDetail.name, + skillLevel: teamDetail.skillLevel, + mannerLevel: teamDetail.mannerLevel, + description: teamDetail.description, + game_info: { + gymName: teamDetail.gymName, + gameTime: gameDetail.gameTime, + gameDuration: gameDetail.gameDuration, + gender: getGender(teamDetail.gender), + ageGroup: getAgeGroup(teamDetail.ageGroup), + }, + member_info: { + leader: leaderInfo, + member, + }, + }; +}; diff --git a/src/dtos/guests.dto.ts b/src/dtos/guests.dto.ts new file mode 100644 index 0000000..e01d87f --- /dev/null +++ b/src/dtos/guests.dto.ts @@ -0,0 +1,92 @@ +import { getAgeGroup } from "../constants/age-group.constant"; +import { getTeamGender } from "../constants/gender.constant"; +import { getStatus } from "../constants/status.constant"; +import { AgeGroup } from "../types/age-group.enum"; +import { Gender } from "../types/gender.enum"; + +interface GuestDetail { + gameTime: string; + description: string | null; + memberCount: number | null; + recruitCount: number | null; + gameDuration: string; + status: number; +} + +interface TeamDetail { + name: string; + logo: string | null; + mannerLevel: number | null; + description: string | null; + gender: Gender; + ageGroup: AgeGroup; + skillLevel: number; + gymName: string | null; +} + +interface UserInfo { + nickname: string; + height: number | null; + avatarUrl: string | null; + Profiles: { + position: string | null; + }; +} + +export const readGuestingResponseDTO = (result) => { + return { + guests: result.guests.map((guesting) => ({ + guestId: guesting.id, + gameTime: guesting.gameTime, + gameDuration: guesting.gameDuration, + teamName: guesting["Team.name"], + teamRegion: guesting["Team.region"], + teamGender: getTeamGender(guesting["Team.gender"]), + memberCount: guesting.memberCount, + teamAgeGroup: getAgeGroup(guesting["Team.ageGroup"]), + teamSkillLevel: guesting["Team.skillLevel"], + recruitCount: guesting.recruitCount, + status: getStatus(guesting.status), + })), + hasNext: result.hasNext, + }; +}; + +export const readGuestingDetailResponseDTO = ( + guestingDetail: GuestDetail, + TeamDetail: TeamDetail, + leaderInfo: UserInfo, + memberInfo: UserInfo[], +) => { + const member = memberInfo.map((info: UserInfo) => ({ + nickname: info.nickname, + height: info.height, + avatarUrl: info.avatarUrl, + position: info["Profiles.position"], + })); + return { + name: TeamDetail.name, + skillLevel: TeamDetail.skillLevel, + mannerLevel: TeamDetail.mannerLevel, + teamDescription: TeamDetail.description, + status: getStatus(guestingDetail.status), + gusting_info: { + guestDescription: guestingDetail.description, + gameTime: guestingDetail.gameTime, + gameDuration: guestingDetail.gameDuration, + gender: getTeamGender(TeamDetail.gender), + ageGroup: getAgeGroup(TeamDetail.ageGroup), + gymName: TeamDetail.gymName, + skillLevel: TeamDetail.skillLevel, + }, + member_info: { + leader: { + nickname: leaderInfo.nickname, + height: leaderInfo.height, + avatarUrl: leaderInfo.avatarUrl, + position: leaderInfo["Profiles.position"], + }, + member, + }, + }; +}; diff --git a/src/dtos/matchings.dto.ts b/src/dtos/matchings.dto.ts new file mode 100644 index 0000000..bb43133 --- /dev/null +++ b/src/dtos/matchings.dto.ts @@ -0,0 +1,55 @@ +import { getAgeGroup } from "../constants/age-group.constant"; +import { getTeamGender } from "../constants/gender.constant"; +import { getGuestUserStatus } from "../constants/guest-status.constant"; + +export const readApplyGuestingUserResponseDTO = (result) => { + return result.map((guestingUser) => ({ + nickname: guestingUser["User.nickname"], + height: guestingUser["User.height"], + avatarUrl: guestingUser["User.avatarUrl"], + position: guestingUser["User.Profiles.position"], + status: getGuestUserStatus(guestingUser.status), + })); +}; + +export const readHostingApplicantsTeamResponseDTO = (teams) => { + return teams.map((team) => ({ + teamId: team["Team.id"], + teamLogo: team["Team.logo"], + teamName: team["Team.name"], + memberCount: team.memberCount, + })); +}; + +export const readMatchingResponseDTO = (guestings, games) => { + const sortedMatch = [...guestings, ...games].sort((a, b) => a.gameTime.getTime() - b.gameTime.getTime()); + return sortedMatch.map((match) => { + if (match.type === "guest") { + return { + type: match.type, + matchId: match.id, + gameTime: match.gameTime, + gameDuration: match.gameDuration, + name: match["Team.name"], + region: match["Team.region"], + gender: getTeamGender(match["Team.gender"]), + memberCount: match.memberCount, + ageGroup: getAgeGroup(match["Team.ageGroup"]), + skillLevel: match["Team.skillLevel"], + }; + } else { + return { + type: match.type, + matchId: match.id, + gameTime: match.gameTime, + gameDuration: match.gameDuration, + name: match["HostTeam.name"], + region: match["HostTeam.region"], + gender: getTeamGender(match["HostTeam.gender"]), + memberCount: match.memberCount, + ageGroup: getAgeGroup(match["HostTeam.ageGroup"]), + skillLevel: match["HostTeam.skillLevel"], + }; + } + }); +}; diff --git a/src/dtos/posts.dto.ts b/src/dtos/posts.dto.ts new file mode 100644 index 0000000..d9b40f7 --- /dev/null +++ b/src/dtos/posts.dto.ts @@ -0,0 +1,49 @@ +export const readPostsResponseDTO = (result) => { + return { + posts: result.posts.map((post) => ({ + id: post.id, + isBookmarked: Boolean(post["Bookmarks.id"]), + title: post.title, + createdAt: post.createdAt, + })), + hasNext: result.hasNext, + }; +}; + +export const readRentPostsResponseDTO = (result) => { + return { + posts: result.posts.map((post) => ({ + id: post.id, + title: post.title, + content: post.content, + status: post.rentStatus, + })), + hasNext: result.hasNext, + }; +}; + +export const readPostResponseDTO = (post, imageUrls, commentCount, comments, isBookmarked) => { + return { + post: { + title: post.title, + contnet: post.content, + link: post.link, + imageUrls: imageUrls, + }, + isBookmarked, + commentCount, + ...readCommentsResonseDTO(comments), + }; +}; + +export const readCommentsResonseDTO = (comments) => { + return { + comments: comments.ascendingComments.map((comment) => ({ + id: comment.id, + nickname: comment["User.nickname"], + content: comment.content, + createdAt: comment.createdAt, + })), + commentHasNext: comments.hasNext, + }; +}; diff --git a/src/dtos/teams.dto.ts b/src/dtos/teams.dto.ts new file mode 100644 index 0000000..fd93e96 --- /dev/null +++ b/src/dtos/teams.dto.ts @@ -0,0 +1,67 @@ +import { AgeGroup } from "../types/age-group.enum"; +import { Gender } from "../types/gender.enum"; + +interface TeamDetail { + name: string; + logo: string | null; + skillLevel: number | null; + mannerLevel: number | null; + description: string | null; + inviteCode: string; + gender: Gender; + ageGroup: AgeGroup; + region: string; + gymName: string; +} + +interface UserInfo { + id: number; + nickname: string; + height: number | null; + avatarUrl: string | null; + Profiles: { + position: string | null; + }; +} + +interface MemberInfo { + User: UserInfo; +} + +export const readTeamDetailResponseDTO = ( + detail: TeamDetail, + leaderInfo: UserInfo, + membersInfo: MemberInfo[], + isTeamLeader: boolean, +) => { + const member = membersInfo.map((memberInfo: MemberInfo) => ({ + id: memberInfo["User.id"], + avatarUrl: memberInfo["User.avatarUrl"], + nickname: memberInfo["User.nickname"], + height: memberInfo["User.height"], + position: memberInfo["User.Profiles.position"], + })); + return { + name: detail.name, + logo: detail.logo, + skillLevel: detail.skillLevel, + mannerLevel: detail.mannerLevel, + description: detail.description, + inviteCode: isTeamLeader ? detail.inviteCode : null, + gender: detail.gender, + ageGroup: detail.ageGroup, + region: detail.region, + gymName: detail.gymName, + participants: { + leader: { + id: leaderInfo.id, + avatarUrl: leaderInfo.avatarUrl, + nickname: leaderInfo.nickname, + height: leaderInfo.height, + position: leaderInfo["Profiles.position"], + }, + member, + }, + isTeamLeader, + }; +}; diff --git a/src/dtos/users.dto.ts b/src/dtos/users.dto.ts new file mode 100644 index 0000000..fb1f3e5 --- /dev/null +++ b/src/dtos/users.dto.ts @@ -0,0 +1,36 @@ +import { getAgeGroup } from "../constants/age-group.constant"; +import { getGender } from "../constants/gender.constant"; +import { AgeGroup } from "../types/age-group.enum"; +import { Gender } from "../types/gender.enum"; + +interface ReadUserProfile { + id: number; + avatarUrl: string | null; + nickname: string; + gender: Exclude | null; + ageGroup: AgeGroup | null; + height: number | null; + Profiles: { + skillLevel: number; + mannerLevel: number; + region: string | null; + position: string | null; + description: string | null; + }; +} + +export const readUserProfileResponseDTO = (profile: ReadUserProfile) => { + return { + id: profile.id, + avatarUrl: profile.avatarUrl, + nickname: profile.nickname, + skillLevel: profile["Profiles.skillLevel"], + mannerLevel: profile["Profiles.mannerLevel"], + gender: profile.gender, + ageGroup: profile.ageGroup, + region: profile["Profiles.region"], + height: profile.height, + position: profile["Profiles.position"], + description: profile["Profiles.description"], + }; +}; diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..21b067a --- /dev/null +++ b/src/middlewares/auth.middleware.ts @@ -0,0 +1,35 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { NextFunction, Response } from "express"; +import { extractAccessToken, extractAccessTokenFromHeader, verifyAccessToken } from "../utils/jwt.util"; + +export const verifyUser = (req, res: Response, next: NextFunction) => { + const accessToken = extractAccessToken(req); + const verified = verifyAccessToken(accessToken); + if (verified.isExpired) { + throw new BaseError(status.ACCESS_TOKEN_EXPIRED); + } else { + req.user = { + id: verified.decoded.id, + nickname: verified.decoded.nickname, + }; + next(); + } +}; + +export const verifyUserIfExists = (req, res: Response, next: NextFunction) => { + const accessToken = extractAccessTokenFromHeader(req); + if (accessToken) { + const verified = verifyAccessToken(accessToken); + if (verified.isExpired) { + throw new BaseError(status.ACCESS_TOKEN_EXPIRED); + } else { + req.user = { + id: verified.decoded.id, + nickname: verified.decoded.nickname, + }; + next(); + } + } + next(); +}; diff --git a/src/middlewares/validate.middleware.ts b/src/middlewares/validate.middleware.ts new file mode 100644 index 0000000..043cbfa --- /dev/null +++ b/src/middlewares/validate.middleware.ts @@ -0,0 +1,38 @@ +import { AnyZodObject, ZodError } from "zod"; +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { NextFunction } from "express"; + +export const validateBody = (schema: AnyZodObject) => (req, res, next: NextFunction) => { + try { + schema.parse(req.body); + next(); + } catch (err: any) { + if (err instanceof ZodError) { + const detail = err.errors.map((err) => ({ + field: err.path[0], + description: err.message, + })); + throw new BaseError(status.REQUEST_VALIDATION_ERROR, detail); + } + } +}; + +export const validate = (schema: AnyZodObject) => (req, res, next: NextFunction) => { + try { + schema.parse({ + body: req.body, + query: req.query, + params: req.params, + }); + next(); + } catch (err: any) { + if (err instanceof ZodError) { + const detail = err.errors.map((err) => ({ + field: err.path.join(": "), + description: err.message, + })); + throw new BaseError(status.REQUEST_VALIDATION_ERROR, detail); + } + } +}; diff --git a/src/models/bookmark.model.ts b/src/models/bookmark.model.ts new file mode 100644 index 0000000..cd3ba43 --- /dev/null +++ b/src/models/bookmark.model.ts @@ -0,0 +1,34 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Bookmark extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Bookmark.init( + { + postId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + userId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Bookmark", + tableName: "bookmark", + paranoid: true, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Bookmark.belongsTo(db.Post, { foreignKey: "post_id" }); + db.Bookmark.belongsTo(db.User, { foreignKey: "user_id" }); + } +} + +module.exports = Bookmark; diff --git a/src/models/comment.model.ts b/src/models/comment.model.ts new file mode 100644 index 0000000..dadba02 --- /dev/null +++ b/src/models/comment.model.ts @@ -0,0 +1,38 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Comment extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Comment.init( + { + postId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + authorId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + content: { + type: new DataTypes.STRING(500), + allowNull: true, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Comment", + tableName: "comment", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Comment.belongsTo(db.Post, { foreignKey: "post_id" }); + db.Comment.belongsTo(db.User, { foreignKey: "author_id" }); + } +} + +module.exports = Comment; diff --git a/src/models/game-apply.model.ts b/src/models/game-apply.model.ts new file mode 100644 index 0000000..a23c740 --- /dev/null +++ b/src/models/game-apply.model.ts @@ -0,0 +1,34 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class GameApply extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + GameApply.init( + { + gameId: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + teamId: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "GameApply", + tableName: "game_apply", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.GameApply.belongsTo(db.Game, { foreignKey: "game_id" }); + db.GameApply.belongsTo(db.Team, { foreignKey: "team_id" }); + } +} + +module.exports = GameApply; diff --git a/src/models/game.model.ts b/src/models/game.model.ts new file mode 100644 index 0000000..dec6222 --- /dev/null +++ b/src/models/game.model.ts @@ -0,0 +1,57 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Game extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Game.init( + { + hostTeamId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + opposingTeamId: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + gameTime: { + type: new DataTypes.DATE(), + allowNull: false, + }, + gameDuration: { + type: new DataTypes.TIME(), + allowNull: false, + }, + category: { + type: new DataTypes.STRING(15), + allowNull: false, + }, + description: { + type: new DataTypes.STRING(400), + allowNull: false, + }, + status: { + type: new DataTypes.INTEGER(), + allowNull: false, + defaultValue: 0, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Game", + tableName: "game", + paranoid: true, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Game.belongsTo(db.Team, { foreignKey: "host_team_id", as: "HostTeam" }); + db.Game.belongsTo(db.Team, { foreignKey: "opposing_team_id" }); + db.Game.hasMany(db.GameApply, { foreignKey: "game_id" }); + db.Game.hasMany(db.TeamReview, { foreignKey: "team_match_id" }); + } +} + +module.exports = Game; diff --git a/src/models/guest-user.model.ts b/src/models/guest-user.model.ts new file mode 100644 index 0000000..0c78024 --- /dev/null +++ b/src/models/guest-user.model.ts @@ -0,0 +1,39 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class GuestUser extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + GuestUser.init( + { + guestId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + userId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + status: { + type: new DataTypes.INTEGER(), + allowNull: false, + defaultValue: 0, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "GuestUser", + tableName: "guest_User", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.GuestUser.belongsTo(db.Guest, { foreignKey: "guest_id" }); + db.GuestUser.belongsTo(db.User, { foreignKey: "user_id" }); + } +} + +module.exports = GuestUser; diff --git a/src/models/guest.model.ts b/src/models/guest.model.ts new file mode 100644 index 0000000..bbae61e --- /dev/null +++ b/src/models/guest.model.ts @@ -0,0 +1,53 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Guest extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Guest.init( + { + teamId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + gameTime: { + type: new DataTypes.DATE(6), + allowNull: false, + }, + description: { + type: new DataTypes.STRING(400), + allowNull: true, + }, + recruitCount: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + gameDuration: { + type: new DataTypes.TIME(), + allowNull: false, + }, + status: { + type: new DataTypes.INTEGER(), + allowNull: false, + defaultValue: 0, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Guest", + tableName: "guest", + paranoid: true, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Guest.hasMany(db.GuestUser, { foreignKey: "guest_id" }); + db.Guest.belongsTo(db.Team, { foreignKey: "team_id" }); + db.Guest.hasMany(db.TeamReview, { foreignKey: "guest_match_id" }); + db.Guest.hasMany(db.UserReview, { foreignKey: "guest_match_id" }); + } +} + +module.exports = Guest; diff --git a/src/models/image.model.ts b/src/models/image.model.ts new file mode 100644 index 0000000..a775137 --- /dev/null +++ b/src/models/image.model.ts @@ -0,0 +1,33 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Image extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Image.init( + { + postId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + url: { + type: new DataTypes.STRING(1000), + allowNull: true, + }, + }, + { + sequelize, + timestamps: false, + underscored: true, + modelName: "Image", + tableName: "image", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Image.belongsTo(db.Post, { foreignKey: "post_id" }); + } +} + +module.exports = Image; diff --git a/src/models/member.model.ts b/src/models/member.model.ts new file mode 100644 index 0000000..44dbdd7 --- /dev/null +++ b/src/models/member.model.ts @@ -0,0 +1,34 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Member extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Member.init( + { + teamId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + userId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Member", + tableName: "member", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Member.belongsTo(db.Team, { foreignKey: "team_id" }); + db.Member.belongsTo(db.User, { foreignKey: "user_id" }); + } +} + +module.exports = Member; diff --git a/src/models/post.model.ts b/src/models/post.model.ts new file mode 100644 index 0000000..fee9020 --- /dev/null +++ b/src/models/post.model.ts @@ -0,0 +1,60 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Post extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Post.init( + { + title: { + type: new DataTypes.STRING(30), + allowNull: false, + }, + content: { + type: new DataTypes.STRING(1000), + allowNull: true, + }, + link: { + type: new DataTypes.STRING(200), + allowNull: true, + }, + authorId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + type: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + rentDate: { + type: new DataTypes.DATE(), + allowNull: true, + }, + rentPlace: { + type: new DataTypes.STRING(100), + allowNull: true, + }, + rentStatus: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Post", + tableName: "post", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Post.belongsTo(db.User, { foreignKey: "author_id" }); + db.Post.hasMany(db.Image, { foreignKey: "post_id" }); + db.Post.hasMany(db.Bookmark, { foreignKey: "post_id" }); + db.Post.hasMany(db.Comment, { foreignKey: "post_id" }); + } +} + +module.exports = Post; diff --git a/src/models/profile.model.ts b/src/models/profile.model.ts new file mode 100644 index 0000000..7aca1ae --- /dev/null +++ b/src/models/profile.model.ts @@ -0,0 +1,53 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Profile extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Profile.init( + { + userId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + category: { + type: new DataTypes.STRING(15), + allowNull: false, + }, + skillLevel: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + mannerLevel: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + region: { + type: new DataTypes.STRING(200), + allowNull: true, + }, + position: { + type: new DataTypes.STRING(50), + allowNull: true, + }, + description: { + type: new DataTypes.STRING(400), + allowNull: true, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Profile", + tableName: "profile", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Profile.belongsTo(db.User, { foreignKey: "user_id" }); + } +} + +module.exports = Profile; diff --git a/src/models/team-review.model.ts b/src/models/team-review.model.ts new file mode 100644 index 0000000..6a2ed36 --- /dev/null +++ b/src/models/team-review.model.ts @@ -0,0 +1,52 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class TeamReview extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + TeamReview.init( + { + reviewerId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + reviewedTeamId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + teamMatchId: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + guestMatchId: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + skillScore: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + mannerScore: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "TeamReview", + tableName: "team_review", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.TeamReview.belongsTo(db.User, { foreignKey: "reviewer_id" }); + db.TeamReview.belongsTo(db.Team, { foreignKey: "reviewed_team_id" }); + db.TeamReview.belongsTo(db.Game, { foreignKey: "team_match_id" }); + db.TeamReview.belongsTo(db.Guest, { foreignKey: "guest_match_id" }); + } +} + +module.exports = TeamReview; diff --git a/src/models/team.model.ts b/src/models/team.model.ts new file mode 100644 index 0000000..2489dbc --- /dev/null +++ b/src/models/team.model.ts @@ -0,0 +1,79 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class Team extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + Team.init( + { + logo: { + type: new DataTypes.STRING(200), + allowNull: true, + }, + name: { + type: new DataTypes.STRING(20), + allowNull: false, + }, + description: { + type: new DataTypes.STRING(400), + allowNull: true, + }, + gender: { + type: new DataTypes.STRING(1), + allowNull: false, + }, + ageGroup: { + type: new DataTypes.STRING(10), + allowNull: false, + }, + region: { + type: new DataTypes.STRING(200), + allowNull: false, + }, + gymName: { + type: new DataTypes.STRING(100), + allowNull: false, + }, + leaderId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + inviteCode: { + type: new DataTypes.STRING(100), + allowNull: false, + }, + skillLevel: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + mannerLevel: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + category: { + type: new DataTypes.STRING(15), + allowNull: false, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "Team", + tableName: "team", + paranoid: true, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.Team.hasMany(db.Member, { foreignKey: "team_id" }); + db.Team.belongsTo(db.User, { foreignKey: "leader_id" }); + db.Team.hasMany(db.Game, { foreignKey: "host_team_id" }); + db.Team.hasMany(db.Game, { foreignKey: "opposing_team_id" }); + db.Game.hasMany(db.GameApply, { foreignKey: "team_id" }); + db.Team.hasMany(db.Guest, { foreignKey: "team_id" }); + db.Team.hasMany(db.TeamReview, { foreignKey: "reviewed_team_id" }); + } +} + +module.exports = Team; diff --git a/src/models/user-review.model.ts b/src/models/user-review.model.ts new file mode 100644 index 0000000..daf4458 --- /dev/null +++ b/src/models/user-review.model.ts @@ -0,0 +1,47 @@ +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; + +class UserReview extends Model, InferCreationAttributes> { + static initiate(sequelize: Sequelize) { + UserReview.init( + { + reviewerId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + revieweeId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + guestMatchId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + skillScore: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + mannerScore: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + }, + { + sequelize, + timestamps: true, + underscored: true, + modelName: "UserReview", + tableName: "user_review", + paranoid: false, + charset: "utf8", + collate: "utf8_general_ci", + }, + ); + } + static associate(db) { + db.UserReview.belongsTo(db.User, { foreignKey: "reviewer_id" }); + db.UserReview.belongsTo(db.User, { foreignKey: "reviewee_id" }); + db.UserReview.belongsTo(db.Guest, { foreignKey: "guest_match_id" }); + } +} + +module.exports = UserReview; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 9e0a404..b417bdc 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -1,35 +1,66 @@ -import { Model, InferAttributes, InferCreationAttributes, CreationOptional, DataTypes, Sequelize } from "sequelize"; +import { Model, InferAttributes, InferCreationAttributes, DataTypes, Sequelize } from "sequelize"; class User extends Model, InferCreationAttributes> { - declare id: CreationOptional; - declare nickname: string; - static initiate(sequelize: Sequelize) { User.init( { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, nickname: { type: new DataTypes.STRING(10), allowNull: false, }, + provider: { + type: new DataTypes.STRING(10), + allowNull: false, + }, + providerId: { + type: new DataTypes.STRING(50), + allowNull: false, + }, + refreshToken: { + type: new DataTypes.STRING(150), + allowNull: true, + }, + gender: { + type: new DataTypes.STRING(1), + allowNull: true, + }, + ageGroup: { + type: new DataTypes.STRING(10), + allowNull: true, + }, + height: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, + avatarUrl: { + type: new DataTypes.STRING(200), + allowNull: true, + }, }, { sequelize, timestamps: true, underscored: true, modelName: "User", - tableName: "users", + tableName: "user", paranoid: true, charset: "utf8", collate: "utf8_general_ci", }, ); } - static associate(db) {} + static associate(db) { + db.User.hasMany(db.Profile, { foreignKey: "user_id" }); + db.User.hasMany(db.Team, { foreignKey: "leader_id" }); + db.User.hasMany(db.Member, { foreignKey: "user_id" }); + db.User.hasMany(db.GuestUser, { foreignKey: "user_id" }); + db.User.hasMany(db.Post, { foreignKey: "author_id" }); + db.User.hasMany(db.Bookmark, { foreignKey: "user_id" }); + db.User.hasMany(db.Comment, { foreignKey: "author_id" }); + db.User.hasMany(db.TeamReview, { foreignKey: "reviewer_id" }); + db.User.hasMany(db.UserReview, { foreignKey: "reviewer_id" }); + db.User.hasMany(db.UserReview, { foreignKey: "reviewee_id" }); + } } module.exports = User; diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts new file mode 100644 index 0000000..8553d63 --- /dev/null +++ b/src/routes/auth.route.ts @@ -0,0 +1,16 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { authGoogle, authKakao, authNaver, refreshAccessToken, logout } from "../controllers/auth.controller"; +import { verifyUser } from "../middlewares/auth.middleware"; + +export const authRouter = express.Router(); + +authRouter.post("/google", asyncHandler(authGoogle)); + +authRouter.post("/kakao", asyncHandler(authKakao)); + +authRouter.post("/naver", asyncHandler(authNaver)); + +authRouter.post("/refresh", asyncHandler(refreshAccessToken)); + +authRouter.post("/logout", verifyUser, asyncHandler(logout)); diff --git a/src/routes/games.route.ts b/src/routes/games.route.ts new file mode 100644 index 0000000..99c5dd8 --- /dev/null +++ b/src/routes/games.route.ts @@ -0,0 +1,42 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { validate } from "../middlewares/validate.middleware"; +import { + fetchGamesByDate, + fetchGamesByGender, + fetchGamesByLevel, + fetchGamesByRegion, + fetchGameDetail, + addGame, + modifyGame, + applyGame, +} from "../controllers/games.controller"; +import { + readGameSchema, + readGameFilterGenderSchema, + readGameFilterLevelSchema, + readGameFilterRegionSchema, + createGameSchema, + updateGameSchema, +} from "../schemas/game.schema"; + +export const gamesRouter = express.Router(); + +// 날짜별 연습경기 모집글 조회 (기본) +gamesRouter.get("/", validate(readGameSchema), asyncHandler(fetchGamesByDate)); + +// 연습경기 모집글 필터링 - 성별/레벨/지역 +gamesRouter.get("/by-gender", validate(readGameFilterGenderSchema), asyncHandler(fetchGamesByGender)); +gamesRouter.get("/by-level", validate(readGameFilterLevelSchema), asyncHandler(fetchGamesByLevel)); +gamesRouter.get("/by-region", validate(readGameFilterRegionSchema), asyncHandler(fetchGamesByRegion)); + +// 연습경기 신청 +gamesRouter.post("/:gameId/application", verifyUser, asyncHandler(applyGame)); + +// 연습경기 모집글 상세 조회 +gamesRouter.get("/:gameId", verifyUser, asyncHandler(fetchGameDetail)); + +// 연습경기 모집글 작성/수정 +gamesRouter.post("/", verifyUser, validate(createGameSchema), asyncHandler(addGame)); +gamesRouter.put("/:gameId", verifyUser, validate(updateGameSchema), asyncHandler(modifyGame)); diff --git a/src/routes/guests.route.ts b/src/routes/guests.route.ts new file mode 100644 index 0000000..3f80eee --- /dev/null +++ b/src/routes/guests.route.ts @@ -0,0 +1,40 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { validate } from "../middlewares/validate.middleware"; +import { + createGuestingSchema, + readGuestFilterGenderSchema, + readGuestFilterLevelSchema, + readGuestFilterRegionSchema, + readGuestSchema, + updateGuestingSchema, +} from "../schemas/guest.schema"; +import { + GuestingPreview, + GuestingPreviewByLevel, + GuestingPreviewByGender, + GuestingPreviewByRegion, + DetailedGuestingPreview, + addGuesting, + modifyGuesting, + applicationGuesting, +} from "../controllers/guests.controller"; + +export const guestsRouter = express.Router(); + +guestsRouter.post("/", verifyUser, validate(createGuestingSchema), asyncHandler(addGuesting)); + +guestsRouter.put("/:guestingId", verifyUser, validate(updateGuestingSchema), asyncHandler(modifyGuesting)); + +guestsRouter.get("/", validate(readGuestSchema), asyncHandler(GuestingPreview)); + +guestsRouter.get("/level", validate(readGuestFilterLevelSchema), asyncHandler(GuestingPreviewByLevel)); + +guestsRouter.get("/gender", validate(readGuestFilterGenderSchema), asyncHandler(GuestingPreviewByGender)); + +guestsRouter.get("/region", validate(readGuestFilterRegionSchema), asyncHandler(GuestingPreviewByRegion)); + +guestsRouter.get("/:guestingId", verifyUser, asyncHandler(DetailedGuestingPreview)); + +guestsRouter.post("/:guestingId/application", verifyUser, asyncHandler(applicationGuesting)); diff --git a/src/routes/matchings.route.ts b/src/routes/matchings.route.ts new file mode 100644 index 0000000..6756830 --- /dev/null +++ b/src/routes/matchings.route.ts @@ -0,0 +1,31 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { validate } from "../middlewares/validate.middleware"; +import { + matchingGuestingPreview, + matchingHostingPreview, + ApplyGuestingUserPreview, + modifyGuestStatus, + fetchHostingApplicantsTeamList, + gameApplicationApproval, +} from "../controllers/matchings.controller"; +import { dateQuery } from "../schemas/fields"; + +export const matchingsRouter = express.Router(); + +matchingsRouter.use(verifyUser); + +matchingsRouter.get("/guesting", validate(dateQuery), asyncHandler(matchingGuestingPreview)); + +matchingsRouter.get("/hosting", validate(dateQuery), asyncHandler(matchingHostingPreview)); + +matchingsRouter.get("/hosting/user/:guestingId", asyncHandler(ApplyGuestingUserPreview)); + +matchingsRouter.patch("/hosting/guest/:guestUserId", asyncHandler(modifyGuestStatus)); + +// 호스팅 내역 > 신청팀 목록 조회 +matchingsRouter.get("/hosting/team/:gameId", asyncHandler(fetchHostingApplicantsTeamList)); + +// 호스팅 내역 > 신청 승인 +matchingsRouter.patch("/hosting/team/:gameId", asyncHandler(gameApplicationApproval)); diff --git a/src/routes/members.route.ts b/src/routes/members.route.ts new file mode 100644 index 0000000..58f0bab --- /dev/null +++ b/src/routes/members.route.ts @@ -0,0 +1,12 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { addMember } from "../controllers/members.controller"; +import { validate } from "../middlewares/validate.middleware"; +import { createMemberSchema } from "../schemas/team.schema"; + +export const membersRouter = express.Router(); + +membersRouter.use(verifyUser); + +membersRouter.post("/", validate(createMemberSchema), asyncHandler(addMember)); diff --git a/src/routes/posts.route.ts b/src/routes/posts.route.ts new file mode 100644 index 0000000..57923fa --- /dev/null +++ b/src/routes/posts.route.ts @@ -0,0 +1,42 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser, verifyUserIfExists } from "../middlewares/auth.middleware"; +import { + addOrRemoveBookmark, + fetchBookmarkedPosts, + fetchPost, + fetchCommunityPosts, + fetchMyPosts, + addCommunityPost, + addComment, + fetchComments, + addRentPost, + fetchRentPosts, +} from "../controllers/posts.controller"; +import { validate } from "../middlewares/validate.middleware"; +import { createPostSchema } from "../schemas/post.schema"; +import { createCommentSchema } from "../schemas/comment.schema"; + +export const postsRouter = express.Router(); + +postsRouter.post("/community", verifyUser, validate(createPostSchema), asyncHandler(addCommunityPost)); + +postsRouter.get("/community", verifyUserIfExists, asyncHandler(fetchCommunityPosts)); + +postsRouter.get("/authors/me", verifyUser, asyncHandler(fetchMyPosts)); + +postsRouter.get("/bookmarks", verifyUser, asyncHandler(fetchBookmarkedPosts)); + +postsRouter.post("/:postId/bookmark", verifyUser, asyncHandler(addOrRemoveBookmark)); + +postsRouter.post("/:postId/comments", verifyUser, validate(createCommentSchema), asyncHandler(addComment)); + +postsRouter.get("/:postId/comments", verifyUser, asyncHandler(fetchComments)); + +postsRouter.get("/:postId", verifyUser, asyncHandler(fetchPost)); + +// 대관정보 글 작성 +postsRouter.post("/rent", verifyUser, validate(createPostSchema), asyncHandler(addRentPost)); + +// 대관정보 글 목록 조회 +postsRouter.get("/rent", verifyUserIfExists, asyncHandler(fetchRentPosts)); diff --git a/src/routes/reviews.route.ts b/src/routes/reviews.route.ts new file mode 100644 index 0000000..082a4b9 --- /dev/null +++ b/src/routes/reviews.route.ts @@ -0,0 +1,14 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { validate } from "../middlewares/validate.middleware"; +import { createTeamReviewSchema } from "../schemas/team-review.schema"; +import { addTeamReview, addUserReview } from "../controllers/reviews.controller"; + +export const reviewsRouter = express.Router(); + +reviewsRouter.use(verifyUser); + +reviewsRouter.post("/team", validate(createTeamReviewSchema), asyncHandler(addTeamReview)); + +reviewsRouter.post("/user", validate(createTeamReviewSchema), asyncHandler(addUserReview)); diff --git a/src/routes/teams.route.ts b/src/routes/teams.route.ts new file mode 100644 index 0000000..447eeec --- /dev/null +++ b/src/routes/teams.route.ts @@ -0,0 +1,27 @@ +import express from "express"; +import asyncHandler from "express-async-handler"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { validate } from "../middlewares/validate.middleware"; +import { createTeamSchema, readTeamPreviewsSchema, updateTeamSchema } from "../schemas/team.schema"; +import { + fetchTeamsAvailById, + fetchTeamPreviews, + fetchTeamDetail, + addTeam, + modifyTeam, +} from "../controllers/teams.controller"; + +export const teamsRouter = express.Router(); + +teamsRouter.use(verifyUser); + +teamsRouter.post("/", validate(createTeamSchema), asyncHandler(addTeam)); + +teamsRouter.get("/", validate(readTeamPreviewsSchema), asyncHandler(fetchTeamPreviews)); + +teamsRouter.put("/:teamId", validate(updateTeamSchema), asyncHandler(modifyTeam)); + +teamsRouter.get("/:teamId", asyncHandler(fetchTeamDetail)); + +// 연습경기 신청 가능 팀 목록 조회 +teamsRouter.get("/apply-avail", asyncHandler(fetchTeamsAvailById)); diff --git a/src/routes/users.route.ts b/src/routes/users.route.ts index ada81a6..c52ec35 100644 --- a/src/routes/users.route.ts +++ b/src/routes/users.route.ts @@ -1,8 +1,15 @@ import express from "express"; import asyncHandler from "express-async-handler"; - -// import { ... } from "../controllers/users.controller"; +import { verifyUser } from "../middlewares/auth.middleware"; +import { validate } from "../middlewares/validate.middleware"; +import { updateUserProfileSchema } from "../schemas/user-profile.schema"; +import { fetchUserProfile, modifyUserProfile } from "../controllers/users.controller"; +import { categoryParam } from "../schemas/fields"; export const usersRouter = express.Router(); -// usersRouter.post("/", asyncHandler(...)); +usersRouter.use(verifyUser); + +usersRouter.get("/:userId/profiles/:category", validate(categoryParam), asyncHandler(fetchUserProfile)); + +usersRouter.put("/profiles/:category", validate(updateUserProfileSchema), asyncHandler(modifyUserProfile)); diff --git a/src/schemas/comment.schema.ts b/src/schemas/comment.schema.ts new file mode 100644 index 0000000..70df907 --- /dev/null +++ b/src/schemas/comment.schema.ts @@ -0,0 +1,12 @@ +import { TypeOf, object, z } from "zod"; +import { contentFieldInComment } from "./fields"; + +const body = object({ + ...contentFieldInComment, +}); + +export const createCommentSchema = object({ + body: body, +}); + +export type CreateCommentBody = TypeOf; diff --git a/src/schemas/fields.ts b/src/schemas/fields.ts new file mode 100644 index 0000000..19142d3 --- /dev/null +++ b/src/schemas/fields.ts @@ -0,0 +1,147 @@ +import { object, z } from "zod"; +import { Gender } from "../types/gender.enum"; +import { Category } from "../types/category.enum"; +import { AgeGroup } from "../types/age-group.enum"; + +export const nicknameField = { nickname: z.string().max(10) }; + +export const descriptionField = { description: z.optional(z.string().max(400)) }; + +export const genderFieldInUser = { gender: z.optional(z.enum([Gender.Female, Gender.Male])) }; + +export const genderFieldInTeam = { gender: z.enum([Gender.Female, Gender.Male, Gender.Mixed]) }; + +const ageGroup = z.enum([ + AgeGroup.Teenagers, + AgeGroup.Twenties, + AgeGroup.Thirties, + AgeGroup.Forties, + AgeGroup.FiftiesAndAbove, +]); + +export const ageGroupFieldInUser = { ageGroup: z.optional(ageGroup) }; + +export const ageGroupFieldInTeam = { ageGroup: ageGroup }; + +export const heightField = { height: z.optional(z.number().int()) }; + +export const regionFieldInProfile = { region: z.optional(z.string()) }; + +export const regionFieldInTeam = { region: z.string() }; + +export const positionField = { position: z.optional(z.string()) }; //TODO: 추후 enum으로 변경 + +export const categoryField = { + category: z.enum([ + Category.Basketball, + Category.Baseball, + Category.Tennis, + Category.Soccer, + Category.Futsal, + Category.Volleyball, + Category.Bowling, + Category.Badminton, + Category.TableTennis, + ]), +}; + +export const logoField = { logo: z.optional(z.string().max(200)) }; + +export const nameField = { name: z.string().max(20) }; + +export const gymNameField = { gymName: z.string().max(100) }; + +export const memberIdsToDeleteField = { memberIdsToDelete: z.optional(z.array(z.number().int())) }; + +export const contentFieldInPost = { content: z.string().max(1000) }; + +export const contentFieldInComment = { content: z.string().max(500) }; + +export const titleField = { title: z.string().max(30) }; + +export const linkField = { link: z.optional(z.string().max(200)) }; + +export const inviteCodeField = { inviteCode: z.string().max(100) }; + +export const categoryParam = object({ + params: object({ + ...categoryField, + }), +}); + +export const hostTeamIdField = { hostTeamId: z.number().int().optional() }; + +export const gameTimeField = { + gameTime: z.preprocess((arg) => { + if (typeof arg == "string") { + return new Date(arg); + } + return arg; + }, z.date()), +}; + +export const descriptionFieldInGame = { description: z.string() }; + +export const teamIdField = { teamId: z.number().int() }; + +export const guestIdField = { guestId: z.number().int() }; + +export const userIdField = { userId: z.number().int() }; + +export const statusField = { status: z.number().int() }; + +export const recruitCountField = { recruitCount: z.number().int() }; + +export const levelField = { level: z.string().max(5) }; + +export const levelFieldInTeam = { + skillLevel: z.string().refine((val) => !Number.isNaN(parseInt(val, 10)), { + message: "Expected number, received a string", + }), +}; + +export const skillScoreField = { skillScore: z.number().int().min(1).max(5) }; + +export const mannerScoreField = { mannerScore: z.number().int().min(1).max(5) }; + +export const guestMatchIdFieldInUserReview = { guestMatchId: z.number().int() }; + +export const guestMatchIdFieldInTeamReview = { guestMatchId: z.optional(z.number().int()) }; + +export const teamMatchIdField = { teamMatchId: z.optional(z.number().int()) }; + +export const revieweeIdField = { revieweeId: z.number().int() }; + +export const dateField = { + date: z.preprocess((arg) => { + if (typeof arg == "string") { + return new Date(arg); + } + return arg; + }, z.date()), +}; + +export const dateQuery = object({ + query: object({ + ...dateField, + }), +}); + +export const rentDateField = { + rentDate: z.optional( + z.preprocess((arg) => { + if (typeof arg == "string") { + return new Date(arg); + } + return arg; + }, z.date()), + ), +}; + +export const rentPlaceField = { rentPlace: z.optional(z.string().max(100)) }; + +export const rentStatusField = { status: z.optional(z.number().int()) }; + +export const gameDurationField = { gameDuration: z.string() }; + +export const avatarUrlField = { avatarUrl: z.optional(z.string().max(200)) }; diff --git a/src/schemas/game-apply.schema.ts b/src/schemas/game-apply.schema.ts new file mode 100644 index 0000000..d7b4d00 --- /dev/null +++ b/src/schemas/game-apply.schema.ts @@ -0,0 +1,16 @@ +import { TypeOf, object, z } from "zod"; +import { teamIdField } from "./fields"; + +// export const applyGame = object({ +// teamIdField, +// }); + +const gameApplyBody = object({ + ...teamIdField, +}); + +export const applyGameSchema = object({ + body: gameApplyBody, +}); + +export type ApplyGameBody = TypeOf; diff --git a/src/schemas/game.schema.ts b/src/schemas/game.schema.ts new file mode 100644 index 0000000..c6d0b1e --- /dev/null +++ b/src/schemas/game.schema.ts @@ -0,0 +1,63 @@ +import { TypeOf, object, z } from "zod"; +import { + hostTeamIdField, + gameTimeField, + descriptionFieldInGame, + categoryField, + dateField, + levelFieldInTeam, + regionFieldInTeam, + genderFieldInTeam, + gameDurationField, +} from "./fields"; + +const body = object({ + ...hostTeamIdField, + ...gameTimeField, + ...gameDurationField, + ...descriptionFieldInGame, +}); + +export const createGameSchema = object({ + body: body, +}); + +export type CreateGameBody = TypeOf; + +export const updateGameSchema = object({ + body: body, +}); + +export type UpdateGameBody = TypeOf; + +export const readGame = { + ...categoryField, + ...dateField, +}; + +export const readGameSchema = object({ + query: object({ + ...readGame, + }), +}); + +export const readGameFilterGenderSchema = object({ + query: object({ + ...readGame, + ...genderFieldInTeam, + }), +}); + +export const readGameFilterLevelSchema = object({ + query: object({ + ...readGame, + ...levelFieldInTeam, + }), +}); + +export const readGameFilterRegionSchema = object({ + query: object({ + ...readGame, + ...regionFieldInTeam, + }), +}); diff --git a/src/schemas/guest.schema.ts b/src/schemas/guest.schema.ts new file mode 100644 index 0000000..0bf028b --- /dev/null +++ b/src/schemas/guest.schema.ts @@ -0,0 +1,72 @@ +import { TypeOf, object } from "zod"; +import { + teamIdField, + gameTimeField, + descriptionField, + recruitCountField, + categoryField, + dateField, + levelField, + regionFieldInTeam, + genderFieldInTeam, + gameDurationField, +} from "./fields"; + +const body = { + ...gameTimeField, + ...descriptionField, + ...recruitCountField, + ...gameDurationField, +}; + +const createGuestBody = object({ + ...teamIdField, + ...body, +}); + +const updateGuestBody = object({ + ...body, +}); + +export const createGuestingSchema = object({ + body: createGuestBody, +}); + +export const updateGuestingSchema = object({ + body: updateGuestBody, +}); + +const readGuest = { + ...categoryField, + ...dateField, +}; + +export const readGuestSchema = object({ + query: object({ + ...readGuest, + }), +}); + +export const readGuestFilterGenderSchema = object({ + query: object({ + ...readGuest, + ...genderFieldInTeam, + }), +}); + +export const readGuestFilterLevelSchema = object({ + query: object({ + ...readGuest, + ...levelField, + }), +}); + +export const readGuestFilterRegionSchema = object({ + query: object({ + ...readGuest, + ...regionFieldInTeam, + }), +}); + +export type CreateGuestingBody = TypeOf; +export type UpdateGuestingBody = TypeOf; diff --git a/src/schemas/post.schema.ts b/src/schemas/post.schema.ts new file mode 100644 index 0000000..548b2aa --- /dev/null +++ b/src/schemas/post.schema.ts @@ -0,0 +1,16 @@ +import { TypeOf, object } from "zod"; +import { contentFieldInPost, linkField, titleField, rentDateField, rentPlaceField } from "./fields"; + +const body = object({ + ...titleField, + ...contentFieldInPost, + ...linkField, + ...rentDateField, + ...rentPlaceField, +}); + +export const createPostSchema = object({ + body: body, +}); + +export type CreatePostBody = TypeOf; diff --git a/src/schemas/team-review.schema.ts b/src/schemas/team-review.schema.ts new file mode 100644 index 0000000..007cca7 --- /dev/null +++ b/src/schemas/team-review.schema.ts @@ -0,0 +1,15 @@ +import { TypeOf, object } from "zod"; +import { guestMatchIdFieldInTeamReview, mannerScoreField, skillScoreField, teamMatchIdField } from "./fields"; + +const createTeamReviewBody = object({ + ...teamMatchIdField, + ...guestMatchIdFieldInTeamReview, + ...skillScoreField, + ...mannerScoreField, +}); + +export const createTeamReviewSchema = object({ + body: createTeamReviewBody, +}); + +export type CreateTeamReviewBody = TypeOf; diff --git a/src/schemas/team.schema.ts b/src/schemas/team.schema.ts new file mode 100644 index 0000000..fa753c8 --- /dev/null +++ b/src/schemas/team.schema.ts @@ -0,0 +1,64 @@ +import { TypeOf, object } from "zod"; +import { + ageGroupFieldInTeam, + categoryField, + descriptionField, + genderFieldInTeam, + gymNameField, + inviteCodeField, + logoField, + memberIdsToDeleteField, + nameField, + regionFieldInTeam, +} from "./fields"; + +const commonFields = { + ...logoField, + ...nameField, + ...descriptionField, + ...genderFieldInTeam, + ...ageGroupFieldInTeam, + ...regionFieldInTeam, + ...gymNameField, +}; + +const createTeamBody = object({ + ...commonFields, + ...categoryField, +}); + +const updateTeamBody = object({ + ...commonFields, + ...memberIdsToDeleteField, +}); + +const updateTeamBodyWithoutMemberIdsToDelete = object({ + ...commonFields, +}); + +const createMemberBody = object({ + ...inviteCodeField, +}); + +export const createTeamSchema = object({ + body: createTeamBody, +}); + +export const updateTeamSchema = object({ + body: updateTeamBody, +}); + +export const createMemberSchema = object({ + body: createMemberBody, +}); + +export const readTeamPreviewsSchema = object({ + query: object({ + ...categoryField, + }), +}); + +export type CreateTeamBody = TypeOf; +export type UpdateTeamBody = TypeOf; +export type UpdateTeamBodyWithoutMemberIdsToDelete = TypeOf; +export type CreateMemberBody = TypeOf; diff --git a/src/schemas/user-profile.schema.ts b/src/schemas/user-profile.schema.ts new file mode 100644 index 0000000..9677792 --- /dev/null +++ b/src/schemas/user-profile.schema.ts @@ -0,0 +1,46 @@ +import { TypeOf, object } from "zod"; +import { + ageGroupFieldInUser, + categoryField, + descriptionField, + genderFieldInUser, + nicknameField, + positionField, + regionFieldInProfile, + heightField, + avatarUrlField, +} from "./fields"; + +const commonFields = { + ...avatarUrlField, + ...nicknameField, + ...genderFieldInUser, + ...ageGroupFieldInUser, + ...heightField, +}; + +const categoryFields = { + ...descriptionField, + ...regionFieldInProfile, + ...positionField, +}; + +const body = object({ + ...commonFields, + ...categoryFields, +}); + +const commonProfile = object(commonFields); + +const categoryProfile = object(categoryFields); + +export const updateUserProfileSchema = object({ + params: object({ + ...categoryField, + }), + body: body, +}); + +export type UpdateUserProfileBody = TypeOf; +export type CommonProfile = TypeOf; +export type CategoryProfile = TypeOf; diff --git a/src/schemas/user-review.schema.ts b/src/schemas/user-review.schema.ts new file mode 100644 index 0000000..71f2e71 --- /dev/null +++ b/src/schemas/user-review.schema.ts @@ -0,0 +1,15 @@ +import { TypeOf, object } from "zod"; +import { guestMatchIdFieldInUserReview, mannerScoreField, revieweeIdField, skillScoreField } from "./fields"; + +const createUserReviewBody = object({ + ...revieweeIdField, + ...guestMatchIdFieldInUserReview, + ...skillScoreField, + ...mannerScoreField, +}); + +export const createTeamReviewSchema = object({ + body: createUserReviewBody, +}); + +export type CreateUserReviewBody = TypeOf; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..ac8fb14 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,143 @@ +import { Request } from "express"; +import { createOrReadUser, updateRefreshToken, deleteRefreshToken } from "./users.service"; +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { getRefreshToken } from "../daos/user.dao"; +import { + generateAccessToken, + generateRefreshToken, + extractAccessToken, + extractRefreshToken, + verifyAccessToken, + isRefreshTokenValid, +} from "../utils/jwt.util"; +import redaxios from "redaxios"; +import { UserInfo } from "../types/user-info.interface"; +import { Provider } from "../types/provider.enum"; +import { Payload } from "../types/payload.interface"; + +export const tokenType = "Bearer "; + +export const googleLogin = async (body) => { + const accessToken = await getGoogleAccessToken(body.code); + const userInfo = await getGoogleUserInfo(accessToken); + const payload = await createOrReadUser(userInfo); + return login(payload); +}; + +const getGoogleAccessToken = async (code: string) => { + const url = "https://oauth2.googleapis.com/token"; + const response = await redaxios.post( + `${url}?client_id=${process.env.GOOGLE_CLIENT_ID}&client_secret=${process.env.GOOGLE_CLIENT_SECRET}&code=${code}&grant_type=authorization_code&redirect_uri=${process.env.GOOGLE_REDIRECT_URI}`, + ); + return response.data.access_token; +}; + +const getGoogleUserInfo = async (accessToken: string) => { + const url = "https://www.googleapis.com/userinfo/v2/me"; + const response = await redaxios.get(`${url}?access_token=${accessToken}`); + return { + provider: Provider.Google, + providerId: response.data.id, + nickname: response.data.name, + }; +}; + +export const kakaoLogin = async (body) => { + const accessToken = await getKakaoAccessToken(body.code); + const userInfo = await getKakaoUserInfo(accessToken); + const payload = await createOrReadUser(userInfo); + return login(payload); +}; + +const getKakaoAccessToken = async (code: string) => { + const url = "https://kauth.kakao.com/oauth/token"; + const response = await redaxios.post( + `${url}?grant_type=authorization_code&client_id=${process.env.KAKAO_CLIENT_ID}&redirect_uri=${process.env.KAKAO_REDIRECT_URI}&code=${code}`, + { + headers: { + "Content-type": "application/x-www-form-urlencoded;charset=utf-8", + }, + }, + ); + return response.data.access_token; +}; + +const getKakaoUserInfo = async (accessToken: string): Promise => { + const url = "https://kapi.kakao.com/v2/user/me"; + const response = await redaxios.get(url, { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-type": "application/x-www-form-urlencoded;charset=utf-8", + }, + }); + return { + provider: Provider.Kakao, + providerId: response.data.id, + nickname: response.data.kakao_account.profile.nickname, + }; +}; + +export const naverLogin = async (body) => { + const accessToken = await getNaverAccessToken(body.code, body.state); + const userInfo = await getNaverUserInfo(accessToken); + const payload = await createOrReadUser(userInfo); + return login(payload); +}; + +const getNaverAccessToken = async (code: string, state: string) => { + const url = "https://nid.naver.com/oauth2.0/token"; + const response = await redaxios.get(url, { + params: { + grant_type: "authorization_code", + client_id: process.env.NAVER_CLIENT_ID, + client_secret: process.env.NAVER_CLIENT_SECRET, + code: code, + state: state, + }, + }); + return response.data.access_token; +}; + +const getNaverUserInfo = async (accessToken: string): Promise => { + const url = "https://openapi.naver.com/v1/nid/me"; + const response = await redaxios.get(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return { + provider: Provider.Naver, + providerId: response.data.response.id, + nickname: response.data.response.name, + }; +}; + +export const login = async (payload: Payload) => { + const accessToken = generateAccessToken(payload); + const refreshToken = generateRefreshToken(); + await updateRefreshToken(refreshToken, payload.id); + return { tokenType, accessToken, refreshToken }; +}; + +export const generateNewAccessToken = async (req: Request) => { + const accessToken = extractAccessToken(req); + const refreshToken = extractRefreshToken(req); + const verified = verifyAccessToken(accessToken); + if (verified.isExpired) { + if (isRefreshTokenValid(refreshToken) && (await isRefreshTokenMatching(refreshToken, verified.decoded.id))) { + return await login({ id: verified.decoded.id, nickname: verified.decoded.nickname }); + } + throw new BaseError(status.REFRESH_TOKEN_VERIFICATION_FAILED); + } + throw new BaseError(status.ACCESS_TOKEN_NOT_EXPIRED); +}; + +const isRefreshTokenMatching = async (refreshToken, userId: number) => { + return refreshToken === (await getRefreshToken(userId)); +}; + +export const logoutUser = async (userId: number) => { + await deleteRefreshToken(userId); + return; +}; diff --git a/src/services/games.service.ts b/src/services/games.service.ts new file mode 100644 index 0000000..93b2b51 --- /dev/null +++ b/src/services/games.service.ts @@ -0,0 +1,117 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { + findGamesByDate, + findGamesByGender, + findGamesByLevel, + findGamesByRegion, + getGameDetail, + insertGame, + setGame, + insertGameApplication, +} from "../daos/game.dao"; +import { + getTeam, + getTeamDetailForGuesting, + getTeamIdByLeaderId, + getTeamCategoryByLeaderId, + getTeamByLeaderId, +} from "../daos/team.dao"; +import { addMemberCount, findMemberInfoByCategory } from "../daos/member.dao"; +import { getUserInfoByCategory, userInfoAttributes } from "../daos/user.dao"; +import { getGameByUserId } from "../daos/game.dao"; +import { checkApplicationExisting } from "../daos/game-apply.dao"; +import { readGameResponseDTO, readGameDetailResponseDTO } from "../dtos/games.dto"; +import { CreateGameBody, UpdateGameBody } from "../schemas/game.schema"; +import { ApplyGameBody } from "../schemas/game-apply.schema"; + +export const readGamesByDate = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const games = await findGamesByDate(query.date, query.category, cursorId); + await addMemberCount(games.games); + return readGameResponseDTO(games); +}; + +export const readGamesByGender = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const games = await findGamesByGender(query.date, query.category, query.gender, cursorId); + await addMemberCount(games.games); + return readGameResponseDTO(games); +}; + +export const readGamesByLevel = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const games = await findGamesByLevel(query.date, query.category, query.skillLevel, cursorId); + await addMemberCount(games.games); + return readGameResponseDTO(games); +}; + +export const readGamesByRegion = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const games = await findGamesByRegion(query.date, query.category, query.region, cursorId); + await addMemberCount(games.games); + return readGameResponseDTO(games); +}; + +export const readGameDetail = async (params) => { + const gameId = params.gameId; + const gameDetail = await getGameDetail(gameId); + if (!gameDetail) { + throw new BaseError(status.GAME_NOT_FOUND); + } + + const teamDetail = await getTeamDetailForGuesting(gameDetail.hostTeamId); + const leaderInfo = await getUserInfoByCategory(teamDetail.leaderId, teamDetail.category); + const memberInfo = await findMemberInfoByCategory(gameDetail.hostTeamId, teamDetail.category); + return readGameDetailResponseDTO(gameDetail, teamDetail, leaderInfo, memberInfo); +}; + +export const createGame = async (userId, body: CreateGameBody) => { + const hostTeamId = await getTeamIdByLeaderId(userId); + const category = await getTeamCategoryByLeaderId(userId); + + const team = await getTeamByLeaderId(hostTeamId, userId); + if (!team) { + throw new BaseError(status.TEAM_LEADER_NOT_FOUND); + } + + await insertGame(hostTeamId, body, category); + return; +}; + +export const updateGame = async (userId, params, body: UpdateGameBody) => { + const gameId = params.gameId; + const game = await getGameByUserId(gameId, userId); + if (!game) { + throw new BaseError(status.GAME_NOT_FOUND); + } + await setGame(game, body); + return; +}; + +export const addGameApplication = async (userId: number, params, body: ApplyGameBody) => { + const gameId = params.gameId; + const teamId = body.teamId; + + // team does not exist + const team = await getTeam(teamId); + console.log(team); + if (!team) { + throw new BaseError(status.TEAM_NOT_FOUND); + } + + // no team of which the user is a leader + const myTeam = await getTeamByLeaderId(teamId, userId); + if (!myTeam) { + throw new BaseError(status.TEAM_LEADER_NOT_FOUND); + } + + // application already exists + const applicationExisting = await checkApplicationExisting(gameId, teamId); + if (applicationExisting) { + throw new BaseError(status.GAME_APPLICATION_ALREADY_EXIST); + } + + await insertGameApplication(gameId, body); + return; +}; diff --git a/src/services/guests.service.ts b/src/services/guests.service.ts new file mode 100644 index 0000000..dc7891e --- /dev/null +++ b/src/services/guests.service.ts @@ -0,0 +1,132 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { + findGuestAll, + findGuestByGender, + findGuestByLevel, + findGuestByRegion, + getCategoryThroughTeamJoin, + getDetailedGuesting, + getGuestingById, + getTeamByGuestingId, + insertGuesting, + setGuesting, +} from "../daos/guest.dao"; +import { addMemberCount, findMemberInfoByCategory } from "../daos/member.dao"; +import { getTeamByLeaderId, getTeamDetailForGuesting } from "../daos/team.dao"; +import { getUserInfoByCategory, getUserProfileByCategory } from "../daos/user.dao"; +import { readGuestingDetailResponseDTO, readGuestingResponseDTO } from "../dtos/guests.dto"; +import { CreateGuestingBody, UpdateGuestingBody } from "../schemas/guest.schema"; +import { + insertGuestUser, + checkForDuplicateGuestUser, + getGuestUserById, + getGuestIdById, + setGuestUserStatus, +} from "../daos/guest-user.dao"; + +export const createGuesting = async (userId: number, body: CreateGuestingBody) => { + const teamId = body.teamId; + const team = await getTeamByLeaderId(teamId, userId); + if (!team) { + throw new BaseError(status.TEAM_LEADER_NOT_FOUND); + } + await insertGuesting(teamId, body); + return; +}; + +export const updateGuesting = async (userId: number, params, body: UpdateGuestingBody) => { + const guestingId = params.guestingId; + const teamId = await getTeamByGuestingId(guestingId, userId); + const guesting = await getGuestingById(guestingId); + if (!guesting || !teamId) { + throw new BaseError(status.GUEST_NOT_FOUND); + } + await setGuesting(guesting, body); + return; +}; + +export const readGuesting = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const guestings = await findGuestAll(query.date, query.category, cursorId); + await addMemberCount(guestings.guests); + return readGuestingResponseDTO(guestings); +}; + +export const readGuestingByGender = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const guestings = await findGuestByGender(query.date, query.category, query.gender, cursorId); + await addMemberCount(guestings.guests); + return readGuestingResponseDTO(guestings); +}; + +export const readGuestingByLevel = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const guestings = await findGuestByLevel(query.date, query.category, query.level, cursorId); + await addMemberCount(guestings.guests); + return readGuestingResponseDTO(guestings); +}; + +export const readGuestingByRegion = async (query) => { + const cursorId = query.cursorId ? parseInt(query.cursorId) : undefined; + const guestings = await findGuestByRegion(query.date, query.category, query.region, cursorId); + await addMemberCount(guestings.guests); + return readGuestingResponseDTO(guestings); +}; + +export const readDetailedGuesting = async (params) => { + const guestingId = params.guestingId; + const guestingDetail = await getDetailedGuesting(guestingId); + if (!guestingDetail) { + throw new BaseError(status.GUEST_NOT_FOUND); + } + + const teamDetail = await getTeamDetailForGuesting(guestingDetail.teamId); + const leaderInfo = await getUserInfoByCategory(teamDetail.leaderId, teamDetail.category); + const memberInfo = await findMemberInfoByCategory(guestingDetail.teamId, teamDetail.category); + return readGuestingDetailResponseDTO(guestingDetail, teamDetail, leaderInfo, memberInfo); +}; + +export const addGuestUser = async (userId: number, params) => { + const guestingId = params.guestingId; + const category = await getCategoryThroughTeamJoin(guestingId); + + const existingGuestUser = await checkForDuplicateGuestUser(userId, guestingId); + if (existingGuestUser) { + throw new BaseError(status.GUESTUSER_ALREADY_EXIST); + } + + const userProfile = await getUserProfileByCategory(userId, category); + if (!isUserProfileValid(userProfile)) { + throw new BaseError(status.NOT_FILL_USER_PROFILE); + } + + await insertGuestUser(guestingId, userId); + return; +}; + +const isUserProfileValid = (userProfile): boolean => { + return ( + // userProfile["Profiles.description"] && + userProfile.gender && userProfile.ageGroup && userProfile["Profiles.region"] + // userProfile.height && + // userProfile["Profiles.position"] + ); +}; + +export const updateGuestUserStatus = async (params) => { + const guestUserId = params.guestUserId; + const guestUser = await getGuestUserById(guestUserId); + if (!guestUser) { + throw new BaseError(status.GUESTUSER_NOT_FOUND); + } + + const guestId = await getGuestIdById(guestUserId); + const guest = await getGuestingById(guestId); + if (guest.status) { + throw new BaseError(status.CLOSED_GUEST); + } + + await setGuestUserStatus(guestUser, guest); + return; +}; diff --git a/src/services/matchings.service.ts b/src/services/matchings.service.ts new file mode 100644 index 0000000..fcfedd3 --- /dev/null +++ b/src/services/matchings.service.ts @@ -0,0 +1,47 @@ +import { findGameByHostTeamsAndGameTime, findGameByOpposingTeamsAndGameTime } from "../daos/game.dao"; +import { getApplyGuestingUser } from "../daos/guest-user.dao"; +import { findGuestingByTeamsAndGameTime, findGuestingByUserAndGameTime } from "../daos/guest.dao"; +import { getTeamsAppliedById } from "../daos/matching.dao"; +import { getMemberCountByTeamId, addMemberCount } from "../daos/member.dao"; +import { findTeamIdByLeaderId } from "../daos/team.dao"; +import { + readApplyGuestingUserResponseDTO, + readHostingApplicantsTeamResponseDTO, + readMatchingResponseDTO, +} from "../dtos/matchings.dto"; + +export const readMatchingGuesting = async (userId, query) => { + const gameTime = query.date; + const matchingGuestings = await findGuestingByUserAndGameTime(userId, query.date); + const teamIds = await findTeamIdByLeaderId(userId); + const matchingGames = await findGameByOpposingTeamsAndGameTime(teamIds, gameTime); + await addMemberCount(matchingGuestings); + await addMemberCount(matchingGames); + return readMatchingResponseDTO(matchingGuestings, matchingGames); +}; + +export const readMatchingHosting = async (userId: number, query) => { + const gameTime = query.date; + const teamIds = await findTeamIdByLeaderId(userId); + const matchingGuestings = await findGuestingByTeamsAndGameTime(teamIds, gameTime); + const matchingGames = await findGameByHostTeamsAndGameTime(teamIds, gameTime); + await addMemberCount(matchingGuestings); + await addMemberCount(matchingGames); + return readMatchingResponseDTO(matchingGuestings, matchingGames); +}; + +export const readApplyGuestingUser = async (params) => { + const guestingId = params.guestingId; + const applyGuestingUser = await getApplyGuestingUser(guestingId); + return readApplyGuestingUserResponseDTO(applyGuestingUser); +}; + +export const readHostingApplicantsTeamList = async (userId, params) => { + const gameId = params.gameId; + const teamsApplied = await getTeamsAppliedById(gameId); + for (const team of teamsApplied) { + team.memberCount = await getMemberCountByTeamId(team); + } + + return readHostingApplicantsTeamResponseDTO(teamsApplied); +}; diff --git a/src/services/members.service.ts b/src/services/members.service.ts new file mode 100644 index 0000000..f5defb6 --- /dev/null +++ b/src/services/members.service.ts @@ -0,0 +1,16 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { insertMember, isMemberExist } from "../daos/member.dao"; +import { getTeamIdByInviteCode } from "../daos/team.dao"; + +export const createMember = async (userId, body) => { + const teamId = await getTeamIdByInviteCode(body.inviteCode); + if (!teamId) { + throw new BaseError(status.NO_JOINABLE_TEAM); + } + if (await isMemberExist(teamId, userId)) { + throw new BaseError(status.ALREADY_JOINED); + } + await insertMember(teamId, userId); + return; +}; diff --git a/src/services/posts.service.ts b/src/services/posts.service.ts new file mode 100644 index 0000000..7c9df29 --- /dev/null +++ b/src/services/posts.service.ts @@ -0,0 +1,90 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { PostType } from "../types/post-type.enum"; +import { CreateCommentBody } from "../schemas/comment.schema"; +import { CreatePostBody } from "../schemas/post.schema"; +import { getBookmark, insertOrDeleteBookmark } from "../daos/bookmark.dao"; +import { findComment, getCommentCount, insertComment } from "../daos/comment.dao"; +import { findImage } from "../daos/image.dao"; +import { findPostByType, findPostByAuthorId, findBookmarkedPost, getPost, insertPost } from "../daos/post.dao"; +import { + readCommentsResonseDTO, + readPostResponseDTO, + readPostsResponseDTO, + readRentPostsResponseDTO, +} from "../dtos/posts.dto"; + +export const readCommunityPosts = async (userId: number | undefined, query) => { + const result = await findPostByType(userId, query.date, query.cursorId, PostType.Community); + return readPostsResponseDTO(result); +}; + +export const readMyPosts = async (userId: number, query) => { + const result = await findPostByAuthorId(userId, query.cursorId); + return readPostsResponseDTO(result); +}; + +export const readBookmarkedPosts = async (userId: number, query) => { + const result = await findBookmarkedPost(userId, query.cursorId); + return readPostsResponseDTO(result); +}; + +export const createOrDeleteBookmark = async (userId: number, params) => { + await insertOrDeleteBookmark(userId, params.postId); + return; +}; + +export const readPost = async (userId: number, params) => { + const postId = params.postId; + const post = handlePostNotFound(await getPost(postId)); + const imageUrls = await findImage(postId); + const commentCount = await getCommentCount(postId); + const comments = await findComment(postId, undefined); + const isBookmarked = await checkIsBookmarked(userId, postId); + return readPostResponseDTO(post, imageUrls, commentCount, comments, isBookmarked); +}; + +const checkIsBookmarked = async (userId: number | undefined, postId: number) => { + if (!userId) { + return null; + } + return Boolean(await getBookmark(userId, postId)); +}; + +export const createCommunityPost = async (userId: number, body: CreatePostBody) => { + await insertPost(userId, body, PostType.Community); + return; +}; + +export const createComment = async (userId: number, params, body: CreateCommentBody) => { + const postId: number = params.postId; + handlePostNotFound(await getPost(postId)); + await insertComment(userId, postId, body); + return; +}; + +const handlePostNotFound = (post) => { + if (!post) { + throw new BaseError(status.POST_NOT_FOUND); + } + return post; +}; + +export const readComments = async (params, query) => { + const cursorId = query.cursorId; + if (isNaN(Number(cursorId))) { + throw new BaseError(status.REQUEST_VALIDATION_ERROR); + } + const comments = await findComment(params.postId, cursorId); + return readCommentsResonseDTO(comments); +}; + +export const createRentPost = async (userId: number, body: CreatePostBody) => { + await insertPost(userId, body, PostType.RentalInfo); + return; +}; + +export const readRentPosts = async (userId: number | undefined, query) => { + const result = await findPostByType(userId, query.date, query.cursorId, PostType.RentalInfo); + return readRentPostsResponseDTO(result); +}; diff --git a/src/services/reviews.service.ts b/src/services/reviews.service.ts new file mode 100644 index 0000000..4ce022a --- /dev/null +++ b/src/services/reviews.service.ts @@ -0,0 +1,89 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { CreateTeamReviewBody } from "../schemas/team-review.schema"; +import { getGame } from "../daos/game.dao"; +import { findLeaderId, getLeaderId } from "../daos/team.dao"; +import { getGuestingByAcceptedUserId } from "../daos/guest.dao"; +import { getExistingTeamReview, insertTeamReview } from "../daos/team-review.dao"; +import { CreateUserReviewBody } from "../schemas/user-review.schema"; +import { getExistingUserReview, insertUserReview } from "../daos/user-review.dao"; + +export const createTeamReview = async (userId: number, body: CreateTeamReviewBody) => { + const { teamMatchId, guestMatchId } = body; + if (!(teamMatchId || guestMatchId) || (teamMatchId && guestMatchId)) { + throw new BaseError(status.MATCH_ID_REQUIRED); + } + + const result = await retrieveReviewedTeamIdAndGameTime(userId, teamMatchId, guestMatchId); + + //validate user write permission + if (!result?.reviewedTeamId) { + throw new BaseError(status.NO_REVIEW_TARGET); + } + validateReviewableTime(result.gameTime, getCurrentTime()); + const review = await getExistingTeamReview(userId, result.reviewedTeamId, teamMatchId, guestMatchId); + if (review) { + throw new BaseError(status.REVIEW_ALREADY_WRITTEN); + } + + await insertTeamReview(userId, result.reviewedTeamId, body); + return; +}; + +const retrieveReviewedTeamIdAndGameTime = async (userId: number, teamMatchId?: number, guestMatchId?: number) => { + if (teamMatchId) { + const teamMatch = await getGame(teamMatchId); + const teams = await findLeaderId(teamMatch.hostTeamId, teamMatch.opposingTeamId); + for (const team of teams) { + if (team.leaderId == userId) { + return { reviewedTeamId: team.id, gameTime: teamMatch.gameTime }; + } + } + } + if (guestMatchId) { + const guestMatch = await getGuestingByAcceptedUserId(guestMatchId, userId); + return { reviewedTeamId: guestMatch?.teamId, gameTime: guestMatch?.gameTime }; + } +}; + +export const createUserReview = async (userId: number, body: CreateUserReviewBody) => { + const { guestMatchId, revieweeId } = body; + const result = await retrieveRevieweeIdAndGameTime(userId, guestMatchId, revieweeId); + + //validate user write permission + if (!result.revieweeId) { + throw new BaseError(status.NO_REVIEW_TARGET); + } + validateReviewableTime(result.gameTime, getCurrentTime()); + const review = await getExistingUserReview(userId, result.revieweeId, guestMatchId); + if (review) { + throw new BaseError(status.REVIEW_ALREADY_WRITTEN); + } + + await insertUserReview(userId, body); + return; +}; + +const retrieveRevieweeIdAndGameTime = async (userId: number, guestMatchId: number, revieweeId: number) => { + const guestMatch = await getGuestingByAcceptedUserId(guestMatchId, revieweeId); + if (!guestMatch) { + return { revieweeId: null, gameTime: null }; + } + const leaderId = await getLeaderId(guestMatch.teamId); + return { + revieweeId: userId === leaderId ? revieweeId : null, + gameTime: guestMatch.gameTime, + }; +}; + +export const validateReviewableTime = (gameTime: Date, currentTime: Date) => { + const timeDifference = currentTime.getTime() - gameTime.getTime(); + const oneMonthInMilliseconds = 30 * 24 * 60 * 60 * 1000; + if (timeDifference < 0 && timeDifference > oneMonthInMilliseconds) { + throw new BaseError(status.REVIEW_NOT_CURRENTLY_WRITABLE); + } +}; + +const getCurrentTime = (): Date => { + return new Date(); +}; diff --git a/src/services/teams.service.ts b/src/services/teams.service.ts new file mode 100644 index 0000000..d9452c5 --- /dev/null +++ b/src/services/teams.service.ts @@ -0,0 +1,67 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { v4 as uuidv4 } from "uuid"; +import { CreateTeamBody, UpdateTeamBody } from "../schemas/team.schema"; +import { + findTeamPreviewByCategory, + getTeamByLeaderId, + getTeamDetail, + insertTeam, + setTeam, + findTeamPreviewByCategoryForLeader, +} from "../daos/team.dao"; +import { deleteMembers, findMemberInfoByCategory, findMemberToDelete } from "../daos/member.dao"; +import { getUserInfoByCategory } from "../daos/user.dao"; +import { updateOpposingTeam } from "../daos/game.dao"; +import { readTeamDetailResponseDTO } from "../dtos/teams.dto"; + +export const readTeamPreviews = async (userId: number, query) => { + return await findTeamPreviewByCategory(userId, query.category); +}; + +export const createTeam = async (userId: number, body: CreateTeamBody) => { + await insertTeam(body, userId, uuidv4()); + return; +}; + +export const updateTeam = async (userId: number, params, body: UpdateTeamBody) => { + const teamId = params.teamId; + const team = await getTeamByLeaderId(params.teamId, userId); + if (!team) { + throw new BaseError(status.TEAM_NOT_FOUND); + } + + const { memberIdsToDelete, ...bodyWithoutMemberIdsToDelete } = body; + if (memberIdsToDelete !== undefined) { + const members = await findMemberToDelete(memberIdsToDelete, teamId); + if (members.length !== memberIdsToDelete?.length) { + throw new BaseError(status.MEMBER_NOT_FOUND); + } + await deleteMembers(members); + } + await setTeam(team, bodyWithoutMemberIdsToDelete); + return; +}; + +export const readTeamDetail = async (userId: number, params) => { + const teamId = params.teamId; + const detail = await getTeamDetail(teamId); + if (!detail) { + throw new BaseError(status.TEAM_NOT_FOUND); + } + const leaderInfo = await getUserInfoByCategory(detail.leaderId, detail.category); + const membersInfo = await findMemberInfoByCategory(teamId, detail.category); + return readTeamDetailResponseDTO(detail, leaderInfo, membersInfo, userId == detail.leaderId); +}; + +export const readTeamAvailPreviewById = async (userId, query) => { + return await findTeamPreviewByCategoryForLeader(userId, query.category); +}; + +export const addOpposingTeam = async (userId: number, params, body) => { + const gameId = params.gameId; + const opposingTeamId = body.teamId; + + await updateOpposingTeam(gameId, opposingTeamId); + return; +}; diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..8b9082c --- /dev/null +++ b/src/services/users.service.ts @@ -0,0 +1,61 @@ +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { Category } from "../types/category.enum"; +import { Payload } from "../types/payload.interface"; +import { UserInfo } from "../types/user-info.interface"; +import { UpdateUserProfileBody, CategoryProfile } from "../schemas/user-profile.schema"; +import { + getUser, + getUserByProviderId, + getUserProfileByCategory, + insertUser, + setCommonProfile, + setRefreshToken, +} from "../daos/user.dao"; +import { getUserProfile, insertCategoryProfile, setCategoryProfile } from "../daos/profile.dao"; +import { readUserProfileResponseDTO } from "../dtos/users.dto"; + +export const createOrReadUser = async (userInfo: UserInfo): Promise => { + let user = await getUserByProviderId(userInfo.provider, userInfo.providerId); + if (!user) { + user = await insertUser(userInfo.provider, userInfo.providerId, userInfo.nickname); + } + return { id: user.id, nickname: user.nickname }; +}; + +export const updateRefreshToken = async (refreshToken: string, userId: number) => { + await setRefreshToken(refreshToken, userId); +}; + +export const readUserProfile = async (params) => { + const profile = await getUserProfileByCategory(params.userId, params.category); + if (!profile) { + throw new BaseError(status.USER_NOT_FOUND); + } + return readUserProfileResponseDTO(profile); +}; + +export const deleteRefreshToken = async (userId: number) => { + await setRefreshToken(null, userId); +}; + +export const updateUserProfile = async (userId, params, body: UpdateUserProfileBody) => { + const user = await getUser(userId); + if (!user) { + throw new BaseError(status.USER_NOT_FOUND); + } + + const { description, region, position, ...commonProfile } = body; + await setCommonProfile(userId, commonProfile); + await createOrUpdateCategoryProfile(userId, params.category, { description, region, position }); + return; +}; + +const createOrUpdateCategoryProfile = async (userId: number, category: Category, categoryProfile: CategoryProfile) => { + const profile = await getUserProfile(userId, category); + if (!profile) { + await insertCategoryProfile(userId, category, categoryProfile); + } else { + await setCategoryProfile(profile.id, categoryProfile); + } +}; diff --git a/src/types/age-group.enum.ts b/src/types/age-group.enum.ts new file mode 100644 index 0000000..26a4a29 --- /dev/null +++ b/src/types/age-group.enum.ts @@ -0,0 +1,7 @@ +export enum AgeGroup { + Teenagers = "-10", + Twenties = "20-29", + Thirties = "30-39", + Forties = "40-49", + FiftiesAndAbove = "50-", +} diff --git a/src/types/category.enum.ts b/src/types/category.enum.ts new file mode 100644 index 0000000..2544aa4 --- /dev/null +++ b/src/types/category.enum.ts @@ -0,0 +1,11 @@ +export enum Category { + Basketball = "basketball", + Baseball = "baseball", + Tennis = "tennis", + Soccer = "soccer", + Futsal = "futsal", + Volleyball = "volleyball", + Bowling = "bowling", + Badminton = "badminton", + TableTennis = "table-tennis", +} diff --git a/src/types/decoded.interface.ts b/src/types/decoded.interface.ts new file mode 100644 index 0000000..fb17d84 --- /dev/null +++ b/src/types/decoded.interface.ts @@ -0,0 +1,6 @@ +import { Payload } from "./payload.interface"; + +export interface Decoded extends Payload { + iat: number; + exp: number; +} diff --git a/src/types/gender.enum.ts b/src/types/gender.enum.ts new file mode 100644 index 0000000..ef747d7 --- /dev/null +++ b/src/types/gender.enum.ts @@ -0,0 +1,5 @@ +export enum Gender { + Female = "F", + Male = "M", + Mixed = "MX", +} diff --git a/src/types/match-type.enum.ts b/src/types/match-type.enum.ts new file mode 100644 index 0000000..25e5aac --- /dev/null +++ b/src/types/match-type.enum.ts @@ -0,0 +1,4 @@ +export enum MatchType { + guest = "guest", + game = "game", +} diff --git a/src/types/payload.interface.ts b/src/types/payload.interface.ts new file mode 100644 index 0000000..5d5b907 --- /dev/null +++ b/src/types/payload.interface.ts @@ -0,0 +1,4 @@ +export interface Payload { + id: number; + nickname: string; +} diff --git a/src/types/post-type.enum.ts b/src/types/post-type.enum.ts new file mode 100644 index 0000000..e3f9676 --- /dev/null +++ b/src/types/post-type.enum.ts @@ -0,0 +1,4 @@ +export enum PostType { + RentalInfo = 1, + Community = 2, +} diff --git a/src/types/provider.enum.ts b/src/types/provider.enum.ts new file mode 100644 index 0000000..3894318 --- /dev/null +++ b/src/types/provider.enum.ts @@ -0,0 +1,5 @@ +export enum Provider { + Google = "google", + Kakao = "kakao", + Naver = "naver", +} diff --git a/src/types/user-info.interface.ts b/src/types/user-info.interface.ts new file mode 100644 index 0000000..379b5cd --- /dev/null +++ b/src/types/user-info.interface.ts @@ -0,0 +1,7 @@ +import { Provider } from "./provider.enum"; + +export interface UserInfo { + provider: Provider; + providerId: string; + nickname: string; +} diff --git a/src/types/verified.interface.ts b/src/types/verified.interface.ts new file mode 100644 index 0000000..e812229 --- /dev/null +++ b/src/types/verified.interface.ts @@ -0,0 +1,6 @@ +import { Decoded } from "./decoded.interface"; + +export interface Verified { + isExpired: boolean; + decoded: Decoded; +} diff --git a/src/utils/jwt.util.ts b/src/utils/jwt.util.ts new file mode 100644 index 0000000..b293df2 --- /dev/null +++ b/src/utils/jwt.util.ts @@ -0,0 +1,63 @@ +import jwt from "jsonwebtoken"; +import { tokenType } from "../services/auth.service"; +import { BaseError } from "../config/error"; +import { status } from "../config/response.status"; +import { Request } from "express"; +import { Payload } from "../types/payload.interface"; +import { Verified } from "../types/verified.interface"; +import { Decoded } from "../types/decoded.interface"; + +export const generateAccessToken = (payload: Payload): string => { + return jwt.sign(payload, process.env.JWT_ACCESS_SECRET!, { expiresIn: "15m" }); +}; + +export const generateRefreshToken = (): string => { + return jwt.sign({}, process.env.JWT_REFRESH_SECRET!, { expiresIn: "12h" }); +}; + +export const extractAccessToken = (req: Request): string => { + const accessToken = extractAccessTokenFromHeader(req); + if (!accessToken) { + throw new BaseError(status.MISSING_ACCESS_TOKEN); + } + return accessToken; +}; + +export const extractAccessTokenFromHeader = (req: Request): string | undefined => { + return req.headers.authorization?.split(tokenType)[1]; +}; + +export const extractRefreshToken = (req: Request) => { + const refreshToken = req.headers.refresh; + if (!refreshToken) { + throw new BaseError(status.MISSING_REFRESH_TOKEN); + } + return refreshToken; +}; + +export const verifyAccessToken = (accessToken: string): Verified => { + try { + return { + isExpired: false, + decoded: jwt.verify(accessToken, process.env.JWT_ACCESS_SECRET!) as Decoded, + }; + } catch (err) { + if (err instanceof jwt.TokenExpiredError) { + return { + isExpired: true, + decoded: jwt.decode(accessToken) as Decoded, + }; + } + throw new BaseError(status.ACCESS_TOKEN_VERIFICATION_FAILED); + } +}; + +export const isRefreshTokenValid = (refreshToken) => { + try { + jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!); + return true; + } catch (err) { + console.log(err); + return false; + } +}; diff --git a/src/utils/paging.util.ts b/src/utils/paging.util.ts new file mode 100644 index 0000000..a8ac293 --- /dev/null +++ b/src/utils/paging.util.ts @@ -0,0 +1,6 @@ +import { Op } from "sequelize"; + +export const generateCursorCondition = (cursorId: number | undefined) => + cursorId ? { id: { [Op.lt]: cursorId } } : {}; + +export const calculateHasNext = (items: Array, limit: number) => items.length === limit; diff --git a/tsconfig.json b/tsconfig.json index ed434fb..ed39ce7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,7 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node"] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ @@ -106,6 +106,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*", "config/**/*.ts", "src/models/**/*.ts"], + "include": ["src/**/*", "src/config/**/*.ts"], "exclude": ["node_modules"] }