From 8f843188b289d704742e506b12353a7917f40b5d Mon Sep 17 00:00:00 2001 From: mingrammer <mingrammer@gmail.com> Date: Thu, 28 Nov 2024 14:57:58 +0900 Subject: [PATCH] feat: implement user APIs --- docker-compose.yml | 6 +- package-lock.json | 407 ++++++++++++++++++ package.json | 7 + webapp/backend/apiserver/app.js | 48 ++- .../apiserver/controllers/userController.js | 282 ++++++++++++ webapp/backend/apiserver/models/Profile.js | 62 +++ webapp/backend/apiserver/models/User.js | 73 ++-- webapp/backend/apiserver/package-lock.json | 152 +++++++ webapp/backend/apiserver/package.json | 2 + webapp/backend/apiserver/routes/user.js | 17 + .../backend/apiserver/services/userService.js | 0 webapp/backend/config/account.js | 5 + webapp/backend/ddl/create-tables.sql | 179 ++++---- 13 files changed, 1102 insertions(+), 138 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 webapp/backend/apiserver/controllers/userController.js create mode 100644 webapp/backend/apiserver/models/Profile.js create mode 100644 webapp/backend/apiserver/routes/user.js create mode 100644 webapp/backend/apiserver/services/userService.js create mode 100644 webapp/backend/config/account.js diff --git a/docker-compose.yml b/docker-compose.yml index 99c668c..0ff9d65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,10 @@ services: MYSQL_PASSWORD: localadmin ports: - "8180:8080" + volumes: + - ./webapp/backend:/app depends_on: - mysql - command: sh -c "until nc -z mysql 3306; do echo Waiting for MySQL; sleep 2; done && node apiserver/app.js" \ No newline at end of file + command: sh -c "until nc -z mysql 3306; do echo Waiting for MySQL; sleep 2; done && npx nodemon -L apiserver/app.js" + + # TODO(minjae): add Redis, Object Storage, etc. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e5749b4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,407 @@ +{ + "name": "crewup", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "argon2": "^0.41.1", + "jsonwebtoken": "^9.0.2", + "sequelize": "^6.37.5" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, + "node_modules/argon2": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz", + "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "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/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.46", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", + "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.2.tgz", + "integrity": "sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize": { + "version": "6.37.5", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.5.tgz", + "integrity": "sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b632a2 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "argon2": "^0.41.1", + "jsonwebtoken": "^9.0.2", + "sequelize": "^6.37.5" + } +} diff --git a/webapp/backend/apiserver/app.js b/webapp/backend/apiserver/app.js index 496bdf2..431855e 100644 --- a/webapp/backend/apiserver/app.js +++ b/webapp/backend/apiserver/app.js @@ -13,6 +13,7 @@ const indexRouter = require('./routes/index'); const crewRouter = require('./routes/crew'); const eventRouter = require('./routes/event'); const regionRouter = require('./routes/region'); +const userRouter = require('./routes/user'); const app = express(); @@ -23,48 +24,49 @@ app.use(cors()); // CORS 허용 // **테이블 동기화** (async () => { - try { - await sequelize.sync({ alter: true }); // alter를 사용하면 기존 테이블 구조를 업데이트 - console.log('✅ Database synchronized successfully.'); - } catch (err) { - console.error('❌ Error synchronizing database:', err); - process.exit(1); // 동기화 실패 시 프로세스 종료 - } + try { + await sequelize.sync({alter: true}); // alter를 사용하면 기존 테이블 구조를 업데이트 + console.log('✅ Database synchronized successfully.'); + } catch (err) { + console.error('❌ Error synchronizing database:', err); + process.exit(1); // 동기화 실패 시 프로세스 종료 + } })(); // **데이터베이스 연결 확인** sequelize - .authenticate() - .then(() => { - console.log('✅ Database connected successfully.'); - }) - .catch((err) => { - console.error('❌ Unable to connect to the database:', err.message); - process.exit(1); // 데이터베이스 연결 실패 시 프로세스 종료 - }); + .authenticate() + .then(() => { + console.log('✅ Database connected successfully.'); + }) + .catch((err) => { + console.error('❌ Unable to connect to the database:', err.message); + process.exit(1); // 데이터베이스 연결 실패 시 프로세스 종료 + }); // **라우터 설정** app.use('/', indexRouter); // 기본 라우터 app.use('/api/crews', crewRouter); // 크루 관련 API app.use('/api/events', eventRouter); // 이벤트 관련 API app.use('/api/regions', regionRouter); // 지역 관련 API +app.use('/api', userRouter); // 사용자 관련 API // **에러 핸들링** app.use((err, req, res, next) => { - console.error(err.stack); // 에러 로그 기록 - res.status(err.status || 500).json({ error: err.message || '서버 오류가 발생했습니다.' }); + console.error(err.stack); // 에러 로그 기록 + res.status(err.status || 500).json({error: err.message || '서버 오류가 발생했습니다.'}); }); // **서버 실행** app.listen(port, () => { - console.log(`✅ Server is running on http://localhost:${port}`); + console.log(`✅ Server is running on http://localhost:${port}`); }); // **프로세스 종료 핸들러 추가** process.on('SIGINT', () => { - console.log('\n❌ Server shutting down...'); - sequelize.close().then(() => { - console.log('✅ Database connection closed.'); - process.exit(0); - }); + console.log('\n❌ Server shutting down...'); + sequelize.close().then(() => { + console.log('✅ Database connection closed.'); + process.exit(0); + }); }); diff --git a/webapp/backend/apiserver/controllers/userController.js b/webapp/backend/apiserver/controllers/userController.js new file mode 100644 index 0000000..9f4d5a3 --- /dev/null +++ b/webapp/backend/apiserver/controllers/userController.js @@ -0,0 +1,282 @@ +const argon2 = require('argon2'); +const jwt = require('jsonwebtoken'); + +const AccountConfig = require('../../config/account'); +const sequelize = require('../../config/database'); +const User = require('../models/User'); +const Profile = require('../models/Profile'); + +const authTokenExpiryTime = '1h'; + +async function hashPassword(password) { + return await argon2.hash(password, { + type: argon2.argon2id, + }); +} + +async function verifyPassword(hash, plainPassword) { + return await argon2.verify(hash, plainPassword); +} + +// JWT 생성 +function generateToken(payload) { + return jwt.sign( + payload, + AccountConfig.JWT_SECRET_KEY, + { + expiresIn: authTokenExpiryTime, + }, + ); +} + +exports.signUp = async (req, res) => { + const {name, email, password} = req.body; + if (!name) { + return res.status(400).json({error: '이름을 입력하세요.'}); + } + + if (!email) { + return res.status(400).json({error: '이메일을 입력하세요.'}); + } + + if (!password) { + return res.status(400).json({error: '비밀번호를 입력하세요.'}); + } + + // Check if the user already exists + const user = await User.findOne({ + where: {email: email}, + }); + + if (user) { + return res.status(409).json({error: '이미 등록된 사용자입니다.'}); + } + + const profile = req.body.profile || {}; + + if (!profile.regionID) { + return res.status(400).json({error: '지역을 입력하세요.'}); + } + + if (!profile.sportTypeID) { + return res.status(400).json({error: '운동 종목을 입력하세요.'}); + } + + try { + const tx = await sequelize.transaction(); + + try { + const user = await User.create({ + name, + email, + password: await hashPassword(password), + }, {transaction: tx}); + + const userProfile = await Profile.create({ + // TODO: Save the image to storage and save the URL to the database + // profileImage: profile.profileImage, + regionID: profile.regionID, + job: profile.job || null, + birthDate: profile.birthDate || null, + experience: profile.experience || null, + introduction: profile.introduction || null, + sportTypeID: profile.sportTypeID, + userID: user.userID, + }, {transaction: tx}); + + await tx.commit(); + + // 생성된 사용자 정보 반환 + res.status(201).json({ + userID: user.userID, + name: user.name, + email: user.email, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + profile: userProfile, + }); + } catch (error) { + await tx.rollback(); + console.error('사용자 생성 중 오류:', error); + res.status(500).json({error: '사용자 생성 중 오류가 발생생했습니다.'}); + } + } catch (error) { + console.error('사용자 생성 중 오류:', error); + res.status(500).json({error: '사용자 생성 중 오류가 발생했습니다.'}); + } +} + +exports.verifyEmail = async (req, res) => { + const {email} = req.body; + + if (!email) { + return res.status(400).json({error: '이메일을 입력하세요.'}); + } + + const user = await User.findOne({ + where: {email: email}, + }); + + if (user) { + return res.status(409).json({ + error: '이미 등록된 사용자입니다.' + }); + } + + res.status(200).json({ + message: '사용 가능한 이메일입니다.' + }); +} + +exports.login = async (req, res) => { + try { + const {email, password} = req.body; + + if (!email) { + return res.status(400).json({error: '이메일을 입력하세요.'}); + } + + if (!password) { + return res.status(400).json({error: '비밀번호를 입력하세요.'}); + } + + // 이메일로 사용자 조회 + const user = await User.findOne({ + where: {email: email}, + }); + + // 사용자가 없을 경우 + if (!user) { + return res.status(404).json({error: '사용자를 찾을 수 없습니다.'}); + } + + // 비밀번호 검증 + const isPasswordMatch = await verifyPassword(user.password, password); + + // 비밀번호가 일치하지 않을 경우 + if (!isPasswordMatch) { + return res.status(401).json({error: '비밀번호가 일치하지 않습니다.'}); + } + + // JWT 토큰 생성 + const token = await generateToken({ + userID: user.userID, + email: user.email, + }); + + // 토큰 반환 + res.status(200).json({ + "auth_token": token, + }); + } catch (error) { + console.error('로그인 중 오류:', error); + res.status(500).json({error: '로그인 중 오류가 발생했습니다.'}); + } +} + +exports.getUserById = async (req, res) => { + // TODO: verify the user by auth token with middleware. #14 + + try { + const {id} = req.params; + const user = await User.findByPk(id); + + if (!user) { + return res.status(404).json({error: '사용자를 찾을 수 없습니다.'}); + } + + const userProfile = await user.getProfile(); + + res.status(200).json({ + userID: user.userID, + name: user.name, + email: user.email, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + profile: userProfile, + }); + } catch (error) { + console.error('사용자 조회 중 오류:', error); + res.status(500).json({error: '사용자 조회 중 오류가 발생했습니다.'}); + } +} + + +exports.updateUser = async (req, res) => { + const tx = await sequelize.transaction(); + + const {id} = req.params; + const {password, profile} = req.body; + + try { + const user = await User.findByPk(id, {transaction: tx}); + + if (!user) { + await tx.rollback(); + return res.status(404).json({error: '사용자를 찾을 수 없습니다.'}); + } + + if (password) user.password = await hashPassword(password); + + await user.save({transaction: tx}); + + let userProfile; + if (profile) { + userProfile = await user.getProfile({transaction: tx}); + if (userProfile) { + // TODO: Save the image to storage and save the URL to the database + // if (profile.profileImage) userProfile.profileImage = profile.profileImage; + if (profile.regionID) userProfile.regionID = profile.regionID; + if (profile.job) userProfile.job = profile.job; + if (profile.birthDate) userProfile.birthDate = profile.birthDate; + if (profile.experience) userProfile.experience = profile.experience; + if (profile.introduction) userProfile.introduction = profile.introduction; + if (profile.sportTypeID) userProfile.sportTypeID = profile.sportTypeID; + + await userProfile.save({transaction: tx}); + } + } + await tx.commit(); + + res.status(200).json({ + userID: user.userID, + name: user.name, + email: user.email, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + profile: userProfile, + }); + } catch (error) { + await tx.rollback(); + console.error('사용자 업데이트 중 오류:', error); + res.status(500).json({error: '사용자 업데이트 중 오류가 발생했습니다.'}); + } +} + +exports.deleteUser = async (req, res) => { + const {id} = req.params; + + const tx = await sequelize.transaction(); + + try { + const user = await User.findByPk(id, {transaction: tx}); + + if (!user) { + await tx.rollback(); + return res.status(404).json({error: '사용자를 찾을 수 없습니다.'}); + } + + const userProfile = await user.getProfile({transaction: tx}); + if (userProfile) { + await userProfile.destroy({transaction: tx}); + } + + await user.destroy({transaction: tx}); + await tx.commit(); + res.status(204).json({message: '사용자가 삭제되었습니다.'}); + } catch (error) { + await tx.rollback(); + console.error('사용자 삭제 중 오류:', error); + res.status(500).json({error: '사용자 삭제 중 오류가 발생했습니다.'}); + } +} diff --git a/webapp/backend/apiserver/models/Profile.js b/webapp/backend/apiserver/models/Profile.js new file mode 100644 index 0000000..7a1c659 --- /dev/null +++ b/webapp/backend/apiserver/models/Profile.js @@ -0,0 +1,62 @@ +const {DataTypes} = require('sequelize'); +const sequelize = require('../../config/database'); +const User = require('./User'); + +const Profile = sequelize.define('Profile', { + profileID: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, // 프로필 ID 자동 증가 + }, + userID: { + type: DataTypes.INTEGER, + allowNull: false, + }, + profileImage: { + type: DataTypes.STRING(255), + allowNull: true, + }, + regionID: { + type: DataTypes.INTEGER, + allowNull: false, + }, + job: { + type: DataTypes.STRING(50), + allowNull: true, + }, + birthDate: { + type: DataTypes.DATE, + allowNull: true, + }, + experience: { + type: DataTypes.STRING(100), + allowNull: true, + }, + introduction: { + type: DataTypes.STRING(70), + allowNull: true, + }, + sportTypeID: { + type: DataTypes.INTEGER, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, +}, { + tableName: 'Profile', // MySQL 테이블 이름 매핑 +}); + +Profile.belongsTo(User, {foreignKey: 'userID'}); +User.hasOne(Profile, {foreignKey: 'userID'}); + +module.exports = Profile; + diff --git a/webapp/backend/apiserver/models/User.js b/webapp/backend/apiserver/models/User.js index e555971..dbb0ecf 100644 --- a/webapp/backend/apiserver/models/User.js +++ b/webapp/backend/apiserver/models/User.js @@ -1,39 +1,48 @@ -const { DataTypes } = require('sequelize'); -const sequelize = require('../../config/database'); // config 폴더는 apiserver의 상위 폴더에 위치 +const {DataTypes} = require('sequelize'); +const sequelize = require('../../config/database'); const User = sequelize.define('User', { - userID: { - type: DataTypes.INTEGER, - primaryKey: true, - allowNull: false, - autoIncrement: true, // 사용자 ID 자동 증가 - }, - name: { - type: DataTypes.STRING(100), - allowNull: false, - }, - email: { - type: DataTypes.STRING(100), - allowNull: false, - unique: true, // 이메일 고유 제약 조건 추가 - validate: { - isEmail: true, // 유효한 이메일 형식 검증 + userID: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, // 사용자 ID 자동 증가 }, - }, - password: { - type: DataTypes.STRING(100), - allowNull: false, - }, -}, { - tableName: 'User', // MySQL 테이블 이름 매핑 - timestamps: false, // createdAt, updatedAt 비활성화 - indexes: [ - { - name: 'idx_user_email', // 인덱스 이름 - unique: true, - fields: ['email'], // 이메일 컬럼에 고유 인덱스 추가 + name: { + type: DataTypes.STRING(100), + allowNull: false, + }, + email: { + type: DataTypes.STRING(100), + allowNull: false, + unique: true, // 이메일 고유 제약 조건 추가 + validate: { + isEmail: true, // 유효한 이메일 형식 검증 + }, + }, + password: { + type: DataTypes.STRING(100), + allowNull: false, }, - ], + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, +}, { + tableName: 'User', // MySQL 테이블 이름 매핑 + indexes: [ + { + name: 'idx_user_email', // 인덱스 이름 + unique: true, + fields: ['email'], // 이메일 컬럼에 고유 인덱스 추가 + }, + ], }); module.exports = User; diff --git a/webapp/backend/apiserver/package-lock.json b/webapp/backend/apiserver/package-lock.json index 3df904d..4322c58 100644 --- a/webapp/backend/apiserver/package-lock.json +++ b/webapp/backend/apiserver/package-lock.json @@ -8,11 +8,13 @@ "name": "backend", "version": "0.0.0", "dependencies": { + "argon2": "^0.41.1", "cookie-parser": "~1.4.4", "cors": "^2.8.5", "debug": "~2.6.9", "dotenv": "^16.4.5", "express": "~4.16.1", + "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "morgan": "^1.10.0", "mysql2": "^3.11.4", @@ -22,6 +24,15 @@ "nodemon": "^3.1.7" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -79,6 +90,21 @@ "node": ">= 8" } }, + "node_modules/argon2": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz", + "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -171,6 +197,12 @@ "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==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -322,6 +354,15 @@ "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", "license": "MIT" }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -608,12 +649,103 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -816,6 +948,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.2.tgz", + "integrity": "sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", diff --git a/webapp/backend/apiserver/package.json b/webapp/backend/apiserver/package.json index e35da70..f352b4c 100644 --- a/webapp/backend/apiserver/package.json +++ b/webapp/backend/apiserver/package.json @@ -6,11 +6,13 @@ "start": "node ./app.js" }, "dependencies": { + "argon2": "^0.41.1", "cookie-parser": "~1.4.4", "cors": "^2.8.5", "debug": "~2.6.9", "dotenv": "^16.4.5", "express": "~4.16.1", + "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "morgan": "^1.10.0", "mysql2": "^3.11.4", diff --git a/webapp/backend/apiserver/routes/user.js b/webapp/backend/apiserver/routes/user.js new file mode 100644 index 0000000..87c7037 --- /dev/null +++ b/webapp/backend/apiserver/routes/user.js @@ -0,0 +1,17 @@ +var express = require('express'); +var router = express.Router(); +const userController = require('../controllers/userController'); + +router.post('/users', userController.signUp); + +router.post('/login', userController.login); + +router.post('/verify_email', userController.verifyEmail); + +router.get('/users/:id', userController.getUserById); + +router.put('/users/:id', userController.updateUser); + +router.delete('/users/:id', userController.deleteUser); + +module.exports = router; diff --git a/webapp/backend/apiserver/services/userService.js b/webapp/backend/apiserver/services/userService.js new file mode 100644 index 0000000..e69de29 diff --git a/webapp/backend/config/account.js b/webapp/backend/config/account.js new file mode 100644 index 0000000..99186df --- /dev/null +++ b/webapp/backend/config/account.js @@ -0,0 +1,5 @@ +const AccountConfig = { + JWT_SECRET_KEY: process.env.JWT_SECRET_KEY || "my_secret_key", +} + +module.exports = AccountConfig; diff --git a/webapp/backend/ddl/create-tables.sql b/webapp/backend/ddl/create-tables.sql index fbb33f1..5ba621f 100644 --- a/webapp/backend/ddl/create-tables.sql +++ b/webapp/backend/ddl/create-tables.sql @@ -1,103 +1,115 @@ -- Region 테이블 (자기 참조 구조) DROP TABLE IF EXISTS `Region`; -CREATE TABLE `Region` ( - `regionID` INT NOT NULL, - `regionName` VARCHAR(100) NOT NULL, - `parentRegionID` INT NULL, +CREATE TABLE `Region` +( + `regionID` INT NOT NULL, + `regionName` VARCHAR(100) NOT NULL, + `parentRegionID` INT NULL, PRIMARY KEY (`regionID`), - FOREIGN KEY (`parentRegionID`) REFERENCES `Region`(`regionID`) + FOREIGN KEY (`parentRegionID`) REFERENCES `Region` (`regionID`) ); -- SportType 테이블 DROP TABLE IF EXISTS `SportType`; -CREATE TABLE `SportType` ( - `sportTypeId` INT NOT NULL, - `sportName` VARCHAR(100) NOT NULL COMMENT '(예: 러닝, 헬스, 클라이밍)', - `metadata` JSON NULL COMMENT '(카테고리와 옵션을 JSON으로 저장)', +CREATE TABLE `SportType` +( + `sportTypeId` INT NOT NULL, + `sportName` VARCHAR(100) NOT NULL COMMENT '(예: 러닝, 헬스, 클라이밍)', + `metadata` JSON NULL COMMENT '(카테고리와 옵션을 JSON으로 저장)', PRIMARY KEY (`sportTypeId`) ); -- User 테이블에 email에 대한 unique 인덱스 추가 DROP TABLE IF EXISTS `User`; -CREATE TABLE `User` ( - `userID` INT NOT NULL, - `name` VARCHAR(100) NOT NULL, - `email` VARCHAR(100) NOT NULL, - `password` VARCHAR(100) NOT NULL, +CREATE TABLE `User` +( + `userID` INT NOT NULL AUTO_INCREMENT, # NOTE: Use UUID instead. + `name` VARCHAR(100) NOT NULL, + `email` VARCHAR(100) NOT NULL, + `password` VARCHAR(100) NOT NULL, + `createdAt` DATETIME NOT NULL, + `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`userID`), UNIQUE INDEX `idx_user_email` (`email`) ); -- Profile 테이블 DROP TABLE IF EXISTS `Profile`; -CREATE TABLE `Profile` ( - `profileID` INT NOT NULL, - `userID` INT NOT NULL, +CREATE TABLE `Profile` +( + `profileID` INT NOT NULL AUTO_INCREMENT, # NOTE: Use UUID instead. + `userID` INT NOT NULL, `profileImage` VARCHAR(255) NULL, - `regionID` INT NOT NULL, - `job` VARCHAR(50) NULL COMMENT 'ex. 학생, 직장인', - `birthdate` DATE NULL, - `experience` VARCHAR(100) NULL COMMENT '직접 입력', - `introduction` VARCHAR(70) NULL COMMENT '직접 입력', - `sportTypeId` INT NOT NULL, + `regionID` INT NOT NULL, + `job` VARCHAR(50) NULL COMMENT 'ex. 학생, 직장인', + `birthDate` DATE NULL, + `experience` VARCHAR(100) NULL COMMENT '직접 입력', + `introduction` VARCHAR(70) NULL COMMENT '직접 입력', + `sportTypeId` INT NOT NULL, + `createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`profileID`), - FOREIGN KEY (`userID`) REFERENCES `User`(`userID`), - FOREIGN KEY (`regionID`) REFERENCES `Region`(`regionID`), - FOREIGN KEY (`sportTypeId`) REFERENCES `SportType`(`sportTypeId`) + FOREIGN KEY (`userID`) REFERENCES `User` (`userID`), + FOREIGN KEY (`regionID`) REFERENCES `Region` (`regionID`), + FOREIGN KEY (`sportTypeId`) REFERENCES `SportType` (`sportTypeId`) ); -- Crew 테이블, (regionID, sportTypeId, createdDate) 복합 인덱스 추가 DROP TABLE IF EXISTS `Crew`; -CREATE TABLE `Crew` ( - `crewID` INT NOT NULL, - `regionID` INT NOT NULL, - `name` VARCHAR(100) NOT NULL, - `sportTypeId` INT NOT NULL, - `capacity` INT NULL, - `fee_krw` DECIMAL(10, 2) NULL, - `description` VARCHAR(255) NULL, - `createdDate` DATE NOT NULL, +CREATE TABLE `Crew` +( + `crewID` INT NOT NULL, + `regionID` INT NOT NULL, + `name` VARCHAR(100) NOT NULL, + `sportTypeId` INT NOT NULL, + `capacity` INT NULL, + `fee_krw` DECIMAL(10, 2) NULL, + `description` VARCHAR(255) NULL, + `createdDate` DATE NOT NULL, PRIMARY KEY (`crewID`), - FOREIGN KEY (`regionID`) REFERENCES `Region`(`regionID`), - FOREIGN KEY (`sportTypeId`) REFERENCES `SportType`(`sportTypeId`), + FOREIGN KEY (`regionID`) REFERENCES `Region` (`regionID`), + FOREIGN KEY (`sportTypeId`) REFERENCES `SportType` (`sportTypeId`), INDEX `idx_crew_region_sport_date` (`regionID`, `sportTypeId`, `createdDate`) -- 수정된 복합 인덱스 ); -- Event 테이블에 (crewID, eventDate) 복합 인덱스 추가 DROP TABLE IF EXISTS `Event`; -CREATE TABLE `Event` ( - `eventID` INT NOT NULL, - `crewID` INT NULL COMMENT '크루 ID (선택 항목으로 변경)', - `regionID` INT NOT NULL, - `name` VARCHAR(100) NULL, - `sportTypeId` INT NOT NULL, - `eventDate` DATE NULL, - `capacity` INT NULL, - `feeCondition` VARCHAR(50) NULL COMMENT '직접 입력', - `userID` INT NOT NULL COMMENT 'event owner', - `createdDate` DATE NOT NULL, +CREATE TABLE `Event` +( + `eventID` INT NOT NULL, + `crewID` INT NULL COMMENT '크루 ID (선택 항목으로 변경)', + `regionID` INT NOT NULL, + `name` VARCHAR(100) NULL, + `sportTypeId` INT NOT NULL, + `eventDate` DATE NULL, + `capacity` INT NULL, + `feeCondition` VARCHAR(50) NULL COMMENT '직접 입력', + `userID` INT NOT NULL COMMENT 'event owner', + `createdDate` DATE NOT NULL, PRIMARY KEY (`eventID`), - FOREIGN KEY (`crewID`) REFERENCES `Crew`(`crewID`), - FOREIGN KEY (`regionID`) REFERENCES `Region`(`regionID`), - FOREIGN KEY (`sportTypeId`) REFERENCES `SportType`(`sportTypeId`), - FOREIGN KEY (`userID`) REFERENCES `User`(`userID`), - INDEX `idx_event_crew_date` (`crewID`, `eventDate`), -- 복합 인덱스 + FOREIGN KEY (`crewID`) REFERENCES `Crew` (`crewID`), + FOREIGN KEY (`regionID`) REFERENCES `Region` (`regionID`), + FOREIGN KEY (`sportTypeId`) REFERENCES `SportType` (`sportTypeId`), + FOREIGN KEY (`userID`) REFERENCES `User` (`userID`), + INDEX `idx_event_crew_date` (`crewID`, `eventDate`), -- 복합 인덱스 INDEX `idx_event_created_date` (`createdDate`) ); -- Post 테이블에 date, crewID, userID 인덱스 추가 DROP TABLE IF EXISTS `Post`; -CREATE TABLE `Post` ( - `postID` INT NOT NULL, - `userID` INT NOT NULL, - `crewID` INT NOT NULL, - `title` VARCHAR(100) NULL, - `content` TEXT NULL, - `postDate` DATE NULL, +CREATE TABLE `Post` +( + `postID` INT NOT NULL, + `userID` INT NOT NULL, + `crewID` INT NOT NULL, + `title` VARCHAR(100) NULL, + `content` TEXT NULL, + `postDate` DATE NULL, + `isNotice` BOOLEAN NULL, PRIMARY KEY (`postID`), - FOREIGN KEY (`userID`) REFERENCES `User`(`userID`), - FOREIGN KEY (`crewID`) REFERENCES `Crew`(`crewID`), + FOREIGN KEY (`userID`) REFERENCES `User` (`userID`), + FOREIGN KEY (`crewID`) REFERENCES `Crew` (`crewID`), -- 추가된 인덱스 INDEX `idx_post_date` (`postDate`), INDEX `idx_post_crew` (`crewID`), @@ -107,38 +119,41 @@ CREATE TABLE `Post` ( -- Comment 테이블에 date 인덱스 추가 DROP TABLE IF EXISTS `Comment`; -CREATE TABLE `Comment` ( - `commentID` INT NOT NULL, - `userID` INT NOT NULL, - `postID` INT NOT NULL, - `content` TEXT NULL, +CREATE TABLE `Comment` +( + `commentID` INT NOT NULL, + `userID` INT NOT NULL, + `postID` INT NOT NULL, + `content` TEXT NULL, `commentDate` DATE NULL, PRIMARY KEY (`commentID`), - FOREIGN KEY (`userID`) REFERENCES `User`(`userID`), - FOREIGN KEY (`postID`) REFERENCES `Post`(`postID`), + FOREIGN KEY (`userID`) REFERENCES `User` (`userID`), + FOREIGN KEY (`postID`) REFERENCES `Post` (`postID`), INDEX `idx_comment_date` (`commentDate`) ); -- UserCrew 테이블 DROP TABLE IF EXISTS `UserCrew`; -CREATE TABLE `UserCrew` ( - `userID` INT NOT NULL, - `crewID` INT NOT NULL, - `role` ENUM('Leader', 'Staff', 'General') NULL COMMENT '크루장, 운영진, 일반인', +CREATE TABLE `UserCrew` +( + `userID` INT NOT NULL, + `crewID` INT NOT NULL, + `role` ENUM ('Leader', 'Staff', 'General') NULL COMMENT '크루장, 운영진, 일반인', PRIMARY KEY (`userID`, `crewID`), - FOREIGN KEY (`userID`) REFERENCES `User`(`userID`), - FOREIGN KEY (`crewID`) REFERENCES `Crew`(`crewID`) + FOREIGN KEY (`userID`) REFERENCES `User` (`userID`), + FOREIGN KEY (`crewID`) REFERENCES `Crew` (`crewID`) ); -- EventParticipants 테이블 DROP TABLE IF EXISTS `EventParticipants`; -CREATE TABLE `EventParticipants` ( - `userID` INT NOT NULL, - `eventID` INT NOT NULL, - `participationDate` DATE NULL, - `status` ENUM('attending', 'canceled') NULL COMMENT '선착순', +CREATE TABLE `EventParticipants` +( + `userID` INT NOT NULL, + `eventID` INT NOT NULL, + `participationDate` DATE NULL, + `status` ENUM ('attending', 'canceled') NULL COMMENT '선착순', PRIMARY KEY (`userID`, `eventID`), - FOREIGN KEY (`userID`) REFERENCES `User`(`userID`), - FOREIGN KEY (`eventID`) REFERENCES `Event`(`eventID`) + FOREIGN KEY (`userID`) REFERENCES `User` (`userID`), + FOREIGN KEY (`eventID`) REFERENCES `Event` (`eventID`) ); -- GitLab