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