diff --git a/@types/environment.d.ts b/@types/environment.d.ts index 25f8ac2..b91f1df 100644 --- a/@types/environment.d.ts +++ b/@types/environment.d.ts @@ -2,5 +2,8 @@ declare namespace NodeJS { export interface ProcessEnv extends NodeJS.ProcessEnv { PORT: string; DATABASE_URL: string; + PASSPORT_KAKAO_CLIENT_ID: string; + PASSPORT_KAKAO_CLIENT_SECRET: string; + EXPRESS_SESSION_SECRET: string; } } diff --git a/@types/express.d.ts b/@types/express.d.ts index 588ed31..8171ba5 100644 --- a/@types/express.d.ts +++ b/@types/express.d.ts @@ -1,7 +1,9 @@ import 'express'; +import {UserModel} from 'src/models/user.model.ts'; declare global { namespace Express { + export interface User extends UserModel {} export interface Response { success(success: any): this; error(error: { diff --git a/package.json b/package.json index 1c3b653..3aee67f 100644 --- a/package.json +++ b/package.json @@ -25,22 +25,34 @@ "license": "ISC", "dependencies": { "@prisma/client": "^6.1.0", + "@quixo3/prisma-session-store": "^3.1.13", "@tsoa/runtime": "^6.6.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^5.0.0-0", + "express-session": "^1.18.1", "http-status-codes": "^2.3.0", + "mqtt": "^5.11.1", + "passport": "^0.7.0", + "passport-kakao": "^1.0.1", "prisma": "^6.1.0", "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1" + "swagger-ui-express": "^5.0.1", + "ws": "^8.18.1" }, "devDependencies": { "@tsconfig/node20": "^20.1.4", + "@types/cookie-parser": "^1.4.8", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/express-session": "^1.18.1", "@types/node": "^22.7.5", + "@types/passport": "^1.0.17", + "@types/passport-kakao": "^1.0.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@types/ws": "^8.18.1", "esbuild": "^0.24.2", "esbuild-plugin-copy": "^2.1.1", "eslint": "^9.17.0", diff --git a/prisma/migrations/20250412113930_add_user_social_account/migration.sql b/prisma/migrations/20250412113930_add_user_social_account/migration.sql new file mode 100644 index 0000000..393647f --- /dev/null +++ b/prisma/migrations/20250412113930_add_user_social_account/migration.sql @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE `users` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `email` VARCHAR(50) NOT NULL, + `name` VARCHAR(30) NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `social_accounts` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `provider` VARCHAR(30) NOT NULL, + `provider_user_id` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + `user_id` BIGINT NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `session` ( + `id` VARCHAR(255) NOT NULL, + `sid` VARCHAR(255) NOT NULL, + `data` VARCHAR(512) NOT NULL, + `expires_at` TIMESTAMP(3) NOT NULL, + + UNIQUE INDEX `session_sid_key`(`sid`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `social_accounts` ADD CONSTRAINT `social_accounts_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250412115914_modify_user_social_account/migration.sql b/prisma/migrations/20250412115914_modify_user_social_account/migration.sql new file mode 100644 index 0000000..89c5421 --- /dev/null +++ b/prisma/migrations/20250412115914_modify_user_social_account/migration.sql @@ -0,0 +1,44 @@ +/* + Warnings: + + - You are about to drop the `social_accounts` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `users` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE `social_accounts` DROP FOREIGN KEY `social_accounts_user_id_fkey`; + +-- DropTable +DROP TABLE `social_accounts`; + +-- DropTable +DROP TABLE `users`; + +-- CreateTable +CREATE TABLE `user` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `email` VARCHAR(50) NOT NULL, + `name` VARCHAR(30) NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `social_account` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `provider` VARCHAR(30) NOT NULL, + `provider_user_id` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + `user_id` BIGINT NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `social_account` ADD CONSTRAINT `social_account_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250428150044_create_group_user_group_todo_concentration_focus_target_and_modify_user/migration.sql b/prisma/migrations/20250428150044_create_group_user_group_todo_concentration_focus_target_and_modify_user/migration.sql new file mode 100644 index 0000000..d48ba10 --- /dev/null +++ b/prisma/migrations/20250428150044_create_group_user_group_todo_concentration_focus_target_and_modify_user/migration.sql @@ -0,0 +1,87 @@ +/* + Warnings: + + - Added the required column `nickname` to the `user` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `user` ADD COLUMN `nickname` VARCHAR(30) NOT NULL, + ADD COLUMN `user_group_id` BIGINT NULL; + +-- CreateTable +CREATE TABLE `group` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(30) NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_group` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `userId` BIGINT NOT NULL, + `groupId` BIGINT NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + UNIQUE INDEX `user_id`(`userId`), + UNIQUE INDEX `group_id`(`groupId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `todo` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(50) NOT NULL, + `user_id` BIGINT NOT NULL, + `is_completed` TINYINT NOT NULL DEFAULT 1, + `start_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `end_time` DATETIME(3) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `concentration` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `todo_id` BIGINT NOT NULL, + `hour` INTEGER NOT NULL, + `start_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `end_time` DATETIME(3) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `focus_target` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `target` VARCHAR(50) NOT NULL, + `start_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `end_time` DATETIME(3) NULL, + `user_id` BIGINT NOT NULL, + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_group` ADD CONSTRAINT `user_group_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_group` ADD CONSTRAINT `user_group_groupId_fkey` FOREIGN KEY (`groupId`) REFERENCES `group`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `todo` ADD CONSTRAINT `todo_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `concentration` ADD CONSTRAINT `concentration_todo_id_fkey` FOREIGN KEY (`todo_id`) REFERENCES `todo`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `focus_target` ADD CONSTRAINT `focus_target_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250429113038_remove_user_nick_name_concentration/migration.sql b/prisma/migrations/20250429113038_remove_user_nick_name_concentration/migration.sql new file mode 100644 index 0000000..d5f3515 --- /dev/null +++ b/prisma/migrations/20250429113038_remove_user_nick_name_concentration/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `nickname` on the `user` table. All the data in the column will be lost. + - You are about to drop the `concentration` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE `concentration` DROP FOREIGN KEY `concentration_todo_id_fkey`; + +-- AlterTable +ALTER TABLE `user` DROP COLUMN `nickname`; + +-- DropTable +DROP TABLE `concentration`; diff --git a/prisma/migrations/20250430125249_create_time_table_and_modify_schedule_todo_focus_target/migration.sql b/prisma/migrations/20250430125249_create_time_table_and_modify_schedule_todo_focus_target/migration.sql new file mode 100644 index 0000000..dc6ff17 --- /dev/null +++ b/prisma/migrations/20250430125249_create_time_table_and_modify_schedule_todo_focus_target/migration.sql @@ -0,0 +1,70 @@ +/* + Warnings: + + - You are about to drop the column `end_time` on the `focus_target` table. All the data in the column will be lost. + - You are about to drop the column `start_time` on the `focus_target` table. All the data in the column will be lost. + - You are about to drop the column `start_time` on the `todo` table. All the data in the column will be lost. + - Added the required column `description` to the `group` table without a default value. This is not possible if the table is not empty. + - Added the required column `description` to the `todo` table without a default value. This is not possible if the table is not empty. + - Made the column `end_time` on table `todo` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE `focus_target` DROP COLUMN `end_time`, + DROP COLUMN `start_time`, + ADD COLUMN `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6); + +-- AlterTable +ALTER TABLE `group` ADD COLUMN `description` VARCHAR(100) NOT NULL; + +-- AlterTable +ALTER TABLE `todo` DROP COLUMN `start_time`, + ADD COLUMN `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + ADD COLUMN `description` VARCHAR(100) NOT NULL, + ADD COLUMN `repeat_date` VARCHAR(6) NULL, + ADD COLUMN `start_date` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + ADD COLUMN `updated_at` TIMESTAMP(6) NULL, + MODIFY `end_time` DATETIME(3) NOT NULL; + +-- CreateTable +CREATE TABLE `schedule` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(50) NOT NULL, + `user_id` BIGINT NOT NULL, + `description` VARCHAR(100) NOT NULL, + `start_date` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `end_date` DATETIME(3) NOT NULL, + `repeat_type` VARCHAR(6) NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `time_table` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `todo_id` BIGINT NOT NULL, + `schedule_id` BIGINT NOT NULL, + `focus_target_id` BIGINT NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `schedule` ADD CONSTRAINT `schedule_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `time_table` ADD CONSTRAINT `time_table_todo_id_fkey` FOREIGN KEY (`todo_id`) REFERENCES `todo`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `time_table` ADD CONSTRAINT `time_table_schedule_id_fkey` FOREIGN KEY (`schedule_id`) REFERENCES `schedule`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `time_table` ADD CONSTRAINT `time_table_focus_target_id_fkey` FOREIGN KEY (`focus_target_id`) REFERENCES `focus_target`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250430154655_modify_detail/migration.sql b/prisma/migrations/20250430154655_modify_detail/migration.sql new file mode 100644 index 0000000..c08d218 --- /dev/null +++ b/prisma/migrations/20250430154655_modify_detail/migration.sql @@ -0,0 +1,23 @@ +/* + Warnings: + + - You are about to drop the column `repeat_date` on the `todo` table. All the data in the column will be lost. + - You are about to drop the column `user_group_id` on the `user` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE `group` MODIFY `description` VARCHAR(100) NOT NULL DEFAULT ''; + +-- AlterTable +ALTER TABLE `time_table` MODIFY `todo_id` BIGINT NULL, + MODIFY `schedule_id` BIGINT NULL, + MODIFY `focus_target_id` BIGINT NULL; + +-- AlterTable +ALTER TABLE `todo` DROP COLUMN `repeat_date`, + ADD COLUMN `repeat_type` VARCHAR(6) NULL, + MODIFY `is_completed` TINYINT NOT NULL DEFAULT 0, + MODIFY `description` VARCHAR(100) NOT NULL DEFAULT ''; + +-- AlterTable +ALTER TABLE `user` DROP COLUMN `user_group_id`; diff --git a/prisma/migrations/20250513145400_delete_todo_and_modify_schedule/migration.sql b/prisma/migrations/20250513145400_delete_todo_and_modify_schedule/migration.sql new file mode 100644 index 0000000..0dde780 --- /dev/null +++ b/prisma/migrations/20250513145400_delete_todo_and_modify_schedule/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `todo_id` on the `time_table` table. All the data in the column will be lost. + - You are about to drop the `todo` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[hostId]` on the table `group` will be added. If there are existing duplicate values, this will fail. + - Added the required column `hostId` to the `group` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `time_table` DROP FOREIGN KEY `time_table_todo_id_fkey`; + +-- DropForeignKey +ALTER TABLE `todo` DROP FOREIGN KEY `todo_user_id_fkey`; + +-- DropIndex +DROP INDEX `time_table_todo_id_fkey` ON `time_table`; + +-- AlterTable +ALTER TABLE `group` ADD COLUMN `hostId` BIGINT NOT NULL; + +-- AlterTable +ALTER TABLE `schedule` ADD COLUMN `notification` TINYINT NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE `time_table` DROP COLUMN `todo_id`; + +-- DropTable +DROP TABLE `todo`; + +-- CreateIndex +CREATE UNIQUE INDEX `host_id` ON `group`(`hostId`); diff --git a/prisma/migrations/20250514034330_modify_schedule_repeat_type/migration.sql b/prisma/migrations/20250514034330_modify_schedule_repeat_type/migration.sql new file mode 100644 index 0000000..a8dd85c --- /dev/null +++ b/prisma/migrations/20250514034330_modify_schedule_repeat_type/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Made the column `repeat_type` on table `schedule` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE `schedule` MODIFY `description` VARCHAR(100) NULL, + MODIFY `repeat_type` VARCHAR(6) NOT NULL DEFAULT '반복 안함'; diff --git a/prisma/migrations/20250514090419_modify_group_description/migration.sql b/prisma/migrations/20250514090419_modify_group_description/migration.sql new file mode 100644 index 0000000..280f975 --- /dev/null +++ b/prisma/migrations/20250514090419_modify_group_description/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `group` MODIFY `description` VARCHAR(100) NULL DEFAULT ''; diff --git a/prisma/migrations/20250514124345_modify_schedule_is_completed/migration.sql b/prisma/migrations/20250514124345_modify_schedule_is_completed/migration.sql new file mode 100644 index 0000000..385d590 --- /dev/null +++ b/prisma/migrations/20250514124345_modify_schedule_is_completed/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `schedule` ADD COLUMN `is_completed` TINYINT NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20250519055738_create_user_focus_target/migration.sql b/prisma/migrations/20250519055738_create_user_focus_target/migration.sql new file mode 100644 index 0000000..502bd8b --- /dev/null +++ b/prisma/migrations/20250519055738_create_user_focus_target/migration.sql @@ -0,0 +1,51 @@ +/* + Warnings: + + - You are about to drop the column `user_id` on the `focus_target` table. All the data in the column will be lost. + - You are about to drop the column `schedule_id` on the `time_table` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE `focus_target` DROP FOREIGN KEY `focus_target_user_id_fkey`; + +-- DropForeignKey +ALTER TABLE `time_table` DROP FOREIGN KEY `time_table_focus_target_id_fkey`; + +-- DropForeignKey +ALTER TABLE `time_table` DROP FOREIGN KEY `time_table_schedule_id_fkey`; + +-- DropIndex +DROP INDEX `focus_target_user_id_fkey` ON `focus_target`; + +-- DropIndex +DROP INDEX `time_table_focus_target_id_fkey` ON `time_table`; + +-- DropIndex +DROP INDEX `time_table_schedule_id_fkey` ON `time_table`; + +-- AlterTable +ALTER TABLE `focus_target` DROP COLUMN `user_id`; + +-- AlterTable +ALTER TABLE `time_table` DROP COLUMN `schedule_id`; + +-- CreateTable +CREATE TABLE `user_focus_target` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `focus_target_id` BIGINT NOT NULL, + `status` TINYINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_focus_target` ADD CONSTRAINT `user_focus_target_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_focus_target` ADD CONSTRAINT `user_focus_target_focus_target_id_fkey` FOREIGN KEY (`focus_target_id`) REFERENCES `focus_target`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `time_table` ADD CONSTRAINT `time_table_focus_target_id_fkey` FOREIGN KEY (`focus_target_id`) REFERENCES `user_focus_target`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250520094638_delete_user_focus_target/migration.sql b/prisma/migrations/20250520094638_delete_user_focus_target/migration.sql new file mode 100644 index 0000000..34e7463 --- /dev/null +++ b/prisma/migrations/20250520094638_delete_user_focus_target/migration.sql @@ -0,0 +1,62 @@ +/* + Warnings: + + - You are about to drop the `user_focus_target` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `user_id` to the `focus_target` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `time_table` DROP FOREIGN KEY `time_table_focus_target_id_fkey`; + +-- DropForeignKey +ALTER TABLE `user_focus_target` DROP FOREIGN KEY `user_focus_target_focus_target_id_fkey`; + +-- DropForeignKey +ALTER TABLE `user_focus_target` DROP FOREIGN KEY `user_focus_target_user_id_fkey`; + +-- DropIndex +DROP INDEX `time_table_focus_target_id_fkey` ON `time_table`; + +-- AlterTable +ALTER TABLE `focus_target` ADD COLUMN `user_id` BIGINT NOT NULL; + +-- DropTable +DROP TABLE `user_focus_target`; + +-- CreateTable +CREATE TABLE `is_offline` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `status` TINYINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `is_empty` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `status` TINYINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `focus_target` ADD CONSTRAINT `focus_target_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `is_offline` ADD CONSTRAINT `is_offline_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `is_empty` ADD CONSTRAINT `is_empty_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `time_table` ADD CONSTRAINT `time_table_focus_target_id_fkey` FOREIGN KEY (`focus_target_id`) REFERENCES `focus_target`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250520102333_modify_focus_target_status/migration.sql b/prisma/migrations/20250520102333_modify_focus_target_status/migration.sql new file mode 100644 index 0000000..909e3bc --- /dev/null +++ b/prisma/migrations/20250520102333_modify_focus_target_status/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `focus_target` MODIFY `status` TINYINT NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20250527094102_create_and_modify_timetable/migration.sql b/prisma/migrations/20250527094102_create_and_modify_timetable/migration.sql new file mode 100644 index 0000000..6e27b0b --- /dev/null +++ b/prisma/migrations/20250527094102_create_and_modify_timetable/migration.sql @@ -0,0 +1,93 @@ +/* + Warnings: + + - You are about to drop the column `is_completed` on the `schedule` table. All the data in the column will be lost. + - You are about to drop the `is_empty` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `is_offline` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `time_table` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE `is_empty` DROP FOREIGN KEY `is_empty_user_id_fkey`; + +-- DropForeignKey +ALTER TABLE `is_offline` DROP FOREIGN KEY `is_offline_user_id_fkey`; + +-- DropForeignKey +ALTER TABLE `time_table` DROP FOREIGN KEY `time_table_focus_target_id_fkey`; + +-- AlterTable +ALTER TABLE `schedule` DROP COLUMN `is_completed`; + +-- DropTable +DROP TABLE `is_empty`; + +-- DropTable +DROP TABLE `is_offline`; + +-- DropTable +DROP TABLE `time_table`; + +-- CreateTable +CREATE TABLE `offline_time_table` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `status` TINYINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `empty_time_table` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `status` TINYINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `disabled_focus_target_time_table` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `focus_target_id` BIGINT NOT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `enabled_focus_target_time_table` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `measurement_start_at` DATETIME(3) NOT NULL, + `measurement_end_at` DATETIME(3) NOT NULL, + `focus_target_id` BIGINT NULL, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NULL, + `status` TINYINT NOT NULL DEFAULT 1, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `offline_time_table` ADD CONSTRAINT `offline_time_table_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `empty_time_table` ADD CONSTRAINT `empty_time_table_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `disabled_focus_target_time_table` ADD CONSTRAINT `disabled_focus_target_time_table_focus_target_id_fkey` FOREIGN KEY (`focus_target_id`) REFERENCES `focus_target`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `enabled_focus_target_time_table` ADD CONSTRAINT `enabled_focus_target_time_table_focus_target_id_fkey` FOREIGN KEY (`focus_target_id`) REFERENCES `focus_target`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250531125553_modify_user_group/migration.sql b/prisma/migrations/20250531125553_modify_user_group/migration.sql new file mode 100644 index 0000000..7416603 --- /dev/null +++ b/prisma/migrations/20250531125553_modify_user_group/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `groupId` on the `user_group` table. All the data in the column will be lost. + - You are about to drop the column `userId` on the `user_group` table. All the data in the column will be lost. + - Added the required column `group_id` to the `user_group` table without a default value. This is not possible if the table is not empty. + - Added the required column `user_id` to the `user_group` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `user_group` DROP FOREIGN KEY `user_group_groupId_fkey`; + +-- DropForeignKey +ALTER TABLE `user_group` DROP FOREIGN KEY `user_group_userId_fkey`; + +-- DropIndex +DROP INDEX `group_id` ON `user_group`; + +-- DropIndex +DROP INDEX `user_id` ON `user_group`; + +-- AlterTable +ALTER TABLE `user_group` DROP COLUMN `groupId`, + DROP COLUMN `userId`, + ADD COLUMN `group_id` BIGINT NOT NULL, + ADD COLUMN `user_id` BIGINT NOT NULL; + +-- AddForeignKey +ALTER TABLE `user_group` ADD CONSTRAINT `user_group_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_group` ADD CONSTRAINT `user_group_group_id_fkey` FOREIGN KEY (`group_id`) REFERENCES `group`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250531135634_modify_group/migration.sql b/prisma/migrations/20250531135634_modify_group/migration.sql new file mode 100644 index 0000000..658e968 --- /dev/null +++ b/prisma/migrations/20250531135634_modify_group/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `hostId` on the `group` table. All the data in the column will be lost. + - Added the required column `host_id` to the `group` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX `host_id` ON `group`; + +-- AlterTable +ALTER TABLE `group` DROP COLUMN `hostId`, + ADD COLUMN `host_id` BIGINT 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 aa158c4..590acde 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,7 +14,166 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -} \ No newline at end of file + id BigInt @id @default(autoincrement()) + email String @db.VarChar(50) @unique(map: "email") + name String @db.VarChar(30) + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + ifofflines OfflineTimeTable[] + isEmpties EmptyTimeTable[] + socialAccounts SocialAccount[] + userGroups UserGroup[] + schedules Schedule[] + focusTargets FocusTarget[] + + @@map("user") +} + +model Group { + id BigInt @id @default(autoincrement()) + name String @db.VarChar(30) + description String? @db.VarChar(100) @default("") + hostId BigInt @map("host_id") + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + userGroups UserGroup[] + + @@map("group") +} + +model UserGroup { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user_id") + groupId BigInt @map("group_id") + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + + @@map("user_group") +} + +model SocialAccount { + id BigInt @id @default(autoincrement()) + provider String @db.VarChar(30) + providerUserId String @db.VarChar(255) @map("provider_user_id") + userId BigInt @map("user_id") + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@map("social_account") +} + +model Session { + id String @id @db.VarChar(255) + sid String @unique @db.VarChar(255) + data String @db.VarChar(512) + expiresAt DateTime @db.Timestamp(3) @map("expires_at") + + @@map("session") +} + +model FocusTarget { + id BigInt @id @default(autoincrement()) + target String @db.VarChar(50) + userId BigInt @map("user_id") + status Int @db.TinyInt @default(0) + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + disabledFocusTargetTimeTables DisabledFocusTargetTimeTable[] + enabledFocusTargetTimeTables EnabledFocusTargetTimeTable[] + + @@map("focus_target") +} + +model OfflineTimeTable { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user_id") + measurementStartAt DateTime @map("measurement_start_at") + measurementEndAt DateTime @map("measurement_end_at") + + status Int @db.TinyInt @default(0) + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@map("offline_time_table") +} + +model EmptyTimeTable { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user_id") + measurementStartAt DateTime @map("measurement_start_at") + measurementEndAt DateTime @map("measurement_end_at") + + status Int @db.TinyInt @default(0) + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("empty_time_table") +} + +model Schedule { + id BigInt @id @default(autoincrement()) + title String @db.VarChar(50) + userId BigInt @map("user_id") + description String? @db.VarChar(100) + startDate DateTime @default(now()) @map("start_date") + endDate DateTime @map("end_date") + repeatType String @db.VarChar(6) @default("반복 안함") @map("repeat_type") + notification Int @db.TinyInt @default(0) + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("schedule") +} + +model DisabledFocusTargetTimeTable { + id BigInt @id @default(autoincrement()) + measurementStartAt DateTime @map("measurement_start_at") + measurementEndAt DateTime @map("measurement_end_at") + focusTargetId BigInt @map("focus_target_id") + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + focusTarget FocusTarget? @relation(fields: [focusTargetId], references: [id], onDelete: Cascade) + + @@map("disabled_focus_target_time_table") +} + +model EnabledFocusTargetTimeTable { + id BigInt @id @default(autoincrement()) + measurementStartAt DateTime @map("measurement_start_at") + measurementEndAt DateTime @map("measurement_end_at") + focusTargetId BigInt? @map("focus_target_id") + + createdAt DateTime @db.Timestamp(6) @default(now()) @map("created_at") + updatedAt DateTime? @db.Timestamp(6) @updatedAt @map("updated_at") + status Int @db.TinyInt @default(1) + + focusTarget FocusTarget? @relation(fields: [focusTargetId], references: [id], onDelete: Cascade) + + @@map("enabled_focus_target_time_table") +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index a6deca3..7a4024f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,14 @@ import process from 'process'; import swaggerUiExpress from 'swagger-ui-express'; import swaggerDocument from '../swagger/swagger.json' assert {type: 'json'}; import {BaseError} from './error.js'; +import passport from 'passport'; +import session from 'express-session'; +import {PrismaSessionStore} from '@quixo3/prisma-session-store'; +import cookieParser from 'cookie-parser'; +import {sessionAuthMiddleware} from './auth.config.js'; +import {prisma} from './db.config.js'; +import {RegisterRoutes} from './routers/tsoaRoutes.js'; +import {authRouter} from './routers/auth.router.js'; dotenv.config(); @@ -20,6 +28,7 @@ app.use(cors()); app.use(express.static('public')); app.use(express.json()); app.use(express.urlencoded({extended: false})); +app.use(cookieParser()); app.use( '/api-docs/', @@ -44,6 +53,46 @@ app.use((req: Request, res: Response, next: NextFunction) => { next(); }); +// Session 설정 +app.use( + session({ + secret: process.env.EXPRESS_SESSION_SECRET!, + resave: false, + saveUninitialized: false, + cookie: { + maxAge: 7 * 24 * 60 * 60 * 1000, // 일주일 + }, + store: new PrismaSessionStore(prisma, { + checkPeriod: 2 * 60 * 1000, + dbRecordIdIsSessionId: true, + serializer: { + stringify: (obj: unknown) => + JSON.stringify(obj, (_, value) => + typeof value === 'bigint' ? value.toString() : value, + ), + parse: (str: string) => + JSON.parse(str, (_, value) => + typeof value === 'string' && /^\d+$/.test(value) + ? BigInt(value) + : value, + ), + }, + }), + }), +); + +app.use(passport.initialize()); +app.use(passport.session()); + +// 로그인 전 +app.use('/oauth2', authRouter); + +// 인증 미들웨어 +app.use(sessionAuthMiddleware); + +// 로그인 후 +RegisterRoutes(app); + app.get('/', (req, res) => { res.send('25-1 Capstone'); }); diff --git a/src/auth.config.ts b/src/auth.config.ts new file mode 100644 index 0000000..831a857 --- /dev/null +++ b/src/auth.config.ts @@ -0,0 +1,174 @@ +import dotenv from 'dotenv'; +import process from 'process'; +import { + Strategy as KakaoStrategy, + Profile as KakaoProfile, +} from 'passport-kakao'; +import {prisma} from './db.config.js'; +import {UserModel} from './models/user.model.js'; +import {SocialProfile} from './models/auth.model.js'; +import {Request, Response, NextFunction} from 'express'; +import {ServerError, AuthError, SessionError} from './error.js'; +dotenv.config(); + +const updateOrCreateSocialAccount = async ( + userId: bigint, + profile: SocialProfile, + provider: string, +): Promise => { + const {id: providerUserId} = profile; + + // 기존 SocialAccount 조회 + const existingSocialAccount = await prisma.socialAccount.findFirst({ + where: {provider, providerUserId: providerUserId.toString(), userId}, + }); + + const updatedAt = new Date(); + if (existingSocialAccount) { + // SocialAccount 업데이트 + await prisma.socialAccount.update({ + where: {id: existingSocialAccount.id}, + data: {updatedAt, status: 1}, + }); + } else { + // 새로운 SocialAccount 생성 + await prisma.socialAccount.create({ + data: { + provider, + providerUserId: providerUserId.toString(), + userId, + createdAt: new Date(), + updatedAt: new Date(), + status: 1, + }, + }); + } +}; + +// 카카오 로그인 +export const kakaoStrategy = new KakaoStrategy( + { + clientID: process.env.PASSPORT_KAKAO_CLIENT_ID!, + clientSecret: process.env.PASSPORT_KAKAO_CLIENT_SECRET!, // Optional in Kakao + callbackURL: 'http://localhost:3000/oauth2/callback/kakao', + }, + async (accessToken, refreshToken, profile, cb) => { + try { + const user = await verifyUser(profile as KakaoProfile, 'KAKAO'); + cb(null, user as UserModel); + } catch (err) { + cb(err); + } + }, +); + +// 사용자 검증 및 생성 함수 +const verifyUser = async ( + profile: SocialProfile, + provider: string, +): Promise<{id: bigint; email: string; name: string}> => { + const userEmail = (profile as KakaoProfile)._json?.kakao_account?.email; + + if (!userEmail) { + throw new AuthError({ + reason: `profile.email이 없습니다: ${JSON.stringify(profile)}`, + }); + } + + // 기존 사용자 조회 + const user = await prisma.user.findFirst({where: {email: userEmail}}); + + const userName = profile.displayName || profile.username || 'Kakao User'; + + if (user) { + // SocialAccount 데이터 추가 또는 업데이트 + const {id, email, name} = user; + await updateOrCreateSocialAccount(id, profile, provider); + + return {id, email, name}; + } + + // 새로운 사용자 생성 + const createdUser = await prisma.user.create({ + data: { + email: userEmail, + name: userName, + createdAt: new Date(), + updatedAt: new Date(), + status: 2, + }, + }); + + await prisma.focusTarget.createMany({ + data: [ + {target: '핸드폰', userId: createdUser.id}, + {target: '책 읽기', userId: createdUser.id}, + {target: 'PC 보기', userId: createdUser.id}, + ], + }); + + // SocialAccount 데이터 추가 + const {id, email, name} = createdUser; + await updateOrCreateSocialAccount(id, profile, provider); + + return {id, email, name}; +}; + +//인증 미들웨어 +export const sessionAuthMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const cookies = req.cookies || {}; + let sessionId = cookies['connect.sid']; + + // 세션 아이디가 없는 경우 + if (!sessionId) { + throw new SessionError({reason: 'No session ID provided'}); + } + + // 's:' 및 서명 제거 + sessionId = sessionId.split('.')[0].replace(/^s:/, ''); + + // 세션 ID가 DB에 존재하는지 확인 + const session = await prisma.session.findUnique({ + where: {sid: sessionId}, + }); + + if (!session) { + throw new SessionError({reason: 'Invalid session ID'}); + } + + // 만료 시간 확인 + const sessionExpiresAt = session.expiresAt; + + if (!sessionExpiresAt || new Date() > sessionExpiresAt) { + await extendSessionExpiration(sessionId); // 만료일 연장 + return; + } + + // 세션 인증 완료 시 + next(); + } catch (error) { + next(error); + } +}; + +// 세션 만료일 연장 함수 +const extendSessionExpiration = async (sid: string): Promise => { + try { + const newExpirationDate = new Date(); + newExpirationDate.setDate(newExpirationDate.getDate() + 7); + + await prisma.session.update({ + where: {sid}, + data: {expiresAt: newExpirationDate}, + }); + } catch { + throw new ServerError({ + reason: 'Failed to extend session expiration for SID', + }); + } +}; diff --git a/src/controllers/tsoa.focus-target.controller.ts b/src/controllers/tsoa.focus-target.controller.ts new file mode 100644 index 0000000..c7b20b8 --- /dev/null +++ b/src/controllers/tsoa.focus-target.controller.ts @@ -0,0 +1,166 @@ +import { + Controller, + Patch, + Route, + Tags, + Request, + Path, + Example, + SuccessResponse, + Response, + Get, +} from '@tsoa/runtime'; +import {Request as ExpressRequest} from 'express'; +import {StatusCodes} from 'http-status-codes'; +import { + FocusTargetListResponse, + FocusTargetResponse, +} from '../models/focus-target.model.js'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoa-response.js'; +import { + FocusTargetListGet, + FocusTargetUpdateDisable, + FocusTargetUpdateEnable, +} from '../services/focus-target.service.js'; + +@Route('focusTarget') +export class FocusTargetController extends Controller { + /** + * 허용 동작을 활성화하는 API입니다. + * + * @summary 허용 동작 활성화 API + * @param focusTargetIdParam + * @returns 허용 동작 활성화 결과를 반환합니다. + */ + @Patch('/enable/:focusTargetId') + @Tags('Focus-Target-Controller') + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'FCT-400', + reason: '허용 동작 업데이트 중 오류가 발생하였습니다.', + data: {focusTargetId: '1'}, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '허용 동작 활성화 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + focusTargetId: '1', + userId: '1', + status: 1, + }, + }) + public async updateFocusTargetEnable( + @Request() req: ExpressRequest, + @Path('focusTargetId') focusTargetIdParam: string, + ): Promise> { + try { + const focusTargetId = BigInt(focusTargetIdParam); + const updatedFocusTarget = await FocusTargetUpdateEnable(focusTargetId); + return new TsoaSuccessResponse(updatedFocusTarget); + } catch (error) { + throw error; + } + } + + /** + * 허용 동작을 비활성화하는 API입니다. + * + * @summary 허용 동작 비활성화 API + * @param focusTargetIdParam + * @returns 허용 동작 비활성화 결과를 반환합니다. + */ + @Patch('/disable/:focusTargetId') + @Tags('Focus-Target-Controller') + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'FCT-400', + reason: '허용 동작 업데이트 중 오류가 발생하였습니다.', + data: {focusTargetId: '1'}, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '허용 동작 비활성화 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + focusTargetId: '1', + userId: '1', + status: 0, + }, + }) + public async updateFocusTargetDisable( + @Request() req: ExpressRequest, + @Path('focusTargetId') focusTargetIdParam: string, + ): Promise> { + try { + const focusTargetId = BigInt(focusTargetIdParam); + const updatedFocusTarget = await FocusTargetUpdateDisable(focusTargetId); + return new TsoaSuccessResponse(updatedFocusTarget); + } catch (error) { + throw error; + } + } + + /** + * 허용 동작 리스트를 조회하는 API입니다. + * + * @summary 허용 동작 리스트 조회 API + * @returns 허용 동작 리스트 조회 결과를 반환합니다. + */ + @Get('/') + @Tags('Focus-Target-Controller') + @SuccessResponse(StatusCodes.OK, '허용 동작 리스트 조회 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + targets: [ + { + id: '1', + target: '책/교재', + userId: '1', + status: 0, + }, + ], + }, + }) + public async GetFocusTargetList( + @Request() req: ExpressRequest, + ): Promise> { + try { + const userId = BigInt(req.user!.id); + const focusTargetList = await FocusTargetListGet(userId); + return new TsoaSuccessResponse(focusTargetList); + } catch (error) { + throw error; + } + } + + // public async getFocusTargetEnable( + // @Request() req: ExpressRequest, + // ): Promise> { + // try{ + // const + // }catch (error) { + // throw error; + // } + // } +} diff --git a/src/controllers/tsoa.group.controller.ts b/src/controllers/tsoa.group.controller.ts new file mode 100644 index 0000000..0b0055d --- /dev/null +++ b/src/controllers/tsoa.group.controller.ts @@ -0,0 +1,215 @@ +import { + Controller, + Example, + Post, + Request, + Route, + SuccessResponse, + Response, + Tags, + Body, + Path, + Get, +} from '@tsoa/runtime'; +import {StatusCodes} from 'http-status-codes'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoa-response.js'; +import {Request as ExpressRequest} from 'express'; +import { + BodyToGroup, + GroupListResponse, + GroupResponse, + GroupUserResponse, +} from '../models/group.model.js'; +import { + groupCreate, + groupGet, + groupJoin, + groupListGet, +} from 'src/services/group.service.js'; +import {bodyToGroup} from 'src/dtos/group.dto.js'; + +@Route('group') +export class GroupController extends Controller { + /** + * 스터디 그룹을 생성하는 API입니다. + * + * @summary 그룹 생성 API + * @param body 그룹 이름, 설명 + * @returns 그룹 생성 결과 + */ + @Post('/') + @Tags('Group-Controller') + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'GRP-400', + reason: '그룹 생성 중 오류가 발생했습니다.', + data: {groupId: '1'}, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '그룹 생성 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + id: '1', + name: 'string', + hostId: '1', + description: 'string', + }, + }) + public async handleGroupAdd( + @Request() req: ExpressRequest, + @Body() body: BodyToGroup, + ): Promise> { + try { + const hostId = BigInt(req.user!.id); + const group = await groupCreate(hostId, bodyToGroup(body)); + return new TsoaSuccessResponse(group); + } catch (error) { + throw error; + } + } + + /** + * 특정 스터디 그룹을 조회하는 API입니다. + * + * @summary 특정 그룹 조회 API + * @param groupIdParam 그룹 ID + * @returns 특정 그룹 조회 결과 + */ + @Get('/:groupId') + @Tags('Group-Controller') + @Response( + StatusCodes.NOT_FOUND, + '존재하지 않는 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'GRP-404', + reason: '특정 그룹 조회 중 오류가 발생했습니다.', + data: {groupId: '1'}, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '특정 그룹 조회 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + id: '1', + name: 'string', + hostId: '1', + description: 'string', + }, + }) + public async handleGroupGet( + @Request() req: ExpressRequest, + @Path('groupId') groupIdParam: string, + ): Promise> { + try { + // const hostId = BigInt(req.user!.id); + const groupId = BigInt(groupIdParam); + const group = await groupGet(groupId); + return new TsoaSuccessResponse(group); + } catch (error) { + throw error; + } + } + + /** + * 내가 속한 스터디 그룹 리스트를 조회하는 API입니다. + * + * @summary 나의 그룹 리스트 조회 API + * @returns 나의 그룹 리스트 조회 결과 + */ + @Get('/') + @Tags('Group-Controller') + @SuccessResponse(StatusCodes.OK, '나의 그룹 리스트 조회 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + groups: [{id: '1', name: 'string', memberCount: 1}], + }, + }) + public async handleGroupListGet( + @Request() req: ExpressRequest, + ): Promise> { + try { + const userId = BigInt(req.user!.id); + const group = await groupListGet(userId); + return new TsoaSuccessResponse(group); + } catch (error) { + throw error; + } + } + + /** + * 스터디 그룹에 가입하는 API입니다. + * + * @summary 그룹 가입 API + * @param body 그룹 이름, 설명 + * @returns 그룹 생성 결과 + */ + @Post('/:groupId/user') + @Tags('Group-Controller') + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'GRP-400', + reason: '그룹 가입 중 오류가 발생했습니다.', + data: {groupId: '1', userId: '1'}, + }, + }, + ) + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'GRP-400', + reason: '이미 가입한 그룹입니다.', + data: {groupId: '1', userId: '1'}, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '그룹 가입 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + userId: '1', + groupId: '1', + }, + }) + public async handledGroupJoin( + @Request() req: ExpressRequest, + @Path('groupId') groupIdParam: string, + ): Promise> { + try { + const userId = BigInt(req.user!.id); + const groupId = BigInt(groupIdParam); + const groupUser = await groupJoin(groupId, userId); + return new TsoaSuccessResponse(groupUser); + } catch (error) { + throw error; + } + } +} diff --git a/src/controllers/tsoa.schedule.controller.ts b/src/controllers/tsoa.schedule.controller.ts new file mode 100644 index 0000000..8af090e --- /dev/null +++ b/src/controllers/tsoa.schedule.controller.ts @@ -0,0 +1,265 @@ +import { + Controller, + Example, + Post, + Request, + Route, + SuccessResponse, + Response, + Tags, + Body, + Get, + Path, +} from '@tsoa/runtime'; +import {StatusCodes} from 'http-status-codes'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoa-response.js'; +import {Request as ExpressRequest} from 'express'; +import { + BodyToSchedule, + ScheduleResponse, + WeeklyScheduleResponse, +} from '../models/schedule.model.js'; +import {bodyToSchedule} from '../dtos/schedule.dto.js'; +import { + scheduleCreate, + scheduleGet, + weeklyScheduleGet, +} from '../services/schedule.service.js'; + +@Route('schedule') +export class ScheduleController extends Controller { + /** + * 일정을 생성하는 API입니다. + * + * @summary 일정 생성 API + * @param body 일정 제목, 설명, 시작일, 종료일, 반복 여부, 알림 + * @returns 일정 생성 결과 + */ + @Post('/') + @Tags('Schedule-Controller') + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'SCH-400', + reason: '일정 생성 중 오류가 발생했습니다.', + data: {scheduleId: '1'}, + }, + }, + ) + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'SCH-400', + reason: '종료일을 시작일과 일치하거나 과거로 설정할 수 없습니다.', + data: { + startDate: new Date('2025-05-17T03:50:25'), + endDate: new Date('2025-05-13T03:50:25'), + }, + }, + }, + ) + @Response( + StatusCodes.BAD_REQUEST, + '유효하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'SCH-400', + reason: '이미 존재하는 일정과 시간이 겹칩니다.', + data: { + startDate: new Date('2025-05-17T03:50:25'), + endDate: new Date('2025-05-18T03:50:25'), + }, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '일정 생성 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + id: '1', + title: 'string', + userId: '1', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + repeatType: '반복 없음', + notification: 0, + }, + }) + public async handleScheduleAdd( + @Request() req: ExpressRequest, + @Body() body: BodyToSchedule, + ): Promise> { + try { + const userId = BigInt(req.user!.id); + const schedule = await scheduleCreate(userId, bodyToSchedule(body)); + return new TsoaSuccessResponse(schedule); + } catch (error) { + throw error; + } + } + + /** + * 주간 일정을 조회하는 API입니다. + * + * @summary 주간 일정 조회 API + * @returns 주간 일정 조회 결과 + */ + @Get('/weekly') + @Tags('Schedule-Controller') + @SuccessResponse(StatusCodes.OK, '주간 일정 조회 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + userId: '1', + schedules: { + '0': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + '1': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + '2': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + '3': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + '4': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + '5': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + '6': [ + { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + }, + ], + }, + }, + }) + public async handleWeekScheduleGet( + @Request() req: ExpressRequest, + ): Promise> { + try { + const userId = BigInt(req.user!.id); + + const weeklySchedule = await weeklyScheduleGet(userId); + console.log('최종 주간 일정:', weeklySchedule); + return new TsoaSuccessResponse(weeklySchedule); + } catch (error) { + throw error; + } + } + + /** + * 특정 일정을 조회하는 API입니다. + * + * @summary 특정 일정 조회 API + * @param 그룹 ID + * @returns 특정 일정 조회 결과 + */ + @Get('/:scheduleId') + @Tags('Schedule-Controller') + @Response( + StatusCodes.NOT_FOUND, + '존재하지 않은 데이터 에러', + { + resultType: 'FAIL', + success: null, + error: { + errorCode: 'SCH-404', + reason: '해당 일정을 찾을 수 없습니다.', + data: { + scheduleId: '1', + }, + }, + }, + ) + @SuccessResponse(StatusCodes.OK, '특정 일정 조회 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + userId: '1', + schedules: { + id: '1', + title: 'string', + description: 'string', + startDate: '2025-01-17T03:50:25', + endDate: '2025-01-17T03:50:25', + repeatType: '반복 안함', + notification: 0, + }, + }, + }) + public async handleScheduleGet( + @Request() req: ExpressRequest, + @Path('scheduleId') scheduleIdParam: string, + ): Promise> { + try { + const scheduleId = BigInt(scheduleIdParam); + const schedule = await scheduleGet(scheduleId); + return new TsoaSuccessResponse(schedule); + } catch (error) { + throw error; + } + } +} diff --git a/src/dtos/focus-target.dto.ts b/src/dtos/focus-target.dto.ts new file mode 100644 index 0000000..fa9ab54 --- /dev/null +++ b/src/dtos/focus-target.dto.ts @@ -0,0 +1,31 @@ +import { + FocusTargetListResponse, + FocusTargetResponse, +} from 'src/models/focus-target.model.js'; + +export const responseFromFocusTarget = ({ + id, + target, + userId, + status, +}: FocusTargetResponse): FocusTargetResponse => { + return { + id, + target, + userId, + status, + }; +}; + +export const responseFromFocusTargetList = ({ + targets, +}: FocusTargetListResponse): FocusTargetListResponse => { + return { + targets: targets.map(target => ({ + id: target.id, + target: target.target, + userId: target.userId, + status: target.status, + })), + }; +}; diff --git a/src/dtos/group.dto.ts b/src/dtos/group.dto.ts new file mode 100644 index 0000000..cd92765 --- /dev/null +++ b/src/dtos/group.dto.ts @@ -0,0 +1,39 @@ +import { + BodyToGroup, + GroupListResponse, + GroupRequest, + GroupResponse, +} from 'src/models/group.model.js'; + +export const responseFromGroup = ({ + id, + hostId, + name, + description, +}: GroupResponse): GroupResponse => { + return { + id, + hostId, + name, + description, + }; +}; + +export const responseFromGroupList = ({ + groups, +}: GroupListResponse): GroupListResponse => { + return { + groups: groups.map(grp => ({ + id: grp.id, + name: grp.name, + memberCount: grp.memberCount, + })), + }; +}; + +export const bodyToGroup = ({name, description}: BodyToGroup): GroupRequest => { + return { + name, + description, + }; +}; diff --git a/src/dtos/schedule.dto.ts b/src/dtos/schedule.dto.ts new file mode 100644 index 0000000..32bffba --- /dev/null +++ b/src/dtos/schedule.dto.ts @@ -0,0 +1,106 @@ +import { + BodyToSchedule, + ScheduleRequest, + ScheduleResponse, + WeeklyScheduleResponse, +} from 'src/models/schedule.model.js'; + +export const responseFromSchedule = ({ + id, + title, + userId, + description, + startDate, + endDate, + repeatType, + notification, +}: ScheduleResponse): ScheduleResponse => { + return { + id, + title, + userId, + description, + startDate, + endDate, + repeatType, + notification, + }; +}; + +export const bodyToSchedule = ({ + title, + description, + startDate, + endDate, + repeatType, + notification, +}: BodyToSchedule): ScheduleRequest => { + return { + title, + description, + startDate, + endDate, + repeatType, + notification, + }; +}; + +export const responseFromWeeklySchedule = ({ + userId, + schedules, +}: WeeklyScheduleResponse): WeeklyScheduleResponse => { + return { + userId, + schedules: { + '0': schedules[0].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + '1': schedules[1].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + '2': schedules[2].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + '3': schedules[3].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + '4': schedules[4].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + '5': schedules[5].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + '6': schedules[6].map(sche => ({ + id: sche.id, + title: sche.title, + startDate: sche.startDate, + endDate: sche.endDate, + description: sche.description, + })), + }, + }; +}; diff --git a/src/error.ts b/src/error.ts index 738c065..e38ec56 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,11 @@ -export type ErrorDetails = null; +export type ErrorDetails = + | {scheduleId?: string} + | {reason?: string} + | {startDate?: Date; endDate?: Date} + | {groupId?: string} + | {groupId?: string; userId?: string} + | {focusTargetId?: string} + | null; export class BaseError extends Error { public statusCode: number; @@ -18,3 +25,143 @@ export class BaseError extends Error { Object.setPrototypeOf(this, new.target.prototype); } } + +// Schedule(일정) 관련 에러 +export class ScheduleCreationError extends BaseError { + constructor(details: {scheduleId: bigint}) { + const ErrorDetails = { + scheduleId: details.scheduleId.toString(), + }; + super(400, 'SCH-400', '일정 생성 중 오류가 발생했습니다.', ErrorDetails); + } +} + +export class ScheduleDateError extends BaseError { + constructor(details: {startDate: Date; endDate: Date}) { + const ErrorDetails = { + startDate: details.startDate, + endDate: details.startDate, + }; + super( + 400, + 'SCH-400', + '종료일을 시작일보다 과거로 설정할 수 없습니다.', + ErrorDetails, + ); + } +} + +export class ScheduleOverlappingError extends BaseError { + constructor(details: {startDate: Date; endDate: Date}) { + const ErrorDetails = { + startDate: details.startDate, + endDate: details.startDate, + }; + super( + 400, + 'SCH-400', + '이미 존재하는 일정과 시간이 겹칩니다.', + ErrorDetails, + ); + } +} + +export class WeeklyScheduleGetError extends BaseError { + constructor(details: {reason: string}) { + super(400, 'SCH-400', '주간 일정 조회 중 오류가 발생했습니다.', details); + } +} + +export class ScheduleNotFoudError extends BaseError { + constructor(details: {scheduleId: bigint}) { + const ErrorDetails = { + scheduleId: details.scheduleId.toString(), + }; + super(404, 'SCH-404', '해당 일정을 찾을 수 없습니다.', ErrorDetails); + } +} + +// Group(그룹) 관련 에러 +export class GroupCreationError extends BaseError { + constructor(details: {groupId: bigint}) { + const ErrorDetails = { + groupId: details.groupId.toString(), + }; + super(400, 'GRP-400', '그룹 생성 중 오류가 발생했습니다.', ErrorDetails); + } +} + +export class GroupNotFoundError extends BaseError { + constructor(details: {groupId: bigint}) { + const ErrorDetails = { + groupId: details.groupId.toString(), + }; + super( + 404, + 'GRP-404', + '특정 그룹 조회 중 오류가 발생했습니다.', + ErrorDetails, + ); + } +} + +export class GroupJoinDuplicateError extends BaseError { + constructor(details: {groupId: bigint; userId: bigint}) { + const ErrorDetails = { + groupId: details.groupId.toString(), + userId: details.userId.toString(), + }; + super(400, 'GRP-400', '이미 가입한 그룹입니다.', ErrorDetails); + } +} + +export class GroupJoinError extends BaseError { + constructor(details: {groupId: bigint; userId: bigint}) { + const ErrorDetails = { + groupId: details.groupId.toString(), + userId: details.userId.toString(), + }; + super(400, 'GRP-400', '그룹 가입 중 오류가 발생하였습니다.', ErrorDetails); + } +} + +// FocusTarget(허용 동작) 관련 에러 +export class FocusTargetEnableError extends BaseError { + constructor(details: {focusTargetId: bigint}) { + const ErrorDetails = { + focusTargetId: details.focusTargetId.toString(), + }; + super( + 400, + 'FCT-400', + '허용 동작 업데이트 중 오류가 발생하였습니다.', + ErrorDetails, + ); + } +} + +// 공용 에러 +export class DBError extends BaseError { + constructor(details?: ErrorDetails) { + super(500, 'DB-001', '데이터베이스 에러입니다.', details); + } +} + +export class ServerError extends BaseError { + constructor(details?: ErrorDetails) { + super(500, 'SER-001', '내부 서버 오류입니다.', details); + } +} + +// 인증 관련 에러 +export class AuthError extends BaseError { + constructor(details: {reason: string}) { + super(401, 'AUT-401', '인증 오류입니다.', details); + } +} + +export class SessionError extends BaseError { + constructor(details: {reason: string}) { + super(401, 'AUT-401', '세션 오류입니다.', details); + } +} diff --git a/src/models/auth.model.ts b/src/models/auth.model.ts new file mode 100644 index 0000000..96eaa79 --- /dev/null +++ b/src/models/auth.model.ts @@ -0,0 +1,4 @@ +import {Profile as KakaoProfile} from 'passport-kakao'; + +// Shared SocialAccount type +export type SocialProfile = KakaoProfile; diff --git a/src/models/focus-target.model.ts b/src/models/focus-target.model.ts new file mode 100644 index 0000000..e3a323c --- /dev/null +++ b/src/models/focus-target.model.ts @@ -0,0 +1,22 @@ +export interface FocusTargetResponse { + id: string; + target: string; + userId: string; + status: number; +} + +export interface FocusTargetState { + targetId: string; + userId: string; + timestamp: Date; + isInitial: Date; +} + +export interface FocusTargetListResponse { + targets: { + id: string; + target: string; + userId: string; + status: number; + }[]; +} diff --git a/src/models/group.model.ts b/src/models/group.model.ts new file mode 100644 index 0000000..2622713 --- /dev/null +++ b/src/models/group.model.ts @@ -0,0 +1,36 @@ +export interface BodyToGroup { + name: string; + description: string | null; +} + +export interface GroupResponse { + id: string; + name: string; + description: string | null; + hostId: string; +} + +export interface GroupRequest { + name: string; + description: string | null; +} + +export interface GroupUserResponse { + id: string; + group: { + id: string; + name: string; + }; + user: { + id: string; + name: string; + }; +} + +export interface GroupListResponse { + groups: { + id: string; + name: string; + memberCount: number; + }[]; +} diff --git a/src/models/schedule.model.ts b/src/models/schedule.model.ts new file mode 100644 index 0000000..35a7acb --- /dev/null +++ b/src/models/schedule.model.ts @@ -0,0 +1,83 @@ +export interface BodyToSchedule { + title: string; + description: string | null; + startDate: Date; + endDate: Date; + repeatType: string; + notification: number; +} + +export interface ScheduleResponse { + id: string; + title: string; + userId: string; + description: string | null; + startDate: Date; + endDate: Date; + repeatType: string; + notification: number; +} + +export interface ScheduleRequest { + title: string; + description: string | null; + startDate: Date; + endDate: Date; + repeatType: string; + notification: number; +} + +export interface WeeklyScheduleResponse { + userId: string; + schedules: { + '0': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + '1': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + '2': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + '3': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + '4': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + '5': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + '6': { + id: string; + title: string; + description: string | null; + startDate: Date; + endDate: Date; + }[]; + }; +} diff --git a/src/models/tsoa-response.ts b/src/models/tsoa-response.ts new file mode 100644 index 0000000..4b03d72 --- /dev/null +++ b/src/models/tsoa-response.ts @@ -0,0 +1,27 @@ +import {ErrorDetails} from '../error.js'; + +export interface ITsoaErrorResponse { + resultType: string; + error: { + errorCode: string; + reason: string; + data: ErrorDetails; + }; + success: null; +} + +export interface ITsoaSuccessResponse { + resultType: string; + error: null; + success: T; +} + +export class TsoaSuccessResponse { + resultType: string = 'SUCCESS'; + error = null; + success: T; + + constructor(data: T) { + this.success = data; + } +} diff --git a/src/models/user.model.ts b/src/models/user.model.ts new file mode 100644 index 0000000..fda6a8b --- /dev/null +++ b/src/models/user.model.ts @@ -0,0 +1,8 @@ +export interface UserModel { + id: bigint; + email: string; + name: string; + createdAt: Date; + updatedAt: Date | null; + status: number; +} diff --git a/src/mqtt-client.ts b/src/mqtt-client.ts new file mode 100644 index 0000000..c7bef22 --- /dev/null +++ b/src/mqtt-client.ts @@ -0,0 +1,22 @@ +import mqtt from 'mqtt'; + +export const mqttClient = mqtt.connect('mqtt://localhost:1883', { + // MOTT 브로커에 연결 + username: '', + password: '', + clientId: `client_${Math.random().toString(16).substr(2, 8)}`, +}); + +mqttClient.on('connect', () => { + // 연결 성공 시 + console.log('mqtt 연결 성공'); + mqttClient.subscribe('state/+', err => { + // topic 구독 + if (err) console.error('mqtt 구독 실패', err); + else console.log('그룹 topic 구독 완료'); + }); +}); + +mqttClient.on('error', error => { + console.error('mqtt 에러', error); +}); diff --git a/src/mqtt-server.ts b/src/mqtt-server.ts new file mode 100644 index 0000000..0c64525 --- /dev/null +++ b/src/mqtt-server.ts @@ -0,0 +1,21 @@ +import {mqttClient} from './mqtt-client.js'; +import {saveFocusTargetState} from './repositories/focus-target.repository.js'; +// import {sendStateToClients} from './websocket-server.js'; + +mqttClient.on('message', async (topic, message) => { + // MQTT 브로커로부터 메시지 수신 시 콜백 실행 + try { + const payload = JSON.parse(message.toString()); // message - 실제 데이터 메시지 (buffer 형태) + const state = { + targetId: payload.targetId, + userId: payload.userId, + timestamp: payload.timestamp, + isInitial: payload.isInitial, + }; + console.log('수신 데이터:', state); + await saveFocusTargetState(state); // DB에 저장 + // sendStateToClients(state); // WebSocket을 통해 연결된 모든 client에게 해당 상태를 브로드캐스트 + } catch (err) { + console.error('메시지 처리 실패', err); + } +}); diff --git a/src/repositories/focus-target.repository.ts b/src/repositories/focus-target.repository.ts new file mode 100644 index 0000000..472cf04 --- /dev/null +++ b/src/repositories/focus-target.repository.ts @@ -0,0 +1,193 @@ +import {prisma} from 'src/db.config.js'; +import { + FocusTargetListResponse, + FocusTargetResponse, + FocusTargetState, +} from 'src/models/focus-target.model.js'; +//import {State} from 'src/websocket-server.js'; + +export const allowFocusTarget = async ( + focusTargetId: bigint, +): Promise => { + const enableFocustTarget = await prisma.focusTarget.update({ + where: { + id: focusTargetId, + }, + data: { + status: 1, + }, + }); + + const formattedUserFocustTarget = { + id: enableFocustTarget.id.toString(), + target: enableFocustTarget.target, + userId: enableFocustTarget.userId.toString(), + status: enableFocustTarget.status, + }; + + return formattedUserFocustTarget; +}; + +export const getFocusTargetStatus = async ( + focusTargetId: bigint, +): Promise => { + const userFocusTarget = await prisma.focusTarget.findFirst({ + where: { + id: focusTargetId, + }, + }); + + if (userFocusTarget === null) { + return null; + } + + const formattedUserFocustTarget = { + id: userFocusTarget.id.toString(), + target: userFocusTarget.target, + userId: userFocusTarget.userId.toString(), + status: userFocusTarget.status, + }; + + return formattedUserFocustTarget; +}; + +export const notAllowFocusTarget = async ( + focusTargetId: bigint, +): Promise => { + const enableFocusTarget = await prisma.focusTarget.update({ + where: { + id: focusTargetId, + }, + data: { + status: 0, + }, + }); + + const formattedUserFocustTarget = { + id: enableFocusTarget.id.toString(), + target: enableFocusTarget.target, + userId: enableFocusTarget.userId.toString(), + status: enableFocusTarget.status, + }; + + return formattedUserFocustTarget; +}; + +export const getFocusTargetList = async ( + userId: bigint, +): Promise => { + const focusTargetList = await prisma.focusTarget.findMany({ + where: { + userId: userId, + }, + select: { + id: true, + target: true, + userId: true, + status: true, + }, + }); + const formattedFocusTargetList = focusTargetList.map(focusTarget => ({ + id: focusTarget.id.toString(), + target: focusTarget.target, + userId: focusTarget.userId.toString(), + status: focusTarget.status, + })); + + const focusTargetResponse = { + targets: formattedFocusTargetList, + }; + + return focusTargetResponse; +}; + +export async function saveFocusTargetState( + data: FocusTargetState, +): Promise { + const targetMap: Record = { + '1': '핸드폰', + '2': '책 읽기', + '3': 'PC 보기', + '4': '자리 비움', + '5': '오프라인', + }; + const checkFocusTargetState = await prisma.focusTarget.findFirst({ + where: { + target: targetMap[data.targetId], + userId: BigInt(data.userId), + }, + }); + + if (checkFocusTargetState === null) { + return null; + } + + while (data.isInitial > data.timestamp) { + if (data.isInitial) { + if (checkFocusTargetState.status === 1) { + await prisma.enabledFocusTargetTimeTable.create({ + data: { + measurementStartAt: data.isInitial, + measurementEndAt: data.timestamp, + focusTargetId: checkFocusTargetState.id, + }, + }); + } else { + await prisma.disabledFocusTargetTimeTable.create({ + data: { + measurementStartAt: data.isInitial, + measurementEndAt: data.timestamp, + focusTargetId: checkFocusTargetState.id, + }, + }); + } + } else { + const latestOffline = await prisma.offlineTimeTable.findFirst({ + orderBy: { + measurementEndAt: 'desc', + }, + }); + const latestEmpty = await prisma.emptyTimeTable.findFirst({ + orderBy: { + measurementEndAt: 'desc', + }, + }); + const latestEnableTarget = + await prisma.enabledFocusTargetTimeTable.findFirst({ + orderBy: { + measurementEndAt: 'desc', + }, + }); + const latestDisableTarget = + await prisma.disabledFocusTargetTimeTable.findFirst({ + orderBy: { + measurementEndAt: 'desc', + }, + }); + const t1 = latestOffline?.measurementEndAt; + const t2 = latestEmpty?.measurementEndAt; + const t3 = latestEnableTarget?.measurementEndAt; + const t4 = latestDisableTarget?.measurementEndAt; + const latestTimes = [t1, t2, t3, t4] + .filter(Boolean) + .reduce((a, b) => (a! > b! ? a : b)); + if (checkFocusTargetState.status === 1) { + await prisma.disabledFocusTargetTimeTable.create({ + data: { + measurementStartAt: latestTimes!, + measurementEndAt: data.timestamp, + focusTargetId: checkFocusTargetState.id, + }, + }); + } else { + await prisma.disabledFocusTargetTimeTable.create({ + data: { + measurementStartAt: latestTimes!, + measurementEndAt: data.timestamp, + focusTargetId: checkFocusTargetState.id, + }, + }); + } + } + } +} diff --git a/src/repositories/group.repository.ts b/src/repositories/group.repository.ts new file mode 100644 index 0000000..4590916 --- /dev/null +++ b/src/repositories/group.repository.ts @@ -0,0 +1,148 @@ +import {prisma} from '../db.config.js'; +import { + BodyToGroup, + GroupListResponse, + GroupUserResponse, +} from 'src/models/group.model.js'; +import {GroupResponse} from '../models/group.model.js'; + +export const createGroup = async ( + data: BodyToGroup, + hostId: bigint, +): Promise => { + const createdGroup = await prisma.group.create({ + data: { + name: data.name, + hostId, + description: data.description, + }, + }); + + await prisma.userGroup.create({ + data: { + userId: hostId, + groupId: createdGroup.id, + }, + }); + + return createdGroup.id; +}; + +export const getGroup = async ( + groupId: bigint, +): Promise => { + const group = await prisma.group.findFirst({ + where: { + id: groupId, + }, + select: { + id: true, + name: true, + hostId: true, + description: true, + }, + }); + + if (group === null) { + return null; + } + + const formattedGroup = { + ...group, + id: group.id.toString(), + hostId: group.hostId.toString(), + }; + + return formattedGroup; +}; + +export const getGroupList = async ( + userId: bigint, +): Promise => { + const groupList = await prisma.userGroup.findMany({ + where: { + userId: userId, + }, + select: { + group: { + select: { + id: true, + name: true, + userGroups: { + select: { + id: true, // 아무거나 하나 선택하면 count용 + }, + }, + }, + }, + }, + }); + + const formattedGroupList = groupList.map(({group}) => ({ + id: group.id.toString(), + name: group.name, + memberCount: group.userGroups.length, + })); + + const groupListResponse = { + groups: formattedGroupList, + }; + + return groupListResponse; +}; + +export const joinGroup = async ( + groupId: bigint, + userId: bigint, +): Promise => { + const checkGroupUser = await prisma.userGroup.findFirst({ + where: { + groupId: groupId, + userId: userId, + }, + }); + if (checkGroupUser !== null) { + return null; + } + const groupUser = await prisma.userGroup.create({ + data: { + groupId: groupId, + userId: userId, + }, + }); + + return groupUser.id; +}; + +export const getGroupUser = async ( + groupUserId: bigint, +): Promise => { + const groupUser = await prisma.userGroup.findFirst({ + where: { + id: groupUserId, + }, + select: { + id: true, + user: true, + group: true, + }, + }); + + if (groupUser === null) { + return null; + } + const formattedGroupUser = { + ...groupUser, + id: groupUser.id.toString(), + user: { + id: groupUser.user.id.toString(), + name: groupUser.user.name, + }, + group: { + id: groupUser.group.id.toString(), + name: groupUser.group.name, + }, + }; + + return formattedGroupUser; +}; diff --git a/src/repositories/schedule.repository.ts b/src/repositories/schedule.repository.ts new file mode 100644 index 0000000..eeb3738 --- /dev/null +++ b/src/repositories/schedule.repository.ts @@ -0,0 +1,143 @@ +import { + BodyToSchedule, + ScheduleResponse, + WeeklyScheduleResponse, +} from 'src/models/schedule.model.js'; +import {prisma} from '../db.config.js'; + +export const createSchedule = async ( + data: BodyToSchedule, + userId: bigint, +): Promise => { + const checkOverlapping = await prisma.schedule.findFirst({ + where: { + userId: userId, + AND: [{startDate: {lt: data.endDate}}, {endDate: {gt: data.startDate}}], + }, + }); + if (checkOverlapping) { + return null; + } + const createdSchedule = await prisma.schedule.create({ + data: { + title: data.title, + userId, + startDate: data.startDate, + endDate: data.endDate, + repeatType: data.repeatType, + notification: data.notification, + description: data.description, + }, + }); + + return createdSchedule.id; +}; + +export const getSchedule = async ( + scheduleId: bigint, +): Promise => { + const schedule = await prisma.schedule.findFirst({ + where: { + id: scheduleId, + }, + select: { + id: true, + title: true, + userId: true, + description: true, + startDate: true, + endDate: true, + repeatType: true, + notification: true, + }, + }); + + if (schedule === null) { + return null; + } + + const formattedSchedule = { + ...schedule, + id: schedule.id.toString(), + userId: schedule.userId.toString(), + }; + + return formattedSchedule; +}; + +export const getWeeklySchedule = async ( + userId: bigint, +): Promise => { + const todayDate = new Date(); + const day = todayDate.getDay(); // 0(일) ~ 6(토) + const startDate = new Date(todayDate); + startDate.setDate(todayDate.getDate() - day); // 0(일) + startDate.setHours(0, 0, 0, 0); + console.log(startDate.toISOString()); + const endDate = new Date(startDate); // 0(일) + endDate.setDate(startDate.getDate() + 6); + endDate.setHours(23, 59, 59, 999); + console.log(endDate.toISOString()); + + const schedules = await prisma.schedule.findMany({ + where: { + userId: userId, + startDate: { + gte: startDate, + lte: endDate, + }, + }, + orderBy: { + startDate: 'asc', + }, + select: { + id: true, + title: true, + description: true, + startDate: true, + endDate: true, + }, + }); + + if (schedules.length !== 0) { + console.log('해당 기간에 일정이 있음', schedules.length); + } + + // const formattedSchedules = { + // schedules: schedules.map(schedule => ({ + // ...schedule, + // id: schedule.id.toString(), + // })), + // }; + + const formattedSchedules = schedules.map(schedule => ({ + ...schedule, + id: schedule.id.toString(), + })); + const weeklySchedule: WeeklyScheduleResponse['schedules'] = { + '0': [], + '1': [], + '2': [], + '3': [], + '4': [], + '5': [], + '6': [], + }; + + for (const sche of formattedSchedules) { + const scheduleDate = new Date(sche.startDate); + const day = scheduleDate + .getDay() + .toString() as keyof WeeklyScheduleResponse['schedules']; + console.log(day); + weeklySchedule[day].push(sche); + } + const formattedWeeklySchedules = { + userId: userId.toString(), + schedules: weeklySchedule, + }; + + console.log(formattedWeeklySchedules); + + return formattedWeeklySchedules; +}; diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts new file mode 100644 index 0000000..736902f --- /dev/null +++ b/src/routers/auth.router.ts @@ -0,0 +1,51 @@ +import express from 'express'; +export const authRouter = express.Router(); +import {UserModel} from '../models/user.model.js'; + +import passport from 'passport'; +import {kakaoStrategy} from '../auth.config.js'; +import {prisma} from '../db.config.js'; + +// BigInt를 문자열로 직렬화 +(BigInt.prototype as unknown as {toJSON: () => string}).toJSON = function () { + return this.toString(); +}; + +//passport 전략 설정 +passport.use(kakaoStrategy); +// serialize 설정 +passport.serializeUser((user, done) => done(null, user)); +// deserializeUser 설정 +passport.deserializeUser(async (user: UserModel, done) => { + try { + // 사용자 조회 + const checkUser = await prisma.user.findUnique({where: {id: user.id}}); + // 사용자 존재 여부 확인 + if (!checkUser) { + return done(new Error('User not found'), null); + } + // 성공적으로 사용자 반환 + done(null, checkUser as UserModel); + } catch (error) { + // 에러 처리 + done(error, null); + } +}); + +// 카카오 로그인 라우트 +authRouter.get( + '/login/kakao', + passport.authenticate('kakao', { + scope: ['profile_nickname', 'account_email'], + }), +); + +// 카카오 로그인 콜백 라우트 +authRouter.get( + '/callback/kakao', + passport.authenticate('kakao', {failureRedirect: '/'}), + (req, res) => { + // 로그인 성공 시 처리 + res.redirect('/user/mypage'); // 온보딩 페이지로 리디렉션(예정) + }, +); diff --git a/src/routers/tsoaRoutes.ts b/src/routers/tsoaRoutes.ts new file mode 100644 index 0000000..f2cd25c --- /dev/null +++ b/src/routers/tsoaRoutes.ts @@ -0,0 +1,529 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import type { TsoaRoute } from '@tsoa/runtime'; +import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { ScheduleController } from './../controllers/tsoa.schedule.controller.js'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { GroupController } from './../controllers/tsoa.group.controller.js'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { FocusTargetController } from './../controllers/tsoa.focus-target.controller.js'; +import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; + + + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + "ScheduleResponse": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "userId": {"dataType":"string","required":true}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "startDate": {"dataType":"datetime","required":true}, + "endDate": {"dataType":"datetime","required":true}, + "repeatType": {"dataType":"string","required":true}, + "notification": {"dataType":"double","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_ScheduleResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"ScheduleResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ErrorDetails": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"nestedObjectLiteral","nestedProperties":{"scheduleId":{"dataType":"string"}}},{"dataType":"nestedObjectLiteral","nestedProperties":{"reason":{"dataType":"string"}}},{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime"},"startDate":{"dataType":"datetime"}}},{"dataType":"nestedObjectLiteral","nestedProperties":{"groupId":{"dataType":"string"}}},{"dataType":"nestedObjectLiteral","nestedProperties":{"userId":{"dataType":"string"},"groupId":{"dataType":"string"}}},{"dataType":"nestedObjectLiteral","nestedProperties":{"focusTargetId":{"dataType":"string"}}},{"dataType":"enum","enums":[null]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaErrorResponse": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"nestedObjectLiteral","nestedProperties":{"data":{"ref":"ErrorDetails","required":true},"reason":{"dataType":"string","required":true},"errorCode":{"dataType":"string","required":true}},"required":true}, + "success": {"dataType":"enum","enums":[null],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "BodyToSchedule": { + "dataType": "refObject", + "properties": { + "title": {"dataType":"string","required":true}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "startDate": {"dataType":"datetime","required":true}, + "endDate": {"dataType":"datetime","required":true}, + "repeatType": {"dataType":"string","required":true}, + "notification": {"dataType":"double","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "WeeklyScheduleResponse": { + "dataType": "refObject", + "properties": { + "userId": {"dataType":"string","required":true}, + "schedules": {"dataType":"nestedObjectLiteral","nestedProperties":{"0":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true},"1":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true},"2":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true},"3":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true},"4":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true},"5":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true},"6":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endDate":{"dataType":"datetime","required":true},"startDate":{"dataType":"datetime","required":true},"description":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"title":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_WeeklyScheduleResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"WeeklyScheduleResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupResponse": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "hostId": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_GroupResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"GroupResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "BodyToGroup": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupListResponse": { + "dataType": "refObject", + "properties": { + "groups": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"memberCount":{"dataType":"double","required":true},"name":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_GroupListResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"GroupListResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupUserResponse": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "group": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}},"required":true}, + "user": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_GroupUserResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"GroupUserResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "FocusTargetResponse": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "target": {"dataType":"string","required":true}, + "userId": {"dataType":"string","required":true}, + "status": {"dataType":"double","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_FocusTargetResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"FocusTargetResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "FocusTargetListResponse": { + "dataType": "refObject", + "properties": { + "targets": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"status":{"dataType":"double","required":true},"userId":{"dataType":"string","required":true},"target":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_FocusTargetListResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"FocusTargetListResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true}); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + + +export function RegisterRoutes(app: Router) { + + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + + + + const argsScheduleController_handleScheduleAdd: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + body: {"in":"body","name":"body","required":true,"ref":"BodyToSchedule"}, + }; + app.post('/schedule', + ...(fetchMiddlewares(ScheduleController)), + ...(fetchMiddlewares(ScheduleController.prototype.handleScheduleAdd)), + + async function ScheduleController_handleScheduleAdd(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsScheduleController_handleScheduleAdd, request, response }); + + const controller = new ScheduleController(); + + await templateService.apiHandler({ + methodName: 'handleScheduleAdd', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsScheduleController_handleWeekScheduleGet: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/schedule/weekly', + ...(fetchMiddlewares(ScheduleController)), + ...(fetchMiddlewares(ScheduleController.prototype.handleWeekScheduleGet)), + + async function ScheduleController_handleWeekScheduleGet(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsScheduleController_handleWeekScheduleGet, request, response }); + + const controller = new ScheduleController(); + + await templateService.apiHandler({ + methodName: 'handleWeekScheduleGet', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsScheduleController_handleScheduleGet: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + scheduleIdParam: {"in":"path","name":"scheduleId","required":true,"dataType":"string"}, + }; + app.get('/schedule/:scheduleId', + ...(fetchMiddlewares(ScheduleController)), + ...(fetchMiddlewares(ScheduleController.prototype.handleScheduleGet)), + + async function ScheduleController_handleScheduleGet(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsScheduleController_handleScheduleGet, request, response }); + + const controller = new ScheduleController(); + + await templateService.apiHandler({ + methodName: 'handleScheduleGet', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsGroupController_handleGroupAdd: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + body: {"in":"body","name":"body","required":true,"ref":"BodyToGroup"}, + }; + app.post('/group', + ...(fetchMiddlewares(GroupController)), + ...(fetchMiddlewares(GroupController.prototype.handleGroupAdd)), + + async function GroupController_handleGroupAdd(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsGroupController_handleGroupAdd, request, response }); + + const controller = new GroupController(); + + await templateService.apiHandler({ + methodName: 'handleGroupAdd', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsGroupController_handleGroupGet: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + groupIdParam: {"in":"path","name":"groupId","required":true,"dataType":"string"}, + }; + app.get('/group/:groupId', + ...(fetchMiddlewares(GroupController)), + ...(fetchMiddlewares(GroupController.prototype.handleGroupGet)), + + async function GroupController_handleGroupGet(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsGroupController_handleGroupGet, request, response }); + + const controller = new GroupController(); + + await templateService.apiHandler({ + methodName: 'handleGroupGet', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsGroupController_handleGroupListGet: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/group', + ...(fetchMiddlewares(GroupController)), + ...(fetchMiddlewares(GroupController.prototype.handleGroupListGet)), + + async function GroupController_handleGroupListGet(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsGroupController_handleGroupListGet, request, response }); + + const controller = new GroupController(); + + await templateService.apiHandler({ + methodName: 'handleGroupListGet', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsGroupController_handledGroupJoin: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + groupIdParam: {"in":"path","name":"groupId","required":true,"dataType":"string"}, + }; + app.post('/group/:groupId/user', + ...(fetchMiddlewares(GroupController)), + ...(fetchMiddlewares(GroupController.prototype.handledGroupJoin)), + + async function GroupController_handledGroupJoin(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsGroupController_handledGroupJoin, request, response }); + + const controller = new GroupController(); + + await templateService.apiHandler({ + methodName: 'handledGroupJoin', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsFocusTargetController_updateFocusTargetEnable: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + focusTargetIdParam: {"in":"path","name":"focusTargetId","required":true,"dataType":"string"}, + }; + app.patch('/focusTarget/enable/:focusTargetId', + ...(fetchMiddlewares(FocusTargetController)), + ...(fetchMiddlewares(FocusTargetController.prototype.updateFocusTargetEnable)), + + async function FocusTargetController_updateFocusTargetEnable(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsFocusTargetController_updateFocusTargetEnable, request, response }); + + const controller = new FocusTargetController(); + + await templateService.apiHandler({ + methodName: 'updateFocusTargetEnable', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsFocusTargetController_updateFocusTargetDisable: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + focusTargetIdParam: {"in":"path","name":"focusTargetId","required":true,"dataType":"string"}, + }; + app.patch('/focusTarget/disable/:focusTargetId', + ...(fetchMiddlewares(FocusTargetController)), + ...(fetchMiddlewares(FocusTargetController.prototype.updateFocusTargetDisable)), + + async function FocusTargetController_updateFocusTargetDisable(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsFocusTargetController_updateFocusTargetDisable, request, response }); + + const controller = new FocusTargetController(); + + await templateService.apiHandler({ + methodName: 'updateFocusTargetDisable', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsFocusTargetController_GetFocusTargetList: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/focusTarget', + ...(fetchMiddlewares(FocusTargetController)), + ...(fetchMiddlewares(FocusTargetController.prototype.GetFocusTargetList)), + + async function FocusTargetController_GetFocusTargetList(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsFocusTargetController_GetFocusTargetList, request, response }); + + const controller = new FocusTargetController(); + + await templateService.apiHandler({ + methodName: 'GetFocusTargetList', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/services/focus-target.service.ts b/src/services/focus-target.service.ts new file mode 100644 index 0000000..aadad81 --- /dev/null +++ b/src/services/focus-target.service.ts @@ -0,0 +1,45 @@ +import { + responseFromFocusTarget, + responseFromFocusTargetList, +} from 'src/dtos/focus-target.dto.js'; +import {FocusTargetEnableError} from 'src/error.js'; +import { + FocusTargetListResponse, + FocusTargetResponse, +} from 'src/models/focus-target.model.js'; +import { + allowFocusTarget, + getFocusTargetList, + notAllowFocusTarget, +} from 'src/repositories/focus-target.repository.js'; + +export const FocusTargetUpdateEnable = async ( + focusTargetId: bigint, +): Promise => { + const updatedFocusTarget = await allowFocusTarget(focusTargetId); + if (updatedFocusTarget === null) { + throw new FocusTargetEnableError({ + focusTargetId: focusTargetId, + }); + } + return responseFromFocusTarget(updatedFocusTarget); +}; + +export const FocusTargetUpdateDisable = async ( + focusTargetId: bigint, +): Promise => { + const updatedFocusTarget = await notAllowFocusTarget(focusTargetId); + if (updatedFocusTarget === null) { + throw new FocusTargetEnableError({ + focusTargetId: focusTargetId, + }); + } + return responseFromFocusTarget(updatedFocusTarget); +}; + +export const FocusTargetListGet = async ( + userId: bigint, +): Promise => { + const focusTargetList = await getFocusTargetList(userId); + return responseFromFocusTargetList(focusTargetList); +}; diff --git a/src/services/group.service.ts b/src/services/group.service.ts new file mode 100644 index 0000000..49531a2 --- /dev/null +++ b/src/services/group.service.ts @@ -0,0 +1,72 @@ +import {responseFromGroup, responseFromGroupList} from 'src/dtos/group.dto.js'; +import { + GroupCreationError, + GroupJoinDuplicateError, + GroupNotFoundError, + GroupJoinError, +} from 'src/error.js'; +import { + BodyToGroup, + GroupListResponse, + GroupResponse, + GroupUserResponse, +} from 'src/models/group.model.js'; +import { + createGroup, + getGroup, + getGroupList, + getGroupUser, + joinGroup, +} from 'src/repositories/group.repository.js'; + +export const groupCreate = async ( + hostId: bigint, + body: BodyToGroup, +): Promise => { + const createdGroupId = await createGroup(body, hostId); + const group = await getGroup(createdGroupId); + if (group === null) { + throw new GroupCreationError({ + groupId: createdGroupId, + }); + } + return responseFromGroup(group); +}; + +export const groupGet = async (groupId: bigint): Promise => { + const group = await getGroup(groupId); + if (group === null) { + throw new GroupNotFoundError({ + groupId: groupId, + }); + } + return responseFromGroup(group); +}; + +export const groupListGet = async ( + userId: bigint, +): Promise => { + const groupList = await getGroupList(userId); + return responseFromGroupList(groupList); +}; + +export const groupJoin = async ( + groupId: bigint, + userId: bigint, +): Promise => { + const groupUserId = await joinGroup(groupId, userId); + if (groupUserId === null) { + throw new GroupJoinDuplicateError({ + groupId: groupId, + userId: userId, + }); + } + const groupUser = await getGroupUser(groupUserId); + if (groupUser === null) { + throw new GroupJoinError({ + groupId: groupId, + userId: userId, + }); + } + return groupUser; +}; diff --git a/src/services/schedule.service.ts b/src/services/schedule.service.ts new file mode 100644 index 0000000..69fd442 --- /dev/null +++ b/src/services/schedule.service.ts @@ -0,0 +1,68 @@ +import { + responseFromSchedule, + responseFromWeeklySchedule, +} from 'src/dtos/schedule.dto.js'; +import { + ScheduleCreationError, + ScheduleDateError, + ScheduleNotFoudError, + ScheduleOverlappingError, +} from 'src/error.js'; +import { + BodyToSchedule, + ScheduleResponse, + WeeklyScheduleResponse, +} from 'src/models/schedule.model.js'; +import { + createSchedule, + getSchedule, + getWeeklySchedule, +} from 'src/repositories/schedule.repository.js'; + +export const scheduleCreate = async ( + userId: bigint, + body: BodyToSchedule, +): Promise => { + const createdScheduleId = await createSchedule(body, userId); + const startDate = body.startDate; + const endDate = body.endDate; + if (endDate <= startDate) { + throw new ScheduleDateError({ + startDate: startDate, + endDate: endDate, + }); + } + if (createdScheduleId === null) { + throw new ScheduleOverlappingError({ + startDate: startDate, + endDate: endDate, + }); + } + const schedule = await getSchedule(createdScheduleId); + if (schedule === null) { + throw new ScheduleCreationError({ + scheduleId: createdScheduleId, + }); + } + return responseFromSchedule(schedule); +}; + +export const scheduleGet = async ( + scheduleId: bigint, +): Promise => { + const schedule = await getSchedule(scheduleId); + if (schedule === null) { + throw new ScheduleNotFoudError({ + scheduleId: scheduleId, + }); + } + return responseFromSchedule(schedule); +}; + +export const weeklyScheduleGet = async ( + userId: bigint, +): Promise => { + const weeklySchedule = await getWeeklySchedule(userId); + console.log('주간 일정: ', weeklySchedule); + return responseFromWeeklySchedule(weeklySchedule); +}; diff --git a/src/websocket-server.ts b/src/websocket-server.ts new file mode 100644 index 0000000..055b0ba --- /dev/null +++ b/src/websocket-server.ts @@ -0,0 +1,33 @@ +import {WebSocketServer, WebSocket} from 'ws'; + +export interface State { + focusTargetId: string; +} + +const wss = new WebSocketServer({port: 8080}); // 포트에서 WebSocket Server 시작 +const clients = new Set(); // 연결된 모든 client를 Set으로 관리 (중복 연결 방지 및 빠른 삭제를 위함) + +wss.on('connection', socket => { + // client 연결 시 + console.log('Client 연결 성공'); + clients.add(socket); // clients Set에 추가 + socket.on('message', message => { + console.log('받은 메시지: ', message.toString()); + }); + socket.on('close', () => { + // 연결이 끊기면(close) Set에서 삭제 + console.log('Client 연결 종료'); + clients.delete(socket); + }); +}); + +export function sendStateToClients(state: State) { + const data = JSON.stringify({type: 'NEW_STATE', payload: state}); + for (const client of clients) { + // 모든 연결된 client에게 전송 + if (client.readyState === WebSocket.OPEN) { + // client socket이 열려 있을 때만 전송 + client.send(data); + } + } +} diff --git a/swagger/swagger.json b/swagger/swagger.json index 0967ef4..d2ce3f4 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1 +1,1566 @@ -{} +{ + "openapi": "3.0.0", + "components": { + "examples": {}, + "headers": {}, + "parameters": {}, + "requestBodies": {}, + "responses": {}, + "schemas": { + "ScheduleResponse": { + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time" + }, + "repeatType": { + "type": "string" + }, + "notification": { + "type": "number", + "format": "double" + } + }, + "required": [ + "id", + "title", + "userId", + "description", + "startDate", + "endDate", + "repeatType", + "notification" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_ScheduleResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/ScheduleResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "ErrorDetails": { + "anyOf": [ + { + "properties": { + "scheduleId": { + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "reason": { + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + } + }, + "type": "object" + }, + { + "properties": { + "groupId": { + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "userId": { + "type": "string" + }, + "groupId": { + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "focusTargetId": { + "type": "string" + } + }, + "type": "object" + } + ], + "nullable": true + }, + "ITsoaErrorResponse": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "properties": { + "data": { + "$ref": "#/components/schemas/ErrorDetails" + }, + "reason": { + "type": "string" + }, + "errorCode": { + "type": "string" + } + }, + "required": [ + "data", + "reason", + "errorCode" + ], + "type": "object" + }, + "success": { + "type": "number", + "enum": [ + null + ], + "nullable": true + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "BodyToSchedule": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time" + }, + "repeatType": { + "type": "string" + }, + "notification": { + "type": "number", + "format": "double" + } + }, + "required": [ + "title", + "description", + "startDate", + "endDate", + "repeatType", + "notification" + ], + "type": "object", + "additionalProperties": false + }, + "WeeklyScheduleResponse": { + "properties": { + "userId": { + "type": "string" + }, + "schedules": { + "properties": { + "0": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "1": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "2": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "3": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "4": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "5": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "6": { + "items": { + "properties": { + "endDate": { + "type": "string", + "format": "date-time" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "endDate", + "startDate", + "description", + "title", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "6", + "5", + "4", + "3", + "2", + "1", + "0" + ], + "type": "object" + } + }, + "required": [ + "userId", + "schedules" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_WeeklyScheduleResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/WeeklyScheduleResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "GroupResponse": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "hostId": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "hostId" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_GroupResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/GroupResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "BodyToGroup": { + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name", + "description" + ], + "type": "object", + "additionalProperties": false + }, + "GroupListResponse": { + "properties": { + "groups": { + "items": { + "properties": { + "memberCount": { + "type": "number", + "format": "double" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "memberCount", + "name", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "groups" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_GroupListResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/GroupListResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "GroupUserResponse": { + "properties": { + "id": { + "type": "string" + }, + "group": { + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "name", + "id" + ], + "type": "object" + }, + "user": { + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "name", + "id" + ], + "type": "object" + } + }, + "required": [ + "id", + "group", + "user" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_GroupUserResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/GroupUserResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "FocusTargetResponse": { + "properties": { + "id": { + "type": "string" + }, + "target": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "status": { + "type": "number", + "format": "double" + } + }, + "required": [ + "id", + "target", + "userId", + "status" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_FocusTargetResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/FocusTargetResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "FocusTargetListResponse": { + "properties": { + "targets": { + "items": { + "properties": { + "status": { + "type": "number", + "format": "double" + }, + "userId": { + "type": "string" + }, + "target": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "status", + "userId", + "target", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "targets" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_FocusTargetListResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/FocusTargetListResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + } + }, + "securitySchemes": { + "sessionAuth": { + "type": "apiKey", + "in": "cookie", + "name": "connect.sid" + } + } + }, + "info": { + "title": "Server", + "version": "1.0.0", + "description": "25-1 Capstone Server", + "license": { + "name": "ISC" + }, + "contact": { + "name": "asjasj3964 ", + "email": "asjasj3964@naver.com" + } + }, + "paths": { + "/schedule": { + "post": { + "operationId": "HandleScheduleAdd", + "responses": { + "200": { + "description": "일정 생성 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ScheduleResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "id": "1", + "title": "string", + "userId": "1", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25", + "repeatType": "반복 없음", + "notification": 0 + } + } + } + } + } + } + }, + "400": { + "description": "유효하지 않은 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "SCH-400", + "reason": "일정 생성 중 오류가 발생했습니다.", + "data": { + "scheduleId": "1" + } + } + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "SCH-400", + "reason": "종료일을 시작일과 일치하거나 과거로 설정할 수 없습니다.", + "data": { + "startDate": "2025-05-16T18:50:25.000Z", + "endDate": "2025-05-12T18:50:25.000Z" + } + } + } + }, + "Example 3": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "SCH-400", + "reason": "이미 존재하는 일정과 시간이 겹칩니다.", + "data": { + "startDate": "2025-05-16T18:50:25.000Z", + "endDate": "2025-05-17T18:50:25.000Z" + } + } + } + } + } + } + } + } + }, + "description": "일정을 생성하는 API입니다.", + "summary": "일정 생성 API", + "tags": [ + "Schedule-Controller" + ], + "security": [], + "parameters": [], + "requestBody": { + "description": "일정 제목, 설명, 시작일, 종료일, 반복 여부, 알림", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BodyToSchedule", + "description": "일정 제목, 설명, 시작일, 종료일, 반복 여부, 알림" + } + } + } + } + } + }, + "/schedule/weekly": { + "get": { + "operationId": "HandleWeekScheduleGet", + "responses": { + "200": { + "description": "주간 일정 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_WeeklyScheduleResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "userId": "1", + "schedules": { + "0": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ], + "1": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ], + "2": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ], + "3": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ], + "4": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ], + "5": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ], + "6": [ + { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25" + } + ] + } + } + } + } + } + } + } + } + }, + "description": "주간 일정을 조회하는 API입니다.", + "summary": "주간 일정 조회 API", + "tags": [ + "Schedule-Controller" + ], + "security": [], + "parameters": [] + } + }, + "/schedule/{scheduleId}": { + "get": { + "operationId": "HandleScheduleGet", + "responses": { + "200": { + "description": "특정 일정 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ScheduleResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "userId": "1", + "schedules": { + "id": "1", + "title": "string", + "description": "string", + "startDate": "2025-01-17T03:50:25", + "endDate": "2025-01-17T03:50:25", + "repeatType": "반복 안함", + "notification": 0 + } + } + } + } + } + } + } + }, + "404": { + "description": "존재하지 않은 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "SCH-404", + "reason": "해당 일정을 찾을 수 없습니다.", + "data": { + "scheduleId": "1" + } + } + } + } + } + } + } + } + }, + "description": "특정 일정을 조회하는 API입니다.", + "summary": "특정 일정 조회 API", + "tags": [ + "Schedule-Controller" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "scheduleId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/group": { + "post": { + "operationId": "HandleGroupAdd", + "responses": { + "200": { + "description": "그룹 생성 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_GroupResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "id": "1", + "name": "string", + "hostId": "1", + "description": "string" + } + } + } + } + } + } + }, + "400": { + "description": "유효하지 않은 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "GRP-400", + "reason": "그룹 생성 중 오류가 발생했습니다.", + "data": { + "groupId": "1" + } + } + } + } + } + } + } + } + }, + "description": "스터디 그룹을 생성하는 API입니다.", + "summary": "그룹 생성 API", + "tags": [ + "Group-Controller" + ], + "security": [], + "parameters": [], + "requestBody": { + "description": "그룹 이름, 설명", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BodyToGroup", + "description": "그룹 이름, 설명" + } + } + } + } + }, + "get": { + "operationId": "HandleGroupListGet", + "responses": { + "200": { + "description": "나의 그룹 리스트 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_GroupListResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "groups": [ + { + "id": "1", + "name": "string", + "memberCount": 1 + } + ] + } + } + } + } + } + } + } + }, + "description": "내가 속한 스터디 그룹 리스트를 조회하는 API입니다.", + "summary": "나의 그룹 리스트 조회 API", + "tags": [ + "Group-Controller" + ], + "security": [], + "parameters": [] + } + }, + "/group/{groupId}": { + "get": { + "operationId": "HandleGroupGet", + "responses": { + "200": { + "description": "특정 그룹 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_GroupResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "id": "1", + "name": "string", + "hostId": "1", + "description": "string" + } + } + } + } + } + } + }, + "404": { + "description": "존재하지 않는 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "GRP-404", + "reason": "특정 그룹 조회 중 오류가 발생했습니다.", + "data": { + "groupId": "1" + } + } + } + } + } + } + } + } + }, + "description": "특정 스터디 그룹을 조회하는 API입니다.", + "summary": "특정 그룹 조회 API", + "tags": [ + "Group-Controller" + ], + "security": [], + "parameters": [ + { + "description": "그룹 ID", + "in": "path", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/group/{groupId}/user": { + "post": { + "operationId": "HandledGroupJoin", + "responses": { + "200": { + "description": "그룹 가입 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_GroupUserResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "userId": "1", + "groupId": "1" + } + } + } + } + } + } + }, + "400": { + "description": "유효하지 않은 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "GRP-400", + "reason": "그룹 가입 중 오류가 발생했습니다.", + "data": { + "groupId": "1", + "userId": "1" + } + } + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "GRP-400", + "reason": "이미 가입한 그룹입니다.", + "data": { + "groupId": "1", + "userId": "1" + } + } + } + } + } + } + } + } + }, + "description": "스터디 그룹에 가입하는 API입니다.", + "summary": "그룹 가입 API", + "tags": [ + "Group-Controller" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/focusTarget/enable/{focusTargetId}": { + "patch": { + "operationId": "UpdateFocusTargetEnable", + "responses": { + "200": { + "description": "허용 동작 활성화 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_FocusTargetResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "focusTargetId": "1", + "userId": "1", + "status": 1 + } + } + } + } + } + } + }, + "400": { + "description": "유효하지 않은 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "FCT-400", + "reason": "허용 동작 업데이트 중 오류가 발생하였습니다.", + "data": { + "focusTargetId": "1" + } + } + } + } + } + } + } + } + }, + "description": "허용 동작을 활성화하는 API입니다.", + "summary": "허용 동작 활성화 API", + "tags": [ + "Focus-Target-Controller" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "focusTargetId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/focusTarget/disable/{focusTargetId}": { + "patch": { + "operationId": "UpdateFocusTargetDisable", + "responses": { + "200": { + "description": "허용 동작 비활성화 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_FocusTargetResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "focusTargetId": "1", + "userId": "1", + "status": 0 + } + } + } + } + } + } + }, + "400": { + "description": "유효하지 않은 데이터 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "FCT-400", + "reason": "허용 동작 업데이트 중 오류가 발생하였습니다.", + "data": { + "focusTargetId": "1" + } + } + } + } + } + } + } + } + }, + "description": "허용 동작을 비활성화하는 API입니다.", + "summary": "허용 동작 비활성화 API", + "tags": [ + "Focus-Target-Controller" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "focusTargetId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/focusTarget": { + "get": { + "operationId": "GetFocusTargetList", + "responses": { + "200": { + "description": "허용 동작 리스트 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_FocusTargetListResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "targets": [ + { + "id": "1", + "target": "책/교재", + "userId": "1", + "status": 0 + } + ] + } + } + } + } + } + } + } + }, + "description": "허용 동작 리스트를 조회하는 API입니다.", + "summary": "허용 동작 리스트 조회 API", + "tags": [ + "Focus-Target-Controller" + ], + "security": [], + "parameters": [] + } + } + }, + "servers": [ + { + "url": "http://localhost:3000", + "description": "local server" + } + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d0c3ed9..d3fe8ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,6 +48,13 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/runtime@^7.23.8", "@babel/runtime@^7.24.5": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== + dependencies: + regenerator-runtime "^0.14.0" + "@esbuild/aix-ppc64@0.23.1": version "0.23.1" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" @@ -690,6 +697,11 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@noble/hashes@^1.1.5": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -711,6 +723,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@paralleldrive/cuid2@^2.2.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz#7f91364d53b89e2c9cb9e02e8dd0f129e834455f" + integrity sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA== + dependencies: + "@noble/hashes" "^1.1.5" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -762,6 +781,15 @@ dependencies: "@prisma/debug" "6.3.1" +"@quixo3/prisma-session-store@^3.1.13": + version "3.1.13" + resolved "https://registry.yarnpkg.com/@quixo3/prisma-session-store/-/prisma-session-store-3.1.13.tgz#a4adef5455102d12897c7fdada0158156c31a262" + integrity sha512-EAuOvYAaAsQ0OqxkdJG/Qs3cxlT4VV8SFHjtsA3G01uB1b6r7xftX3oeg7mcG0HN/DI1qOqwvy3YFoJ38ls0iA== + dependencies: + "@paralleldrive/cuid2" "^2.2.0" + ts-dedent "^2.2.0" + type-fest "^2.5.2" + "@scarf/scarf@=1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" @@ -830,6 +858,11 @@ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537" integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg== +"@types/cookie-parser@^1.4.8": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.8.tgz#d2215e7915f624fbfe4233da8f063f511679f1f3" + integrity sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ== + "@types/cookies@*": version "0.9.0" resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.0.tgz#a2290cfb325f75f0f28720939bee854d4142aee2" @@ -862,6 +895,13 @@ "@types/range-parser" "*" "@types/send" "*" +"@types/express-session@^1.18.1": + version "1.18.1" + resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.18.1.tgz#67c629a34b60a63a4724f359aac0c0e6d1f15365" + integrity sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg== + dependencies: + "@types/express" "*" + "@types/express@*", "@types/express@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" @@ -942,6 +982,21 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== +"@types/passport-kakao@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/passport-kakao/-/passport-kakao-1.0.3.tgz#b42e02e5649b5574d6bab35a74bad7a8d05f334f" + integrity sha512-McK5kpeiOptvjIPkiA/QS3k82//z5JM7Y3yBLMDvEA0QMMhFMcWUHhyebL1Qy+0i0n8jS+Oe4U6xAvkU22SXXA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*", "@types/passport@^1.0.17": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6" + integrity sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg== + dependencies: + "@types/express" "*" + "@types/qs@*": version "6.9.18" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2" @@ -952,6 +1007,14 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== +"@types/readable-stream@^4.0.0": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-4.0.18.tgz#5d8d15d26c776500ce573cae580787d149823bfc" + integrity sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + "@types/semver@^7.3.12": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -987,6 +1050,13 @@ "@types/express" "*" "@types/serve-static" "*" +"@types/ws@^8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" @@ -1157,6 +1227,13 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" @@ -1255,11 +1332,26 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bl@^6.0.8: + version "6.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.1.0.tgz#cc35ce7a2e8458caa8c8fb5deeed6537b73e4504" + integrity sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw== + dependencies: + "@types/readable-stream" "^4.0.0" + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + body-parser@1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -1315,6 +1407,19 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtins@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.1.0.tgz#6d85eeb360c4ebc166c3fdef922a15aa7316a5e8" @@ -1438,11 +1543,26 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commist@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/commist/-/commist-3.2.0.tgz#da9c8e5f245ac21510badc4b10c46b5bcc9b56cd" + integrity sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -1462,11 +1582,24 @@ content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== +cookie-parser@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== + cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" @@ -1477,6 +1610,11 @@ cookie@0.7.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -1533,7 +1671,7 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -1945,6 +2083,16 @@ etag@^1.8.1, etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -1960,6 +2108,20 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +express-session@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.1.tgz#88d0bbd41878882840f24ec6227493fcb167e8d5" + integrity sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.7" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.1" + uid-safe "~2.1.5" + express@^4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -2075,6 +2237,14 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-unique-numbers@^8.0.13: + version "8.0.13" + resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz#3c87232061ff5f408a216e1f0121232f76f695d7" + integrity sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g== + dependencies: + "@babel/runtime" "^7.23.8" + tslib "^2.6.2" + fastq@^1.6.0: version "1.19.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.0.tgz#a82c6b7c2bb4e44766d865f07997785fecfdcb89" @@ -2420,6 +2590,11 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +help-me@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" + integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -2474,6 +2649,11 @@ iconv-lite@^0.5.2: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -2510,7 +2690,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2534,6 +2714,14 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -2619,6 +2807,11 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +js-sdsl@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2631,6 +2824,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -2729,7 +2927,7 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -lru-cache@^10.2.0: +lru-cache@^10.2.0, lru-cache@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== @@ -2886,7 +3084,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.5: +minimist@^1.2.5, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -2896,6 +3094,35 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +mqtt-packet@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-9.0.2.tgz#fe6ae2c36fe3f269d11b3fe663b53648f3b3700a" + integrity sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA== + dependencies: + bl "^6.0.8" + debug "^4.3.4" + process-nextick-args "^2.0.1" + +mqtt@^5.11.1: + version "5.11.1" + resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-5.11.1.tgz#9b130e83e75f94ae19f87995522f37c2cb0e8916" + integrity sha512-GHDhnWW/npS6KTnmfcbmfIC9GNkNt7bx7AL6M4TUAIPbBNA454LTYXyp09P0OcaDuSrWS9U0XXicuRgBslbdhA== + dependencies: + commist "^3.2.0" + concat-stream "^2.0.0" + debug "^4.4.0" + help-me "^5.0.0" + lru-cache "^10.4.3" + minimist "^1.2.8" + mqtt-packet "^9.0.2" + number-allocator "^1.0.14" + readable-stream "^4.7.0" + rfdc "^1.4.1" + socks "^2.8.3" + split2 "^4.2.0" + worker-timers "^7.1.8" + ws "^8.18.0" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -2994,6 +3221,19 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +number-allocator@^1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/number-allocator/-/number-allocator-1.0.14.tgz#1f2e32855498a7740dcc8c78bed54592d930ee4d" + integrity sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA== + dependencies: + debug "^4.3.1" + js-sdsl "4.3.0" + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + object-assign@^4: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3011,6 +3251,11 @@ on-finished@2.4.1, on-finished@^2.4.1: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@1.4.0, once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3102,6 +3347,37 @@ parseurl@^1.3.3, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +passport-kakao@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/passport-kakao/-/passport-kakao-1.0.1.tgz#e0593558afc0aa6244fc298be676b259f62b5e7a" + integrity sha512-uItaYRVrTHL6iGPMnMZvPa/O1GrAdh/V6EMjOHcFlQcVroZ9wgG7BZ5PonMNJCxfHQ3L2QVNRnzhKWUzSsumbw== + dependencies: + passport-oauth2 "~1.1.2" + pkginfo "~0.3.0" + +passport-oauth2@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.1.2.tgz#bd7163b1b6090371868dc4ef6f9f2e1e4cc4b948" + integrity sha512-wpsGtJDHHQUjyc9WcV9FFB0bphFExpmKtzkQrxpH1vnSr6RcWa3ZEGHx/zGKAh2PN7Po9TKYB1fJeOiIBspNPA== + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -3145,6 +3421,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + picocolors@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -3155,6 +3436,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pkginfo@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + integrity sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -3181,6 +3467,16 @@ prisma@^6.1.0: optionalDependencies: fsevents "2.3.3" +process-nextick-args@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -3223,6 +3519,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== + range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -3267,6 +3568,26 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +readable-stream@^3.0.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.2.0, readable-stream@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -3287,6 +3608,11 @@ reflect-metadata@^0.2.2: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regexpp@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -3329,6 +3655,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -3364,11 +3695,16 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -3520,6 +3856,19 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks@^2.8.3: + version "2.8.4" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.4.tgz#07109755cdd4da03269bda4725baa061ab56d5cc" + integrity sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -3551,6 +3900,16 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3" integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== +split2@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -3583,6 +3942,13 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -3725,6 +4091,11 @@ ts-api-utils@^2.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== +ts-dedent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + ts-deepmerge@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-7.0.2.tgz#6333adcde83e4c42366e9a9a7f955c74ee913547" @@ -3797,6 +4168,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.5.2: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.0.tgz#7d249c2e2af716665cc149575dadb8b3858653af" @@ -3814,6 +4190,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typescript-eslint@^8.23.0: version "8.24.0" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.24.0.tgz#cc655e71885ecb8280342b422ad839a2e2e46a96" @@ -3833,6 +4214,18 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" @@ -3860,7 +4253,12 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -utils-merge@1.0.1: +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== @@ -3900,6 +4298,34 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== +worker-timers-broker@^6.1.8: + version "6.1.8" + resolved "https://registry.yarnpkg.com/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz#08f64e5931b77fadc55f0c7388c077a7dd17e4c7" + integrity sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ== + dependencies: + "@babel/runtime" "^7.24.5" + fast-unique-numbers "^8.0.13" + tslib "^2.6.2" + worker-timers-worker "^7.0.71" + +worker-timers-worker@^7.0.71: + version "7.0.71" + resolved "https://registry.yarnpkg.com/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz#f96138bafbcfaabea116603ce23956e05e76db6a" + integrity sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ== + dependencies: + "@babel/runtime" "^7.24.5" + tslib "^2.6.2" + +worker-timers@^7.1.8: + version "7.1.8" + resolved "https://registry.yarnpkg.com/worker-timers/-/worker-timers-7.1.8.tgz#f53072c396ac4264fd3027914f4ab793c92d90be" + integrity sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw== + dependencies: + "@babel/runtime" "^7.24.5" + tslib "^2.6.2" + worker-timers-broker "^6.1.8" + worker-timers-worker "^7.0.71" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -3940,6 +4366,11 @@ write-file-atomic@^4.0.0: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^8.18.0, ws@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"