From 6a0770bc744aa551cd28e98845ab81e4a7340af5 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Fri, 29 Nov 2024 21:09:51 +0900 Subject: [PATCH 01/19] =?UTF-8?q?fix:=20=EB=8F=84=EB=AC=B4=EC=A7=80?= =?UTF-8?q?=EC=9D=B4=ED=95=B4=ED=95=A0=EC=88=98=EC=97=84=EB=8A=94=EC=88=9C?= =?UTF-8?q?=ED=99=98=EC=A2=85=EC=86=8D=EC=84=B1=EC=9D=B4=EC=8A=88=EB=A5=BC?= =?UTF-8?q?=EC=96=B4=EC=B0=8C=EC=A0=80=EC=B0=8C=ED=95=B4=EA=B2=B0..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/auth/auth.module.ts | 2 +- src/modules/base/auth/core/auth.service.ts | 4 ++-- src/modules/base/essay/core/essay.service.ts | 2 +- src/modules/base/user/user.module.ts | 6 ++++-- src/modules/extensions/user/home/core/home.service.ts | 10 +++++----- src/modules/extensions/user/home/home.module.ts | 4 ++-- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/modules/base/auth/auth.module.ts b/src/modules/base/auth/auth.module.ts index 8a196cd..5e7aac2 100644 --- a/src/modules/base/auth/auth.module.ts +++ b/src/modules/base/auth/auth.module.ts @@ -29,7 +29,7 @@ import { UserModule } from '../user/user.module'; ToolModule, NicknameModule, AwsModule, - HomeModule, + forwardRef(() => HomeModule), forwardRef(() => UserModule), ], controllers: [AuthLocalController, AuthOauthController, AuthManagementController], diff --git a/src/modules/base/auth/core/auth.service.ts b/src/modules/base/auth/core/auth.service.ts index f77da18..f8a1771 100644 --- a/src/modules/base/auth/core/auth.service.ts +++ b/src/modules/base/auth/core/auth.service.ts @@ -1,5 +1,5 @@ import { HttpService } from '@nestjs/axios'; -import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { InjectRedis } from '@nestjs-modules/ioredis'; @@ -30,7 +30,7 @@ export class AuthService { constructor( @InjectRedis() private readonly redis: Redis, @Inject('IUserRepository') private readonly userRepository: IUserRepository, - private readonly userService: UserService, + @Inject(forwardRef(() => UserService)) private readonly userService: UserService, private readonly mailService: MailService, private readonly utilsService: ToolService, private readonly nicknameService: NicknameService, diff --git a/src/modules/base/essay/core/essay.service.ts b/src/modules/base/essay/core/essay.service.ts index 8b39604..71aa902 100644 --- a/src/modules/base/essay/core/essay.service.ts +++ b/src/modules/base/essay/core/essay.service.ts @@ -47,8 +47,8 @@ export class EssayService { private readonly badgeService: BadgeService, private readonly viewService: ViewService, private readonly bookmarkService: BookmarkService, - private readonly alertService: AlertService, private readonly supportService: SupportService, + @Inject(forwardRef(() => AlertService)) private readonly alertService: AlertService, @Inject(forwardRef(() => UserService)) private readonly userService: UserService, @InjectRedis() private readonly redis: Redis, ) {} diff --git a/src/modules/base/user/user.module.ts b/src/modules/base/user/user.module.ts index 5c3bdb2..f64e0ef 100644 --- a/src/modules/base/user/user.module.ts +++ b/src/modules/base/user/user.module.ts @@ -14,6 +14,7 @@ import { DeactivationReason } from '../../../entities/deactivationReason.entity' import { Essay } from '../../../entities/essay.entity'; import { User } from '../../../entities/user.entity'; import { AwsModule } from '../../adapters/aws/aws.module'; +import { HomeModule } from '../../extensions/user/home/home.module'; import { MailModule } from '../../utils/mail/mail.module'; import { NicknameModule } from '../../utils/nickname/nickname.module'; import { ToolModule } from '../../utils/tool/tool.module'; @@ -35,18 +36,19 @@ import { EssayModule } from '../essay/essay.module'; }), inject: [ConfigService], }), - MailModule, AwsModule, ToolModule, + MailModule, NicknameModule, + forwardRef(() => HomeModule), forwardRef(() => AuthModule), forwardRef(() => EssayModule), ], controllers: [UserQueryController, UserCommandController], providers: [ UserService, - { provide: 'IUserRepository', useClass: UserRepository }, UserProcessor, + { provide: 'IUserRepository', useClass: UserRepository }, strategies.JwtStrategy, ], exports: [UserService, { provide: 'IUserRepository', useClass: UserRepository }], diff --git a/src/modules/extensions/user/home/core/home.service.ts b/src/modules/extensions/user/home/core/home.service.ts index bcdd550..fc991f0 100644 --- a/src/modules/extensions/user/home/core/home.service.ts +++ b/src/modules/extensions/user/home/core/home.service.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { InjectRedis } from '@nestjs-modules/ioredis'; import Redis from 'ioredis'; import Redlock, { Lock } from 'redlock'; @@ -21,8 +21,8 @@ export class HomeService { constructor( @Inject('IHomeRepository') private readonly homeRepository: IHomeRepository, private readonly geulroquisService: GeulroquisService, - private readonly userService: UserService, - private readonly utilsService: ToolService, + private readonly toolService: ToolService, + @Inject(forwardRef(() => UserService)) private readonly userService: UserService, @Inject('REDLOCK') private readonly redlock: Redlock, @InjectRedis() private readonly redis: Redis, ) {} @@ -86,7 +86,7 @@ export class HomeService { isActive: theme.id === activeThemeId, })); - const themesDto = this.utilsService.transformToDto(ThemeResDto, themesWithOwnershipAndActive); + const themesDto = this.toolService.transformToDto(ThemeResDto, themesWithOwnershipAndActive); return { themes: themesDto }; } @@ -173,7 +173,7 @@ export class HomeService { owned: userItemIds.has(item.id), })); - const itemsDto = this.utilsService.transformToDto(ItemResDto, itemsWithOwnership); + const itemsDto = this.toolService.transformToDto(ItemResDto, itemsWithOwnership); return { items: itemsDto }; } diff --git a/src/modules/extensions/user/home/home.module.ts b/src/modules/extensions/user/home/home.module.ts index 3fc27e5..848234c 100644 --- a/src/modules/extensions/user/home/home.module.ts +++ b/src/modules/extensions/user/home/home.module.ts @@ -23,10 +23,10 @@ import { GeulroquisModule } from '../../essay/geulroquis/geulroquis.module'; ConfigModule, JwtModule.register({}), TypeOrmModule.forFeature([Item, Theme, UserTheme, UserItem, UserHomeLayout, UserHomeItem]), - GeulroquisModule, ToolModule, + UserModule, + forwardRef(() => GeulroquisModule), forwardRef(() => AuthModule), - forwardRef(() => UserModule), ], controllers: [HomeController], providers: [ From 9493e2a551f433072f3f8c8587ed5ce8d92869f4 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Sat, 30 Nov 2024 09:22:58 +0900 Subject: [PATCH 02/19] =?UTF-8?q?refactor:=20HomeModule=20CQRS=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=8B=9C=EB=B2=94=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.js | 15 + package-lock.json | 2436 +++++++++++++++-- package.json | 8 + src/app.module.ts | 3 +- .../base/essay/api/essay.query.controller.ts | 2 +- .../user/home/api/home.controller.ts | 180 -- .../user/home/api/home.query.controller.ts | 81 + .../extensions/user/home/core/home.service.ts | 43 +- .../extensions/user/home/home.module.ts | 9 +- .../utils/seeder/core/seeder.service.ts | 20 +- src/modules/utils/seeder/seeder.module.ts | 6 +- src/modules/utils/tool/core/tool.service.ts | 15 +- 12 files changed, 2443 insertions(+), 375 deletions(-) delete mode 100644 src/modules/extensions/user/home/api/home.controller.ts create mode 100644 src/modules/extensions/user/home/api/home.query.controller.ts diff --git a/eslint.config.js b/eslint.config.js index 8ebf37c..d8dd2a6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -54,6 +54,10 @@ module.exports = [ plugins: { prettier: require('eslint-plugin-prettier'), import: require('eslint-plugin-import'), + sonarjs: require('eslint-plugin-sonarjs'), + security: require('eslint-plugin-security'), + promise: require('eslint-plugin-promise'), + 'optimize-regex': require('eslint-plugin-optimize-regex'), }, rules: { 'prettier/prettier': 'error', @@ -65,6 +69,17 @@ module.exports = [ alphabetize: { order: 'asc', caseInsensitive: true }, }, ], + 'sonarjs/no-duplicate-string': 'warn', + 'sonarjs/no-identical-functions': 'warn', + 'sonarjs/cognitive-complexity': ['warn', 15], + + 'security/detect-object-injection': 'warn', + 'security/detect-unsafe-regex': 'error', + + 'promise/always-return': 'error', + 'promise/no-return-wrap': 'error', + + 'optimize-regex/optimize-regex': 'warn', }, }, ]; diff --git a/package-lock.json b/package-lock.json index fdc8481..1d4ebc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@nestjs/common": "^10.4.4", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.4", + "@nestjs/cqrs": "^10.2.8", "@nestjs/devtools-integration": "^0.1.6", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", @@ -99,7 +100,14 @@ "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsonc": "^2.18.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-optimize-regex": "^1.2.1", "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-security": "^3.0.1", + "eslint-plugin-sonarjs": "^2.0.4", + "eslint-plugin-yml": "^1.15.0", "jest": "^29.7.0", "moment-timezone": "^0.5.46", "prettier": "^3.3.3", @@ -1243,13 +1251,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -1297,14 +1306,44 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/eslint-parser": { + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz", + "integrity": "sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/@babel/generator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", - "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7", + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -1314,13 +1353,13 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", - "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1429,14 +1468,14 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", - "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1475,9 +1514,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", - "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, "license": "MIT", "engines": { @@ -1549,9 +1588,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", "engines": { @@ -1559,9 +1598,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "license": "MIT", "engines": { @@ -1607,30 +1646,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", - "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.8" + "@babel/types": "^7.26.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1825,6 +1848,48 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", + "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", @@ -1884,13 +1949,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", - "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2313,6 +2378,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz", + "integrity": "sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-syntax-flow": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", @@ -2683,6 +2765,75 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", + "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", + "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", + "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", + "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", @@ -2967,6 +3118,24 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.7.tgz", + "integrity": "sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-flow-strip-types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -2982,6 +3151,27 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-typescript": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz", @@ -3016,32 +3206,32 @@ } }, "node_modules/@babel/template": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", - "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", - "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/generator": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -3050,15 +3240,14 @@ } }, "node_modules/@babel/types": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", - "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -5112,6 +5301,34 @@ } } }, + "node_modules/@nestjs/cqrs": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@nestjs/cqrs/-/cqrs-10.2.8.tgz", + "integrity": "sha512-nes4J9duwogme6CzNg8uhF5WVbSKYnNotjhHP+3kJxe6RTzcvJDZN10KpROjWJALEVO5fNmKnkGMecoOfqYzYA==", + "license": "MIT", + "dependencies": { + "uuid": "11.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0" + } + }, + "node_modules/@nestjs/cqrs/node_modules/uuid": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/@nestjs/devtools-integration": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@nestjs/devtools-integration/-/devtools-integration-0.1.6.tgz", @@ -5478,6 +5695,40 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8502,6 +8753,16 @@ "dev": true, "license": "MIT" }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -8560,6 +8821,37 @@ "dev": true, "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", @@ -8619,6 +8911,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", @@ -8659,6 +8968,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -8710,6 +9026,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "1.7.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", @@ -8721,7 +9047,17 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/babel-jest": { + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", @@ -9319,6 +9655,19 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bull": { "version": "4.16.3", "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.3.tgz", @@ -10440,6 +10789,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -10552,6 +10908,46 @@ } } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -11287,6 +11683,61 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", + "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -11436,6 +11887,35 @@ } } }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -11461,48 +11941,1159 @@ "resolve": "^1.22.4" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-json-compat-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz", + "integrity": "sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esquery": "^1.6.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": "*", + "jsonc-eslint-parser": "^2.4.0" + }, + "peerDependenciesMeta": { + "@eslint/json": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-jsonc": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.18.2.tgz", + "integrity": "sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "eslint-compat-utils": "^0.6.0", + "eslint-json-compat-utils": "^0.2.1", + "espree": "^9.6.1", + "graphemer": "^1.4.0", + "jsonc-eslint-parser": "^2.0.4", + "natural-compare": "^1.4.0", + "synckit": "^0.6.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-jsonc/node_modules/eslint-compat-utils": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz", + "integrity": "sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-jsonc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsonc/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-jsonc/node_modules/synckit": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.6.2.tgz", + "integrity": "sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", + "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "~5.1.3", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-optimize-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-optimize-regex/-/eslint-plugin-optimize-regex-1.2.1.tgz", + "integrity": "sha512-fUaU7Tj1G/KSTDTABJw4Wp427Rl7RPl9ViYTu1Jrv36fJw4DFhd4elPdXiuYtdPsNsvzn9GcVlKEssGIVjw0UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "^0.1.21" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-promise": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", + "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.36.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz", + "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-security": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz", + "integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-sonarjs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-2.0.4.tgz", + "integrity": "sha512-XVVAB/t0WSgHitHNajIcIDmviCO8kB9VSsrjy+4WUEVM3eieY9SDHEtCDaOMTjj6XMtcAr8BFDXCFaP005s+tg==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@babel/core": "7.25.2", + "@babel/eslint-parser": "7.25.1", + "@babel/plugin-proposal-decorators": "7.24.7", + "@babel/preset-env": "7.25.4", + "@babel/preset-flow": "7.24.7", + "@babel/preset-react": "7.24.7", + "@eslint-community/regexpp": "4.11.1", + "@typescript-eslint/eslint-plugin": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "builtin-modules": "3.3.0", + "bytes": "3.1.2", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-jsx-a11y": "6.10.0", + "eslint-plugin-react": "7.36.1", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-scope": "8.1.0", + "functional-red-black-tree": "1.0.1", + "jsx-ast-utils": "3.3.5", + "minimatch": "10.0.1", + "scslre": "0.3.0", + "semver": "7.6.3", + "typescript": "5.6.2", + "vue-eslint-parser": "9.4.3" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@babel/plugin-proposal-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", + "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@babel/preset-env": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", + "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.4", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.25.4", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ms": "^2.1.1" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7" + "@typescript-eslint/types": "7.16.1", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=4" + "node": "^18.18.0 || >=20.0.0" }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", + "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", "dev": true, "license": "MIT", "dependencies": { @@ -11514,7 +13105,7 @@ "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.9.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", @@ -11523,17 +13114,16 @@ "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", @@ -11544,7 +13134,7 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", @@ -11554,33 +13144,93 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "minimist": "^1.2.0" + "brace-expansion": "^1.1.7" }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", "bin": { - "json5": "lib/cli.js" + "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-plugin-import/node_modules/strip-bom": { + "node_modules/eslint-plugin-sonarjs/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", @@ -11590,7 +13240,7 @@ "node": ">=4" } }, - "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "node_modules/eslint-plugin-sonarjs/node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", @@ -11603,35 +13253,54 @@ "strip-bom": "^3.0.0" } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "node_modules/eslint-plugin-sonarjs/node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": ">=14.17" + } + }, + "node_modules/eslint-plugin-yml": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-1.15.0.tgz", + "integrity": "sha512-leC8APYVOsKyWUlvRwVhewytK5wS70BfMqIaUplFstRfzCoVp0YoEroV4cUEvQrBj93tQ3M9LcjO/ewr6D4kjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.2", + "eslint-compat-utils": "^0.5.0", + "lodash": "^4.17.21", + "natural-compare": "^1.4.0", + "yaml-eslint-parser": "^1.2.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" + "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } + "eslint": ">=6.0.0" } }, "node_modules/eslint-scope": { @@ -11651,6 +13320,32 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -12917,8 +14612,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -13967,6 +15662,23 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -14612,6 +16324,23 @@ "node": ">=6" } }, + "node_modules/iterator.prototype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -16636,24 +18365,74 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-eslint-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz", + "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/jsonc-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "MIT" + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/jsonc-eslint-parser/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "license": "MIT", + "license": "ISC", "bin": { - "json5": "lib/cli.js" + "semver": "bin/semver.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/jsonc-parser": { @@ -16731,6 +18510,22 @@ "node": ">=10" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -16822,6 +18617,26 @@ "node": ">=6" } }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -17149,6 +18964,19 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -21081,6 +22909,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -21110,6 +22955,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.fromentries": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", @@ -22203,6 +24063,25 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -22557,6 +24436,19 @@ "node": ">=12" } }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -22622,6 +24514,30 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -22641,6 +24557,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/regexpu-core": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", @@ -22980,6 +24909,16 @@ ], "license": "MIT" }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -23083,6 +25022,21 @@ "dev": true, "license": "MIT" }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, "node_modules/semantic-release": { "version": "24.1.2", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.1.2.tgz", @@ -23879,6 +25833,19 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-combiner2": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", @@ -24000,6 +25967,59 @@ "node": ">=8" } }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -24745,16 +26765,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -25890,6 +27900,79 @@ "node": ">= 0.8" } }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -26348,6 +28431,37 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yaml-eslint-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.2.3.tgz", + "integrity": "sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "lodash": "^4.17.21", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index a01c67e..bb7bfb5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@nestjs/common": "^10.4.4", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.4", + "@nestjs/cqrs": "^10.2.8", "@nestjs/devtools-integration": "^0.1.6", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", @@ -114,7 +115,14 @@ "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsonc": "^2.18.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-optimize-regex": "^1.2.1", "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-security": "^3.0.1", + "eslint-plugin-sonarjs": "^2.0.4", + "eslint-plugin-yml": "^1.15.0", "jest": "^29.7.0", "moment-timezone": "^0.5.46", "prettier": "^3.3.3", diff --git a/src/app.module.ts b/src/app.module.ts index 9d344f9..6ee83c2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -93,7 +93,8 @@ export class AppModule implements OnModuleInit, NestModule { await this.seederService.initializeServer(); await this.seederService.initializeAdmin(); await this.seederService.initializeAppVersions(); - // await this.seederService.initializeNicknames(); + await this.seederService.initializeDefaultTheme(); + // await this.seederService.initializeNicknames() } this.configService.set('APP_INITIALIZING', false); diff --git a/src/modules/base/essay/api/essay.query.controller.ts b/src/modules/base/essay/api/essay.query.controller.ts index f661d1a..8be01b1 100644 --- a/src/modules/base/essay/api/essay.query.controller.ts +++ b/src/modules/base/essay/api/essay.query.controller.ts @@ -37,7 +37,7 @@ export class EssayQueryController { **쿼리 파라미터:** - \`page\` (number, optional): 조회할 페이지를 지정합니다. 기본값은 1입니다. - \`limit\` (number, optional): 조회할 에세이 수를 지정합니다. 기본값은 10입니다. - - \`pageType\`: '나만의 글'에선 'private', '발행한 글'에선 'public' 를 사용합니다. 'story'인 경우 자신의 에세이인지 검증합니다. + - \`pageType\`: '나만의 글'에선 'private', '발행한 글'에선 'public' 를 사용합니다. 'story'인 경우 자신의 에세이만 조회할 수 있습니다. - \`storyId\`: (number, optional): \`pageType.STORY\`인 경우 제공. **동작 과정:** diff --git a/src/modules/extensions/user/home/api/home.controller.ts b/src/modules/extensions/user/home/api/home.controller.ts deleted file mode 100644 index e7b2b70..0000000 --- a/src/modules/extensions/user/home/api/home.controller.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { Controller, Get, Param, ParseIntPipe, Post, Query, Req, UseGuards } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Request as ExpressRequest } from 'express'; - -import { JwtAuthGuard } from '../../../../../common/guards/jwtAuth.guard'; -import { GeulroquisUrlResDto } from '../../../essay/geulroquis/dto/response/geulroquisUrlRes.dto'; -import { HomeService } from '../core/home.service'; -import { ItemsResDto } from '../dto/response/itemsRes.dto'; -import { ThemesResDto } from '../dto/response/themesRes.dto'; - -@ApiTags('Home') -@Controller('home') -@UseGuards(JwtAuthGuard) -export class HomeController { - constructor(private readonly homeService: HomeService) {} - - @Get('geulroquis') - @ApiOperation({ - summary: '오늘의 글로키 이미지 주소 조회', - description: ` - 매일 자정마다 초기화되는 글로키 이미지 주소를 조회합니다. - - **주의 사항:** - - 유효하지 않은 토큰을 제공하면 \`404 Not Found\` 에러가 발생합니다. - `, - }) - @ApiResponse({ status: 200, type: GeulroquisUrlResDto }) - async todayGeulroquis() { - return this.homeService.todayGeulroquis(); - } - - @Get('themes') - @ApiOperation({ - summary: '테마 리스트', - description: ` - 현재 서비스에서 제공되는 모든 테마를 조회하고, 사용자가 이미 소유한 테마인지 여부를 확인합니다. - 이 API는 사용자에게 제공되는 테마와 그 소유 상태를 한 번에 파악할 수 있는 기능을 제공합니다. - - **동작 과정:** - 1. 사용자가 테마를 처음 조회할 때 기본 테마인 'Workaholic' 테마가 자동으로 부여됩니다. - 2. 사용자에게 할당된 모든 테마를 데이터베이스에서 조회합니다. - 3. 서비스에서 제공 중인 전체 테마 목록을 조회합니다. - 4. 조회된 테마 목록을 사용자가 소유한 테마와 비교하여 각 테마의 소유 여부를 판단합니다. - 5. 사용자가 소유한 테마와 소유하지 않은 테마 정보를 함께 포함한 리스트를 클라이언트에게 반환합니다. - `, - }) - @ApiResponse({ status: 200, type: ThemesResDto }) - async getThemes(@Req() req: ExpressRequest) { - return this.homeService.getThemes(req.user.id); - } - - @Post('themes/buy/:themeId') - @ApiOperation({ - summary: '테마 구매', - description: ` - 사용자가 특정 테마를 구매합니다. - - **경로 파라미터:** - - \`themeId\`: 구매할 테마 아이디 - - **동작 과정:** - 1. 사용자의 고유 ID와 구매할 테마의 ID를 인자로 받아 처리합니다. - 2. 먼저, 사용자가 이미 해당 테마를 소유하고 있는지 확인합니다. - 3. 만약 이미 소유한 테마라면, '이미 소유한 테마입니다'라는 오류 메시지를 반환합니다. - 4. 소유하지 않은 테마인 경우, 사용자의 재화(gems)를 확인하여 해당 테마를 구매할 수 있는지 검증합니다. - 5. 재화가 충분하지 않다면, '재화가 부족합니다'라는 오류 메시지를 반환합니다. - 6. 재화가 충분한 경우, 테마의 가격만큼 사용자의 재화를 차감하고, 사용자의 테마 목록에 해당 테마를 추가합니다. - 7. 테마 구매가 성공적으로 완료되었음을 클라이언트에게 알립니다. - - **주의 사항:** - - 서버에서 트랜잭션 관리를 하겠지만, 클라이언트 측에서도 구매요청 시 충돌방지를 위해 통신이 끝나기까지 다른 조작이 불가능하도록 하면 좋을 것 같습니다. - `, - }) - @ApiResponse({ status: 201 }) - async buyTheme(@Req() req: ExpressRequest, @Param('themeId', ParseIntPipe) themeId: number) { - return this.homeService.buyTheme(req.user.id, themeId); - } - - @Post('themes/activate/:themeId') - @ApiOperation({ - summary: '사용자 테마 활성화', - description: ` - 사용자가 소유한 테마 중에서 선택한 테마로 현재 사용 중인 테마를 변경합니다. - 기존에 사용 중인 테마는 비활성화되고, 선택한 테마의 레이아웃이 활성화됩니다. - - **쿼리 파라미터:** - - \`themeId\`: 변경하려는 테마의 ID - - **동작 과정:** - 1. 클라이언트는 변경하고자 하는 테마의 ID를 서버에 전달합니다. - 2. 서버는 사용자가 해당 테마를 소유하고 있는지 확인합니다. - 3. 사용자가 해당 테마를 소유하고 있다면, 현재 사용 중인 테마 레이아웃을 비활성화하고 선택한 테마의 레이아웃을 활성화합니다. - 4. 변경된 테마 레이아웃을 저장합니다. - `, - }) - @ApiResponse({ status: 200 }) - async changeTheme(@Req() req: ExpressRequest, @Param('themeId', ParseIntPipe) themeId: number) { - return this.homeService.changeTheme(req.user.id, themeId); - } - - @Get('items') - @ApiOperation({ - summary: '아이템 조회', - description: ` - 사용자가 선택한 테마와 포지션에 따라 아이템 목록을 조회하고, 각 아이템이 사용자가 소유한 것인지 여부를 확인합니다. - - **쿼리 파라미터:** - - \`themeId\`: 조회할 아이템들의 테마 - - \`position\`: 조회할 아이템들의 포지션 - - **동작 과정:** - 1. 클라이언트에서 선택한 테마 ID와 포지션을 기준으로 아이템을 조회합니다. - 2. 해당 테마와 포지션에 해당하는 아이템 목록을 캐시에서 조회합니다. - 3. 캐시된 데이터가 없으면 데이터베이스에서 아이템을 조회하고, 그 결과를 캐시합니다. - 4. 사용자가 이미 소유한 아이템 목록을 데이터베이스에서 조회합니다. - 5. 모든 아이템에 대해 사용자가 소유한 여부를 판별하고, 소유 여부 정보를 아이템 리스트에 추가합니다. - 6. 사용자에게 테마와 포지션에 따른 아이템 리스트를 소유 여부와 함께 반환합니다. - - 이 API는 특정 테마와 위치에서 사용 가능한 아이템 목록과 그 소유 상태를 조회하는 데 유용합니다. - `, - }) - @ApiResponse({ status: 200, type: ItemsResDto }) - async getItems( - @Req() req: ExpressRequest, - @Query('themeId') themeId: number, - @Query('position') position: string, - ) { - return this.homeService.getItems(req.user.id, themeId, position); - } - - @Post('items/buy/:itemId') - @ApiOperation({ - summary: '아이템 구매', - description: ` - 사용자가 특정 아이템을 구매합니다. - - **동작 과정:** - 1. 사용자의 고유 ID와 구매할 아이템의 ID를 인자로 받아 처리합니다. - 2. 사용자가 이미 해당 아이템을 소유하고 있는지 확인합니다. - 3. 만약 이미 소유한 아이템이라면, '이미 소유한 아이템입니다'라는 오류 메시지를 반환합니다. - 4. 소유하지 않은 아이템인 경우, 사용자의 재화(gems)를 확인하여 해당 아이템을 구매할 수 있는지 검증합니다. - 5. 재화가 충분하지 않다면, '재화가 부족합니다'라는 오류 메시지를 반환합니다. - 6. 재화가 충분한 경우, 아이템의 가격만큼 사용자의 재화를 차감하고, 사용자의 아이템 목록에 해당 아이템을 추가합니다. - 7. 아이템 구매가 성공적으로 완료되었음을 클라이언트에게 알립니다. - - **주의 사항:** - - 서버에서 트랜잭션 관리를 하겠지만, 클라이언트 측에서도 구매요청 시 충돌방지를 위해 통신이 끝나기까지 다른 조작이 불가능하도록 하면 좋을 것 같습니다. - `, - }) - @ApiResponse({ status: 201 }) - async buyItem(@Req() req: ExpressRequest, @Param('itemId', ParseIntPipe) itemId: number) { - return this.homeService.buyItem(req.user.id, itemId); - } - - @Post('items/activate/:itemId') - @ApiOperation({ - summary: '아이템 활성화', - description: ` - 사용자가 소유한 아이템을 현재 사용 중인 테마의 레이아웃에 활성화합니다. - - **동작 과정:** - 1. 클라이언트는 활성화할 아이템의 ID를 서버에 전달합니다. - 2. 서버는 현재 활성화된 테마의 레이아웃을 가져옵니다. - 3. 아이템이 현재 테마에 속해 있는지 확인하고, 사용자가 해당 아이템을 소유하고 있는지 확인합니다. - 4. 동일한 포지션에 이미 활성화된 아이템이 있는 경우, 기존 아이템을 비활성화합니다. - 5. 선택한 아이템을 활성화하여 레이아웃에 등록합니다. - - **경로 파라미터:** - - \`itemId\`: 활성화할 아이템의 ID. - - **예외 처리:** - - \`활성화된 레이아웃이 없습니다.\`: 사용자가 활성화한 테마 레이아웃이 없는 경우 발생. - - \`해당 아이템이 존재하지 않거나, 현재 테마에 속하지 않습니다.\`: 아이템이 현재 테마에 속하지 않거나 존재하지 않을 경우 발생. - - \`해당 아이템을 소유하고 있지 않습니다.\`: 사용자가 해당 아이템을 소유하지 않은 경우 발생. - `, - }) - async activateItem(@Req() req: ExpressRequest, @Param('itemId', ParseIntPipe) itemId: number) { - return this.homeService.activateItem(req.user.id, itemId); - } -} diff --git a/src/modules/extensions/user/home/api/home.query.controller.ts b/src/modules/extensions/user/home/api/home.query.controller.ts new file mode 100644 index 0000000..16db004 --- /dev/null +++ b/src/modules/extensions/user/home/api/home.query.controller.ts @@ -0,0 +1,81 @@ +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Request as ExpressRequest } from 'express'; + +import { JwtAuthGuard } from '../../../../../common/guards/jwtAuth.guard'; +import { GeulroquisUrlResDto } from '../../../essay/geulroquis/dto/response/geulroquisUrlRes.dto'; +import { HomeService } from '../core/home.service'; +import { ItemsResDto } from '../dto/response/itemsRes.dto'; +import { ThemesResDto } from '../dto/response/themesRes.dto'; + +@ApiTags('Home-query') +@Controller('home') +@UseGuards(JwtAuthGuard) +export class HomeQueryController { + constructor(private readonly homeService: HomeService) {} + + @Get('geulroquis') + @ApiOperation({ + summary: '오늘의 글로키 이미지 주소 조회', + description: ` + 매일 자정마다 초기화되는 글로키 이미지 주소를 조회합니다. + + **주의 사항:** + - 유효하지 않은 토큰을 제공하면 \`404 Not Found\` 에러가 발생합니다. + `, + }) + @ApiResponse({ status: 200, type: GeulroquisUrlResDto }) + async todayGeulroquis() { + return this.homeService.todayGeulroquis(); + } + + @Get('themes') + @ApiOperation({ + summary: '테마 리스트', + description: ` + 현재 서비스에서 제공되는 모든 테마를 조회하고, 사용자가 이미 소유한 테마인지 여부를 확인합니다. + 이 API는 사용자에게 제공되는 테마와 그 소유 상태를 한 번에 파악할 수 있는 기능을 제공합니다. + + **동작 과정:** + 1. 사용자가 테마를 처음 조회할 때 기본 테마인 'Workaholic' 테마가 자동으로 부여됩니다. + 2. 사용자에게 할당된 모든 테마를 데이터베이스에서 조회합니다. + 3. 서비스에서 제공 중인 전체 테마 목록을 조회합니다. + 4. 조회된 테마 목록을 사용자가 소유한 테마와 비교하여 각 테마의 소유 여부를 판단합니다. + 5. 사용자가 소유한 테마와 소유하지 않은 테마 정보를 함께 포함한 리스트를 클라이언트에게 반환합니다. + `, + }) + @ApiResponse({ status: 200, type: ThemesResDto }) + async getThemes(@Req() req: ExpressRequest) { + return this.homeService.getThemes(req.user.id); + } + + @Get('items') + @ApiOperation({ + summary: '아이템 조회', + description: ` + 사용자가 선택한 테마와 포지션에 따라 아이템 목록을 조회하고, 각 아이템이 사용자가 소유한 것인지 여부를 확인합니다. + + **쿼리 파라미터:** + - \`themeId\`: 조회할 아이템들의 테마 + - \`position\`: 조회할 아이템들의 포지션 + + **동작 과정:** + 1. 클라이언트에서 선택한 테마 ID와 포지션을 기준으로 아이템을 조회합니다. + 2. 해당 테마와 포지션에 해당하는 아이템 목록을 캐시에서 조회합니다. + 3. 캐시된 데이터가 없으면 데이터베이스에서 아이템을 조회하고, 그 결과를 캐시합니다. + 4. 사용자가 이미 소유한 아이템 목록을 데이터베이스에서 조회합니다. + 5. 모든 아이템에 대해 사용자가 소유한 여부를 판별하고, 소유 여부 정보를 아이템 리스트에 추가합니다. + 6. 사용자에게 테마와 포지션에 따른 아이템 리스트를 소유 여부와 함께 반환합니다. + + 이 API는 특정 테마와 위치에서 사용 가능한 아이템 목록과 그 소유 상태를 조회하는 데 유용합니다. + `, + }) + @ApiResponse({ status: 200, type: ItemsResDto }) + async getItems( + @Req() req: ExpressRequest, + @Query('themeId') themeId: number, + @Query('position') position: string, + ) { + return this.homeService.getItems(req.user.id, themeId, position); + } +} diff --git a/src/modules/extensions/user/home/core/home.service.ts b/src/modules/extensions/user/home/core/home.service.ts index fc991f0..c7cb13e 100644 --- a/src/modules/extensions/user/home/core/home.service.ts +++ b/src/modules/extensions/user/home/core/home.service.ts @@ -6,6 +6,7 @@ import { Transactional } from 'typeorm-transactional'; import { Item } from '../../../../../entities/item.entity'; import { Theme } from '../../../../../entities/theme.entity'; +import { User } from '../../../../../entities/user.entity'; import { UserHomeItem } from '../../../../../entities/userHomeItem.entity'; import { UserHomeLayout } from '../../../../../entities/userHomeLayout.entity'; import { UserTheme } from '../../../../../entities/userTheme.entity'; @@ -27,7 +28,7 @@ export class HomeService { @InjectRedis() private readonly redis: Redis, ) {} - private async acquireLock(lockKey: string, ttl: number) { + async acquireLock(lockKey: string, ttl: number) { try { return await this.redlock.acquire([lockKey], ttl); } catch (err) { @@ -38,7 +39,7 @@ export class HomeService { } } - private async releaseLock(lock: Lock) { + async releaseLock(lock: Lock) { try { await lock.release(); } catch (err) { @@ -67,6 +68,7 @@ export class HomeService { let activeLayout: UserHomeLayout; const userThemes = await this.homeRepository.findUserThemes(userId); + if (userThemes.length === 0) { const { userTheme, userLayout } = await this.createDefaultTheme(userId); userThemes.push(userTheme); @@ -76,6 +78,7 @@ export class HomeService { if (!activeLayout) { activeLayout = await this.homeRepository.findActiveLayoutByUserId(userId); } + const activeThemeId = activeLayout ? activeLayout.theme.id : null; const userThemeIds = new Set(userThemes.map((theme) => theme.theme.id)); @@ -110,42 +113,30 @@ export class HomeService { return { userTheme, userLayout }; } - async buyTheme(userId: number, themeId: number) { - const lockKey = `buyThemeLock:${userId}`; - const ttl = 10000; - - const lock = await this.acquireLock(lockKey, ttl); - - try { - await this.executeBuyThemeTransaction(userId, themeId); - } catch (err) { - throw new HttpException('테마 구매중 오류가 발생했습니다.', HttpStatus.INTERNAL_SERVER_ERROR); - } finally { - await this.releaseLock(lock); + async getThemePrice(themeId: number): Promise { + const theme = await this.homeRepository.findThemeById(themeId); + if (!theme) { + throw new HttpException('존재하지 않는 테마입니다.', HttpStatus.BAD_REQUEST); } + return theme.price; } @Transactional() - private async executeBuyThemeTransaction(userId: number, themeId: number) { - const user = await this.userService.fetchUserEntityById(userId); - const userThemes = await this.homeRepository.findUserThemes(userId); + async addThemeToUser(user: User, themeId: number): Promise { + const userThemes = await this.homeRepository.findUserThemes(user.id); const alreadyOwned = userThemes.some((userTheme) => userTheme.theme.id === themeId); - if (alreadyOwned) throw new HttpException('이미 소유한 테마입니다.', HttpStatus.BAD_REQUEST); + if (alreadyOwned) { + throw new HttpException('이미 소유한 테마입니다.', HttpStatus.BAD_REQUEST); + } const theme = await this.homeRepository.findThemeById(themeId); - if (!theme) throw new HttpException('존재하지 않는 테마입니다.', HttpStatus.BAD_REQUEST); - - if (user.gems < theme.price) { - throw new HttpException('재화가 부족합니다.', HttpStatus.BAD_REQUEST); + if (!theme) { + throw new HttpException('존재하지 않는 테마입니다.', HttpStatus.BAD_REQUEST); } - user.gems -= theme.price; - await this.homeRepository.saveNewUserTheme(user, theme); await this.homeRepository.createNewUserHomeLayout(user, theme); - - await this.userService.updateUser(userId, user); } async getItems(userId: number, themeId: number, position: string) { diff --git a/src/modules/extensions/user/home/home.module.ts b/src/modules/extensions/user/home/home.module.ts index 848234c..4eabe73 100644 --- a/src/modules/extensions/user/home/home.module.ts +++ b/src/modules/extensions/user/home/home.module.ts @@ -1,9 +1,12 @@ import { forwardRef, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { HomeController } from './api/home.controller'; +import { HomeCommandController } from './api/home.command.controller'; +import { HomeQueryController } from './api/home.query.controller'; +import { CommandHandlers } from './command'; import { HomeService } from './core/home.service'; import { HomeRepository } from './infrastructure/home.repository'; import { RedlockProvider } from '../../../../config/redlock.provider'; @@ -20,6 +23,7 @@ import { GeulroquisModule } from '../../essay/geulroquis/geulroquis.module'; @Module({ imports: [ + CqrsModule, ConfigModule, JwtModule.register({}), TypeOrmModule.forFeature([Item, Theme, UserTheme, UserItem, UserHomeLayout, UserHomeItem]), @@ -28,11 +32,12 @@ import { GeulroquisModule } from '../../essay/geulroquis/geulroquis.module'; forwardRef(() => GeulroquisModule), forwardRef(() => AuthModule), ], - controllers: [HomeController], + controllers: [HomeQueryController, HomeCommandController], providers: [ HomeService, { provide: 'IHomeRepository', useClass: HomeRepository }, RedlockProvider, + ...CommandHandlers, ], exports: [HomeService], }) diff --git a/src/modules/utils/seeder/core/seeder.service.ts b/src/modules/utils/seeder/core/seeder.service.ts index 4fbc75c..87668d0 100644 --- a/src/modules/utils/seeder/core/seeder.service.ts +++ b/src/modules/utils/seeder/core/seeder.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import * as bcrypt from 'bcrypt'; import { Repository } from 'typeorm'; @@ -8,6 +9,7 @@ import { Admin } from '../../../../entities/admin.entity'; import { AppVersions } from '../../../../entities/appVersions.entity'; import { BasicNickname } from '../../../../entities/basicNickname.entity'; import { Server } from '../../../../entities/server.entity'; +import { Theme } from '../../../../entities/theme.entity'; import { ToolService } from '../../tool/core/tool.service'; @Injectable() @@ -21,10 +23,26 @@ export class SeederService { private readonly serverRepository: Repository, @InjectRepository(AppVersions) private readonly appVersionsRepository: Repository, - + @InjectRepository(Theme) + private readonly themeRepository: Repository, + private readonly configService: ConfigService, private readonly utilsService: ToolService, ) {} + async initializeDefaultTheme() { + const theme = await this.themeRepository.findOne({ where: { id: 1 } }); + if (!theme) { + const defaultTheme = this.themeRepository.create({ + id: 1, + name: 'workaholic', + price: 0, + url: this.configService.get('DEFAULT_THEME_URL'), + }); + + await this.themeRepository.save(defaultTheme); + } + } + async initializeNicknames(): Promise { console.log('Basic nickname created started'); const nicknames: any = []; diff --git a/src/modules/utils/seeder/seeder.module.ts b/src/modules/utils/seeder/seeder.module.ts index 1ea1226..6a811c3 100644 --- a/src/modules/utils/seeder/seeder.module.ts +++ b/src/modules/utils/seeder/seeder.module.ts @@ -6,10 +6,14 @@ import { Admin } from '../../../entities/admin.entity'; import { AppVersions } from '../../../entities/appVersions.entity'; import { BasicNickname } from '../../../entities/basicNickname.entity'; import { Server } from '../../../entities/server.entity'; +import { Theme } from '../../../entities/theme.entity'; import { ToolModule } from '../tool/tool.module'; @Module({ - imports: [TypeOrmModule.forFeature([Admin, BasicNickname, Server, AppVersions]), ToolModule], + imports: [ + TypeOrmModule.forFeature([Admin, BasicNickname, Server, AppVersions, Theme]), + ToolModule, + ], providers: [SeederService], exports: [SeederService], }) diff --git a/src/modules/utils/tool/core/tool.service.ts b/src/modules/utils/tool/core/tool.service.ts index da8b1a3..ed69398 100644 --- a/src/modules/utils/tool/core/tool.service.ts +++ b/src/modules/utils/tool/core/tool.service.ts @@ -202,7 +202,7 @@ export class ToolService { } sentences(text: string, minLength: number, maxLength: number) { - const sentenceEndings = /([.?!]+)/; + const sentenceEndings = /([!.?]+)/; return text .split(sentenceEndings) .reduce((acc, current, index, array) => { @@ -270,7 +270,7 @@ export class ToolService { } extractContentFromHtml(htmlContent: string): string { - const bodyContentMatch = htmlContent.match(/]*>([\s\S]*?)<\/body>/i); + const bodyContentMatch = htmlContent.match(/]*>([\S\s]*?)<\/body>/i); if (bodyContentMatch && bodyContentMatch[1]) { return bodyContentMatch[1]; @@ -307,4 +307,15 @@ export class ToolService { } return newTrendScore; } + + useScopeExit(onExit: () => void): () => void { + let called = false; + + return () => { + if (!called) { + called = true; + onExit(); + } + }; + } } From 21ae3e817cf693128e1533fd05f659ef80872823 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Sat, 30 Nov 2024 09:23:02 +0900 Subject: [PATCH 03/19] =?UTF-8?q?refactor:=20HomeModule=20CQRS=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=8B=9C=EB=B2=94=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/home/api/home.command.controller.ts | 117 ++++++++++++++++++ .../user/home/command/activateItem.command.ts | 6 + .../user/home/command/buyItem.command.ts | 6 + .../user/home/command/buyTheme.command.ts | 6 + .../user/home/command/changeTheme.command.ts | 6 + .../command/handler/activateItem.handler.ts | 0 .../handler/base/baseCommand.handler.ts | 15 +++ .../home/command/handler/buyItem.handler.ts | 0 .../home/command/handler/buyTheme.handler.ts | 34 +++++ .../command/handler/changeTheme.handler.ts | 0 .../extensions/user/home/command/index.ts | 15 +++ .../user/home/query/getItems.payload.ts | 7 ++ .../user/home/query/getThemes.payload.ts | 3 + .../home/query/getTodayGeulroquis.payload.ts | 1 + .../user/home/query/handler/index.ts | 0 15 files changed, 216 insertions(+) create mode 100644 src/modules/extensions/user/home/api/home.command.controller.ts create mode 100644 src/modules/extensions/user/home/command/activateItem.command.ts create mode 100644 src/modules/extensions/user/home/command/buyItem.command.ts create mode 100644 src/modules/extensions/user/home/command/buyTheme.command.ts create mode 100644 src/modules/extensions/user/home/command/changeTheme.command.ts create mode 100644 src/modules/extensions/user/home/command/handler/activateItem.handler.ts create mode 100644 src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts create mode 100644 src/modules/extensions/user/home/command/handler/buyItem.handler.ts create mode 100644 src/modules/extensions/user/home/command/handler/buyTheme.handler.ts create mode 100644 src/modules/extensions/user/home/command/handler/changeTheme.handler.ts create mode 100644 src/modules/extensions/user/home/command/index.ts create mode 100644 src/modules/extensions/user/home/query/getItems.payload.ts create mode 100644 src/modules/extensions/user/home/query/getThemes.payload.ts create mode 100644 src/modules/extensions/user/home/query/getTodayGeulroquis.payload.ts create mode 100644 src/modules/extensions/user/home/query/handler/index.ts diff --git a/src/modules/extensions/user/home/api/home.command.controller.ts b/src/modules/extensions/user/home/api/home.command.controller.ts new file mode 100644 index 0000000..0afedc1 --- /dev/null +++ b/src/modules/extensions/user/home/api/home.command.controller.ts @@ -0,0 +1,117 @@ +import { Controller, Param, ParseIntPipe, Post, Req, UseGuards } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Request as ExpressRequest } from 'express'; + +import { JwtAuthGuard } from '../../../../../common/guards/jwtAuth.guard'; +import { BuyThemeCommand } from '../command/buyTheme.command'; +import { HomeService } from '../core/home.service'; + +@ApiTags('Home-command') +@Controller('home') +@UseGuards(JwtAuthGuard) +export class HomeCommandController { + constructor( + private readonly homeService: HomeService, + private readonly commandBus: CommandBus, + ) {} + + @Post('themes/buy/:themeId') + @ApiOperation({ + summary: '테마 구매', + description: ` + 사용자가 특정 테마를 구매합니다. + + **경로 파라미터:** + - \`themeId\`: 구매할 테마 아이디 + + **동작 과정:** + 1. 사용자의 고유 ID와 구매할 테마의 ID를 인자로 받아 처리합니다. + 2. 먼저, 사용자가 이미 해당 테마를 소유하고 있는지 확인합니다. + 3. 만약 이미 소유한 테마라면, '이미 소유한 테마입니다'라는 오류 메시지를 반환합니다. + 4. 소유하지 않은 테마인 경우, 사용자의 재화(gems)를 확인하여 해당 테마를 구매할 수 있는지 검증합니다. + 5. 재화가 충분하지 않다면, '재화가 부족합니다'라는 오류 메시지를 반환합니다. + 6. 재화가 충분한 경우, 테마의 가격만큼 사용자의 재화를 차감하고, 사용자의 테마 목록에 해당 테마를 추가합니다. + 7. 테마 구매가 성공적으로 완료되었음을 클라이언트에게 알립니다. + + **주의 사항:** + - 서버에서 트랜잭션 관리를 하겠지만, 클라이언트 측에서도 구매요청 시 충돌방지를 위해 통신이 끝나기까지 다른 조작이 불가능하도록 하면 좋을 것 같습니다. + `, + }) + @ApiResponse({ status: 201 }) + async buyTheme(@Req() req: ExpressRequest, @Param('themeId', ParseIntPipe) themeId: number) { + return this.commandBus.execute(new BuyThemeCommand(req.user.id, themeId)); + } + + @Post('themes/activate/:themeId') + @ApiOperation({ + summary: '사용자 테마 활성화', + description: ` + 사용자가 소유한 테마 중에서 선택한 테마로 현재 사용 중인 테마를 변경합니다. + 기존에 사용 중인 테마는 비활성화되고, 선택한 테마의 레이아웃이 활성화됩니다. + + **쿼리 파라미터:** + - \`themeId\`: 변경하려는 테마의 ID + + **동작 과정:** + 1. 클라이언트는 변경하고자 하는 테마의 ID를 서버에 전달합니다. + 2. 서버는 사용자가 해당 테마를 소유하고 있는지 확인합니다. + 3. 사용자가 해당 테마를 소유하고 있다면, 현재 사용 중인 테마 레이아웃을 비활성화하고 선택한 테마의 레이아웃을 활성화합니다. + 4. 변경된 테마 레이아웃을 저장합니다. + `, + }) + @ApiResponse({ status: 200 }) + async changeTheme(@Req() req: ExpressRequest, @Param('themeId', ParseIntPipe) themeId: number) { + return this.homeService.changeTheme(req.user.id, themeId); + } + + @Post('items/buy/:itemId') + @ApiOperation({ + summary: '아이템 구매', + description: ` + 사용자가 특정 아이템을 구매합니다. + + **동작 과정:** + 1. 사용자의 고유 ID와 구매할 아이템의 ID를 인자로 받아 처리합니다. + 2. 사용자가 이미 해당 아이템을 소유하고 있는지 확인합니다. + 3. 만약 이미 소유한 아이템이라면, '이미 소유한 아이템입니다'라는 오류 메시지를 반환합니다. + 4. 소유하지 않은 아이템인 경우, 사용자의 재화(gems)를 확인하여 해당 아이템을 구매할 수 있는지 검증합니다. + 5. 재화가 충분하지 않다면, '재화가 부족합니다'라는 오류 메시지를 반환합니다. + 6. 재화가 충분한 경우, 아이템의 가격만큼 사용자의 재화를 차감하고, 사용자의 아이템 목록에 해당 아이템을 추가합니다. + 7. 아이템 구매가 성공적으로 완료되었음을 클라이언트에게 알립니다. + + **주의 사항:** + - 서버에서 트랜잭션 관리를 하겠지만, 클라이언트 측에서도 구매요청 시 충돌방지를 위해 통신이 끝나기까지 다른 조작이 불가능하도록 하면 좋을 것 같습니다. + `, + }) + @ApiResponse({ status: 201 }) + async buyItem(@Req() req: ExpressRequest, @Param('itemId', ParseIntPipe) itemId: number) { + return this.homeService.buyItem(req.user.id, itemId); + } + + @Post('items/activate/:itemId') + @ApiOperation({ + summary: '아이템 활성화', + description: ` + 사용자가 소유한 아이템을 현재 사용 중인 테마의 레이아웃에 활성화합니다. + + **동작 과정:** + 1. 클라이언트는 활성화할 아이템의 ID를 서버에 전달합니다. + 2. 서버는 현재 활성화된 테마의 레이아웃을 가져옵니다. + 3. 아이템이 현재 테마에 속해 있는지 확인하고, 사용자가 해당 아이템을 소유하고 있는지 확인합니다. + 4. 동일한 포지션에 이미 활성화된 아이템이 있는 경우, 기존 아이템을 비활성화합니다. + 5. 선택한 아이템을 활성화하여 레이아웃에 등록합니다. + + **경로 파라미터:** + - \`itemId\`: 활성화할 아이템의 ID. + + **예외 처리:** + - \`활성화된 레이아웃이 없습니다.\`: 사용자가 활성화한 테마 레이아웃이 없는 경우 발생. + - \`해당 아이템이 존재하지 않거나, 현재 테마에 속하지 않습니다.\`: 아이템이 현재 테마에 속하지 않거나 존재하지 않을 경우 발생. + - \`해당 아이템을 소유하고 있지 않습니다.\`: 사용자가 해당 아이템을 소유하지 않은 경우 발생. + `, + }) + async activateItem(@Req() req: ExpressRequest, @Param('itemId', ParseIntPipe) itemId: number) { + return this.homeService.activateItem(req.user.id, itemId); + } +} diff --git a/src/modules/extensions/user/home/command/activateItem.command.ts b/src/modules/extensions/user/home/command/activateItem.command.ts new file mode 100644 index 0000000..2be778f --- /dev/null +++ b/src/modules/extensions/user/home/command/activateItem.command.ts @@ -0,0 +1,6 @@ +export class ActivateItemCommand { + constructor( + public readonly userId: number, + public readonly itemId: number, + ) {} +} diff --git a/src/modules/extensions/user/home/command/buyItem.command.ts b/src/modules/extensions/user/home/command/buyItem.command.ts new file mode 100644 index 0000000..e0e6ca7 --- /dev/null +++ b/src/modules/extensions/user/home/command/buyItem.command.ts @@ -0,0 +1,6 @@ +export class BuyItemCommand { + constructor( + public readonly userId: number, + public readonly itemId: number, + ) {} +} diff --git a/src/modules/extensions/user/home/command/buyTheme.command.ts b/src/modules/extensions/user/home/command/buyTheme.command.ts new file mode 100644 index 0000000..ffb0009 --- /dev/null +++ b/src/modules/extensions/user/home/command/buyTheme.command.ts @@ -0,0 +1,6 @@ +export class BuyThemeCommand { + constructor( + public readonly userId: number, + public readonly themeId: number, + ) {} +} diff --git a/src/modules/extensions/user/home/command/changeTheme.command.ts b/src/modules/extensions/user/home/command/changeTheme.command.ts new file mode 100644 index 0000000..4ed4e16 --- /dev/null +++ b/src/modules/extensions/user/home/command/changeTheme.command.ts @@ -0,0 +1,6 @@ +export class ChangeThemeCommand { + constructor( + public readonly userId: number, + public readonly themeId: number, + ) {} +} diff --git a/src/modules/extensions/user/home/command/handler/activateItem.handler.ts b/src/modules/extensions/user/home/command/handler/activateItem.handler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts b/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts new file mode 100644 index 0000000..41677bf --- /dev/null +++ b/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts @@ -0,0 +1,15 @@ +import { ICommandHandler } from '@nestjs/cqrs'; + +import { UserService } from '../../../../../../base/user/core/user.service'; +import { ToolService } from '../../../../../../utils/tool/core/tool.service'; +import { HomeService } from '../../../core/home.service'; + +export abstract class BaseCommandHandler implements ICommandHandler { + constructor( + protected readonly userService: UserService, + protected readonly homeService: HomeService, + protected readonly toolService: ToolService, + ) {} + + abstract execute(command: TCommand): Promise; +} diff --git a/src/modules/extensions/user/home/command/handler/buyItem.handler.ts b/src/modules/extensions/user/home/command/handler/buyItem.handler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts b/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts new file mode 100644 index 0000000..f1fd1b2 --- /dev/null +++ b/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts @@ -0,0 +1,34 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; + +import { BuyThemeCommand } from '../buyTheme.command'; +import { BaseCommandHandler } from './base/baseCommand.handler'; + +@CommandHandler(BuyThemeCommand) +export class BuyThemeHandler extends BaseCommandHandler { + async execute(command: BuyThemeCommand): Promise { + const { userId, themeId } = command; + + const lockKey = `buyThemeLock:${userId}`; + const ttl = 10000; + + const lock = await this.homeService.acquireLock(lockKey, ttl); + + // useScopeExit으로 lock 해제를 예약 + const releaseLock = this.toolService.useScopeExit(() => this.homeService.releaseLock(lock)); + + const user = await this.userService.fetchUserEntityById(userId); + const themePrice = await this.homeService.getThemePrice(themeId); + + if (user.gems < themePrice) { + throw new HttpException('재화가 부족합니다.', HttpStatus.BAD_REQUEST); + } + + user.gems -= themePrice; + + await this.homeService.addThemeToUser(user, themeId); + await this.userService.updateUser(user.id, user); + + releaseLock(); + } +} diff --git a/src/modules/extensions/user/home/command/handler/changeTheme.handler.ts b/src/modules/extensions/user/home/command/handler/changeTheme.handler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/extensions/user/home/command/index.ts b/src/modules/extensions/user/home/command/index.ts new file mode 100644 index 0000000..d3a7dd5 --- /dev/null +++ b/src/modules/extensions/user/home/command/index.ts @@ -0,0 +1,15 @@ +// handlers/index.ts +// import { BuyItemHandler } from './command/buyItem.handler'; +import { BuyThemeHandler } from './handler/buyTheme.handler'; +// import { ChangeThemeHandler } from './command/changeTheme.handler'; +// import { ActivateItemHandler } from './commands/activate-item.handler'; +// import { GetItemsHandler } from './queries/get-items.handler'; +// import { GetThemesHandler } from './queries/get-themes.handler'; +// import { GetTodayGeulroquisHandler } from './queries/get-today-geulroquis.handler'; + +export const CommandHandlers = [ + BuyThemeHandler, + // ChangeThemeHandler, + // BuyItemHandler, + // ActivateItemHandler, +]; diff --git a/src/modules/extensions/user/home/query/getItems.payload.ts b/src/modules/extensions/user/home/query/getItems.payload.ts new file mode 100644 index 0000000..4ed8730 --- /dev/null +++ b/src/modules/extensions/user/home/query/getItems.payload.ts @@ -0,0 +1,7 @@ +export class GetItemsPayload { + constructor( + public readonly userId: number, + public readonly themeId: number, + public readonly position: string, + ) {} +} diff --git a/src/modules/extensions/user/home/query/getThemes.payload.ts b/src/modules/extensions/user/home/query/getThemes.payload.ts new file mode 100644 index 0000000..23f4ba8 --- /dev/null +++ b/src/modules/extensions/user/home/query/getThemes.payload.ts @@ -0,0 +1,3 @@ +export class GetThemesPayload { + constructor(public readonly userId: number) {} +} diff --git a/src/modules/extensions/user/home/query/getTodayGeulroquis.payload.ts b/src/modules/extensions/user/home/query/getTodayGeulroquis.payload.ts new file mode 100644 index 0000000..ae73ef1 --- /dev/null +++ b/src/modules/extensions/user/home/query/getTodayGeulroquis.payload.ts @@ -0,0 +1 @@ +export class GetTodayGeulroquisPayload {} diff --git a/src/modules/extensions/user/home/query/handler/index.ts b/src/modules/extensions/user/home/query/handler/index.ts new file mode 100644 index 0000000..e69de29 From fc91f690de81a35d5f79a2d355fbe54aaa573ee9 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Sat, 30 Nov 2024 09:36:22 +0900 Subject: [PATCH 04/19] =?UTF-8?q?chore:=20release=20bot=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 2 ++ release.config.js | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bed7051..4e8b4c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v3 diff --git a/release.config.js b/release.config.js index d97617f..99856b4 100644 --- a/release.config.js +++ b/release.config.js @@ -26,4 +26,5 @@ module.exports = { { type: 'test', release: false }, { type: 'chore', release: false }, ], + tagFormat: 'v${version}', }; From 87933e219e601480f46f1a418c24fa5e208d1756 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Mon, 2 Dec 2024 10:07:55 +0900 Subject: [PATCH 05/19] =?UTF-8?q?refactor:=20home=20api=20=EC=BB=A4?= =?UTF-8?q?=EB=A7=A8=EB=93=9C=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A0=88=EB=94=94=EC=8A=A4=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 +- .../adapters/redis/core/redis.service.ts | 43 +++++ src/modules/adapters/redis/redis.module.ts | 18 ++ src/modules/base/admin/core/admin.service.ts | 42 ++--- .../essay/geulroquis/geulroquis.module.ts | 2 + .../user/home/api/home.command.controller.ts | 19 ++- .../command/handler/activateItem.handler.ts | 38 +++++ .../handler/base/baseCommand.handler.ts | 2 + .../home/command/handler/buyItem.handler.ts | 40 +++++ .../home/command/handler/buyTheme.handler.ts | 10 +- .../command/handler/changeTheme.handler.ts | 33 ++++ .../extensions/user/home/command/index.ts | 19 +-- .../extensions/user/home/core/home.service.ts | 154 +++++------------- .../extensions/user/home/home.module.ts | 3 +- 14 files changed, 260 insertions(+), 167 deletions(-) create mode 100644 src/modules/adapters/redis/core/redis.service.ts create mode 100644 src/modules/adapters/redis/redis.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6ee83c2..71cf860 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,9 +46,7 @@ import { ToolModule } from './modules/utils/tool/tool.module'; envFilePath: '../.env', }), TypeOrmModule.forRootAsync(TypeormConfig), - RedisModule.forRootAsync({ - useFactory: () => redisConfig, - }), + RedisModule, AdminModule, UserModule, SeederModule, diff --git a/src/modules/adapters/redis/core/redis.service.ts b/src/modules/adapters/redis/core/redis.service.ts new file mode 100644 index 0000000..c4e4bf4 --- /dev/null +++ b/src/modules/adapters/redis/core/redis.service.ts @@ -0,0 +1,43 @@ +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import Redis from 'ioredis'; +import Redlock, { Lock } from 'redlock'; + +@Injectable() +export class RedisService { + constructor( + @InjectRedis() private readonly redis: Redis, + @Inject('REDLOCK') private readonly redlock: Redlock, + ) {} + + async acquireLock(lockKey: string, ttl: number): Promise { + try { + return await this.redlock.acquire([lockKey], ttl); + } catch (err) { + throw new HttpException( + '락을 획득할 수 없습니다. 잠시 후 다시 시도하세요.', + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + } + + async releaseLock(lock: Lock): Promise { + try { + await lock.release(); + } catch (err) { + console.error('락 해제 실패:', err.message); + } + } + + async setCache(key: string, value: string, ttl?: number): Promise { + await this.redis.set(key, value, 'EX', ttl); + } + + async getCache(key: string): Promise { + return this.redis.get(key); + } + + async deleteCache(key: string): Promise { + await this.redis.del(key); + } +} diff --git a/src/modules/adapters/redis/redis.module.ts b/src/modules/adapters/redis/redis.module.ts new file mode 100644 index 0000000..dad2455 --- /dev/null +++ b/src/modules/adapters/redis/redis.module.ts @@ -0,0 +1,18 @@ +import { Global, Module } from '@nestjs/common'; +import { RedisModule as NestJsRedisModule } from '@nestjs-modules/ioredis'; + +import { RedisService } from './core/redis.service'; +import { redisConfig } from '../../../config/redis.config'; +import { RedlockProvider } from '../../../config/redlock.provider'; + +@Global() +@Module({ + imports: [ + NestJsRedisModule.forRootAsync({ + useFactory: () => redisConfig, + }), + ], + providers: [RedisService, RedlockProvider], + exports: [RedisService, RedlockProvider, NestJsRedisModule], +}) +export class RedisModule {} diff --git a/src/modules/base/admin/core/admin.service.ts b/src/modules/base/admin/core/admin.service.ts index ab93d9c..ab5259e 100644 --- a/src/modules/base/admin/core/admin.service.ts +++ b/src/modules/base/admin/core/admin.service.ts @@ -46,36 +46,36 @@ import { ToolService } from '../../../utils/tool/core/tool.service'; import { EssayService } from '../../essay/core/essay.service'; import { IEssayRepository } from '../../essay/infrastructure/iessay.repository'; import { UserService } from '../../user/core/user.service'; +import { ProfileImageUrlResDto } from '../../user/dto/response/profileImageUrlRes.dto'; +import { UserResDto } from '../../user/dto/response/userRes.dto'; +import { IUserRepository } from '../../user/infrastructure/iuser.repository'; +import { CreateAdminDto } from '../dto/createAdmin.dto'; +import { AdminRegisterReqDto } from '../dto/request/adminRegisterReq.dto'; +import { AdminUpdateReqDto } from '../dto/request/adminUpdateReq.dto'; import { CreateAdminReqDto } from '../dto/request/createAdminReq.dto'; +import { CreateItemReqDto } from '../dto/request/createItemReq.dto'; +import { CreateNoticeReqDto } from '../dto/request/createNoticeReq.dto'; +import { CreateThemeReqDto } from '../dto/request/createThemeReq.dto'; import { ProcessReqDto } from '../dto/request/processReq.dto'; +import { UpdateEssayStatusReqDto } from '../dto/request/updateEssayStatusReq.dto'; +import { UpdateFullUserReqDto } from '../dto/request/updateFullUserReq.dto'; +import { UpdateNoticeReqDto } from '../dto/request/updateNoticeReq.dto'; +import { AdminResDto } from '../dto/response/adminRes.dto'; import { DashboardResDto } from '../dto/response/dashboardRes.dto'; +import { DetailReviewResDto } from '../dto/response/detailReviewRes.dto'; +import { EssayInfoResDto } from '../dto/response/essayInfoRes.dto'; +import { FullEssayResDto } from '../dto/response/fullEssayRes.dto'; +import { FullInquiryResDto } from '../dto/response/fullInquiryRes.dto'; +import { FullUserResDto } from '../dto/response/fullUserRes.dto'; +import { HistoriesResDto } from '../dto/response/historiesRes.dto'; +import { NoticeWithProcessorResDto } from '../dto/response/noticeWithProcessorRes.dto'; import { ReportDetailResDto } from '../dto/response/reportDetailRes.dto'; import { ReportResDto } from '../dto/response/reportRes.dto'; import { ReportsResDto } from '../dto/response/reportsRes.dto'; import { ReviewResDto } from '../dto/response/reviewRes.dto'; -import { DetailReviewResDto } from '../dto/response/detailReviewRes.dto'; -import { HistoriesResDto } from '../dto/response/historiesRes.dto'; -import { FullUserResDto } from '../dto/response/fullUserRes.dto'; import { SavedAdminResDto } from '../dto/response/savedAdminRes.dto'; import { UserDetailResDto } from '../dto/response/userDetailRes.dto'; -import { UpdateFullUserReqDto } from '../dto/request/updateFullUserReq.dto'; -import { CreateAdminDto } from '../dto/createAdmin.dto'; -import { EssayInfoResDto } from '../dto/response/essayInfoRes.dto'; -import { FullEssayResDto } from '../dto/response/fullEssayRes.dto'; -import { UpdateEssayStatusReqDto } from '../dto/request/updateEssayStatusReq.dto'; -import { AdminResDto } from '../dto/response/adminRes.dto'; -import { AdminUpdateReqDto } from '../dto/request/adminUpdateReq.dto'; -import { ProfileImageUrlResDto } from '../../user/dto/response/profileImageUrlRes.dto'; -import { AdminRegisterReqDto } from '../dto/request/adminRegisterReq.dto'; -import { CreateNoticeReqDto } from '../dto/request/createNoticeReq.dto'; -import { UpdateNoticeReqDto } from '../dto/request/updateNoticeReq.dto'; -import { NoticeWithProcessorResDto } from '../dto/response/noticeWithProcessorRes.dto'; -import { FullInquiryResDto } from '../dto/response/fullInquiryRes.dto'; -import { CreateThemeReqDto } from '../dto/request/createThemeReq.dto'; -import { CreateItemReqDto } from '../dto/request/createItemReq.dto'; -import { UserResDto } from '../../user/dto/response/userRes.dto'; import { IAdminRepository } from '../infrastructure/iadmin.repository'; -import { IUserRepository } from '../../user/infrastructure/iuser.repository'; @Injectable() export class AdminService { @@ -958,7 +958,7 @@ export class AdminService { async deleteUser(adminId: number, userId: number) { if (adminId !== 1) throw new HttpException('접근 권한이 없습니다.', HttpStatus.FORBIDDEN); - const todayDate = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15); + const todayDate = new Date().toISOString().replace(/[.:-]/g, '').slice(0, 15); await this.userRepository.deleteAccount(userId, todayDate); } diff --git a/src/modules/extensions/essay/geulroquis/geulroquis.module.ts b/src/modules/extensions/essay/geulroquis/geulroquis.module.ts index be34c31..8600476 100644 --- a/src/modules/extensions/essay/geulroquis/geulroquis.module.ts +++ b/src/modules/extensions/essay/geulroquis/geulroquis.module.ts @@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { GeulroquisService } from './core/geulroquis.service'; import { GeulroquisRepository } from './infrastructure/geulroquis.repository'; import { Geulroquis } from '../../../../entities/geulroguis.entity'; +import { RedisModule } from '../../../adapters/redis/redis.module'; import { AuthModule } from '../../../base/auth/auth.module'; import { UserModule } from '../../../base/user/user.module'; import { ToolModule } from '../../../utils/tool/tool.module'; @@ -13,6 +14,7 @@ import { ToolModule } from '../../../utils/tool/tool.module'; imports: [ JwtModule.register({}), TypeOrmModule.forFeature([Geulroquis]), + RedisModule, ToolModule, forwardRef(() => AuthModule), forwardRef(() => UserModule), diff --git a/src/modules/extensions/user/home/api/home.command.controller.ts b/src/modules/extensions/user/home/api/home.command.controller.ts index 0afedc1..74527d5 100644 --- a/src/modules/extensions/user/home/api/home.command.controller.ts +++ b/src/modules/extensions/user/home/api/home.command.controller.ts @@ -4,17 +4,18 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request as ExpressRequest } from 'express'; import { JwtAuthGuard } from '../../../../../common/guards/jwtAuth.guard'; -import { BuyThemeCommand } from '../command/buyTheme.command'; -import { HomeService } from '../core/home.service'; +import { + BuyThemeCommand, + ChangeThemeCommand, + BuyItemCommand, + ActivateItemCommand, +} from '../command'; @ApiTags('Home-command') @Controller('home') @UseGuards(JwtAuthGuard) export class HomeCommandController { - constructor( - private readonly homeService: HomeService, - private readonly commandBus: CommandBus, - ) {} + constructor(private readonly commandBus: CommandBus) {} @Post('themes/buy/:themeId') @ApiOperation({ @@ -62,7 +63,7 @@ export class HomeCommandController { }) @ApiResponse({ status: 200 }) async changeTheme(@Req() req: ExpressRequest, @Param('themeId', ParseIntPipe) themeId: number) { - return this.homeService.changeTheme(req.user.id, themeId); + return this.commandBus.execute(new ChangeThemeCommand(req.user.id, themeId)); } @Post('items/buy/:itemId') @@ -86,7 +87,7 @@ export class HomeCommandController { }) @ApiResponse({ status: 201 }) async buyItem(@Req() req: ExpressRequest, @Param('itemId', ParseIntPipe) itemId: number) { - return this.homeService.buyItem(req.user.id, itemId); + return this.commandBus.execute(new BuyItemCommand(req.user.id, itemId)); } @Post('items/activate/:itemId') @@ -112,6 +113,6 @@ export class HomeCommandController { `, }) async activateItem(@Req() req: ExpressRequest, @Param('itemId', ParseIntPipe) itemId: number) { - return this.homeService.activateItem(req.user.id, itemId); + return this.commandBus.execute(new ActivateItemCommand(req.user.id, itemId)); } } diff --git a/src/modules/extensions/user/home/command/handler/activateItem.handler.ts b/src/modules/extensions/user/home/command/handler/activateItem.handler.ts index e69de29..5bcc06f 100644 --- a/src/modules/extensions/user/home/command/handler/activateItem.handler.ts +++ b/src/modules/extensions/user/home/command/handler/activateItem.handler.ts @@ -0,0 +1,38 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; +import { Transactional } from 'typeorm-transactional'; + +import { ActivateItemCommand } from '../activateItem.command'; +import { BaseCommandHandler } from './base/baseCommand.handler'; + +@CommandHandler(ActivateItemCommand) +export class ActivateItemHandler extends BaseCommandHandler { + @Transactional() + async execute(command: ActivateItemCommand) { + const { userId, itemId } = command; + + const activeLayout = await this.homeService.getActiveLayout(userId); + if (!activeLayout) { + throw new HttpException('활성화된 레이아웃이 없습니다.', HttpStatus.BAD_REQUEST); + } + + const item = await this.homeService.getItemById(itemId); + if (!item || item.theme.id !== activeLayout.theme.id) { + throw new HttpException( + '해당 아이템이 존재하지 않거나, 현재 테마에 속하지 않습니다.', + HttpStatus.BAD_REQUEST, + ); + } + + const userOwnsItem = await this.homeService.userOwnsItem(userId, itemId); + if (!userOwnsItem) { + throw new HttpException('해당 아이템을 소유하고 있지 않습니다.', HttpStatus.FORBIDDEN); + } + + await this.homeService.deactivateItemAtPosition(activeLayout, item.position); + + await this.homeService.activateItemInLayout(activeLayout, item); + + await this.homeService.clearUserCache(userId); + } +} diff --git a/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts b/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts index 41677bf..34f4db6 100644 --- a/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts +++ b/src/modules/extensions/user/home/command/handler/base/baseCommand.handler.ts @@ -1,5 +1,6 @@ import { ICommandHandler } from '@nestjs/cqrs'; +import { RedisService } from '../../../../../../adapters/redis/core/redis.service'; import { UserService } from '../../../../../../base/user/core/user.service'; import { ToolService } from '../../../../../../utils/tool/core/tool.service'; import { HomeService } from '../../../core/home.service'; @@ -9,6 +10,7 @@ export abstract class BaseCommandHandler implements ICommandHandler; diff --git a/src/modules/extensions/user/home/command/handler/buyItem.handler.ts b/src/modules/extensions/user/home/command/handler/buyItem.handler.ts index e69de29..2381307 100644 --- a/src/modules/extensions/user/home/command/handler/buyItem.handler.ts +++ b/src/modules/extensions/user/home/command/handler/buyItem.handler.ts @@ -0,0 +1,40 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; +import { Transactional } from 'typeorm-transactional'; + +import { BuyItemCommand } from '../buyItem.command'; +import { BaseCommandHandler } from './base/baseCommand.handler'; + +@CommandHandler(BuyItemHandler) +export class BuyItemHandler extends BaseCommandHandler { + @Transactional() + async execute(command: BuyItemCommand) { + const { userId, itemId } = command; + + const lockKey = `buy-item-lock:${userId}`; + const ttl = 10000; + + const lock = await this.redisService.acquireLock(lockKey, ttl); + const releaseLock = this.toolService.useScopeExit(() => this.redisService.releaseLock(lock)); + + const user = await this.userService.fetchUserEntityById(userId); + const userItems = await this.homeService.getUserItems(userId); + + if (userItems.some((userItem) => userItem.id === itemId)) + throw new HttpException('이미 소유한 아이템입니다.', HttpStatus.BAD_REQUEST); + + const item = await this.homeService.getItemById(itemId); + if (item) throw new HttpException('존재하지 않는 아이템입니다.', HttpStatus.BAD_REQUEST); + + if (user.gems < item.price) + throw new HttpException('재화가 부족합니다.', HttpStatus.BAD_REQUEST); + + user.gems -= item.price; + await this.homeService.addItemToUser(user, item); + await this.userService.updateUser(userId, user); + + await this.homeService.clearUserCache(userId); + + releaseLock(); + } +} diff --git a/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts b/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts index f1fd1b2..ad21fed 100644 --- a/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts +++ b/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts @@ -1,21 +1,23 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { CommandHandler } from '@nestjs/cqrs'; +import { Transactional } from 'typeorm-transactional'; import { BuyThemeCommand } from '../buyTheme.command'; import { BaseCommandHandler } from './base/baseCommand.handler'; @CommandHandler(BuyThemeCommand) export class BuyThemeHandler extends BaseCommandHandler { + @Transactional() async execute(command: BuyThemeCommand): Promise { const { userId, themeId } = command; const lockKey = `buyThemeLock:${userId}`; const ttl = 10000; - const lock = await this.homeService.acquireLock(lockKey, ttl); + const lock = await this.redisService.acquireLock(lockKey, ttl); - // useScopeExit으로 lock 해제를 예약 - const releaseLock = this.toolService.useScopeExit(() => this.homeService.releaseLock(lock)); + // useScopeExit로 lock 해제 예약 + const releaseLock = this.toolService.useScopeExit(() => this.redisService.releaseLock(lock)); const user = await this.userService.fetchUserEntityById(userId); const themePrice = await this.homeService.getThemePrice(themeId); @@ -29,6 +31,8 @@ export class BuyThemeHandler extends BaseCommandHandler { await this.homeService.addThemeToUser(user, themeId); await this.userService.updateUser(user.id, user); + await this.homeService.clearUserCache(userId); + releaseLock(); } } diff --git a/src/modules/extensions/user/home/command/handler/changeTheme.handler.ts b/src/modules/extensions/user/home/command/handler/changeTheme.handler.ts index e69de29..70f9b9a 100644 --- a/src/modules/extensions/user/home/command/handler/changeTheme.handler.ts +++ b/src/modules/extensions/user/home/command/handler/changeTheme.handler.ts @@ -0,0 +1,33 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; +import { Transactional } from 'typeorm-transactional'; + +import { ChangeThemeCommand } from '../changeTheme.command'; +import { BaseCommandHandler } from './base/baseCommand.handler'; + +@CommandHandler(ChangeThemeCommand) +export class ChangeThemeHandler extends BaseCommandHandler { + @Transactional() + async execute(command: ChangeThemeCommand) { + const { userId, themeId } = command; + + const userThemes = await this.homeService.getUserThemes(userId); + const ownsTheme = userThemes.some((theme) => theme.id === themeId); + if (!ownsTheme) + throw new HttpException('해당 테마를 소유하고 있지 않습니다.', HttpStatus.BAD_REQUEST); + + const currentLayout = await this.homeService.getActiveLayout(userId); + if (currentLayout) await this.homeService.deactivateLayout(currentLayout); + + const newLayout = await this.homeService.getLayoutByTheme(userId, themeId); + if (!newLayout) + throw new HttpException( + '해당 테마에 대한 레이아웃이 존재하지 않습니다.', + HttpStatus.NOT_FOUND, + ); + + await this.homeService.activateLayout(newLayout); + + await this.homeService.clearUserCache(userId); + } +} diff --git a/src/modules/extensions/user/home/command/index.ts b/src/modules/extensions/user/home/command/index.ts index d3a7dd5..8bde159 100644 --- a/src/modules/extensions/user/home/command/index.ts +++ b/src/modules/extensions/user/home/command/index.ts @@ -1,15 +1,4 @@ -// handlers/index.ts -// import { BuyItemHandler } from './command/buyItem.handler'; -import { BuyThemeHandler } from './handler/buyTheme.handler'; -// import { ChangeThemeHandler } from './command/changeTheme.handler'; -// import { ActivateItemHandler } from './commands/activate-item.handler'; -// import { GetItemsHandler } from './queries/get-items.handler'; -// import { GetThemesHandler } from './queries/get-themes.handler'; -// import { GetTodayGeulroquisHandler } from './queries/get-today-geulroquis.handler'; - -export const CommandHandlers = [ - BuyThemeHandler, - // ChangeThemeHandler, - // BuyItemHandler, - // ActivateItemHandler, -]; +export * from './buyTheme.command'; +export * from './changeTheme.command'; +export * from './activateItem.command'; +export * from './buyItem.command'; diff --git a/src/modules/extensions/user/home/core/home.service.ts b/src/modules/extensions/user/home/core/home.service.ts index c7cb13e..389b5d5 100644 --- a/src/modules/extensions/user/home/core/home.service.ts +++ b/src/modules/extensions/user/home/core/home.service.ts @@ -1,7 +1,4 @@ import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { InjectRedis } from '@nestjs-modules/ioredis'; -import Redis from 'ioredis'; -import Redlock, { Lock } from 'redlock'; import { Transactional } from 'typeorm-transactional'; import { Item } from '../../../../../entities/item.entity'; @@ -9,7 +6,9 @@ import { Theme } from '../../../../../entities/theme.entity'; import { User } from '../../../../../entities/user.entity'; import { UserHomeItem } from '../../../../../entities/userHomeItem.entity'; import { UserHomeLayout } from '../../../../../entities/userHomeLayout.entity'; +import { UserItem } from '../../../../../entities/userItem.entity'; import { UserTheme } from '../../../../../entities/userTheme.entity'; +import { RedisService } from '../../../../adapters/redis/core/redis.service'; import { UserService } from '../../../../base/user/core/user.service'; import { ToolService } from '../../../../utils/tool/core/tool.service'; import { GeulroquisService } from '../../../essay/geulroquis/core/geulroquis.service'; @@ -24,29 +23,9 @@ export class HomeService { private readonly geulroquisService: GeulroquisService, private readonly toolService: ToolService, @Inject(forwardRef(() => UserService)) private readonly userService: UserService, - @Inject('REDLOCK') private readonly redlock: Redlock, - @InjectRedis() private readonly redis: Redis, + private readonly redisService: RedisService, ) {} - async acquireLock(lockKey: string, ttl: number) { - try { - return await this.redlock.acquire([lockKey], ttl); - } catch (err) { - throw new HttpException( - '락을 획득할 수 없습니다. 잠시 후 다시 시도하세요.', - HttpStatus.SERVICE_UNAVAILABLE, - ); - } - } - - async releaseLock(lock: Lock) { - try { - await lock.release(); - } catch (err) { - console.error('락 해제 실패:', err); - } - } - async todayGeulroquis() { const url = await this.geulroquisService.todayGeulroquis(); return { url: url }; @@ -54,7 +33,7 @@ export class HomeService { @Transactional() async getThemes(userId: number) { - const cachedThemes = await this.redis.get('linkedout:themes'); + const cachedThemes = await this.redisService.getCache('linkedout:themes'); let allThemes: Theme[]; @@ -62,7 +41,7 @@ export class HomeService { allThemes = JSON.parse(cachedThemes); } else { allThemes = await this.homeRepository.findAllThemes(); - await this.redis.set('linkedout:themes', JSON.stringify(allThemes)); + await this.redisService.setCache('linkedout:themes', JSON.stringify(allThemes)); } let activeLayout: UserHomeLayout; @@ -143,7 +122,7 @@ export class HomeService { const cacheKey = `items:theme:${themeId}:${position || 'all'}`; let items: Item[] | null = null; - const cachedItems = await this.redis.get(cacheKey); + const cachedItems = await this.redisService.getCache(cacheKey); if (cachedItems) { items = JSON.parse(cachedItems); @@ -152,7 +131,7 @@ export class HomeService { if (!items) { items = await this.homeRepository.findItemsByThemeAndPosition(themeId, position); - await this.redis.set(cacheKey, JSON.stringify(items)); + await this.redisService.setCache(cacheKey, JSON.stringify(items)); } const userItems = await this.homeRepository.findUserItemsByTheme(userId, themeId); @@ -168,114 +147,61 @@ export class HomeService { return { items: itemsDto }; } - async buyItem(userId: number, itemId: number) { - const lockKey = `buy-item-lock:${userId}`; - const ttl = 10000; - - const lock = await this.acquireLock(lockKey, ttl); - - try { - await this.executeBuyItemTransaction(userId, itemId); - } catch (err) { - throw new HttpException( - '아이템 구매 중 오류가 발생했습니다.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } finally { - await this.releaseLock(lock); - } + async getUserItems(userId: number): Promise { + return this.homeRepository.findUserItems(userId); } - @Transactional() - private async executeBuyItemTransaction(userId: number, itemId: number) { - const user = await this.userService.fetchUserEntityById(userId); - const userItems = await this.homeRepository.findUserItems(userId); - - const alreadyOwned = userItems.some((userItem) => userItem.item.id === itemId); - if (alreadyOwned) throw new HttpException('이미 소유한 아이템입니다.', HttpStatus.BAD_REQUEST); - - const item = await this.homeRepository.findItemById(itemId); - if (!item) throw new HttpException('존재하지 않는 아이템입니다.', HttpStatus.BAD_REQUEST); - - if (user.gems < item.price) { - throw new HttpException('재화가 부족합니다.', HttpStatus.BAD_REQUEST); - } - - user.gems -= item.price; + async getItemById(itemId: number): Promise { + return this.homeRepository.findItemById(itemId); + } + async addItemToUser(user: User, item: Item): Promise { await this.homeRepository.saveNewUserItem(user, item); - await this.userService.updateUser(userId, user); } - @Transactional() - async changeTheme(userId: number, themeId: number): Promise { - const userThemes = await this.homeRepository.findUserThemes(userId); - await this.checkUserOwnsTheme(userThemes, themeId); - - const currentLayout = await this.homeRepository.findUserCurrentLayout(userId); - if (currentLayout) { - currentLayout.isActive = false; - await this.homeRepository.saveUserHomeLayout(currentLayout); - } - - const newLayout = await this.homeRepository.findUserActivateLayout(userId, themeId); - if (!newLayout) { - throw new HttpException( - '해당 테마에 대한 레이아웃이 존재하지 않습니다.', - HttpStatus.NOT_FOUND, - ); - } + async getUserThemes(userId: number): Promise { + return this.homeRepository.findUserThemes(userId); + } - newLayout.isActive = true; - await this.homeRepository.saveUserHomeLayout(newLayout); - await this.redis.del(`user:${userId}`); + async getActiveLayout(userId: number): Promise { + return this.homeRepository.findUserCurrentLayout(userId); } - private async checkUserOwnsTheme(userThemes: UserTheme[], themeId: number): Promise { - const ownsTheme = userThemes.some((userTheme) => userTheme.theme.id === themeId); - if (!ownsTheme) { - throw new HttpException('해당 테마를 소유하고 있지 않습니다.', HttpStatus.BAD_REQUEST); - } + async deactivateLayout(layout: UserHomeLayout): Promise { + layout.isActive = false; + await this.homeRepository.saveUserHomeLayout(layout); } - @Transactional() - async activateItem(userId: number, itemId: number) { - // 현재 활성화된 레이아웃 가져오기 - const activeLayout = await this.homeRepository.findUserCurrentHomeLayout(userId); + async getLayoutByTheme(userId: number, themeId: number): Promise { + return this.homeRepository.findUserActivateLayout(userId, themeId); + } - if (!activeLayout) { - throw new HttpException('활성화된 레이아웃이 없습니다.', HttpStatus.BAD_REQUEST); - } + async activateLayout(layout: UserHomeLayout): Promise { + layout.isActive = true; + await this.homeRepository.saveUserHomeLayout(layout); + } - // 아이템 가져오기 및 검증 - const item = await this.homeRepository.findItemById(itemId); - if (!item || item.theme.id !== activeLayout.theme.id) { - throw new HttpException( - '해당 아이템이 존재하지 않거나, 현재 테마에 속하지 않습니다.', - HttpStatus.BAD_REQUEST, - ); - } + async clearUserCache(userId: number): Promise { + await this.redisService.deleteCache(`user:${userId}`); + } - // 사용자가 해당 아이템을 소유했는지 확인 - const userOwnsItem = await this.homeRepository.findUserItemById(userId, itemId); - if (!userOwnsItem) { - throw new HttpException('해당 아이템을 소유하고 있지 않습니다.', HttpStatus.FORBIDDEN); - } + async userOwnsItem(userId: number, itemId: number): Promise { + const userItem = await this.homeRepository.findUserItemById(userId, itemId); + return !!userItem; + } - // 같은 포지션에 있는 기존 아이템 비활성화 - const existingItem = activeLayout.homeItems.find( - (homeItem) => homeItem.item.position === item.position, - ); + async deactivateItemAtPosition(layout: UserHomeLayout, position: string): Promise { + const existingItem = layout.homeItems.find((homeItem) => homeItem.item.position === position); if (existingItem) { await this.homeRepository.removeUserHomeItem(existingItem); } + } - // 새 아이템 활성화 + async activateItemInLayout(layout: UserHomeLayout, item: Item): Promise { const newUserHomeItem = new UserHomeItem(); - newUserHomeItem.layout = activeLayout; + newUserHomeItem.layout = layout; newUserHomeItem.item = item; await this.homeRepository.saveUserHomeItem(newUserHomeItem); - await this.redis.del(`user:${userId}`); } } diff --git a/src/modules/extensions/user/home/home.module.ts b/src/modules/extensions/user/home/home.module.ts index 4eabe73..93c0457 100644 --- a/src/modules/extensions/user/home/home.module.ts +++ b/src/modules/extensions/user/home/home.module.ts @@ -6,7 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { HomeCommandController } from './api/home.command.controller'; import { HomeQueryController } from './api/home.query.controller'; -import { CommandHandlers } from './command'; +import { CommandHandlers } from './command/handler'; import { HomeService } from './core/home.service'; import { HomeRepository } from './infrastructure/home.repository'; import { RedlockProvider } from '../../../../config/redlock.provider'; @@ -36,7 +36,6 @@ import { GeulroquisModule } from '../../essay/geulroquis/geulroquis.module'; providers: [ HomeService, { provide: 'IHomeRepository', useClass: HomeRepository }, - RedlockProvider, ...CommandHandlers, ], exports: [HomeService], From 5e7a3d142cd56461684c08e32f0defbeb340cdb4 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Mon, 2 Dec 2024 10:08:00 +0900 Subject: [PATCH 06/19] =?UTF-8?q?refactor:=20home=20api=20=EC=BB=A4?= =?UTF-8?q?=EB=A7=A8=EB=93=9C=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A0=88=EB=94=94=EC=8A=A4=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/user/home/command/handler/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/modules/extensions/user/home/command/handler/index.ts diff --git a/src/modules/extensions/user/home/command/handler/index.ts b/src/modules/extensions/user/home/command/handler/index.ts new file mode 100644 index 0000000..a93cc7a --- /dev/null +++ b/src/modules/extensions/user/home/command/handler/index.ts @@ -0,0 +1,11 @@ +import { ActivateItemHandler } from './activateItem.handler'; +import { BuyItemHandler } from './buyItem.handler'; +import { BuyThemeHandler } from './buyTheme.handler'; +import { ChangeThemeHandler } from './changeTheme.handler'; + +export const CommandHandlers = [ + BuyThemeHandler, + ChangeThemeHandler, + BuyItemHandler, + ActivateItemHandler, +]; From a0901250ef4cdf96abc5f19fdcc55c59cb5edb7e Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Mon, 2 Dec 2024 10:40:31 +0900 Subject: [PATCH 07/19] =?UTF-8?q?refactor:=20home=20api=20=EC=BB=A4?= =?UTF-8?q?=EB=A7=A8=EB=93=9C=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A0=88=EB=94=94=EC=8A=A4=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 1 - src/modules/base/user/core/user.service.ts | 2 +- src/modules/base/user/tests/user.service.spec.ts | 2 +- .../extensions/user/home/command/handler/buyItem.handler.ts | 4 +++- .../extensions/user/home/command/handler/buyTheme.handler.ts | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 71cf860..67d240b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -58,7 +58,6 @@ import { ToolModule } from './modules/utils/tool/tool.module'; FollowModule, StoryModule, MailModule, - RedisModule, ToolModule, AwsModule, BadgeModule, diff --git a/src/modules/base/user/core/user.service.ts b/src/modules/base/user/core/user.service.ts index 194cb72..9a1800d 100644 --- a/src/modules/base/user/core/user.service.ts +++ b/src/modules/base/user/core/user.service.ts @@ -201,7 +201,7 @@ export class UserService { @Transactional() async deleteAccount(userId: number) { const userIds = [userId]; - const todayDate = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15); + const todayDate = new Date().toISOString().replace(/[.:-]/g, '').slice(0, 15); await this.userRepository.deleteAccount(userId, todayDate); const batchSize = 5; diff --git a/src/modules/base/user/tests/user.service.spec.ts b/src/modules/base/user/tests/user.service.spec.ts index 8038db8..d7c9eb1 100644 --- a/src/modules/base/user/tests/user.service.spec.ts +++ b/src/modules/base/user/tests/user.service.spec.ts @@ -398,7 +398,7 @@ describe('UserService', () => { describe('deleteAccount', () => { it('should delete user account', async () => { const userId = 1; - const todayDate = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15); + const todayDate = new Date().toISOString().replace(/[.:-]/g, '').slice(0, 15); await service.deleteAccount(userId); diff --git a/src/modules/extensions/user/home/command/handler/buyItem.handler.ts b/src/modules/extensions/user/home/command/handler/buyItem.handler.ts index 2381307..7e5bca3 100644 --- a/src/modules/extensions/user/home/command/handler/buyItem.handler.ts +++ b/src/modules/extensions/user/home/command/handler/buyItem.handler.ts @@ -15,6 +15,8 @@ export class BuyItemHandler extends BaseCommandHandler { const ttl = 10000; const lock = await this.redisService.acquireLock(lockKey, ttl); + + // todo useScopeExit의 현재 구현은 반환된 함수가 명시적으로 호출될 때만 정리를 실행합니다. releaseLock()이 호출되기 전에 오류가 발생하면 잠금이 해제되지 않습니다. 이는 잠금 획득과 releaseLock() 호출 사이에 오류(예: HTTP 예외)를 발생시킬 수 있는 여러 작업이 존재하는 buyTheme.handler.ts 및 buyItem.handler.ts에서 분명합니다. const releaseLock = this.toolService.useScopeExit(() => this.redisService.releaseLock(lock)); const user = await this.userService.fetchUserEntityById(userId); @@ -24,7 +26,7 @@ export class BuyItemHandler extends BaseCommandHandler { throw new HttpException('이미 소유한 아이템입니다.', HttpStatus.BAD_REQUEST); const item = await this.homeService.getItemById(itemId); - if (item) throw new HttpException('존재하지 않는 아이템입니다.', HttpStatus.BAD_REQUEST); + if (!item) throw new HttpException('존재하지 않는 아이템입니다.', HttpStatus.BAD_REQUEST); if (user.gems < item.price) throw new HttpException('재화가 부족합니다.', HttpStatus.BAD_REQUEST); diff --git a/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts b/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts index ad21fed..f5849c4 100644 --- a/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts +++ b/src/modules/extensions/user/home/command/handler/buyTheme.handler.ts @@ -14,9 +14,9 @@ export class BuyThemeHandler extends BaseCommandHandler { const lockKey = `buyThemeLock:${userId}`; const ttl = 10000; + // todo useScopeExit의 현재 구현은 반환된 함수가 명시적으로 호출될 때만 정리를 실행합니다. releaseLock()이 호출되기 전에 오류가 발생하면 잠금이 해제되지 않습니다. 이는 잠금 획득과 releaseLock() 호출 사이에 오류(예: HTTP 예외)를 발생시킬 수 있는 여러 작업이 존재하는 buyTheme.handler.ts 및 buyItem.handler.ts에서 분명합니다. const lock = await this.redisService.acquireLock(lockKey, ttl); - // useScopeExit로 lock 해제 예약 const releaseLock = this.toolService.useScopeExit(() => this.redisService.releaseLock(lock)); const user = await this.userService.fetchUserEntityById(userId); From bf10137d1af8e1b134eca2ff1e2083cae6c746ed Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Thu, 5 Dec 2024 16:22:59 +0900 Subject: [PATCH 08/19] =?UTF-8?q?fix:=20=EB=A9=94=EC=9D=BC=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/utils/mail/core/mail.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/utils/mail/core/mail.service.ts b/src/modules/utils/mail/core/mail.service.ts index 431f0c9..3382c51 100644 --- a/src/modules/utils/mail/core/mail.service.ts +++ b/src/modules/utils/mail/core/mail.service.ts @@ -23,7 +23,10 @@ export class MailService { } private getHtmlTemplate(title: string, message: string, template: string, options?: string) { - const templatePath = path.resolve(process.cwd(), `src/modules/mail/template/${template}.html`); + const templatePath = path.resolve( + process.cwd(), + `src/modules/utils/mail/template/${template}.html`, + ); let html = fs.readFileSync(templatePath, 'utf8'); html = html.replace(/{{title}}/g, title); html = html.replace(/{{message}}/g, message); From bd09e373e002e412441e8ffc90d67f388d1c8fee Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Thu, 5 Dec 2024 16:32:33 +0900 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20=EB=A9=94=EC=9D=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/utils/mail/core/mail.service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/utils/mail/core/mail.service.ts b/src/modules/utils/mail/core/mail.service.ts index 3382c51..4954b53 100644 --- a/src/modules/utils/mail/core/mail.service.ts +++ b/src/modules/utils/mail/core/mail.service.ts @@ -8,6 +8,7 @@ import * as nodemailer from 'nodemailer'; @Injectable() export class MailService { private transporter: nodemailer.Transporter; + private readonly logoPath: string = 'src/modules/utils/mail/template/logo.png'; constructor(private configService: ConfigService) { this.transporter = nodemailer.createTransport({ @@ -52,7 +53,7 @@ export class MailService { attachments: [ { filename: 'logo.png', - path: path.resolve(process.cwd(), 'src/modules/mail/template/logo.png'), + path: path.resolve(process.cwd(), this.logoPath), cid: 'logo', contentDisposition: 'inline', }, @@ -73,7 +74,7 @@ export class MailService { attachments: [ { filename: 'logo.png', - path: path.resolve(process.cwd(), 'src/modules/mail/template/logo.png'), + path: path.resolve(process.cwd(), this.logoPath), cid: 'logo', contentDisposition: 'inline', }, @@ -94,7 +95,7 @@ export class MailService { attachments: [ { filename: 'logo.png', - path: path.resolve(process.cwd(), 'src/modules/mail/template/logo.png'), + path: path.resolve(process.cwd(), this.logoPath), cid: 'logo', contentDisposition: 'inline', }, @@ -127,7 +128,7 @@ export class MailService { attachments: [ { filename: 'logo.png', - path: path.resolve(process.cwd(), 'src/modules/mail/template/logo.png'), + path: path.resolve(process.cwd(), this.logoPath), cid: 'logo', contentDisposition: 'inline', }, @@ -160,7 +161,7 @@ export class MailService { attachments: [ { filename: 'logo.png', - path: path.resolve(process.cwd(), 'src/modules/mail/template/logo.png'), + path: path.resolve(process.cwd(), this.logoPath), cid: 'logo', contentDisposition: 'inline', }, From 37f9a01a9c2ec73bf3b406bdd6a86adecb50f406 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Fri, 13 Dec 2024 11:08:21 +0900 Subject: [PATCH 10/19] =?UTF-8?q?fix:=20=20=EC=97=90=EC=84=B8=EC=9D=B4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/typeorm.config.ts | 2 +- src/entities/essay.entity.ts | 4 +- .../essay/infrastructure/essay.repository.ts | 44 ++++++++++++++----- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/config/typeorm.config.ts b/src/config/typeorm.config.ts index 421de5b..da12442 100644 --- a/src/config/typeorm.config.ts +++ b/src/config/typeorm.config.ts @@ -31,7 +31,7 @@ export const TypeormConfig: TypeOrmModuleAsyncOptions = { connectionTimeoutMillis: 5000, }, ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : undefined, - logging: false, + logging: true, logger: 'advanced-console', }), async dataSourceFactory(option) { diff --git a/src/entities/essay.entity.ts b/src/entities/essay.entity.ts index 7cc1009..24255e7 100644 --- a/src/entities/essay.entity.ts +++ b/src/entities/essay.entity.ts @@ -89,11 +89,11 @@ export class Essay { device: Device; @JoinTable({ name: 'essay_tags' }) - @ManyToMany(() => Tag, (tag) => tag.essays, { onDelete: 'CASCADE' }) + @ManyToMany(() => Tag, (tag) => tag.essays, { onDelete: 'CASCADE', nullable: true }) tags: Tag[]; @JoinColumn({ name: 'story_id' }) - @ManyToOne(() => Story, (story) => story.essays, { onDelete: 'CASCADE' }) + @ManyToOne(() => Story, (story) => story.essays, { onDelete: 'CASCADE', nullable: true }) story: Story; @Index() diff --git a/src/modules/base/essay/infrastructure/essay.repository.ts b/src/modules/base/essay/infrastructure/essay.repository.ts index 9009894..a769256 100644 --- a/src/modules/base/essay/infrastructure/essay.repository.ts +++ b/src/modules/base/essay/infrastructure/essay.repository.ts @@ -127,26 +127,48 @@ export class EssayRepository implements IEssayRepository { } async findTargetUserEssays(userId: number, storyId: number, page: number, limit: number) { - const queryBuilder = this.essayRepository + const subQuery = this.essayRepository .createQueryBuilder('essay') .leftJoinAndSelect('essay.author', 'author') - .leftJoinAndSelect('essay.story', 'story') - .leftJoinAndSelect('essay.tags', 'tags') - .where('essay.author.id = :userId', { userId }) + .where('essay.author_id = :userId', { userId }) .andWhere('essay.status != :linkedOutStatus', { linkedOutStatus: EssayStatus.LINKEDOUT }) .andWhere('essay.status != :privateStatus', { privateStatus: EssayStatus.PRIVATE }) - .andWhere('essay.status != :burialStatus', { burialStatus: EssayStatus.BURIAL }); + .andWhere('essay.status != :burialStatus', { burialStatus: EssayStatus.BURIAL }) + .andWhere('essay.deletedDate IS NULL'); if (storyId !== undefined) { - queryBuilder.andWhere('essay.story.id = :storyId', { storyId }); - queryBuilder.orderBy('essay.createdDate', 'ASC'); - } else { - queryBuilder.orderBy('essay.createdDate', 'DESC'); + subQuery.andWhere('essay.story_id = :storyId', { storyId }); } - queryBuilder.offset((page - 1) * limit).limit(limit); + const idsResult = await subQuery + .orderBy('essay.createdDate', 'DESC') + .offset((page - 1) * limit) + .limit(limit) + .getRawMany(); - const [essays, total] = await queryBuilder.getManyAndCount(); + const essayIds = idsResult.map((row) => row.essay_id); + + if (essayIds.length === 0) { + return { essays: [], total: 0 }; + } + + const total = await this.essayRepository + .createQueryBuilder('essay') + .where('essay.author_id = :userId', { userId }) + .andWhere('essay.status != :linkedOutStatus', { linkedOutStatus: EssayStatus.LINKEDOUT }) + .andWhere('essay.status != :privateStatus', { privateStatus: EssayStatus.PRIVATE }) + .andWhere('essay.status != :burialStatus', { burialStatus: EssayStatus.BURIAL }) + .andWhere('essay.deletedDate IS NULL') + .getCount(); + + const essays = await this.essayRepository + .createQueryBuilder('essay') + .leftJoinAndSelect('essay.author', 'author') + .leftJoinAndSelect('essay.tags', 'tags') + .leftJoinAndSelect('essay.story', 'story') + .whereInIds(essayIds) + .orderBy('essay.createdDate', 'DESC') + .getMany(); return { essays, total }; } From a11dcc12235f7d4643302af0368eba5bbe8506f5 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Fri, 13 Dec 2024 11:14:50 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=9D=94=EC=A0=81=20=EC=A0=9C=EA=B1=B0...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/essay/core/essay.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/base/essay/core/essay.service.ts b/src/modules/base/essay/core/essay.service.ts index 71aa902..17e281d 100644 --- a/src/modules/base/essay/core/essay.service.ts +++ b/src/modules/base/essay/core/essay.service.ts @@ -93,8 +93,7 @@ export class EssayService { const savedEssay = await this.essayRepository.saveEssay(essayData); - const x = this.utilsService.transformToDto(EssayResDto, savedEssay); - console.log(x); + return this.utilsService.transformToDto(EssayResDto, savedEssay); } private async evaluateUserReputation(user: User) { From 000c92226a468c5a1a0c49a1417f8d15a4040013 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Tue, 17 Dec 2024 00:04:16 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=88=84=EB=9D=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/typeorm.config.ts | 2 +- src/modules/base/essay/core/essay.service.ts | 1 + src/modules/base/essay/infrastructure/essay.repository.ts | 1 + tsconfig.json | 8 ++++---- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/config/typeorm.config.ts b/src/config/typeorm.config.ts index da12442..421de5b 100644 --- a/src/config/typeorm.config.ts +++ b/src/config/typeorm.config.ts @@ -31,7 +31,7 @@ export const TypeormConfig: TypeOrmModuleAsyncOptions = { connectionTimeoutMillis: 5000, }, ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : undefined, - logging: true, + logging: false, logger: 'advanced-console', }), async dataSourceFactory(option) { diff --git a/src/modules/base/essay/core/essay.service.ts b/src/modules/base/essay/core/essay.service.ts index 17e281d..d4c91cd 100644 --- a/src/modules/base/essay/core/essay.service.ts +++ b/src/modules/base/essay/core/essay.service.ts @@ -67,6 +67,7 @@ export class EssayService { HttpStatus.BAD_REQUEST, ); } + console.log(data); const user = await this.userService.fetchUserEntityById(requester.id); const tags = (await this.tagService.getTags(data.tags)) || []; diff --git a/src/modules/base/essay/infrastructure/essay.repository.ts b/src/modules/base/essay/infrastructure/essay.repository.ts index a769256..c0cc477 100644 --- a/src/modules/base/essay/infrastructure/essay.repository.ts +++ b/src/modules/base/essay/infrastructure/essay.repository.ts @@ -55,6 +55,7 @@ export class EssayRepository implements IEssayRepository { title: data.title, content: data.content, linkedOutGauge: data.linkedOutGauge, + thumbnail: data.thumbnail, status: data.status, device: data.device, author: data.author, diff --git a/tsconfig.json b/tsconfig.json index 720f474..7b22715 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { "compilerOptions": { "module": "commonjs", + "target": "ES2021", + "outDir": "./dist", + "baseUrl": "./", + "sourceMap": true, "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", "incremental": true, "skipLibCheck": true, "strictNullChecks": false, From e28574c082701fea340105c57ffad34c624126fd Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Wed, 18 Dec 2024 15:06:04 +0900 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20=EC=98=A4=EB=A6=AC=EC=A7=84=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.ts b/src/main.ts index 5cf84e8..c6ed549 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,6 +33,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule, { snapshot: true }); const allowedOrigins = [ + 'app://.', 'https://linkedoutapp.com', 'https://linkedout-umber.vercel.app', 'https://admin.linkedoutapp.com', From 1c8ac8b6c78bfdb32ee492d28158777acd0df4ed Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Fri, 20 Dec 2024 17:33:51 +0900 Subject: [PATCH 14/19] =?UTF-8?q?chore:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b2a524..ee91cfc 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ docker run --name your_container_name -p 6379:6379 -d redis:6 ### 4. 서버와 데이터베이스 연결 서버를 실행시키기 전 데이터베이스 컨테이너들을 활성화시키고 환경변수가 모두 등록되어야 합니다. -디스코드 서버의 `서버자료`를 참조하세요. +디스코드 서버의 `서버자료`를 참조하거나 담당자를 호출하세요(멱살) *** From 5b5f65862d97461bc281fb2f76b345c90be4587f Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Mon, 23 Dec 2024 04:08:19 +0900 Subject: [PATCH 15/19] =?UTF-8?q?test:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20oauth=20=ED=95=B8=EB=93=A4=EB=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/auth/api/auth.oauth.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/base/auth/api/auth.oauth.controller.ts b/src/modules/base/auth/api/auth.oauth.controller.ts index 06251e4..3a06f71 100644 --- a/src/modules/base/auth/api/auth.oauth.controller.ts +++ b/src/modules/base/auth/api/auth.oauth.controller.ts @@ -60,11 +60,11 @@ export class AuthOauthController { req.user = await this.authService.oauthLogin(req.user); const jwt = await this.authService.login(req); - let redirectUrl = this.configService.get('WEB_REGISTER_REDIRECT'); - - redirectUrl += `?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; + // let redirectUrl = this.configService.get('WEB_REGISTER_REDIRECT'); + // redirectUrl += `?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; + // res.redirect(redirectUrl); - res.redirect(redirectUrl); + res.json({ accessToken: jwt.accessToken, refreshToken: jwt.refreshToken }); } @Post('google/mobile') From 1828c8db2ca54ef4fe138bf61b85e61ef462941a Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Mon, 23 Dec 2024 15:24:20 +0900 Subject: [PATCH 16/19] =?UTF-8?q?test:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20oauth=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/auth/api/auth.oauth.controller.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/base/auth/api/auth.oauth.controller.ts b/src/modules/base/auth/api/auth.oauth.controller.ts index 3a06f71..9a2fd27 100644 --- a/src/modules/base/auth/api/auth.oauth.controller.ts +++ b/src/modules/base/auth/api/auth.oauth.controller.ts @@ -62,9 +62,8 @@ export class AuthOauthController { // let redirectUrl = this.configService.get('WEB_REGISTER_REDIRECT'); // redirectUrl += `?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; - // res.redirect(redirectUrl); - - res.json({ accessToken: jwt.accessToken, refreshToken: jwt.refreshToken }); + const redirectUrl = `app://./web/login?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; + res.redirect(redirectUrl); } @Post('google/mobile') From 2cdce1fd108755e0d86467a9d9d13efd159dc3c7 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Mon, 23 Dec 2024 16:08:30 +0900 Subject: [PATCH 17/19] =?UTF-8?q?test:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20oauth=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/auth/api/auth.oauth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/base/auth/api/auth.oauth.controller.ts b/src/modules/base/auth/api/auth.oauth.controller.ts index 9a2fd27..58fb4d6 100644 --- a/src/modules/base/auth/api/auth.oauth.controller.ts +++ b/src/modules/base/auth/api/auth.oauth.controller.ts @@ -62,7 +62,7 @@ export class AuthOauthController { // let redirectUrl = this.configService.get('WEB_REGISTER_REDIRECT'); // redirectUrl += `?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; - const redirectUrl = `app://./web/login?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; + const redirectUrl = `app://./home?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; res.redirect(redirectUrl); } From e2ebb183088b4a83ac1c260b76d500aea50f4013 Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Tue, 24 Dec 2024 00:06:09 +0900 Subject: [PATCH 18/19] =?UTF-8?q?fix:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20oauth=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/auth/api/auth.oauth.controller.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/base/auth/api/auth.oauth.controller.ts b/src/modules/base/auth/api/auth.oauth.controller.ts index 58fb4d6..d46e32e 100644 --- a/src/modules/base/auth/api/auth.oauth.controller.ts +++ b/src/modules/base/auth/api/auth.oauth.controller.ts @@ -60,9 +60,8 @@ export class AuthOauthController { req.user = await this.authService.oauthLogin(req.user); const jwt = await this.authService.login(req); - // let redirectUrl = this.configService.get('WEB_REGISTER_REDIRECT'); - // redirectUrl += `?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; - const redirectUrl = `app://./home?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; + let redirectUrl = this.configService.get('WEB_REGISTER_REDIRECT'); + redirectUrl += `?accessToken=${jwt.accessToken}&refreshToken=${jwt.refreshToken}`; res.redirect(redirectUrl); } From f71451eed0f54e510a21aee2e4ba93100e344ebe Mon Sep 17 00:00:00 2001 From: JoDaeChan Date: Thu, 26 Dec 2024 16:01:29 +0900 Subject: [PATCH 19/19] =?UTF-8?q?fix:=20=EC=9D=BC=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A1=A0=20oauth=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/base/auth/api/auth.oauth.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/base/auth/api/auth.oauth.controller.ts b/src/modules/base/auth/api/auth.oauth.controller.ts index d46e32e..b614a1f 100644 --- a/src/modules/base/auth/api/auth.oauth.controller.ts +++ b/src/modules/base/auth/api/auth.oauth.controller.ts @@ -48,7 +48,7 @@ export class AuthOauthController { 1. 구글로부터 전달된 사용자 정보를 검증합니다. 2. 사용자가 처음 로그인하는 경우, 새로운 계정을 생성합니다. 3. 기존 사용자라면, 로그인 정보를 업데이트합니다. - 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 리다이렉션 합니다. + 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 웹앱으로 리다이렉션 합니다. **주의 사항:** - 유효하지 않은 구글 사용자 정보가 전달될 경우, 인증이 실패할 수 있습니다. @@ -122,7 +122,7 @@ export class AuthOauthController { 1. 카카오로부터 전달된 사용자 정보를 검증합니다. 2. 사용자가 처음 로그인하는 경우, 새로운 계정을 생성합니다. 3. 기존 사용자라면, 로그인 정보를 업데이트합니다. - 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 리다이렉션 합니다. + 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 웹앱으로 리다이렉션 합니다. **주의 사항:** - 유효하지 않은 카카오 사용자 정보가 전달될 경우, 인증이 실패할 수 있습니다. @@ -197,7 +197,7 @@ export class AuthOauthController { 1. 네이버로부터 전달된 사용자 정보를 검증합니다. 2. 사용자가 처음 로그인하는 경우, 새로운 계정을 생성합니다. 3. 기존 사용자라면, 로그인 정보를 업데이트합니다. - 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 리다이렉션 합니다. + 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 웹앱으로 리다이렉션 합니다. **주의 사항:** - 유효하지 않은 네이버 사용자 정보가 전달될 경우, 인증이 실패할 수 있습니다. @@ -272,7 +272,7 @@ export class AuthOauthController { 1. 애플로부터 전달된 사용자 정보를 검증합니다. 2. 사용자가 처음 로그인하는 경우, 새로운 계정을 생성합니다. 3. 기존 사용자라면, 로그인 정보를 업데이트합니다. - 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 리다이렉션 합니다. + 4. 인증에 성공하면 쿼리스트링에 JWT를 세팅하고 웹앱으로 리다이렉션 합니다. **주의 사항:** - 유효하지 않은 네이버 사용자 정보가 전달될 경우, 인증이 실패할 수 있습니다.