diff --git a/package-lock.json b/package-lock.json index 9315f0c..1514c22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,14 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.15", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", "axios": "^1.7.9", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", @@ -33,6 +35,7 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -40,6 +43,7 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", @@ -2761,6 +2765,25 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.15.tgz", + "integrity": "sha512-KZAxNEADPwoORixh3NJgGYWMVGORVPKeTqjD7hbF8TPDLKWWxru9yasBQwEz2/wXH/WgpkQbbaYwx4nUjCIVpw==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", @@ -2846,6 +2869,30 @@ } } }, + "node_modules/@nestjs/websockets": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.15.tgz", + "integrity": "sha512-OmCUJwvtagzXfMVko595O98UI3M9zg+URL+/HV7vd3QPMCZ3uGCKSq15YYJ99LHJn9NyK4e4Szm2KnHtUg2QzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3793,6 +3840,12 @@ "node": ">=16.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3897,6 +3950,21 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -3904,6 +3972,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5108,6 +5185,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -5826,6 +5912,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -6223,6 +6331,62 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/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/enhanced-resolve": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", @@ -9642,6 +9806,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -11037,6 +11211,98 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/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/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/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/socket.io/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/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -12360,6 +12626,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index b75f88d..49524a9 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,14 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.15", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", "axios": "^1.7.9", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", @@ -46,6 +48,7 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -53,6 +56,7 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", diff --git a/prisma/migrations/20250107090813_creat_chat_tables/migration.sql b/prisma/migrations/20250107090813_creat_chat_tables/migration.sql new file mode 100644 index 0000000..420f7b8 --- /dev/null +++ b/prisma/migrations/20250107090813_creat_chat_tables/migration.sql @@ -0,0 +1,457 @@ +-- CreateTable +CREATE TABLE `User` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `nickname` VARCHAR(191) NOT NULL, + `auth_provider` VARCHAR(191) NOT NULL, + `profile_url` VARCHAR(191) NULL, + `role_id` INTEGER NOT NULL, + `introduce` VARCHAR(191) NULL, + `status_id` INTEGER NOT NULL, + `apply_count` INTEGER NULL DEFAULT 0, + `post_count` INTEGER NULL DEFAULT 0, + `push_alert` BOOLEAN NOT NULL DEFAULT false, + `following_alert` BOOLEAN NOT NULL DEFAULT false, + `project_alert` BOOLEAN NOT NULL DEFAULT false, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `User_email_key`(`email`), + INDEX `User_role_id_fkey`(`role_id`), + INDEX `User_status_id_fkey`(`status_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Role` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Role_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Project` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `description` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Project_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectLink` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `project_id` INTEGER NOT NULL, + `type_id` INTEGER NOT NULL, + `url` VARCHAR(191) NOT NULL, + + INDEX `ProjectLink_project_id_fkey`(`project_id`), + INDEX `ProjectLink_type_id_fkey`(`type_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `LinkType` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `LinkType_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProgrammerData` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `github_username` VARCHAR(191) NOT NULL, + `github_url` VARCHAR(191) NOT NULL, + `commit_count` INTEGER NOT NULL, + `contribution_data` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `ProgrammerData_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ArtistData` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `soundcloud_url` VARCHAR(191) NOT NULL, + `portfolio_url` VARCHAR(191) NOT NULL, + `music_data` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `ArtistData_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Status` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Resume` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `introduce` VARCHAR(191) NOT NULL, + + INDEX `Resume_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Skill` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Skill_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserSkill` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `skill_id` INTEGER NOT NULL, + + INDEX `UserSkill_skill_id_fkey`(`skill_id`), + UNIQUE INDEX `UserSkill_user_id_skill_id_key`(`user_id`, `skill_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserLink` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `platform` VARCHAR(191) NOT NULL, + `link` VARCHAR(191) NOT NULL, + + INDEX `UserLink_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Follows` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `following_user_id` INTEGER NOT NULL, + `followed_user_id` INTEGER NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `Follows_followed_user_id_fkey`(`followed_user_id`), + UNIQUE INDEX `Follows_following_user_id_followed_user_id_key`(`following_user_id`, `followed_user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedPost` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `content` VARCHAR(191) NOT NULL, + `thumbnail_url` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + `view` INTEGER NOT NULL, + `comment_count` INTEGER NOT NULL, + `like_count` INTEGER NOT NULL, + + INDEX `FeedPost_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedPostTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `tag_id` INTEGER NOT NULL, + + INDEX `FeedPostTag_tag_id_fkey`(`tag_id`), + UNIQUE INDEX `FeedPostTag_post_id_tag_id_key`(`post_id`, `tag_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedComment` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + `content` VARCHAR(191) NOT NULL, + `image_url` VARCHAR(191) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + INDEX `FeedComment_post_id_fkey`(`post_id`), + INDEX `FeedComment_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedLike` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `FeedLike_post_id_fkey`(`post_id`), + INDEX `FeedLike_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectPost` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `content` VARCHAR(191) NOT NULL, + `thumbnail_url` VARCHAR(191) NOT NULL, + `role` INTEGER NOT NULL, + `unit` VARCHAR(191) NOT NULL, + `start_date` DATETIME(3) NOT NULL, + `end_date` DATETIME(3) NOT NULL, + `work_type_id` INTEGER NOT NULL, + `recruiting` BOOLEAN NOT NULL, + `applicant_count` INTEGER NOT NULL, + `view` INTEGER NOT NULL, + `saved_count` INTEGER NOT NULL, + + INDEX `ProjectPost_user_id_fkey`(`user_id`), + INDEX `ProjectPost_work_type_id_fkey`(`work_type_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `WorkType` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectSave` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `ProjectSave_post_id_fkey`(`post_id`), + INDEX `ProjectSave_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectDetailRole` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `detail_role_id` INTEGER NOT NULL, + + INDEX `ProjectDetailRole_detail_role_id_fkey`(`detail_role_id`), + INDEX `ProjectDetailRole_post_id_fkey`(`post_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `DetailRole` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `role_id` INTEGER NOT NULL, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectPostTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `tag_id` INTEGER NOT NULL, + + INDEX `ProjectPostTag_tag_id_fkey`(`tag_id`), + UNIQUE INDEX `ProjectPostTag_post_id_tag_id_key`(`post_id`, `tag_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserApplyProject` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `UserApplyProject_post_id_fkey`(`post_id`), + INDEX `UserApplyProject_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Room` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + `active` BOOLEAN NOT NULL DEFAULT true, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Room_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `room_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `joined_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Message` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `room_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `photo_url` VARCHAR(191) NULL, + `message` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Message_status` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `message_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `is_read` BOOLEAN NOT NULL DEFAULT false, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `User` ADD CONSTRAINT `User_role_id_fkey` FOREIGN KEY (`role_id`) REFERENCES `Role`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `User` ADD CONSTRAINT `User_status_id_fkey` FOREIGN KEY (`status_id`) REFERENCES `Status`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Project` ADD CONSTRAINT `Project_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectLink` ADD CONSTRAINT `ProjectLink_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `Project`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectLink` ADD CONSTRAINT `ProjectLink_type_id_fkey` FOREIGN KEY (`type_id`) REFERENCES `LinkType`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProgrammerData` ADD CONSTRAINT `ProgrammerData_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ArtistData` ADD CONSTRAINT `ArtistData_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Resume` ADD CONSTRAINT `Resume_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserSkill` ADD CONSTRAINT `UserSkill_skill_id_fkey` FOREIGN KEY (`skill_id`) REFERENCES `Skill`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserSkill` ADD CONSTRAINT `UserSkill_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserLink` ADD CONSTRAINT `UserLink_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Follows` ADD CONSTRAINT `Follows_followed_user_id_fkey` FOREIGN KEY (`followed_user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Follows` ADD CONSTRAINT `Follows_following_user_id_fkey` FOREIGN KEY (`following_user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPost` ADD CONSTRAINT `FeedPost_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPostTag` ADD CONSTRAINT `FeedPostTag_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPostTag` ADD CONSTRAINT `FeedPostTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `FeedTag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedComment` ADD CONSTRAINT `FeedComment_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedComment` ADD CONSTRAINT `FeedComment_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedLike` ADD CONSTRAINT `FeedLike_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedLike` ADD CONSTRAINT `FeedLike_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPost` ADD CONSTRAINT `ProjectPost_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPost` ADD CONSTRAINT `ProjectPost_work_type_id_fkey` FOREIGN KEY (`work_type_id`) REFERENCES `WorkType`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectSave` ADD CONSTRAINT `ProjectSave_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectSave` ADD CONSTRAINT `ProjectSave_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectDetailRole` ADD CONSTRAINT `ProjectDetailRole_detail_role_id_fkey` FOREIGN KEY (`detail_role_id`) REFERENCES `DetailRole`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectDetailRole` ADD CONSTRAINT `ProjectDetailRole_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPostTag` ADD CONSTRAINT `ProjectPostTag_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPostTag` ADD CONSTRAINT `ProjectPostTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `ProjectTag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserApplyProject` ADD CONSTRAINT `UserApplyProject_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserApplyProject` ADD CONSTRAINT `UserApplyProject_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Room_users` ADD CONSTRAINT `Room_users_room_id_fkey` FOREIGN KEY (`room_id`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Room_users` ADD CONSTRAINT `Room_users_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_room_id_fkey` FOREIGN KEY (`room_id`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message_status` ADD CONSTRAINT `Message_status_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message_status` ADD CONSTRAINT `Message_status_message_id_fkey` FOREIGN KEY (`message_id`) REFERENCES `Message`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250109063122_fix_chat_table/migration.sql b/prisma/migrations/20250109063122_fix_chat_table/migration.sql new file mode 100644 index 0000000..072405a --- /dev/null +++ b/prisma/migrations/20250109063122_fix_chat_table/migration.sql @@ -0,0 +1,59 @@ +/* + Warnings: + + - You are about to drop the column `room_id` on the `Message` table. All the data in the column will be lost. + - You are about to drop the `Room` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Room_users` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `channel_id` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `Message` DROP FOREIGN KEY `Message_room_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Room_users` DROP FOREIGN KEY `Room_users_room_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Room_users` DROP FOREIGN KEY `Room_users_user_id_fkey`; + +-- DropIndex +DROP INDEX `Message_room_id_fkey` ON `Message`; + +-- AlterTable +ALTER TABLE `Message` DROP COLUMN `room_id`, + ADD COLUMN `channel_id` VARCHAR(191) NOT NULL; + +-- DropTable +DROP TABLE `Room`; + +-- DropTable +DROP TABLE `Room_users`; + +-- CreateTable +CREATE TABLE `Channel` ( + `id` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL DEFAULT 'default_channel_name', + `active` BOOLEAN NOT NULL DEFAULT true, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Channel_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `channel_id` VARCHAR(191) NOT NULL, + `user_id` INTEGER NOT NULL, + `joined_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250109085641_fix_message_table/migration.sql b/prisma/migrations/20250109085641_fix_message_table/migration.sql new file mode 100644 index 0000000..6fdbbfb --- /dev/null +++ b/prisma/migrations/20250109085641_fix_message_table/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `message` on the `Message` table. All the data in the column will be lost. + - You are about to drop the column `photo_url` on the `Message` table. All the data in the column will be lost. + - Added the required column `content` to the `Message` table without a default value. This is not possible if the table is not empty. + - Added the required column `type` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `Message` DROP COLUMN `message`, + DROP COLUMN `photo_url`, + ADD COLUMN `content` VARCHAR(191) NOT NULL, + ADD COLUMN `type` VARCHAR(191) NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..8a21669 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc2ebdf..d5cdfed 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,7 +40,9 @@ model User { UserApplyProject UserApplyProject[] UserLinks UserLink[] UserSkills UserSkill[] - + Channel_users Channel_users[] + Message Message[] + Message_status Message_status[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -144,12 +146,12 @@ model UserLink { } model Follows { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) following_user_id Int followed_user_id Int - created_at DateTime @default(now()) - followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) - following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) + created_at DateTime @default(now()) + followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) + following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) @@unique([following_user_id, followed_user_id]) @@index([followed_user_id], map: "Follows_followed_user_id_fkey") @@ -305,3 +307,43 @@ model UserApplyProject { @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } + +model Channel { + id String @id + name String @default("default_channel_name") + active Boolean @default(true) + created_at DateTime @default(now()) + Channel_users Channel_users[] + Message Message[] +} + +model Channel_users { + id Int @id @default(autoincrement()) + channel_id String + user_id Int + joined_at DateTime @default(now()) + channel Channel @relation(fields: [channel_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) +} + +model Message { + id Int @id @default(autoincrement()) + channel_id String + user_id Int + type String + content String + created_at DateTime @default(now()) + user User @relation(fields: [user_id], references: [id]) + channel Channel @relation(fields: [channel_id], references: [id]) + message_status Message_status[] +} + +model Message_status { + id Int @id @default(autoincrement()) + message_id Int + user_id Int + is_read Boolean @default(false) + user User @relation(fields: [user_id], references: [id]) + message Message @relation(fields: [message_id], references: [id]) +} + diff --git a/src/app.module.ts b/src/app.module.ts index 5c900ea..6cd327c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; +import { ChatGateway } from './chat/chat.gateway'; +import { ChatModule } from './chat/chat.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -9,6 +11,9 @@ import { UserModule } from '@modules/user/user.module'; }), AuthModule, // Auth 모듈 추가 UserModule, + ChatModule, + ], + providers: [ChatGateway], ], }) export class AppModule {} diff --git a/src/chat/chat.gateway.spec.ts b/src/chat/chat.gateway.spec.ts new file mode 100644 index 0000000..34daca9 --- /dev/null +++ b/src/chat/chat.gateway.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatGateway } from './chat.gateway'; + +describe('ChatGateway', () => { + let gateway: ChatGateway; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChatGateway], + }).compile(); + + gateway = module.get(ChatGateway); + }); + + it('should be defined', () => { + expect(gateway).toBeDefined(); + }); +}); diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts new file mode 100644 index 0000000..28962cb --- /dev/null +++ b/src/chat/chat.gateway.ts @@ -0,0 +1,78 @@ +import { + SubscribeMessage, + WebSocketGateway, + MessageBody, + WebSocketServer, + ConnectedSocket, +} from '@nestjs/websockets'; +import { ChatService } from './chat.service'; +import { JwtService } from '@nestjs/jwt'; +import { Server, Socket } from 'socket.io'; + +@WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) +export class ChatGateway { + constructor( + private readonly chatService: ChatService, + private readonly jwtService: JwtService + ) {} + @WebSocketServer() server: Server; + // 채팅방 참여 + @SubscribeMessage('joinChannel') + async handleJoinChannel( + @MessageBody() data: { channelId: string; userId: string }, + @ConnectedSocket() client: Socket + ) { + const { channelId, userId } = data; + + // userId JWT 토큰 값이라 디코딩 해야함 + const user = this.jwtService.decode(userId); + + // 존재하는 채팅방인지 확인 + const existData = await this.chatService.channelExist(channelId, user.id); + if (existData) { + // 채팅방 참여 + client.join(channelId); + return existData; + } + + // 채팅방 생성 + await this.chatService.createChannel(channelId); + + // 채팅방 참여 + client.join(channelId); + + // 채팅방 멤버 저장 + await this.chatService.joinChannel(channelId, user.id); + } + + // 메세지 송수신 + @SubscribeMessage('sendMessage') + async handleSendMessage( + @MessageBody() + data: { + type: string; + content: string; + user: any; + channelId: string; + } + ) { + // user 디코딩 + const user = this.jwtService.decode(data.user); + + // 메세지 데이터 저장 + await this.chatService.createMessage( + data.type, + data.channelId, + user.id, + data.content + ); + + // 유저 정보 추가 + const userData = await this.chatService.getSenderProfile(user.id); + data.user = userData; + const createdAt = new Date(); + const sendData = { ...data, createdAt }; + + this.server.to(data.channelId).emit('message', sendData); + } +} diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts new file mode 100644 index 0000000..1e54921 --- /dev/null +++ b/src/chat/chat.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ChatService } from './chat.service'; +import { PrismaModule } from '@src/prisma/prisma.module'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [PrismaModule, JwtModule], + providers: [ChatService], +}) +export class ChatModule {} diff --git a/src/chat/chat.service.spec.ts b/src/chat/chat.service.spec.ts new file mode 100644 index 0000000..110cd7d --- /dev/null +++ b/src/chat/chat.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatService } from './chat.service'; + +describe('ChatService', () => { + let service: ChatService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChatService], + }).compile(); + + service = module.get(ChatService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts new file mode 100644 index 0000000..41476a9 --- /dev/null +++ b/src/chat/chat.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; + +@Injectable() +export class ChatService { + constructor(private readonly prisma: PrismaService) {} + + // 이미 존재하는 채팅방인지 확인 + // 없다면 생성, 있다면 매핑 테이블 확인 + // 매핑 테이블 유저 데이터 있다면 대화 내용 불러오기, 없다면 매핑 테이블에 유저 데이터 생성 + async channelExist(channelId, userId) { + const exist = await this.prisma.channel.count({ + where: { id: channelId }, + }); + + // 존재하지 않으면 함수 종료 후 컨트롤러에서 채팅방 생성 로직 재개 + if (!exist) return false; + + // 매핑 테이블 channel_users에 유저 데이터 있는지 확인 + const userExist = await this.prisma.channel_users.findFirst({ + where: { + channel_id: channelId, + user_id: userId, + }, + }); + + // 있다면 대화내용 불러오기 + if (userExist) { + const message = await this.prisma.message.findMany({ + where: { + channel_id: channelId, + }, + }); + return message; + } else { + // 없다면 매핑 테이블에 유저 데이터 추가 + await this.prisma.channel_users.create({ + data: { + channel_id: channelId, + user_id: userId, + }, + }); + const notice = `${userId}님이 채팅방에 참가했습니다`; + return { notice }; + } + } + + // 채팅방 생성 + async createChannel(id) { + await this.prisma.channel.create({ data: { id } }); + } + + // 채팅방 멤버 저장 + async joinChannel(channelId, userId) { + await this.prisma.channel_users.create({ + data: { + channel_id: channelId, + user_id: userId, + }, + }); + } + // 메세지 저장 + async createMessage(type, channelId, userId, content) { + await this.prisma.message.create({ + data: { + type, + content, + channel_id: channelId, + user_id: userId, + }, + }); + } + + // 유저 정보 확인 + async getSenderProfile(userId) { + const data = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + id: true, + email: true, + name: true, + nickname: true, + role_id: true, + profile_url: true, + }, + }); + return data; + } + // 메세지 상태 업데이트 +} diff --git a/src/main.ts b/src/main.ts index c74f07b..0f27b22 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,12 +2,14 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; +import * as cookieParser from 'cookie-parser'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); + app.use(cookieParser()); app.enableCors({ - origin: true, + origin: ['http://localhost:5173', 'http://localhost:8080'], credentials: true, exposedHeaders: ['Authorization'], }); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index db5a133..e6da713 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -35,14 +35,15 @@ export class AuthController { // Authorization Code 교환 및 사용자 정보 가져오기 const { user, accessToken, refreshToken, isExistingUser } = await this.authService.handleGoogleCallback(code); - - console.log(refreshToken); + console.log('accessToken: ' + accessToken); + console.log('refreshToken: ' + refreshToken); // 리프레시 토큰을 HTTP-Only 쿠키로 설정 res.cookie('refreshToken', refreshToken, { - httpOnly: true, + httpOnly: false, secure: false, - sameSite: 'none', // CSRF 방지 - maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 + sameSite: 'none', + path: '/', + maxAge: 7 * 24 * 60 * 60 * 1000, }); return res.status(HttpStatusCodes.OK).json( @@ -96,16 +97,7 @@ export class AuthController { @Res() res: Response ) { const userId = req.user?.user_id; - console.log(userId); - const { user, message } = await this.authService.updateUserRole( - userId, - roleId - ); - const serviceResult = await this.authService.updateUserRole(userId, roleId); - - console.log('Service Result in Controller:', serviceResult); - // 응답 객체 생성 const responseBody = { message: { @@ -114,51 +106,128 @@ export class AuthController { }, user: serviceResult.user, }; - - console.log('Response Body:', responseBody); - // 응답 반환 return res.status(HttpStatusCodes.OK).json(responseBody); } + // @Post('refresh') + // async refreshAccessToken(@Req() req: any, @Res() res: Response) { + // console.log('Refresh Token API 호출'); + // const refreshToken = req.cookies['refreshToken']; + // + // if (!refreshToken) { + // throw new HttpException( + // ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + // ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + // ); + // } + // //const userId = req.user?.user_id; + // const userId = this.authService.getUserIdFromRefreshToken(refreshToken); + // console.log('user_id: ' + userId); + // const isValid = await this.authService.validateRefreshToken( + // userId, + // refreshToken + // ); + // if (!isValid) { + // const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; + // throw new HttpException(error.text, error.code); + // } + // + // const newAccessToken = this.authService.generateAccessToken(userId); + // const newRefreshToken = this.authService.generateRefreshToken(userId); + // res.cookie('refreshToken', newRefreshToken, { + // httpOnly: true, + // secure: false, + // sameSite: 'none', + // maxAge: 7 * 24 * 60 * 60 * 1000, + // }); + // return res.status(HttpStatusCodes.OK).json( + // new ApiResponse( + // HttpStatusCodes.OK, + // '액세스 토큰이 성공적으로 갱신되었습니다.', + // { + // accessToken: newAccessToken, + // } + // ) + // ); + // } + @Post('refresh') async refreshAccessToken(@Req() req: any, @Res() res: Response) { + console.log('Refresh Token API 호출'); const refreshToken = req.cookies['refreshToken']; - + console.log('refreshToken from Cookie: ' + refreshToken); + // 1. Refresh Token이 없는 경우 예외 처리 if (!refreshToken) { + console.error('리프레쉬 토큰 없음'); throw new HttpException( ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code ); } - //const userId = req.user?.user_id; - const userId = this.authService.getUserIdFromRefreshToken(refreshToken); - const isValid = await this.authService.validateRefreshToken( - userId, - refreshToken - ); - if (!isValid) { - const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; - throw new HttpException(error.text, error.code); + console.log('요청된 Refresh Token:', refreshToken); + + try { + // 2. Refresh Token에서 User ID 추출 + const userId = this.authService.getUserIdFromRefreshToken(refreshToken); + if (!userId) { + console.error('Refresh Token으로부터 User ID 추출 실패'); + throw new HttpException( + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + ); + } + console.log('추출된 User ID:', userId); + + // 3. Refresh Token 유효성 검증 + const isValid = await this.authService.validateRefreshToken( + userId, + refreshToken + ); + console.log('Refresh Token 유효성 검증 결과:', isValid); + + if (!isValid) { + console.error('Refresh Token이 Redis와 일치하지 않음'); + throw new HttpException( + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + ); + } + + // 4. 새로운 Access Token 및 Refresh Token 생성 + const newAccessToken = this.authService.generateAccessToken(userId); + const newRefreshToken = this.authService.generateRefreshToken(userId); + + console.log('새로운 Access Token:', newAccessToken); + console.log('새로운 Refresh Token:', newRefreshToken); + + // 5. Redis에 새로운 Refresh Token 저장 + await this.authService.storeRefreshToken(userId, newRefreshToken); + + // 6. Refresh Token을 쿠키에 저장 + res.cookie('refreshToken', refreshToken, { + httpOnly: false, + secure: false, + sameSite: 'none', + path: '/', + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + return res.status(HttpStatusCodes.OK).json( + new ApiResponse( + HttpStatusCodes.OK, + '액세스 토큰이 성공적으로 갱신되었습니다.', + { + accessToken: newAccessToken, + } + ) + ); + } catch (error) { + console.error('Refresh Token 갱신 중 오류 발생:', error.message); + return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Internal Server Error', + }); } - - const newAccessToken = await this.authService.generateAccessToken(userId); - const newRefreshToken = await this.authService.generateRefreshToken(userId); - res.cookie('refreshToken', newRefreshToken, { - httpOnly: true, - secure: true, - sameSite: 'strict', - maxAge: 7 * 24 * 60 * 60 * 1000, - }); - return res.status(HttpStatusCodes.OK).json( - new ApiResponse( - HttpStatusCodes.OK, - '액세스 토큰이 성공적으로 갱신되었습니다.', - { - accessToken: newAccessToken, - } - ) - ); } @Post('logout') diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 74b3159..7c4d48b 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -9,13 +9,14 @@ import { GoogleStrategy } from './strategies/google.strategy'; import { PrismaService } from '@src/prisma/prisma.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { RedisModule } from '../redis/redis.module'; +import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ PassportModule, RedisModule, JwtModule.register({ - secret: process.env.JWT_SECRET, // JWT 비밀키 설정 - signOptions: { expiresIn: '1h' }, // 기본 만료 시간 + secret: process.env.ACCESS_TOKEN_SECRET, // 비밀키 설정 + signOptions: { expiresIn: '1m' }, // 기본 만료 시간 }), ], controllers: [AuthController], diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 89a4e79..f53f3d8 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -63,10 +63,11 @@ export class AuthService { isExistingUser, }; } catch (error) { - throw new HttpException( - ErrorMessages.SERVER.INTERNAL_ERROR.text, - ErrorMessages.SERVER.INTERNAL_ERROR.code - ); + console.error(error); + // throw new HttpException( + // ErrorMessages.SERVER.INTERNAL_ERROR.text, + // ErrorMessages.SERVER.INTERNAL_ERROR.code + // ); } } @@ -182,13 +183,15 @@ export class AuthService { } generateAccessToken(userId: number): string { + console.log(`Access Token 생성: userId=${userId}`); return this.jwtService.sign( { userId }, - { expiresIn: '15m', secret: process.env.ACCESS_TOKEN_SECRET } + { expiresIn: '1m', secret: process.env.ACCESS_TOKEN_SECRET } ); } generateRefreshToken(userId: number): string { + console.log(`Refresh Token 생성: userId=${userId}`); return this.jwtService.sign( { userId }, { expiresIn: '7d', secret: process.env.REFRESH_TOKEN_SECRET } @@ -197,11 +200,14 @@ export class AuthService { getUserIdFromRefreshToken(refreshToken: string): number | null { try { + console.log('Refresh Token 디코딩 중...'); const payload = this.jwtService.verify(refreshToken, { secret: process.env.REFRESH_TOKEN_SECRET, }); + console.log('Refresh Token 디코딩 성공:', payload); return payload.userId; } catch (error) { + console.error('Refresh Token 디코딩 실패:', error.message); return null; } } @@ -209,13 +215,18 @@ export class AuthService { async storeRefreshToken(userId: number, refreshToken: string): Promise { const key = `refresh_token:${userId}`; const ttl = 7 * 24 * 60 * 60; // 7일 + console.log( + `Redis에 Refresh Token 저장: key=${key}, token=${refreshToken}` + ); await this.redisService.set(key, refreshToken, ttl); } // 리프레시 토큰 검증 async validateRefreshToken(userId: number, token: string): Promise { const key = `refresh_token:${userId}`; + console.log(`Redis에서 Refresh Token 조회: key=${key}`); const storedToken = await this.redisService.get(key); + console.log('Redis에서 조회된 Refresh Token:', storedToken); return storedToken === token; } @@ -262,8 +273,6 @@ export class AuthService { }, message: roleMessages[roleId], }; - - console.log('Service Result:', result); // 디버깅 return result; } } diff --git a/src/modules/redis/redis.service.ts b/src/modules/redis/redis.service.ts index 308a778..1a122ef 100644 --- a/src/modules/redis/redis.service.ts +++ b/src/modules/redis/redis.service.ts @@ -16,4 +16,10 @@ export class RedisService { async del(key: string): Promise { await this.redis.del(key); } + + async exists(key: string): Promise { + const result = await this.redis.exists(key); + return result === 1; + } + }