From d86c3bf7d835e153d09313e993d7b4407c4ddd00 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 06:11:16 +0900 Subject: [PATCH 01/24] =?UTF-8?q?=F0=9F=9A=9A=20Chore:=20mediasoup=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sfu 서버 역할의 모듈을 만들고 이를 돕기 위해 mediasoup를 설정하였다 --- rep/main_backend/package.json | 1 + rep/main_backend/pnpm-lock.yaml | 178 ++++++++++++++++-- .../src/3-1.infra/media/media.module.ts | 13 ++ .../src/3-1.infra/media/mediasoup/config.ts | 32 ++++ .../src/3-1.infra/media/mediasoup/media.ts | 53 ++++++ .../3-2.presentation/webrtc/sfu/sfu.module.ts | 10 + .../websocket/signaling/signaling.gateway.ts | 3 + .../websocket/signaling/signaling.module.ts | 2 +- rep/main_backend/src/app.module.ts | 4 + 9 files changed, 275 insertions(+), 21 deletions(-) create mode 100644 rep/main_backend/src/3-1.infra/media/media.module.ts create mode 100644 rep/main_backend/src/3-1.infra/media/mediasoup/config.ts create mode 100644 rep/main_backend/src/3-1.infra/media/mediasoup/media.ts create mode 100644 rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts diff --git a/rep/main_backend/package.json b/rep/main_backend/package.json index 3b5bbee1b..d11f641ff 100644 --- a/rep/main_backend/package.json +++ b/rep/main_backend/package.json @@ -41,6 +41,7 @@ "graphql": "^16.12.0", "graphql-type-json": "^0.3.2", "jose": "^6.1.3", + "mediasoup": "^3.19.14", "mysql2": "^3.15.3", "redis": "^5.10.0", "reflect-metadata": "^0.1.13", diff --git a/rep/main_backend/pnpm-lock.yaml b/rep/main_backend/pnpm-lock.yaml index 75d58fb55..f688ce7cb 100644 --- a/rep/main_backend/pnpm-lock.yaml +++ b/rep/main_backend/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: jose: specifier: ^6.1.3 version: 6.1.3 + mediasoup: + specifier: ^3.19.14 + version: 3.19.14 mysql2: specifier: ^3.15.3 version: 3.16.0 @@ -709,6 +712,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -1374,6 +1381,9 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/ini@4.1.1': + resolution: {integrity: sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -1942,6 +1952,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -2106,6 +2120,10 @@ packages: cssfilter@0.0.10: resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2524,6 +2542,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -2563,6 +2585,9 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -2594,6 +2619,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + formidable@2.1.5: resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} @@ -2739,6 +2768,10 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + h264-profile-level-id@2.3.2: + resolution: {integrity: sha512-hnq1UDlw7WGJV6GCr/g7wnkHYUjdAY2bis9rgn2JqSdQS2WfVvnt1ZE9g8nTguracodf5LLKZOwURsDN49YtBQ==} + engines: {node: '>=20'} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -2829,6 +2862,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -3310,6 +3347,10 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + mediasoup@3.19.14: + resolution: {integrity: sha512-8ESNnyvxD5XgMSORMR258T/RIcL6e0/eDOTeHM4GG4dezJOdgwdbjBRWkfL9f4oHKtpar2NgbIPEqg62uWhoXg==} + engines: {node: '>=22'} + memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} @@ -3376,6 +3417,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -3431,6 +3476,11 @@ packages: resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} @@ -3443,6 +3493,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -4020,6 +4074,10 @@ packages: engines: {node: '>=6.4.0'} deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -4048,6 +4106,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + engines: {node: '>=18'} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -4322,6 +4384,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4437,6 +4503,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -5131,7 +5201,7 @@ snapshots: '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5288,7 +5358,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -5336,7 +5406,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5377,7 +5447,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5395,6 +5465,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -6236,7 +6310,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) fflate: 0.8.2 token-types: 6.1.1 transitivePeerDependencies: @@ -6329,6 +6403,8 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/ini@4.1.1': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -6420,7 +6496,7 @@ snapshots: '@typescript-eslint/types': 8.50.0 '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.50.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: @@ -6430,7 +6506,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) '@typescript-eslint/types': 8.50.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6449,7 +6525,7 @@ snapshots: '@typescript-eslint/types': 8.50.0 '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) '@typescript-eslint/utils': 8.50.0(eslint@8.57.1)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 8.57.1 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -6464,7 +6540,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) '@typescript-eslint/types': 8.50.0 '@typescript-eslint/visitor-keys': 8.50.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 @@ -6899,7 +6975,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) http-errors: 2.0.0 iconv-lite: 0.7.1 on-finished: 2.4.1 @@ -7005,6 +7081,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@3.0.0: {} + chrome-trace-event@1.0.4: {} ci-info@3.9.0: {} @@ -7157,6 +7235,8 @@ snapshots: cssfilter@0.0.10: {} + data-uri-to-buffer@4.0.1: {} + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -7187,9 +7267,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 dedent@1.7.0: {} @@ -7280,7 +7362,7 @@ snapshots: base64id: 2.0.0 cookie: 0.7.2 cors: 2.8.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) engine.io-parser: 5.2.3 ws: 8.18.3 transitivePeerDependencies: @@ -7412,7 +7494,7 @@ snapshots: eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 8.57.1 eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.0 @@ -7502,7 +7584,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -7660,6 +7742,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} figures@3.2.0: @@ -7697,7 +7784,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -7722,6 +7809,8 @@ snapshots: keyv: 4.5.4 rimraf: 3.0.2 + flatbuffers@25.9.23: {} + flatted@3.3.3: {} follow-redirects@1.15.11: {} @@ -7760,6 +7849,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + formidable@2.1.5: dependencies: '@paralleldrive/cuid2': 2.3.1 @@ -7899,6 +7992,12 @@ snapshots: graphql@16.12.0: {} + h264-profile-level-id@2.3.2(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -7985,6 +8084,8 @@ snapshots: inherits@2.0.4: {} + ini@6.0.0: {} + inquirer@8.2.6: dependencies: ansi-escapes: 4.3.2 @@ -8195,7 +8296,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -8650,6 +8751,17 @@ snapshots: media-typer@1.1.0: {} + mediasoup@3.19.14: + dependencies: + '@types/ini': 4.1.1 + debug: 4.4.3(supports-color@10.2.2) + flatbuffers: 25.9.23 + h264-profile-level-id: 2.3.2(supports-color@10.2.2) + ini: 6.0.0 + node-fetch: 3.3.2 + supports-color: 10.2.2 + tar: 7.5.2 + memfs@3.5.3: dependencies: fs-monkey: 1.1.0 @@ -8697,6 +8809,10 @@ snapshots: minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -8749,6 +8865,8 @@ snapshots: node-addon-api@8.5.0: {} + node-domexception@1.0.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 @@ -8757,6 +8875,12 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build@4.8.4: {} node-int64@0.4.0: {} @@ -9223,7 +9347,7 @@ snapshots: socket.io-adapter@2.5.6: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -9233,7 +9357,7 @@ snapshots: socket.io-parser@4.2.5: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -9242,7 +9366,7 @@ snapshots: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) engine.io: 6.6.5 socket.io-adapter: 2.5.6 socket.io-parser: 4.2.5 @@ -9370,7 +9494,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) fast-safe-stringify: 2.1.1 form-data: 4.0.5 formidable: 2.1.5 @@ -9388,6 +9512,8 @@ snapshots: transitivePeerDependencies: - supports-color + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -9408,6 +9534,14 @@ snapshots: tapable@2.3.0: {} + tar@7.5.2: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + terser-webpack-plugin@5.3.16(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -9694,6 +9828,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webpack-node-externals@3.0.0: {} @@ -9826,6 +9962,8 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/rep/main_backend/src/3-1.infra/media/media.module.ts b/rep/main_backend/src/3-1.infra/media/media.module.ts new file mode 100644 index 000000000..3a3cc6f5c --- /dev/null +++ b/rep/main_backend/src/3-1.infra/media/media.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { MediasoupService } from "./mediasoup/media"; + + +@Module({ + providers : [ + MediasoupService, + ], + exports : [ + MediasoupService, + ] +}) +export class MediaModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts new file mode 100644 index 000000000..62bb2da02 --- /dev/null +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts @@ -0,0 +1,32 @@ +import { type WorkerSettings } from "mediasoup/types"; + + +export const mediaSoupWorkerConfig : WorkerSettings = { + // log에 남길 레벨을 정한다. ( warn ) + logLevel : process.env.NODE_ENV === "production" ? "warn" : "debug", + + logTags : process.env.NODE_ENV === "production" ? + [ + "info", "ice", "dtls", "rtp" + ] + : + [ + "info", + "ice", // ice 후보 찾기 등 할때 로깅을 남기려고 한다. + "dtls", // dtls 핸드세이킹 할때 로깅 남기기 + "srtp", // rtp 패킷을 암호화해서 보내는 부분 로깅 + "rtp", // rtp 전송할때 로깅을 남김 + "rtcp" // rtp를 제어하는 프로토콜인데 이 부분에서 로깅을 남긴다. + ], + + // udp 사용 포트 + rtcMinPort: 40000, + rtcMaxPort: 49999, + + // 현재나의 로그 메타데이터를 추가하려면 + appData : { + env : process.env.NODE_ENV + // 나중에 ip를 추가해서 어느 ip에 worker인지도 확인이 가능하고 탐색할때 유용하다. + } + +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts new file mode 100644 index 000000000..719fcdb05 --- /dev/null +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts @@ -0,0 +1,53 @@ +import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; +import { type Worker } from "mediasoup/types"; +import * as mediasoup from "mediasoup"; +import * as os from "os"; +import { mediaSoupWorkerConfig } from "./config"; + + +@Injectable() +export class MediasoupService implements OnModuleInit, OnModuleDestroy { + + private readonly logger = new Logger(MediasoupService.name); + + // 가용 가능한 worker ( cpu 개수당 보통 한개의 worker를 부여한다. ) + private readonly workers : Array = []; // 이 infra에 worker들이 관리한다. ( 나중에 분리 가능 ) + private workerIdx : number = 0; // 현재 worker 위치 + + // 처음에 이 모듈의 의존성을 부여할때 실행 + async onModuleInit() : Promise { + const workerCount : number = Number(os.cpus().length) // 일단은 worker = cpu 갯수로 가고 나중에 조정할 수도? + + this.logger.log(`mediasoup에서 가용할 worker의 갯수: ${workerCount}`); + + // worker 생성 + for ( let i = 0; i < workerCount; i++ ) { + const worker = await mediasoup.createWorker(mediaSoupWorkerConfig); + + // worker가 죽었을때 속성 부여 + worker.on("died", () => { + this.logger.warn(`mediasoup에 ${worker.pid} 사망`); + if (process.env.NODE_ENV === "production") process.exit(1); // 배포환경에서 해당 worker 프로세스를 종료 ( 재시작 루트를 다시 만들어 줘야 한다. -> 배포 환경이라면 ) -> 극단적으로 하는 이유는 이 기능이 거의 가장 핵심이 되기 때문이다. + }); + + this.workers.push(worker); + }; + }; + + // module이 내려가면 worker를 정리해주어야 한다. ( 안정성 높이기 위해 ) + async onModuleDestroy() : Promise { + for (const worker of this.workers) { + try { worker.close(); } catch (e) { this.logger.error(e); } + } + this.workers.length = 0; // 메모리에도 삭제 + }; + + // worker 가져오기 ( round-robin 알고리즘 사용 ) + picWorker() : Worker { + if ( this.workers.length === 0 ) throw new Error("worker가 존재하지 않습니다."); + + const worker = this.workers[this.workerIdx]; + this.workerIdx = ( this.workerIdx + 1 ) % this.workers.length; + return worker; + }; +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts new file mode 100644 index 000000000..3fa9831d5 --- /dev/null +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts @@ -0,0 +1,10 @@ +import { MediaModule } from "@infra/media/media.module"; +import { Module } from "@nestjs/common"; + + +@Module({ + imports : [ + MediaModule + ] +}) +export class SfuModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index 27231cfdb..8a9097667 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -115,4 +115,7 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec }; }; + // 방에 가입 한 후 라우터 생성 + + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts index 9b8dac75f..4f0b7aaa8 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts @@ -12,7 +12,7 @@ import { SignalingWebsocketGateway } from "./signaling.gateway"; @Module({ imports : [ - AuthWebsocketModule + AuthWebsocketModule, ], providers : [ // sfu 자체적인 모듈 diff --git a/rep/main_backend/src/app.module.ts b/rep/main_backend/src/app.module.ts index ca73e5bec..f3d4ac970 100644 --- a/rep/main_backend/src/app.module.ts +++ b/rep/main_backend/src/app.module.ts @@ -16,6 +16,8 @@ import { S3DiskModule } from '@infra/disk/s3/disk'; import { RedisChannelModule } from '@infra/channel/redis/channel'; import { RoomModule } from "@present/http/room/room.module"; import { SignalingWebsocketModule } from "@present/websocket/signaling/signaling.module"; +import { MediaModule } from "@infra/media/media.module"; +import { SfuModule } from "./3-2.presentation/webrtc/sfu/sfu.module"; @Module({ @@ -29,12 +31,14 @@ import { SignalingWebsocketModule } from "@present/websocket/signaling/signalin JwtModule, // jwt를 사용하기 위한 모듈 S3DiskModule, // s3를 사용하기 위한 모듈 RedisChannelModule, // redis를 활용한 pub sub을 이용하기 위한 모듈 + MediaModule, // media를 다루기 위한 모듈 // 우리가 집적 만든 모듈 SettingModule, // 헬스 체크를 위한 모듈 AuthModule, // 인증과 관련된 모듈 RoomModule, // 회의방 생성과 관련된 모듈 SignalingWebsocketModule , // 사실상 시그널링 서버의 역할을 하는 모듈 + SfuModule, // sfu와 관련되 모듈 ( 나중에 sfu 서버를 따로 분리할것을 생각하고 만든 모듈 그러니 sfu server라고 생각하면 될것 같다. ) ], controllers: [], From 5c431e07642eb374d2c83fcfdbff6326fd4f2b02 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 06:31:14 +0900 Subject: [PATCH 02/24] =?UTF-8?q?=F0=9F=94=A8=20Fix:=20docker=EC=97=90?= =?UTF-8?q?=EC=84=9C=20mediasoup=20=EB=B9=8C=EB=93=9C=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=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 --- rep/main_backend/Dockerfile | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/rep/main_backend/Dockerfile b/rep/main_backend/Dockerfile index 3be8e45f8..5e2d50e1d 100644 --- a/rep/main_backend/Dockerfile +++ b/rep/main_backend/Dockerfile @@ -5,6 +5,11 @@ FROM node:22-slim AS base WORKDIR /app +# python3, make, g++ mediasoup에게 필요한 패키지 추가 +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 make g++ pkg-config git \ + && rm -rf /var/lib/apt/lists/* + # pnpm && yarn과 같은 패키지 매니저 사용 허용 & pnpm 설치 RUN corepack enable \ && corepack prepare pnpm@10.0.0 --activate @@ -22,20 +27,24 @@ RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/stor COPY . . -# 빌드 +# 빌드 후 prod만 남기기 RUN pnpm build +RUN pnpm prune --prod # 실제 실행 -> 이것만 남음 -FROM base AS runner +FROM node:22-slim AS runner -# 필요한 의존성 가져오기 -COPY package.json pnpm-lock.yaml ./ +# 현재 문제가 발생하는 pnpm i를 (mediasoup문제) 해결하기 위해서 이를 주석처리하고 안전하게 build한 것을 사용 +# # 필요한 의존성 가져오기 +# COPY package.json pnpm-lock.yaml ./ -# 따로 설치해서 실행이 되도록 하기 -> 기존 캐시 최대한 유지 -RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/store \ - pnpm install --prod --frozen-lockfile +# # 따로 설치해서 실행이 되도록 하기 -> 기존 캐시 최대한 유지 +# RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/store \ +# pnpm install --prod --frozen-lockfile -# 빌드 패키지에서 빌드 결과물 가져오기 +# 빌드 패키지에서 실행에 필요한 빌드 결과물 가져오기 +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist # 실행 관련 From 24af1e8ab6738166635f89165c15f40e953a44a9 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 06:47:32 +0900 Subject: [PATCH 03/24] =?UTF-8?q?=F0=9F=94=A8=20Fix:=20script=20igore?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/https/nginx.backend.conf | 2 +- deploy/https/nginx.gateway.conf | 2 +- deploy/test/nginx.backend.conf | 2 +- deploy/test/nginx.gateway.conf | 2 +- rep/main_backend/Dockerfile | 9 +++++++++ rep/main_backend/src/3-1.infra/media/mediasoup/media.ts | 2 +- 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/deploy/https/nginx.backend.conf b/deploy/https/nginx.backend.conf index c1b7975da..04d832087 100644 --- a/deploy/https/nginx.backend.conf +++ b/deploy/https/nginx.backend.conf @@ -23,7 +23,7 @@ http { listen 80; server_name _; - # 마찬가지로 실제 배포 환경에서도 sse 연결이 가능하도록 하는 것이 중요하다. + # 마찬가지로 실제 배포 환경에서도 sse 연결이 가능하도록 하는 것이 중요하다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. location ~ ^/api/cards/sse$ { proxy_pass http://main_backend_app; diff --git a/deploy/https/nginx.gateway.conf b/deploy/https/nginx.gateway.conf index f83335f9f..5b130a543 100644 --- a/deploy/https/nginx.gateway.conf +++ b/deploy/https/nginx.gateway.conf @@ -41,7 +41,7 @@ server { # Docker에 dns 서버 resolver 127.0.0.11 ipv6=off; - # test와 마찬가지로 sse 연결에 대해서 설정을 해두는 것이 중요하다. + # test와 마찬가지로 sse 연결에 대해서 설정을 해두는 것이 중요하다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. location ~ ^/api/cards/sse$ { proxy_pass http://main_backend_app; diff --git a/deploy/test/nginx.backend.conf b/deploy/test/nginx.backend.conf index a80c0e04a..4f65483ce 100644 --- a/deploy/test/nginx.backend.conf +++ b/deploy/test/nginx.backend.conf @@ -26,7 +26,7 @@ http { listen 80; server_name _; - # sse를 사용하는 경우 버퍼링을 꺼주고 오래 연결이 가능하도록 해주어야 한다. + # sse를 사용하는 경우 버퍼링을 꺼주고 오래 연결이 가능하도록 해주어야 한다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. location ~ ^/api/cards/sse$ { proxy_pass http://main_backend_app; diff --git a/deploy/test/nginx.gateway.conf b/deploy/test/nginx.gateway.conf index 31f3f7623..95cc42bc0 100644 --- a/deploy/test/nginx.gateway.conf +++ b/deploy/test/nginx.gateway.conf @@ -25,7 +25,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - # sse를 위한 수정 - gateway도 수정해 줘야 백엔드에서 받을 수 있다. ( 백엔드 쪽이 살아있어도 gateway가 죽으면 의미가 없기에 ) + # sse를 위한 수정 - gateway도 수정해 줘야 백엔드에서 받을 수 있다. ( 백엔드 쪽이 살아있어도 gateway가 죽으면 의미가 없기에 ) -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. location ~ ^/api/cards/sse$ { proxy_pass http://main-backend-nginx; diff --git a/rep/main_backend/Dockerfile b/rep/main_backend/Dockerfile index 5e2d50e1d..bd78591b9 100644 --- a/rep/main_backend/Dockerfile +++ b/rep/main_backend/Dockerfile @@ -20,11 +20,20 @@ FROM base AS build # pnpm 설치를 위한 파일 복사 COPY package.json pnpm-lock.yaml ./ +# mediasoup는 C++로 만든 바이너리 패키지를 준비하는 패키지가 필요한다. 이 스크립트 설치를 막는 문제가 발생한다. -> 이를 해결하기 위한 요소1 +RUN pnpm config set ignore-scripts false + # 임시 캐시 볼륨을 마운트 하고 (이미지에는 안남고 빌드 캐시로만) -> pnpm-store라는 캐시 공간을 공유하고 그 공간을 정한다. # buildkit을 이용해서 빌드를 빠르게 한다 RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile +# 현재 발생하는 mediasoup poinstall 안되는 사례를 이걸 이용해서 안전하게 하려한다. +# RUN set -eux; \ +# node -e "const path=require('path'); console.log(path.dirname(require.resolve('mediasoup/package.json')))" > /tmp/MSDIR; \ +# cd "$(cat /tmp/MSDIR)"; \ +# npm run postinstall + COPY . . # 빌드 후 prod만 남기기 diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts index 719fcdb05..4cd3d8675 100644 --- a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts @@ -16,7 +16,7 @@ export class MediasoupService implements OnModuleInit, OnModuleDestroy { // 처음에 이 모듈의 의존성을 부여할때 실행 async onModuleInit() : Promise { - const workerCount : number = Number(os.cpus().length) // 일단은 worker = cpu 갯수로 가고 나중에 조정할 수도? + const workerCount : number = Number(Math.min(4, os.cpus().length)) // 일단은 worker = cpu 갯수로 가고 나중에 조정할 수도? ( 최대 4 ) this.logger.log(`mediasoup에서 가용할 worker의 갯수: ${workerCount}`); From 3858a3823592c9d5aeace491f8d226f71e6f6653 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 06:49:39 +0900 Subject: [PATCH 04/24] =?UTF-8?q?=F0=9F=94=A8=20Fix:=20=EC=A7=91=EC=A0=81?= =?UTF-8?q?=20pointinstall=EC=9D=84=20=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rep/main_backend/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rep/main_backend/Dockerfile b/rep/main_backend/Dockerfile index bd78591b9..812cfe67a 100644 --- a/rep/main_backend/Dockerfile +++ b/rep/main_backend/Dockerfile @@ -29,10 +29,10 @@ RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/stor pnpm install --frozen-lockfile # 현재 발생하는 mediasoup poinstall 안되는 사례를 이걸 이용해서 안전하게 하려한다. -# RUN set -eux; \ -# node -e "const path=require('path'); console.log(path.dirname(require.resolve('mediasoup/package.json')))" > /tmp/MSDIR; \ -# cd "$(cat /tmp/MSDIR)"; \ -# npm run postinstall +RUN set -eux; \ + node -e "const path=require('path'); console.log(path.dirname(require.resolve('mediasoup/package.json')))" > /tmp/MSDIR; \ + cd "$(cat /tmp/MSDIR)"; \ + npm run postinstall COPY . . From 4cdc7e1c84ac2814c0ca197ec4018f249c6a1e84 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 06:57:37 +0900 Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=94=A8=20Fix:=20mediasoup=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20=ED=8C=A8=ED=82=A4=EC=A7=80=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 - native기반의 mediasoup에 빌드 스크립트를 허용 --- rep/main_backend/Dockerfile | 9 +-------- rep/main_backend/package.json | 6 ++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rep/main_backend/Dockerfile b/rep/main_backend/Dockerfile index 812cfe67a..d3405d22a 100644 --- a/rep/main_backend/Dockerfile +++ b/rep/main_backend/Dockerfile @@ -20,19 +20,12 @@ FROM base AS build # pnpm 설치를 위한 파일 복사 COPY package.json pnpm-lock.yaml ./ -# mediasoup는 C++로 만든 바이너리 패키지를 준비하는 패키지가 필요한다. 이 스크립트 설치를 막는 문제가 발생한다. -> 이를 해결하기 위한 요소1 -RUN pnpm config set ignore-scripts false - # 임시 캐시 볼륨을 마운트 하고 (이미지에는 안남고 빌드 캐시로만) -> pnpm-store라는 캐시 공간을 공유하고 그 공간을 정한다. # buildkit을 이용해서 빌드를 빠르게 한다 RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile -# 현재 발생하는 mediasoup poinstall 안되는 사례를 이걸 이용해서 안전하게 하려한다. -RUN set -eux; \ - node -e "const path=require('path'); console.log(path.dirname(require.resolve('mediasoup/package.json')))" > /tmp/MSDIR; \ - cd "$(cat /tmp/MSDIR)"; \ - npm run postinstall +RUN pnpm rebuild mediasoup COPY . . diff --git a/rep/main_backend/package.json b/rep/main_backend/package.json index d11f641ff..5673348cd 100644 --- a/rep/main_backend/package.json +++ b/rep/main_backend/package.json @@ -91,5 +91,11 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "mediasoup", + "argon2" + ] } } From f230adc42358da7629227e08654c3a980ca2f09b Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 08:42:47 +0900 Subject: [PATCH 06/24] =?UTF-8?q?=F0=9F=8C=88=20Update:=20nginx=EC=97=90?= =?UTF-8?q?=20websocket=EB=A7=81=ED=81=AC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - websocket도 nginx에서 잘 처리할 수 있게 nginx설정을 변경하였습니다 --- deploy/https/nginx.backend.conf | 55 ++++++++++++++++++- deploy/https/nginx.gateway.conf | 51 ++++++++++++++--- deploy/test/nginx.backend.conf | 55 +++++++++++++++++-- deploy/test/nginx.gateway.conf | 50 +++++++++++++++-- rep/main_backend/Dockerfile | 1 + .../websocket/signaling/signaling.service.ts | 7 +++ 6 files changed, 197 insertions(+), 22 deletions(-) diff --git a/deploy/https/nginx.backend.conf b/deploy/https/nginx.backend.conf index 04d832087..37817681c 100644 --- a/deploy/https/nginx.backend.conf +++ b/deploy/https/nginx.backend.conf @@ -1,8 +1,9 @@ # 이제는 거의 동일 worker_processes auto; +# 4096개의 연결로 변경 events { - worker_connections 1024; + worker_connections 4096; } http { @@ -19,30 +20,78 @@ http { server main-backend-app-3:8080 max_fails=3 fail_timeout=10s; } + # websocket용 upstream + upstream main_backend_ws { + # load balancer를 hash로 해서 바뀌지 않도록 해야 한다. + ip_hash; + + server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; + server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; + server main-backend-app-3:8080 max_fails=3 fail_timeout=10s; + } + server { listen 80; server_name _; - # 마찬가지로 실제 배포 환경에서도 sse 연결이 가능하도록 하는 것이 중요하다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. - location ~ ^/api/cards/sse$ { + location ^~ /api/ws/ { + proxy_pass http://main_backend_ws; + + # 전역적으로 설정했지만 안전성을 위해서 한번 더 + proxy_http_version 1.1; + + # 웹소켓 업그레이드 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + + # 연결 높이기 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + # 실시간 성을 위해서 추가 + proxy_buffering off; + + # 이건 업스트림 변경을 일으키지 않게 한다. + proxy_next_upstream off; + } + + # sse를 사용하는 경우 버퍼링을 꺼주고 오래 연결이 가능하도록 해주어야 한다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. + location ^~ /api/sse/ { proxy_pass http://main_backend_app; proxy_http_version 1.1; proxy_set_header Connection ""; + proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # buffering 꺼주기 -> 실시간으로 바로 보낼 수 있도록 하기 위함이다. proxy_buffering off; proxy_cache off; add_header X-Accel-Buffering no; + # 오랫동안 연결되도록 하기 1시간 proxy_read_timeout 3600s; proxy_send_timeout 3600s; + # 응답을 메모리/디스크에 쌓지 않고 바로 전달하도록 한다. proxy_request_buffering off; + chunked_transfer_encoding on; + + # 스트리밍에 경우 업스트림을 교체하지 않는다. proxy_next_upstream off; + + # sse가 캐시되지 않도록 명시 + add_header Cache-Control "no-cache, no-store"; + gzip off; } diff --git a/deploy/https/nginx.gateway.conf b/deploy/https/nginx.gateway.conf index 5b130a543..e032b95b0 100644 --- a/deploy/https/nginx.gateway.conf +++ b/deploy/https/nginx.gateway.conf @@ -29,10 +29,6 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - # keep alive 사용을 위한 체크 - proxy_http_version 1.1; - proxy_set_header Connection ""; - # 443에도 certbot을 확인할 수 있도록 수정 location /.well-known/acme-challenge/ { root /var/www/certbot; @@ -41,16 +37,43 @@ server { # Docker에 dns 서버 resolver 127.0.0.11 ipv6=off; - # test와 마찬가지로 sse 연결에 대해서 설정을 해두는 것이 중요하다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. - location ~ ^/api/cards/sse$ { - proxy_pass http://main_backend_app; + # websocket 연결 설정 ( test 서버와 거의 동일 ) + location = /api/ws { + return 301 /api/ws/; + } + + location ^~ /api/ws/ { + proxy_pass http://main-backend-nginx; + # 전역적으로 설정했지만 안전성을 위해서 한번 더 proxy_http_version 1.1; - proxy_set_header Connection ""; + + # 웹소켓 업그레이드 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + + # 연결 높이기 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + # 실시간 성을 위해서 추가 + proxy_buffering off; + } + + + # test와 마찬가지로 sse 연결에 대해서 설정을 해두는 것이 중요하다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. + location ^~ /api/sse/ { + proxy_pass http://main-backend-nginx; + + # keep alive 설정 + proxy_http_version 1.1; + proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; @@ -60,14 +83,21 @@ server { proxy_send_timeout 3600s; proxy_request_buffering off; - proxy_next_upstream off; gzip off; + add_header Cache-Control "no-cache, no-store"; + + # 응답을 한번에 다 보내는게 아닌 조각 단위로 계속 나누어서 보내도록 설정 + chunked_transfer_encoding on; } # backend로 연결 location /api/ { proxy_pass http://main-backend-nginx; + # keep alive 사용을 위한 체크 + proxy_http_version 1.1; + proxy_set_header Connection ""; + # gateway도 설정을 안하면 api가 기다리든 말든 끊어버리니 api gateway도 설정을 해주는 것이 중요하다. -> 나중에 업로드 api만 따로 변경 client_max_body_size 10m; proxy_connect_timeout 5s; @@ -86,6 +116,9 @@ server { location / { proxy_pass http://frontend:3000; + proxy_http_version 1.1; + proxy_set_header Connection ""; + # 연결 지연시간 설정 proxy_connect_timeout 3s; proxy_send_timeout 30s; diff --git a/deploy/test/nginx.backend.conf b/deploy/test/nginx.backend.conf index 4f65483ce..a337c2d31 100644 --- a/deploy/test/nginx.backend.conf +++ b/deploy/test/nginx.backend.conf @@ -1,9 +1,9 @@ # 동시에 처리 가능한 프로세스 수 설정 worker_processes auto; -# 하나의 process는 1024개의 동시 연결이다. +# 하나의 process는 4096개의 동시 연결이다. events { - worker_connections 1024; + worker_connections 4096; } http { @@ -12,7 +12,7 @@ http { # load balancer least_conn; - # backend 헬스 체크 추가 + # backend 헬스 체크 추가 ( pool의 크기 ) keepalive 64; # 10초 동안 3번의 연결실패가 일어나면 그 앱은 닫는다. @@ -22,16 +22,59 @@ http { server main-backend-app-3:8080 max_fails=3 fail_timeout=10s; } + # websocket용 upstream 추가 + upstream main_backend_ws { + # load balancer를 hash로 해서 바뀌지 않도록 해야 한다. + ip_hash; + + server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; + server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; + server main-backend-app-3:8080 max_fails=3 fail_timeout=10s; + } + server { listen 80; server_name _; + # 웹소켓 관련 추가 + location = /api/ws { + return 301 /api/ws/; + } + + location ^~ /api/ws/ { + proxy_pass http://main_backend_ws; + + # 전역적으로 설정했지만 안전성을 위해서 한번 더 + proxy_http_version 1.1; + + # 웹소켓 업그레이드 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + + # 연결 높이기 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + # 실시간 성을 위해서 추가 + proxy_buffering off; + + # 이건 업스트림 변경을 일으키지 않게 한다. + proxy_next_upstream off; + } + # sse를 사용하는 경우 버퍼링을 꺼주고 오래 연결이 가능하도록 해주어야 한다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. - location ~ ^/api/cards/sse$ { + location ^~ /api/sse/ { proxy_pass http://main_backend_app; proxy_http_version 1.1; proxy_set_header Connection ""; + proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -48,10 +91,14 @@ http { # 응답을 메모리/디스크에 쌓지 않고 바로 전달하도록 한다. proxy_request_buffering off; + chunked_transfer_encoding on; # 스트리밍에 경우 업스트림을 교체하지 않는다. proxy_next_upstream off; + # sse가 캐시되지 않도록 명시 + add_header Cache-Control "no-cache"; + gzip off; } diff --git a/deploy/test/nginx.gateway.conf b/deploy/test/nginx.gateway.conf index 95cc42bc0..a5dd6931b 100644 --- a/deploy/test/nginx.gateway.conf +++ b/deploy/test/nginx.gateway.conf @@ -15,20 +15,49 @@ server { # Docker Dns로 TTL 주기마다 DNS를 재조회 한다. resolver 127.0.0.11 ipv6=off; - - # keep alive 설정 + # http 1.1 사용 proxy_http_version 1.1; - proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - # sse를 위한 수정 - gateway도 수정해 줘야 백엔드에서 받을 수 있다. ( 백엔드 쪽이 살아있어도 gateway가 죽으면 의미가 없기에 ) -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. - location ~ ^/api/cards/sse$ { + # 웹소켓 관련 추가 + # 리다이렉트도 추가 + location = /api/ws { + return 301 /api/ws/; + } + + location ^~ /api/ws/ { + proxy_pass http://main-backend-nginx; + + # 전역적으로 설정했지만 안전성을 위해서 한번 더 + proxy_http_version 1.1; + + # 웹소켓 업그레이드 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + + # 연결 높이기 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + # 실시간 성을 위해서 추가 + proxy_buffering off; + } + + # sse를 사용을 위한 부분 + location ^~ /api/sse/ { proxy_pass http://main-backend-nginx; + # keep alive 설정 proxy_http_version 1.1; proxy_set_header Connection ""; @@ -41,14 +70,20 @@ server { proxy_request_buffering off; gzip off; + add_header Cache-Control "no-cache"; + # 응답을 한번에 다 보내는게 아닌 조각 단위로 계속 나누어서 보내도록 설정 + chunked_transfer_encoding on; } # backend 프록시 전달 - location /api/ { + location /api/ { # Dns 탐색 proxy_pass http://main-backend-nginx; + # keep alive 설정 + proxy_set_header Connection ""; + client_max_body_size 10m; proxy_connect_timeout 5s; proxy_send_timeout 1m; @@ -69,6 +104,9 @@ server { location / { proxy_pass http://frontend:3000; + # keep alive 설정 + proxy_set_header Connection ""; + # frontend는 연결 시간을 짧게 잡아서 유저가 오류를 빨리 찾아낼 수 있도록 설정합니다. proxy_connect_timeout 3s; proxy_send_timeout 30s; diff --git a/rep/main_backend/Dockerfile b/rep/main_backend/Dockerfile index d3405d22a..3e8dfebd6 100644 --- a/rep/main_backend/Dockerfile +++ b/rep/main_backend/Dockerfile @@ -25,6 +25,7 @@ COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm-store-backend,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile +# ci에서 mediasoup 부분은 한번더 install해서 해결해보아야 한다. RUN pnpm rebuild mediasoup COPY . . diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index 90cdec92d..321a3e34a 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -56,6 +56,13 @@ export class SignalingWebsocketService { // ip를 파싱할때 사용하는 함수 private extractClientIp(client : Socket) : string { + + // 아래에서 위조 될수 있으니 최우선은 이 ip를 기준으로 한다. + const realIp = client.handshake.headers['x-real-ip']; + if (typeof realIp === 'string') { + return realIp; + } + // nginx가 클라이언트의 원 IP를 전달하기 위해서 만든 헤더이다. ( 즉 Nginx가 집적 걸어줌 ) const forwarded = client.handshake.headers['x-forwarded-for']; From 5b67df8daa5fdf100bd6d694f8415c1de45c03a0 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 09:16:23 +0900 Subject: [PATCH 07/24] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20dns=20?= =?UTF-8?q?=EC=9E=AC=EC=A1=B0=ED=9A=8C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 10초간 dns를 유지하고 2초동안만 기다리도록 설정했습니다. - 조회할때 dns를 재조회하도록 로직을 수정했습니다. --- deploy/https/nginx.gateway.conf | 18 ++++++++++++------ deploy/test/nginx.gateway.conf | 21 ++++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/deploy/https/nginx.gateway.conf b/deploy/https/nginx.gateway.conf index e032b95b0..8eb9d5062 100644 --- a/deploy/https/nginx.gateway.conf +++ b/deploy/https/nginx.gateway.conf @@ -35,7 +35,11 @@ server { } # Docker에 dns 서버 - resolver 127.0.0.11 ipv6=off; + # 최대 10초까지만 같은 이름을 쓰고 2초 까지 기다린다 + resolver 127.0.0.11 ipv6=off valid=10s; + resolver_timeout 2s; + + set $backend_upstream http://main-backend-nginx; # websocket 연결 설정 ( test 서버와 거의 동일 ) location = /api/ws { @@ -43,7 +47,7 @@ server { } location ^~ /api/ws/ { - proxy_pass http://main-backend-nginx; + proxy_pass $backend_upstream; # 전역적으로 설정했지만 안전성을 위해서 한번 더 proxy_http_version 1.1; @@ -69,7 +73,7 @@ server { # test와 마찬가지로 sse 연결에 대해서 설정을 해두는 것이 중요하다. -> 지금은 사용하지 않지만 마지막에 sse가 있으면 이를 이용하도록 할 예정이다. location ^~ /api/sse/ { - proxy_pass http://main-backend-nginx; + proxy_pass $backend_upstream; # keep alive 설정 proxy_http_version 1.1; @@ -92,7 +96,7 @@ server { # backend로 연결 location /api/ { - proxy_pass http://main-backend-nginx; + proxy_pass $backend_upstream; # keep alive 사용을 위한 체크 proxy_http_version 1.1; @@ -105,16 +109,18 @@ server { proxy_read_timeout 1m; } + set $frontend_upstream http://frontend:3000; + # NEXT에 캐싱서버 -> NEXT는 기본적으로 압축하는 로직이 존재한다. location /_next/static/ { - proxy_pass http://frontend:3000; + proxy_pass $frontend_upstream; expires 1y; add_header Cache-Control "public, immutable"; access_log off; } location / { - proxy_pass http://frontend:3000; + proxy_pass $frontend_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; diff --git a/deploy/test/nginx.gateway.conf b/deploy/test/nginx.gateway.conf index a5dd6931b..aa43ee25c 100644 --- a/deploy/test/nginx.gateway.conf +++ b/deploy/test/nginx.gateway.conf @@ -12,8 +12,9 @@ server { # application/javascript text/javascript application/rss+xml # image/svg+xml; - # Docker Dns로 TTL 주기마다 DNS를 재조회 한다. - resolver 127.0.0.11 ipv6=off; + # Docker Dns로 TTL 주기마다 DNS를 재조회 한다. ( 10초 마다 확인하기 ) + resolver 127.0.0.11 ipv6=off valid=10s; + resolver_timeout 2s; # http 1.1 사용 proxy_http_version 1.1; @@ -23,6 +24,9 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # 백엔드도 DNS를 항상 조회하게 해야 한다. + set $backend_upstream http://main-backend-nginx; + # 웹소켓 관련 추가 # 리다이렉트도 추가 location = /api/ws { @@ -30,7 +34,7 @@ server { } location ^~ /api/ws/ { - proxy_pass http://main-backend-nginx; + proxy_pass $backend_upstream; # 전역적으로 설정했지만 안전성을 위해서 한번 더 proxy_http_version 1.1; @@ -55,7 +59,7 @@ server { # sse를 사용을 위한 부분 location ^~ /api/sse/ { - proxy_pass http://main-backend-nginx; + proxy_pass $backend_upstream; # keep alive 설정 proxy_http_version 1.1; @@ -79,7 +83,7 @@ server { # backend 프록시 전달 location /api/ { # Dns 탐색 - proxy_pass http://main-backend-nginx; + proxy_pass $backend_upstream; # keep alive 설정 proxy_set_header Connection ""; @@ -91,10 +95,13 @@ server { } + # 항상 DNS를 재조회 하도록 해서 안정성을 높인다. + set $frontend_upstream http://frontend:3000; + # NEXT용 static caching # NEXT는 자체적으로 해시화가 있어서 장기 캐싱에도 안전하다고 합니다. location /_next/static/ { - proxy_pass http://frontend:3000; + proxy_pass $frontend_upstream; expires 1y; add_header Cache-Control "public, immutable"; access_log off; @@ -102,7 +109,7 @@ server { # 나머지는 frontend로 전달 location / { - proxy_pass http://frontend:3000; + proxy_pass $frontend_upstream; # keep alive 설정 proxy_set_header Connection ""; From cb7a201db6728254981f56b33beceed3facaa5a2 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 12:51:37 +0900 Subject: [PATCH 08/24] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20websocket=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존의 websocket 연결의 오해했던 부분을 바로잡고 http handshake 과정에서 잡는걸로 수정하였다. - namespace를 잘 정리하고 이에 대해서 문제를 해결하고자 했다 --- deploy/https/nginx.backend.conf | 8 +++++++- deploy/test/nginx.backend.conf | 11 ++++++++--- .../http/room/room.controller.ts | 3 +++ .../3-2.presentation/webrtc/sfu/sfu.module.ts | 7 +++++++ .../3-2.presentation/webrtc/sfu/sfu.service.ts | 17 +++++++++++++++++ .../websocket/signaling/signaling.gateway.ts | 16 +++++++++++++--- .../websocket/websocket.constants.ts | 7 +++++-- 7 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts diff --git a/deploy/https/nginx.backend.conf b/deploy/https/nginx.backend.conf index 37817681c..f187680c5 100644 --- a/deploy/https/nginx.backend.conf +++ b/deploy/https/nginx.backend.conf @@ -23,7 +23,7 @@ http { # websocket용 upstream upstream main_backend_ws { # load balancer를 hash로 해서 바뀌지 않도록 해야 한다. - ip_hash; + hash $arg_room_id consistent; server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; @@ -34,7 +34,13 @@ http { listen 80; server_name _; + location = /api/ws { + return 301 /api/ws/; + } + location ^~ /api/ws/ { + if ($arg_room_id = "") { return 400; } + proxy_pass http://main_backend_ws; # 전역적으로 설정했지만 안전성을 위해서 한번 더 diff --git a/deploy/test/nginx.backend.conf b/deploy/test/nginx.backend.conf index a337c2d31..e7ed15b68 100644 --- a/deploy/test/nginx.backend.conf +++ b/deploy/test/nginx.backend.conf @@ -24,8 +24,9 @@ http { # websocket용 upstream 추가 upstream main_backend_ws { - # load balancer를 hash로 해서 바뀌지 않도록 해야 한다. - ip_hash; + # load_balancer 떄문에 바뀌지 않도록 이를 수정해준다. ( room_id를 기반으로 앱을 매핑 ) + # consistent를 이용해서 서버가 늘거나 삭제되도 대부분은 그대로 서버를 유지하도록 한다. + hash $arg_room_id consistent; server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; @@ -39,9 +40,13 @@ http { # 웹소켓 관련 추가 location = /api/ws { return 301 /api/ws/; - } + } location ^~ /api/ws/ { + # room_id가 없으면 에러를 발생시켜야 한다. + # nginx는 $arg_<파라미터이름>으로 매핑을 시킨다. + if ($arg_room_id = "") { return 400; } + proxy_pass http://main_backend_ws; # 전역적으로 설정했지만 안전성을 위해서 한번 더 diff --git a/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts b/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts index 68737ef11..0334c5da2 100644 --- a/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts +++ b/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts @@ -33,4 +33,7 @@ export class RoomController { return this.roomService.createRoomService(dto); }; + // 어디 웹소켓에 매핑할때 사용되는 api + + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts index 3fa9831d5..5cc5d0ea3 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts @@ -1,10 +1,17 @@ import { MediaModule } from "@infra/media/media.module"; import { Module } from "@nestjs/common"; +import { SfuService } from "./sfu.service"; @Module({ imports : [ MediaModule + ], + providers : [ + SfuService, + ], + exports : [ + SfuService, ] }) export class SfuModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts new file mode 100644 index 000000000..88a677790 --- /dev/null +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -0,0 +1,17 @@ +import { MediasoupService } from "@infra/media/mediasoup/media"; +import { Injectable } from "@nestjs/common"; + + +@Injectable() +export class SfuService { + + // sfu 서버가 관리하는 로직 ( 메모리 낭비가 있는데 어떻게 하면 좀 효율적으로 저장이 가능할까? ) + + + constructor( + private readonly mediaSoupService : MediasoupService, + ) {} + + // 1. router 생성 관련 함수 + +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index 8a9097667..7c016d94c 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -6,7 +6,7 @@ import { SignalingWebsocketService } from "./signaling.service"; import { TokenDto } from "@app/auth/commands/dto"; import { PayloadRes } from "@app/auth/queries/dto"; import { JwtWsGuard } from "../auth/guards/jwt.guard"; -import { WEBSOCKET_AUTH_CLIENT_EVENT_NAME, WEBSOCKET_NAMESPACE, WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME, WEBSOCKET_SIGNALING_EVENT_NAME } from "../websocket.constants"; +import { WEBSOCKET_AUTH_CLIENT_EVENT_NAME, WEBSOCKET_NAMESPACE, WEBSOCKET_PATH, WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME, WEBSOCKET_SIGNALING_EVENT_NAME } from "../websocket.constants"; import { JoinRoomValidate, SocketPayload } from "./signaling.validate"; import { ConnectResult, ConnectRoomDto } from "@app/room/commands/dto"; import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; @@ -14,12 +14,13 @@ import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; @WebSocketGateway({ namespace : WEBSOCKET_NAMESPACE.SIGNALING, + path : WEBSOCKET_PATH, // http 핸드세이킹이 있을때 붙게 되는 cors : { origin : process.env.NODE_ALLOWED_ORIGIN?.split(",").map((origin) => origin.trim()), credentials : process.env.NODE_ALLOWED_CREDENTIALS === "true" }, transports : ["websocket"], - pingTimeout: 60 * 60 * 1000 // ping pong 허용 시간 + pingTimeout: 20 * 1000 // ping pong 허용 시간 ( 20초 ) }) export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @@ -115,7 +116,16 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec }; }; - // 방에 가입 한 후 라우터 생성 + // 방에 가입 한 후 라우터 생성 -> 방에 가입 시킨 다음에 본격적으로 라우터를 생성하는 이벤트를 주어야 한다. + // 따로 분리해놓은 이유는 -> 정확히 방에 가입한 사람들만 이것을 이용하게 하고 싶기 때문이다. + @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.CONNECT_ROUTER) + async connectRouterGateway( + @ConnectedSocket() client : Socket + ) { + // 1. sfu 서버에 router 생성 요청 + + }; + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts index 737f9f2d9..3c4d53b59 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts @@ -1,8 +1,10 @@ +// websocket연결 path +export const WEBSOCKET_PATH = "/api/ws/" // websocket 연결할때 사용하는 namespace export const WEBSOCKET_NAMESPACE = Object.freeze({ - SIGNALING : "/api/ws/signal" + SIGNALING : "/signal" } as const); export const WEBSOCKET_AUTH_CLIENT_EVENT_NAME = Object.freeze({ @@ -12,7 +14,8 @@ export const WEBSOCKET_AUTH_CLIENT_EVENT_NAME = Object.freeze({ // export const WEBSOCKET_SIGNALING_EVENT_NAME = Object.freeze({ - JOIN_ROOM : "signaling:ws:join_room" + JOIN_ROOM : "signaling:ws:join_room", + CONNECT_ROUTER : "signaling:ws:connect_router" } as const); export const WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME = Object.freeze({ From 8ed96d6824b225b6b43adc88c9b0a8f2c711f3e7 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 18:34:36 +0900 Subject: [PATCH 09/24] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20room=5Fid?= =?UTF-8?q?=EC=97=90=EC=84=9C=20room=5Fcode=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - room_id를 전달하는 것이 아닌 room_code를 전달하는 것으로 기존의 code를 최대한 살리고 http를 한번더 보내는 수도를 줄였다 --- deploy/https/nginx.backend.conf | 4 ++-- deploy/test/nginx.backend.conf | 4 ++-- .../src/3-2.presentation/http/room/room.controller.ts | 3 --- .../src/3-2.presentation/webrtc/sfu/sfu.service.ts | 1 + .../src/3-2.presentation/webrtc/sfu/sfu.validate.ts | 0 5 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts diff --git a/deploy/https/nginx.backend.conf b/deploy/https/nginx.backend.conf index f187680c5..3eaa6d1e3 100644 --- a/deploy/https/nginx.backend.conf +++ b/deploy/https/nginx.backend.conf @@ -23,7 +23,7 @@ http { # websocket용 upstream upstream main_backend_ws { # load balancer를 hash로 해서 바뀌지 않도록 해야 한다. - hash $arg_room_id consistent; + hash $arg_room_code consistent; server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; @@ -39,7 +39,7 @@ http { } location ^~ /api/ws/ { - if ($arg_room_id = "") { return 400; } + if ($arg_room_code = "") { return 400; } proxy_pass http://main_backend_ws; diff --git a/deploy/test/nginx.backend.conf b/deploy/test/nginx.backend.conf index e7ed15b68..20e31a0b2 100644 --- a/deploy/test/nginx.backend.conf +++ b/deploy/test/nginx.backend.conf @@ -26,7 +26,7 @@ http { upstream main_backend_ws { # load_balancer 떄문에 바뀌지 않도록 이를 수정해준다. ( room_id를 기반으로 앱을 매핑 ) # consistent를 이용해서 서버가 늘거나 삭제되도 대부분은 그대로 서버를 유지하도록 한다. - hash $arg_room_id consistent; + hash $arg_room_code consistent; server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; @@ -45,7 +45,7 @@ http { location ^~ /api/ws/ { # room_id가 없으면 에러를 발생시켜야 한다. # nginx는 $arg_<파라미터이름>으로 매핑을 시킨다. - if ($arg_room_id = "") { return 400; } + if ($arg_room_code = "") { return 400; } proxy_pass http://main_backend_ws; diff --git a/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts b/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts index 0334c5da2..68737ef11 100644 --- a/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts +++ b/rep/main_backend/src/3-2.presentation/http/room/room.controller.ts @@ -33,7 +33,4 @@ export class RoomController { return this.roomService.createRoomService(dto); }; - // 어디 웹소켓에 매핑할때 사용되는 api - - }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 88a677790..e3cc9385e 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -14,4 +14,5 @@ export class SfuService { // 1. router 생성 관련 함수 + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts new file mode 100644 index 000000000..e69de29bb From e58721a8247d0b2e0f3da745c3782bd5b31308d5 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 19:43:18 +0900 Subject: [PATCH 10/24] =?UTF-8?q?=F0=9F=93=8D=20Feat:=20router=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20sfu=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/presentation/sfu/sfu.error.ts | 10 +++ .../src/3-1.infra/media/mediasoup/config.ts | 41 +++++++++- .../src/3-1.infra/media/mediasoup/media.ts | 14 +++- .../webrtc/sfu/sfu.service.ts | 74 ++++++++++++++++++- .../webrtc/sfu/sfu.validate.ts | 11 +++ 5 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts diff --git a/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts b/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts new file mode 100644 index 000000000..c7bcde238 --- /dev/null +++ b/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts @@ -0,0 +1,10 @@ +import { BaseError } from '../../error'; + +export class SfuError extends BaseError { + constructor(err: Error) { + super({ + message: `${err}`, + status: 500, + }); + } +} \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts index 62bb2da02..80978661e 100644 --- a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts @@ -1,6 +1,7 @@ -import { type WorkerSettings } from "mediasoup/types"; +import { RouterOptions, RtpCodecCapability, type WorkerSettings } from "mediasoup/types"; +// worker에 쓰이는 config export const mediaSoupWorkerConfig : WorkerSettings = { // log에 남길 레벨을 정한다. ( warn ) logLevel : process.env.NODE_ENV === "production" ? "warn" : "debug", @@ -29,4 +30,42 @@ export const mediaSoupWorkerConfig : WorkerSettings = { // 나중에 ip를 추가해서 어느 ip에 worker인지도 확인이 가능하고 탐색할때 유용하다. } +}; + +// router에 쓰이는 config ( router가 허용하는 config ) +export const mediaSoupRouterConfig : RouterOptions = { + + mediaCodecs: > [ + // audio 설정 + { + kind : "audio", + mimeType: "audio/opus", // 음성에서 기본적으로 사용하는 코덱이다. + clockRate: 48000, + // stereo 타입으로 화면공유에서 사용되는 사운드, 음악도 허용 한다는 의미 + channels : 2 + }, + + // video + 화면공유 설정 + { + kind: "video", + mimeType: "video/VP8", + clockRate: 90000, + // 비트레이트를 안정적으로 관리하기 위해서 사용하는요소 - simulcast에서 제어할때 사용할 것이다. + rtcpFeedback : [ + // 패킷 재전송 요청에 사용됨 + { type : "nack" }, + // 특정 키프레임을 요청하며 화질 복구에 사용됨 + { type : "nack", parameter : "pli" }, + // 전체 키프레임 요청 + { type : "ccm", parameter : "fir" }, + // 수신자가 감당 가능한 최대 비트레이트 알려줄때 사용 ( chrome 계열에서 사용 ) + { type : "goog-remb" }, + // 현재 패킷 전송에서 네트워크 상태를 계산할때 사용 + { type : "transport-cc" } + ], + parameters: { + "x-google-start-bitrate": 1000 // 첫 시작에서 쓰일 bite_rate + } + } + ] }; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts index 4cd3d8675..8405f9af2 100644 --- a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts @@ -43,11 +43,17 @@ export class MediasoupService implements OnModuleInit, OnModuleDestroy { }; // worker 가져오기 ( round-robin 알고리즘 사용 ) - picWorker() : Worker { + picWorker() : { worker : Worker, workerIdx : number} { if ( this.workers.length === 0 ) throw new Error("worker가 존재하지 않습니다."); - const worker = this.workers[this.workerIdx]; - this.workerIdx = ( this.workerIdx + 1 ) % this.workers.length; - return worker; + const workerIdx : number = this.workerIdx; + const worker = this.workers[workerIdx]; + this.workerIdx = ( workerIdx + 1 ) % this.workers.length; + return { worker, workerIdx }; + }; + + // 특정 worker 가져오기 + getWorker(worker_idx : number) : Worker | undefined { + return this.workers[worker_idx]; }; }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index e3cc9385e..7ede1b2a9 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,18 +1,86 @@ import { MediasoupService } from "@infra/media/mediasoup/media"; -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; +import { RoomEntry } from "./sfu.validate"; +import { SfuError } from "@error/presentation/sfu/sfu.error"; +import { mediaSoupRouterConfig } from "@infra/media/mediasoup/config"; @Injectable() export class SfuService { // sfu 서버가 관리하는 로직 ( 메모리 낭비가 있는데 어떻게 하면 좀 효율적으로 저장이 가능할까? ) + private readonly logger = new Logger(SfuService.name); + // wokre들과 router 정리 + private readonly roomRouters = new Map(); // room_id : RoomEntry + private readonly createRoomRoutings = new Map>(); // room_id : Promise -> 생성하고 있는 중인 룸 + constructor( private readonly mediaSoupService : MediasoupService, ) {} - // 1. router 생성 관련 함수 - + private async createRoomRouting(room_id : string) : Promise { + try { + const { worker, workerIdx } = this.mediaSoupService.picWorker(); + + // 여기서 허용 가능한 router에 config를 부여할 수 있다. + const router = await worker.createRouter( + mediaSoupRouterConfig + ); + + // 기본 설정 + const roomEntry : RoomEntry = { + worker_idx : workerIdx, + room_id, + router, + worker_pid : worker.pid, + created_at : new Date() + }; + + // 메모리에 저장 + this.roomRouters.set(room_id, roomEntry); + + // router가 만약 내려간다면? ( 근데 worker가 내려가면 애초에 그 방은 내려가니까 worker에 죽음은 그 프로세스 전체에 삭제 ) + router.observer.on("close", () => { + const closedRouterEntry = this.roomRouters.get(room_id); + if ( closedRouterEntry?.router === router ) { + // 방이랑 worker_id 기록한거 삭제 + this.roomRouters.delete(room_id); + }; + }); + + // 여기에서 대표 producer transport도 나중에 등록 예정 + + return roomEntry; + } catch (err) { + this.logger.error(err); // error가 발생한다면 + throw new SfuError(err); + } finally { + this.createRoomRoutings.delete(room_id); + }; + }; + + // 1. router 생성 관련 함수 -> 생성 혹은 얻는 이유는 방을 만들었다고 무조건 router를 부여하면 비어있는 방에 낭비가 심할 수 있기에 들어와야 활성화가 된다. + async getOrCreateRoomRouter(room_id : string) : Promise { + + // 이미 생성된 router라면 + const roomRouterExist = this.roomRouters.get(room_id); + if ( roomRouterExist && !roomRouterExist.router.closed ) { + return roomRouterExist; + }; + + // 생성 중이라면 + const createRoomRoutingExist = this.createRoomRoutings.get(room_id); + if ( createRoomRoutingExist ) { + return createRoomRoutingExist; + }; + + // 아무것도 없다면 새롭게 생성할 수 있다. + const roomEntry = this.createRoomRouting(room_id); + this.createRoomRoutings.set(room_id, roomEntry); + + return roomEntry; + }; }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts index e69de29bb..a8af60e83 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts @@ -0,0 +1,11 @@ +import { Router } from "mediasoup/types"; + + +// room_id에 대해서 worker, router, room_id, created_at으로 메모리에서 worker에서 router를 어떻게 관리하는지 작성한다. +export type RoomEntry = { + room_id : string; + worker_idx : number; // 이 라우터를 운영하기 위해서 필요한 정보 + worker_pid : number; // worker를 디버깅 하기 위해서 필요한 정보 + router : Router; // 실질적으로 기능구현에 사용되는 라우터 + created_at : Date; +}; \ No newline at end of file From 2999de93f628d19ab4575bf881c76cd3f8d4c41a Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Thu, 8 Jan 2026 20:59:31 +0900 Subject: [PATCH 11/24] =?UTF-8?q?=F0=9F=93=8D=20Feat:=20sfu=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EC=9D=98=20sdp=20=EB=8B=A8=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/signalling/signalling.error.ts | 10 ++++++++++ .../src/3-1.infra/media/media.module.ts | 3 ++- .../src/3-2.presentation/webrtc/sfu/sfu.module.ts | 4 ---- .../3-2.presentation/webrtc/sfu/sfu.service.ts | 11 +++++++++++ .../websocket/signaling/signaling.gateway.ts | 15 +++++++++++++-- .../websocket/signaling/signaling.module.ts | 6 ++++++ .../websocket/signaling/signaling.service.ts | 12 +++++++++++- .../websocket/websocket.constants.ts | 2 +- 8 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 rep/main_backend/src/0.common/error/presentation/signalling/signalling.error.ts diff --git a/rep/main_backend/src/0.common/error/presentation/signalling/signalling.error.ts b/rep/main_backend/src/0.common/error/presentation/signalling/signalling.error.ts new file mode 100644 index 000000000..c3636e9c0 --- /dev/null +++ b/rep/main_backend/src/0.common/error/presentation/signalling/signalling.error.ts @@ -0,0 +1,10 @@ +import { BaseError } from '../../error'; + +export class NotConnectSignalling extends BaseError { + constructor() { + super({ + message: '화상 회의방에 연결하지 못했습니다.', + status: 500, + }); + } +} \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/media.module.ts b/rep/main_backend/src/3-1.infra/media/media.module.ts index 3a3cc6f5c..7ac1cb6b5 100644 --- a/rep/main_backend/src/3-1.infra/media/media.module.ts +++ b/rep/main_backend/src/3-1.infra/media/media.module.ts @@ -1,7 +1,8 @@ -import { Module } from "@nestjs/common"; +import { Global, Module } from "@nestjs/common"; import { MediasoupService } from "./mediasoup/media"; +@Global() @Module({ providers : [ MediasoupService, diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts index 5cc5d0ea3..64bcf7401 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts @@ -1,12 +1,8 @@ -import { MediaModule } from "@infra/media/media.module"; import { Module } from "@nestjs/common"; import { SfuService } from "./sfu.service"; @Module({ - imports : [ - MediaModule - ], providers : [ SfuService, ], diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 7ede1b2a9..3e1ead542 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -83,4 +83,15 @@ export class SfuService { return roomEntry; }; + // 방이 닫혔을때 로직도 구현해두자 + closeRoomRouter(room_id : string) { + const entry = this.roomRouters.get(room_id); + if ( !entry ) return; + try { + entry.router.close(); + } finally { + this.roomRouters.delete(room_id); // 근데 생각해보면 close를 해두어서 삭제되기는 할거다. + }; + }; + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index 7c016d94c..3169672ec 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -1,6 +1,6 @@ // 시그널링 서버의 역할이라고 할 수 있을 것 같다. import { Logger, UsePipes, ValidationPipe } from "@nestjs/common"; -import { ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets"; +import { ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer, WsException } from "@nestjs/websockets"; import { Server, Socket } from "socket.io" import { SignalingWebsocketService } from "./signaling.service"; import { TokenDto } from "@app/auth/commands/dto"; @@ -118,12 +118,23 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec // 방에 가입 한 후 라우터 생성 -> 방에 가입 시킨 다음에 본격적으로 라우터를 생성하는 이벤트를 주어야 한다. // 따로 분리해놓은 이유는 -> 정확히 방에 가입한 사람들만 이것을 이용하게 하고 싶기 때문이다. + // reture을 안준 이유도 sdp를 하는 방식이 다 다르기 때문이다. @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.CONNECT_ROUTER) async connectRouterGateway( @ConnectedSocket() client : Socket ) { - // 1. sfu 서버에 router 생성 요청 + const room_id : string = client.data.room_id; + try { + // 1. sfu 서버에 room_id에 router에 설정을 요구한다 이를 바탕으로 SDP를 할 예정 + const rtpCapabilities = await this.signalingService.sdpNegotiate(room_id); + + // 2. sdp 정보 반환 + return { ok : true, rtpCapabilities }; + } catch (err){ + this.logger.error(err); + throw new WsException({ message : err.message ?? "에러 발생", status : err.status ?? 500 }); + }; }; diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts index 4f0b7aaa8..8bc77c921 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.module.ts @@ -8,11 +8,13 @@ import { DeleteRoomDatasToRedis, InsertRoomDatasToRedis } from "@infra/cache/red import { SignalingWebsocketService } from "./signaling.service"; import { AuthWebsocketModule } from "../auth/auth.module"; import { SignalingWebsocketGateway } from "./signaling.gateway"; +import { SfuModule } from "@present/webrtc/sfu/sfu.module"; @Module({ imports : [ AuthWebsocketModule, + SfuModule ], providers : [ // sfu 자체적인 모듈 @@ -21,6 +23,8 @@ import { SignalingWebsocketGateway } from "./signaling.gateway"; CompareRoomArgonHash, // usecase 모아두기 + + // 방에 연결하기 위한 usecase { provide : ConnectRoomUsecase, useFactory : ( @@ -44,6 +48,8 @@ import { SignalingWebsocketGateway } from "./signaling.gateway"; DeleteHardRoomParticipantInfoDataToMysql ] }, + + // 방의 연결을 끊기 위한 usecase { provide : DisconnectRoomUsecase, useFactory : ( diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index 321a3e34a..5b66736fe 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -8,6 +8,8 @@ import { v7 as uuidV7 } from "uuid"; import { SocketPayload } from "./signaling.validate"; import { PayloadRes } from "@app/auth/queries/dto"; import { UnthorizedError } from "@error/application/user/user.error"; +import { SfuService } from "@present/webrtc/sfu/sfu.service"; +import { NotConnectSignalling } from "@error/presentation/signalling/signalling.error"; @Injectable() @@ -15,7 +17,8 @@ export class SignalingWebsocketService { constructor( private readonly disconnectRoomUsecase : DisconnectRoomUsecase, - private readonly connectRoomUsecase : ConnectRoomUsecase + private readonly connectRoomUsecase : ConnectRoomUsecase, + private readonly sfuServer : SfuService, ) {} parseJwtToken( client : Socket ) : TokenDto | undefined { @@ -99,4 +102,11 @@ export class SignalingWebsocketService { }; }; + // sdp 협상에 필요한 함수 + async sdpNegotiate( room_id : string ) { + if ( !room_id || room_id === "" ) throw new NotConnectSignalling(); + const entry = await this.sfuServer.getOrCreateRoomRouter(room_id); + return entry.router.rtpCapabilities; // sfu 서버에 codex 정보들 ( 나중에 변경 가능성 농후하다. ) + }; + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts index 3c4d53b59..4149e5f0e 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts @@ -19,5 +19,5 @@ export const WEBSOCKET_SIGNALING_EVENT_NAME = Object.freeze({ } as const); export const WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME = Object.freeze({ - JOINED : "room:joined" + JOINED : "room:joined", } as const); \ No newline at end of file From e8915977656c67676e2294a193ef00479335f0a5 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 00:12:45 +0900 Subject: [PATCH 12/24] =?UTF-8?q?=F0=9F=93=8D=20Feat:=20sfu=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20transport=20=EB=B6=80=EB=B6=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/presentation/sfu/sfu.error.ts | 9 ++ .../src/3-1.infra/media/mediasoup/config.ts | 12 +- .../webrtc/sfu/sfu.service.ts | 111 +++++++++++++++++- .../webrtc/sfu/sfu.validate.ts | 11 +- .../websocket/signaling/signaling.gateway.ts | 4 +- .../websocket/websocket.constants.ts | 2 +- 6 files changed, 138 insertions(+), 11 deletions(-) diff --git a/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts b/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts index c7bcde238..bfe316932 100644 --- a/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts +++ b/rep/main_backend/src/0.common/error/presentation/sfu/sfu.error.ts @@ -7,4 +7,13 @@ export class SfuError extends BaseError { status: 500, }); } +} + +export class SfuErrorMessage extends BaseError { + constructor(message : string) { + super({ + message, + status: 500, + }); + } } \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts index 80978661e..95f025041 100644 --- a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts @@ -1,4 +1,4 @@ -import { RouterOptions, RtpCodecCapability, type WorkerSettings } from "mediasoup/types"; +import { RouterOptions, RtpCodecCapability, type TransportListenIp, type WorkerSettings } from "mediasoup/types"; // worker에 쓰이는 config @@ -68,4 +68,12 @@ export const mediaSoupRouterConfig : RouterOptions = { } } ] -}; \ No newline at end of file +}; + +// listenIps에 대해서 +export const listenIps : Array = [ + { + ip : "0.0.0.0", // 허용하는 프론트엔드 ip, + announcedIp: process.env.NODE_APP_SFU_PUBLIC_IP ?? "127.0.0.1" // ice 검증에서 나의 ip를 소개 클라이언트는 이 ip로 접근한다. 즉 udp로 접근이 가능한 ip를 알려주어야 한다. + } +]; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 3e1ead542..6473ec331 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,8 +1,9 @@ import { MediasoupService } from "@infra/media/mediasoup/media"; import { Injectable, Logger } from "@nestjs/common"; -import { RoomEntry } from "./sfu.validate"; -import { SfuError } from "@error/presentation/sfu/sfu.error"; -import { mediaSoupRouterConfig } from "@infra/media/mediasoup/config"; +import { RoomEntry, TransportEntry } from "./sfu.validate"; +import { SfuError, SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; +import { mediaSoupRouterConfig, listenIps } from "@infra/media/mediasoup/config"; +import { Router, WebRtcTransport } from "mediasoup/types"; @Injectable() @@ -15,6 +16,8 @@ export class SfuService { private readonly roomRouters = new Map(); // room_id : RoomEntry private readonly createRoomRoutings = new Map>(); // room_id : Promise -> 생성하고 있는 중인 룸 + // transport가 저장이 되야 한다. + private readonly transports = new Map(); // transport_id : transport 저장 ( 나중에 transport를 ) constructor( private readonly mediaSoupService : MediasoupService, @@ -35,6 +38,7 @@ export class SfuService { room_id, router, worker_pid : worker.pid, + transport_ids : new Set(), created_at : new Date() }; @@ -84,14 +88,111 @@ export class SfuService { }; // 방이 닫혔을때 로직도 구현해두자 - closeRoomRouter(room_id : string) { + closeRoomRouter(room_id : string) : void { const entry = this.roomRouters.get(room_id); if ( !entry ) return; + + // transport를 관리 + for (const transport_id of entry.transport_ids) { + const t = this.transports.get(transport_id); + if (t && !t.closed) { + try { t.close(); } catch {} + } + this.transports.delete(transport_id); + }; + try { - entry.router.close(); + if (!entry.router.closed) entry.router.close(); } finally { this.roomRouters.delete(room_id); // 근데 생각해보면 close를 해두어서 삭제되기는 할거다. }; }; + // room의 정보를 최신으로 바꾸는 로직이다. + private patchRoomEntry(room_id : string, patch : (entry : RoomEntry) => void) : void { + const entry = this.roomRouters.get(room_id); + if ( !entry ) return; + patch(entry); + + this.roomRouters.set(room_id, entry); + }; + + private transPortLogging(room_id : string, transport : WebRtcTransport) : void { + // transport에 대해서 내부 디버깅을 위해서 추가 + + // ice 상태에 대해서 로깅 + transport.on("icestatechange", (state) => { + this.logger.debug({room_id, transportId : transport.id, state}, "transport에 ice부분에 상태가 변했습니다."); + }); + + // dtls 핸드세이킹 과정중 상태변화 로깅 + transport.on("dtlsstatechange", (state) => { + this.logger.debug({room_id, transportId : transport.id, state}, "transport에 dtls부분에 상태가 변했습니다."); + }); + }; + + // 2. transport 부분 생성 + async createTransPort(room_id : string) : Promise { + + const roomEntry = this.roomRouters.get(room_id); + if ( !roomEntry ) throw new SfuErrorMessage("room_id를 다시 확인해주세요"); + const router : Router = roomEntry.router; + + let transport : WebRtcTransport | undefined; + + try { + // ip 설정을 해두고 + transport = await router.createWebRtcTransport({ + listenIps, + enableUdp : true, // udp 허용 + enableTcp : true, // tcp 허용 + preferUdp : true, // udp가 최우선 + initialAvailableOutgoingBitrate: 1_000_000, // 첫 바이트는 이정도 + }); + + // transport 로깅 + this.transPortLogging(room_id, transport); + + // 최대 전송율 정하기 + try { + await transport.setMaxIncomingBitrate(1_500_000); // 최대 + } catch (err) { + this.logger.warn(err); // 에러 메시지만 이유는 화면공유, 음성, 비디오등 다 다른데 하나로 고정할수는 없음 ( 상황에 따라서 나중에 개선해야 함 ) + } + + // transport id를 가져온다. + const transportId : string = transport.id; + + // 메모리에 저장 + this.transports.set(transportId, transport); + + // router 갱신 + this.patchRoomEntry(room_id, (entry) => { + entry.transport_ids.add(transportId); + }); + + // transport 없어졌을때 이벤트 생성 + transport.observer.on("close", () => { + this.transports.delete(transportId); + + // router 갱신 + this.patchRoomEntry(room_id, (entry) => { + entry.transport_ids.delete(transportId); + }); + }); + + return { + transportId: transportId, + iceParameters: transport.iceParameters, // ice에 파라미터 정보 + iceCandidates: transport.iceCandidates, // ice 후보들 전달 + dtlsParameters: transport.dtlsParameters // dtls 핸드세이크를 위한 파라미터들 + }; + } catch (err) { + if ( transport && !transport.closed ) transport.close(); + throw err; + } + + }; + + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts index a8af60e83..17e484794 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts @@ -1,4 +1,4 @@ -import { Router } from "mediasoup/types"; +import { DtlsParameters, IceCandidate, IceParameters, Router } from "mediasoup/types"; // room_id에 대해서 worker, router, room_id, created_at으로 메모리에서 worker에서 router를 어떻게 관리하는지 작성한다. @@ -7,5 +7,14 @@ export type RoomEntry = { worker_idx : number; // 이 라우터를 운영하기 위해서 필요한 정보 worker_pid : number; // worker를 디버깅 하기 위해서 필요한 정보 router : Router; // 실질적으로 기능구현에 사용되는 라우터 + transport_ids : Set; // transport들이 저장된 라우터 created_at : Date; +}; + +// transport에 대해서 정보를 저장한다. +export type TransportEntry = { + transportId: string; + iceParameters: IceParameters; + iceCandidates: Array; + dtlsParameters: DtlsParameters; }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index 3169672ec..66f8819b5 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -119,8 +119,8 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec // 방에 가입 한 후 라우터 생성 -> 방에 가입 시킨 다음에 본격적으로 라우터를 생성하는 이벤트를 주어야 한다. // 따로 분리해놓은 이유는 -> 정확히 방에 가입한 사람들만 이것을 이용하게 하고 싶기 때문이다. // reture을 안준 이유도 sdp를 하는 방식이 다 다르기 때문이다. - @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.CONNECT_ROUTER) - async connectRouterGateway( + @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.NEGOTIATE_SDP) + async sdpNegotiateGateway( @ConnectedSocket() client : Socket ) { const room_id : string = client.data.room_id; diff --git a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts index 4149e5f0e..5dd859331 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts @@ -15,7 +15,7 @@ export const WEBSOCKET_AUTH_CLIENT_EVENT_NAME = Object.freeze({ // export const WEBSOCKET_SIGNALING_EVENT_NAME = Object.freeze({ JOIN_ROOM : "signaling:ws:join_room", - CONNECT_ROUTER : "signaling:ws:connect_router" + NEGOTIATE_SDP : "signaling:ws:negotiate_sdp" } as const); export const WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME = Object.freeze({ From 81686e0786713b4ad9616be86ee5f012b44d915a Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 00:55:00 +0900 Subject: [PATCH 13/24] =?UTF-8?q?=F0=9F=8C=88=20Update:=20docker=20compose?= =?UTF-8?q?=20dfu=20=EC=84=9C=EB=B2=84=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 클라이언트가 가야하는 sfu 서버에 webrtc를 못보내는 문제가 발생할수 있는데 그 문제를 해결하였음 --- deploy/test/docker-compose.yml | 13 +++++++++++++ deploy/test/nginx.backend.conf | 19 +++++++++++-------- .../src/3-1.infra/media/mediasoup/config.ts | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/deploy/test/docker-compose.yml b/deploy/test/docker-compose.yml index 1f2bec57f..7ccc1e06f 100644 --- a/deploy/test/docker-compose.yml +++ b/deploy/test/docker-compose.yml @@ -39,6 +39,19 @@ services: - clobby-network restart: unless-stopped + # sfu 서버 -> 현재 sfu는 이걸로 가고 위에 로드밸런싱을 갖춘 아키텍처는 나중에 저장 로직으로 사용하면 된다. ( 배포는 또 다 분리할거니까 괜찮을 거다. ) + sfu: + container_name: sfu + image: "${MAIN_BACKEND_IMG}:latest" + env_file: + - ./.env.backend + networks: + - clobby-network + # 보안 주의 + ports: + - "40000-41999:40000-41999/udp" + - "40000-41999:40000-41999/tcp" + # frontend 빌드 frontend: container_name: frontend diff --git a/deploy/test/nginx.backend.conf b/deploy/test/nginx.backend.conf index 20e31a0b2..3c453ca83 100644 --- a/deploy/test/nginx.backend.conf +++ b/deploy/test/nginx.backend.conf @@ -24,13 +24,16 @@ http { # websocket용 upstream 추가 upstream main_backend_ws { - # load_balancer 떄문에 바뀌지 않도록 이를 수정해준다. ( room_id를 기반으로 앱을 매핑 ) - # consistent를 이용해서 서버가 늘거나 삭제되도 대부분은 그대로 서버를 유지하도록 한다. - hash $arg_room_code consistent; - - server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; - server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; - server main-backend-app-3:8080 max_fails=3 fail_timeout=10s; + # sfu 서버로 사용하게 되어서 주석 처리 ( 나중에 websocket은 이걸 사용할 수 있다. ) + # # load_balancer 떄문에 바뀌지 않도록 이를 수정해준다. ( room_id를 기반으로 앱을 매핑 ) + # # consistent를 이용해서 서버가 늘거나 삭제되도 대부분은 그대로 서버를 유지하도록 한다. + # hash $arg_room_code consistent; + + # server main-backend-app-1:8080 max_fails=3 fail_timeout=10s; + # server main-backend-app-2:8080 max_fails=3 fail_timeout=10s; + # server main-backend-app-3:8080 max_fails=3 fail_timeout=10s; + server sfu:8080; + keepalive 64; } server { @@ -45,7 +48,7 @@ http { location ^~ /api/ws/ { # room_id가 없으면 에러를 발생시켜야 한다. # nginx는 $arg_<파라미터이름>으로 매핑을 시킨다. - if ($arg_room_code = "") { return 400; } + # if ($arg_room_code = "") { return 400; } proxy_pass http://main_backend_ws; diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts index 95f025041..7c79a945a 100644 --- a/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/config.ts @@ -22,7 +22,7 @@ export const mediaSoupWorkerConfig : WorkerSettings = { // udp 사용 포트 rtcMinPort: 40000, - rtcMaxPort: 49999, + rtcMaxPort: process.env.NODE_ENV === "production" ? 41999 : 40199, // 현재나의 로그 메타데이터를 추가하려면 appData : { From 5954905bc14ad97e67644b618694115717ea7740 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 01:05:53 +0900 Subject: [PATCH 14/24] =?UTF-8?q?=F0=9F=93=8D=20Feat:=20sfu-client=20ice?= =?UTF-8?q?=20=ED=98=91=EC=83=81=ED=95=98=EB=8A=94=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websocket/signaling/signaling.gateway.ts | 20 +++++++++++++++++-- .../websocket/signaling/signaling.service.ts | 7 +++++++ .../websocket/websocket.constants.ts | 3 ++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index 66f8819b5..aa7afa3b9 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -131,12 +131,28 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec // 2. sdp 정보 반환 return { ok : true, rtpCapabilities }; - } catch (err){ + } catch (err) { this.logger.error(err); throw new WsException({ message : err.message ?? "에러 발생", status : err.status ?? 500 }); }; }; - + // 여기서 추가할 점은 ( send, recv에 역할을 알 수 있는 매개변수가 있다면 좋을것이다 왜냐하면 이걸 이용해서 두개다 협상을 할거니까 ) + @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.NEGOTIATE_ICE) + async iceNegotiateGateway( + @ConnectedSocket() client : Socket + ) { + const room_id : string = client.data.room_id; + + try { + // 1. sfu 서버에서 ice 협상에 필요한 정보 요구하고 dtls에 필요한 정보도 요구 + const transportOptions = await this.signalingService.iceNegotiate(room_id); + + return { ok : true, transportOptions }; + } catch (err) { + this.logger.error(err); + throw new WsException({ message : err.message ?? "에러 발생", status : err.status ?? 500 }); + }; + }; }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index 5b66736fe..96b7f3436 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -109,4 +109,11 @@ export class SignalingWebsocketService { return entry.router.rtpCapabilities; // sfu 서버에 codex 정보들 ( 나중에 변경 가능성 농후하다. ) }; + // ice 협상을 위해 sfu서버에 ice parameter와 후보들을 알려준다 그리고 dtls 핸드세이킹을 위한 파라미터도 전달 + async iceNegotiate( room_id : string ) { + if ( !room_id || room_id === "" ) throw new NotConnectSignalling(); + const transportOptions = await this.sfuServer.createTransPort(room_id); + return transportOptions; + }; + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts index 5dd859331..2d81c0f36 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts @@ -15,7 +15,8 @@ export const WEBSOCKET_AUTH_CLIENT_EVENT_NAME = Object.freeze({ // export const WEBSOCKET_SIGNALING_EVENT_NAME = Object.freeze({ JOIN_ROOM : "signaling:ws:join_room", - NEGOTIATE_SDP : "signaling:ws:negotiate_sdp" + NEGOTIATE_SDP : "signaling:ws:negotiate_sdp", + NEGOTIATE_ICE : "signaling:ws:negotiate_ice" } as const); export const WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME = Object.freeze({ From 5305e27e19cceb361c94156ce8a2ce70701caaf2 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 05:46:23 +0900 Subject: [PATCH 15/24] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20create=20trans?= =?UTF-8?q?port=20=EC=95=88=EC=A0=95=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/dto/create-room-transport.dto.ts | 16 +++++++ .../src/3-1.infra/cache/cache.constants.ts | 12 +++++ .../src/3-1.infra/cache/redis/cache.ts | 7 ++- .../cache/redis/room/room.outbound.ts | 5 +- .../3-1.infra/cache/redis/sfu/sfu.inbound.ts | 0 .../3-1.infra/cache/redis/sfu/sfu.outbound.ts | 48 +++++++++++++++++++ .../webrtc/sfu/sfu.service.ts | 44 +++++++++++++---- .../webrtc/sfu/sfu.validate.ts | 5 ++ .../websocket/signaling/signaling.gateway.ts | 34 +++++++++++-- .../websocket/signaling/signaling.service.ts | 24 ++++++++-- .../websocket/signaling/signaling.validate.ts | 26 +++++++++- .../websocket/websocket.constants.ts | 4 +- 12 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 rep/main_backend/src/2.application/room/commands/dto/create-room-transport.dto.ts create mode 100644 rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts create mode 100644 rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts diff --git a/rep/main_backend/src/2.application/room/commands/dto/create-room-transport.dto.ts b/rep/main_backend/src/2.application/room/commands/dto/create-room-transport.dto.ts new file mode 100644 index 000000000..f12aecebe --- /dev/null +++ b/rep/main_backend/src/2.application/room/commands/dto/create-room-transport.dto.ts @@ -0,0 +1,16 @@ + + +export type CreateTransportDto = { + room_id : string; + socket_id : string; + user_id : string; + type : "send" | "recv" +}; + +export type CreateRoomTransportDto = { + room_id : string; + socket_id : string; + transport_id : string; + user_id : string; + type : "send" | "recv" +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/cache/cache.constants.ts b/rep/main_backend/src/3-1.infra/cache/cache.constants.ts index 042530770..2f2122f45 100644 --- a/rep/main_backend/src/3-1.infra/cache/cache.constants.ts +++ b/rep/main_backend/src/3-1.infra/cache/cache.constants.ts @@ -46,4 +46,16 @@ export const CACHE_ROOM_SOCKETS_KEY_PROPS_NAME = Object.freeze({ USER_ID : "user_id", ROOM_ID : "room_id", IP : "ip" +} as const); + +// sfu와 관련된 cache 정보 +export const CACHE_SFU_NAMESPACE_NAME = Object.freeze({ + TRANSPORT_INFO : "cache:sfu:transports" +} as const); + +export const CACHE_SFU_TRANSPORTS_KEY_NAME = Object.freeze({ + SOCKET_ID : "socket_id", + USER_ID : "user_id", + TYPE : "type", + ROOM_ID : "room_id" } as const); \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/cache/redis/cache.ts b/rep/main_backend/src/3-1.infra/cache/redis/cache.ts index eb9350b1a..6a00f8f40 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/cache.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/cache.ts @@ -9,6 +9,7 @@ import { REDIS_SERVER } from '../cache.constants'; import { SelectHsetDataFromRedis } from './user/user.inbound'; import { DeleteRoomDatasToRedis, InsertRoomDatasToRedis, InsertRoomDataToRedis } from './room/room.outbound'; import { SelectRoomInfoFromRedis } from './room/room.inbound'; +import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis, } from "./sfu/sfu.outbound" @Global() @@ -52,6 +53,8 @@ import { SelectRoomInfoFromRedis } from './room/room.inbound'; SelectRoomInfoFromRedis, // roominfo 정보를 찾을때 사용 InsertRoomDatasToRedis, // room data들을 저장할때 사용 DeleteRoomDatasToRedis, // room정보들을 삭제하기 위해 사용 + CreateSfuTransportInfoToRedis, // transport들의 정보를 저장하기 위한 로직 + DeleteSfuTransportInfoToRedis // transport가 만약 에러가 발생하거나 삭제될때 발동하는 로직 ], exports: [ REDIS_SERVER, @@ -61,7 +64,9 @@ import { SelectRoomInfoFromRedis } from './room/room.inbound'; InsertRoomDataToRedis, SelectRoomInfoFromRedis, InsertRoomDatasToRedis, - DeleteRoomDatasToRedis + DeleteRoomDatasToRedis, + CreateSfuTransportInfoToRedis, + DeleteSfuTransportInfoToRedis ], }) export class RedisModule {} diff --git a/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts index 6958df6a9..8e4d12d73 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts @@ -1,10 +1,11 @@ -import { DeleteDatasToCache, InsertDataToCache } from "@app/ports/cache/cache.outbound"; +import { DeleteDatasToCache, DeleteDataToCache, InsertDataToCache } from "@app/ports/cache/cache.outbound"; import { Inject, Injectable } from "@nestjs/common"; import { type RedisClientType } from "redis"; -import { CACHE_ROOM_INFO_KEY_NAME, CACHE_ROOM_MEMBERS_KEY_PROPS_NAME, CACHE_ROOM_NAMESPACE_NAME, CACHE_ROOM_SOCKETS_KEY_PROPS_NAME, CACHE_ROOM_SUB_NAMESPACE_NAME, REDIS_SERVER } from "../../cache.constants"; +import { CACHE_ROOM_INFO_KEY_NAME, CACHE_ROOM_MEMBERS_KEY_PROPS_NAME, CACHE_ROOM_NAMESPACE_NAME, CACHE_ROOM_SOCKETS_KEY_PROPS_NAME, CACHE_ROOM_SUB_NAMESPACE_NAME, CACHE_SFU_NAMESPACE_NAME, CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; import { RoomProps } from "@domain/room/vo"; import { InsertRoomDataDto } from "@app/room/commands/dto"; import { CacheError } from "@error/infra/infra.error"; +import { CreateRoomTransportDto } from "@app/room/commands/dto/create-room-transport.dto"; @Injectable() diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts new file mode 100644 index 000000000..8376f908e --- /dev/null +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts @@ -0,0 +1,48 @@ +import { DeleteDataToCache, InsertDataToCache } from "@app/ports/cache/cache.outbound"; +import { Inject, Injectable } from "@nestjs/common"; +import { type RedisClientType } from "redis"; +import { CACHE_SFU_NAMESPACE_NAME, CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; +import { CreateRoomTransportDto } from "@app/room/commands/dto/create-room-transport.dto"; + + + +@Injectable() +export class CreateSfuTransportInfoToRedis extends InsertDataToCache> { + + constructor( + @Inject(REDIS_SERVER) cache : RedisClientType, + ) { super(cache); }; + + async insert(entity: CreateRoomTransportDto): Promise { + + const namespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${entity.transport_id}`; + + await this.cache.hSet(namespace, { + [CACHE_SFU_TRANSPORTS_KEY_NAME.ROOM_ID] : entity.room_id, + [CACHE_SFU_TRANSPORTS_KEY_NAME.SOCKET_ID] : entity.socket_id, + [CACHE_SFU_TRANSPORTS_KEY_NAME.TYPE] : entity.type, + [CACHE_SFU_TRANSPORTS_KEY_NAME.USER_ID] : entity.user_id + }); + + return true; + }; +}; + +@Injectable() +export class DeleteSfuTransportInfoToRedis extends DeleteDataToCache> { + + constructor( + @Inject(REDIS_SERVER) cache : RedisClientType, + ) { super(cache); }; + + // namespace는 해당 transport_id에 대해서 삭제이다. -> 잘 봐주어야 함 + async deleteNamespace(namespace: string): Promise { + // 문자 검증? 이것도 필요해 보인다. + + + await this.cache.del(namespace); + + return true; + } + +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 6473ec331..a8d6dfb5c 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,9 +1,12 @@ import { MediasoupService } from "@infra/media/mediasoup/media"; import { Injectable, Logger } from "@nestjs/common"; -import { RoomEntry, TransportEntry } from "./sfu.validate"; +import { ConnectTransportType, RoomEntry, TransportEntry } from "./sfu.validate"; import { SfuError, SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; import { mediaSoupRouterConfig, listenIps } from "@infra/media/mediasoup/config"; -import { Router, WebRtcTransport } from "mediasoup/types"; +import { Router, Transport, WebRtcTransport } from "mediasoup/types"; +import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; +import { CreateRoomTransportDto, CreateTransportDto } from "@app/room/commands/dto/create-room-transport.dto"; +import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; @Injectable() @@ -21,6 +24,8 @@ export class SfuService { constructor( private readonly mediaSoupService : MediasoupService, + private readonly insertTranportInfoToRedis : CreateSfuTransportInfoToRedis, + private readonly deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis ) {} private async createRoomRouting(room_id : string) : Promise { @@ -131,10 +136,10 @@ export class SfuService { }); }; - // 2. transport 부분 생성 - async createTransPort(room_id : string) : Promise { + // 2. transport 부분 생성 ( 나중에 전체적인 부분 usecase로 빼자고 ) + async createTransPort(dto : CreateTransportDto) : Promise { - const roomEntry = this.roomRouters.get(room_id); + const roomEntry = this.roomRouters.get(dto.room_id); if ( !roomEntry ) throw new SfuErrorMessage("room_id를 다시 확인해주세요"); const router : Router = roomEntry.router; @@ -151,7 +156,7 @@ export class SfuService { }); // transport 로깅 - this.transPortLogging(room_id, transport); + this.transPortLogging(dto.room_id, transport); // 최대 전송율 정하기 try { @@ -167,18 +172,29 @@ export class SfuService { this.transports.set(transportId, transport); // router 갱신 - this.patchRoomEntry(room_id, (entry) => { + this.patchRoomEntry(dto.room_id, (entry) => { entry.transport_ids.add(transportId); }); + // cache에 정보 저장 + const validate : CreateRoomTransportDto = { + ...dto, + transport_id : transportId + } + await this.insertTranportInfoToRedis.insert(validate); + // transport 없어졌을때 이벤트 생성 transport.observer.on("close", () => { this.transports.delete(transportId); // router 갱신 - this.patchRoomEntry(room_id, (entry) => { + this.patchRoomEntry(dto.room_id, (entry) => { entry.transport_ids.delete(transportId); }); + + // cache에 삭제 + const namespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${transportId}`; + this.deleteTransportInfoToRedis.deleteNamespace(namespace); }); return { @@ -194,5 +210,17 @@ export class SfuService { }; + // 3. transport connect 연결 ( 이때 부터는 이제 sfu와 webrtc 통신이 가능해졌다고 생각하면 된다. ) + async connectTransport(dto : ConnectTransportType) : Promise { + // 검증하기 + + + // transport 가져오기 + const transport : Transport | undefined = this.transports.get(dto.transport_id); + + if ( !transport ) throw new SfuErrorMessage("transport_id를 다시 확인해주세요."); + + await transport.connect({ dtlsParameters : dto.dtlsParameters }); + }; }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts index 17e484794..b5ce4c1aa 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts @@ -17,4 +17,9 @@ export type TransportEntry = { iceParameters: IceParameters; iceCandidates: Array; dtlsParameters: DtlsParameters; +}; + +export type ConnectTransportType = { + transport_id : string; + dtlsParameters: DtlsParameters }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index aa7afa3b9..5003774e8 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -7,7 +7,7 @@ import { TokenDto } from "@app/auth/commands/dto"; import { PayloadRes } from "@app/auth/queries/dto"; import { JwtWsGuard } from "../auth/guards/jwt.guard"; import { WEBSOCKET_AUTH_CLIENT_EVENT_NAME, WEBSOCKET_NAMESPACE, WEBSOCKET_PATH, WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME, WEBSOCKET_SIGNALING_EVENT_NAME } from "../websocket.constants"; -import { JoinRoomValidate, SocketPayload } from "./signaling.validate"; +import { DtlsHandshakeValidate, JoinRoomValidate, NegotiateIceValidate, SocketPayload } from "./signaling.validate"; import { ConnectResult, ConnectRoomDto } from "@app/room/commands/dto"; import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; @@ -139,14 +139,17 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec // 여기서 추가할 점은 ( send, recv에 역할을 알 수 있는 매개변수가 있다면 좋을것이다 왜냐하면 이걸 이용해서 두개다 협상을 할거니까 ) @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.NEGOTIATE_ICE) + @UsePipes(new ValidationPipe({ + whitelist : true, + transform : true + })) async iceNegotiateGateway( - @ConnectedSocket() client : Socket + @ConnectedSocket() client : Socket, + @MessageBody() validate : NegotiateIceValidate ) { - const room_id : string = client.data.room_id; - try { // 1. sfu 서버에서 ice 협상에 필요한 정보 요구하고 dtls에 필요한 정보도 요구 - const transportOptions = await this.signalingService.iceNegotiate(room_id); + const transportOptions = await this.signalingService.iceNegotiate(client, validate.type); return { ok : true, transportOptions }; } catch (err) { @@ -155,4 +158,25 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec }; }; + // dtls 핸드세이크 과정 -> 사실상 여기에서 webrtc가 준비되었다고 보면 된다. - 보안 검증도 여기서 거쳐야 한다. + @SubscribeMessage(WEBSOCKET_SIGNALING_EVENT_NAME.DTLS_HANDSHAKE) + @UsePipes(new ValidationPipe({ + whitelist : true, + transform : true + })) + async dtlsHandshakeGateway( + @MessageBody() validate : DtlsHandshakeValidate + ) { + try { + // 1. dtls 핸드세이크를 거칠것이다. + await this.signalingService.dtlsHandshake(validate); + + return { ok : true }; + } catch (err){ + this.logger.error(err); + throw new WsException({ message : err.message ?? "에러 발생", status : err.status ?? 500 }); + } + }; + + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index 96b7f3436..d6e376de4 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -5,11 +5,13 @@ import * as cookie from "cookie"; import { ConnectResult, ConnectRoomDto, DisconnectRoomDto } from "@app/room/commands/dto"; import { ConnectRoomUsecase, DisconnectRoomUsecase } from "@app/room/commands/usecase"; import { v7 as uuidV7 } from "uuid"; -import { SocketPayload } from "./signaling.validate"; +import { DtlsHandshakeValidate, SocketPayload } from "./signaling.validate"; import { PayloadRes } from "@app/auth/queries/dto"; import { UnthorizedError } from "@error/application/user/user.error"; import { SfuService } from "@present/webrtc/sfu/sfu.service"; import { NotConnectSignalling } from "@error/presentation/signalling/signalling.error"; +import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; +import { CreateRoomTransportDto, CreateTransportDto } from "@/2.application/room/commands/dto/create-room-transport.dto"; @Injectable() @@ -78,6 +80,10 @@ export class SignalingWebsocketService { return client.handshake.address; }; + makeRoomNamespace(room_id : string) : string { + return `${CHANNEL_NAMESPACE.SIGNALING}:${room_id}` + } + makeSocketData({ payload, socket } : { payload : PayloadRes | undefined, socket : Socket }) : SocketPayload { if ( payload ) return { ...payload, ip : this.extractClientIp(socket), socket_id : socket.id, is_guest : false @@ -110,10 +116,22 @@ export class SignalingWebsocketService { }; // ice 협상을 위해 sfu서버에 ice parameter와 후보들을 알려준다 그리고 dtls 핸드세이킹을 위한 파라미터도 전달 - async iceNegotiate( room_id : string ) { + async iceNegotiate( client : Socket, type : "send" | "recv" ) { + const room_id : string = client.data.room_id; + const payload : SocketPayload = client.data.user; + const dto : CreateTransportDto = { + room_id, + socket_id : payload.socket_id, + user_id : payload.user_id, + type,}; if ( !room_id || room_id === "" ) throw new NotConnectSignalling(); - const transportOptions = await this.sfuServer.createTransPort(room_id); + const transportOptions = await this.sfuServer.createTransPort(dto); return transportOptions; }; + // dtls 핸드 세이크를 위한 + async dtlsHandshake( validate : DtlsHandshakeValidate) : Promise { + await this.sfuServer.connectTransport(validate); + } + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.validate.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.validate.ts index 72e618ac7..b0f9bf34f 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.validate.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.validate.ts @@ -1,4 +1,5 @@ -import { IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from "class-validator"; +import { IsIn, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, MinLength } from "class-validator"; +import { type DtlsParameters } from "mediasoup/types"; // socket에서 사용할 @@ -29,4 +30,25 @@ export class JoinRoomValidate { @IsString() @MaxLength(16) // 16글자? nickname? : string; -} \ No newline at end of file +} + +export class NegotiateIceValidate { + @IsNotEmpty() + @IsIn([ "send", "recv" ]) + type : "send" | "recv" +}; + +export class DtlsHandshakeValidate { + + @IsNotEmpty() + @IsString() + transport_id : string; + + @IsNotEmpty() + @IsObject() + dtlsParameters: DtlsParameters; + + @IsNotEmpty() + @IsIn([ "send", "recv" ]) + type : "send" | "recv"; +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts index 2d81c0f36..70b042600 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/websocket.constants.ts @@ -16,9 +16,11 @@ export const WEBSOCKET_AUTH_CLIENT_EVENT_NAME = Object.freeze({ export const WEBSOCKET_SIGNALING_EVENT_NAME = Object.freeze({ JOIN_ROOM : "signaling:ws:join_room", NEGOTIATE_SDP : "signaling:ws:negotiate_sdp", - NEGOTIATE_ICE : "signaling:ws:negotiate_ice" + NEGOTIATE_ICE : "signaling:ws:negotiate_ice", + DTLS_HANDSHAKE : "signaling:ws:dtls_handshake" } as const); export const WEBSOCKET_SIGNALING_CLIENT_EVENT_NAME = Object.freeze({ JOINED : "room:joined", + ADMISSION : "room:admission" } as const); \ No newline at end of file From 06d6d1e8e64e908905f4efe2b4112b9b6b8eba8f Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 06:06:55 +0900 Subject: [PATCH 16/24] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20sfu=20transpor?= =?UTF-8?q?t=EB=A5=BC=20connect=20=ED=95=98=EB=8A=94=EB=8D=B0=20=EC=95=88?= =?UTF-8?q?=EC=A0=95=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.application/room/commands/dto/index.ts | 3 +- .../queries/dto/check-room-transport.dto.ts | 9 +++++ .../2.application/room/queries/dto/index.ts | 1 + .../src/3-1.infra/cache/redis/cache.ts | 7 ++-- .../3-1.infra/cache/redis/sfu/sfu.inbound.ts | 34 +++++++++++++++++++ .../3-1.infra/cache/redis/sfu/sfu.outbound.ts | 2 +- .../webrtc/sfu/sfu.service.ts | 13 +++++-- .../webrtc/sfu/sfu.validate.ts | 4 +++ .../websocket/signaling/signaling.gateway.ts | 3 +- .../websocket/signaling/signaling.service.ts | 13 +++++-- 10 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 rep/main_backend/src/2.application/room/queries/dto/check-room-transport.dto.ts diff --git a/rep/main_backend/src/2.application/room/commands/dto/index.ts b/rep/main_backend/src/2.application/room/commands/dto/index.ts index bce9a7089..47ae6d90f 100644 --- a/rep/main_backend/src/2.application/room/commands/dto/index.ts +++ b/rep/main_backend/src/2.application/room/commands/dto/index.ts @@ -1,3 +1,4 @@ export * from "./create-room.dto"; export * from "./connect-room.dto"; -export * from "./disconnect-room.dto" \ No newline at end of file +export * from "./disconnect-room.dto" +export * from "./create-room-transport.dto"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/room/queries/dto/check-room-transport.dto.ts b/rep/main_backend/src/2.application/room/queries/dto/check-room-transport.dto.ts new file mode 100644 index 000000000..015b61c42 --- /dev/null +++ b/rep/main_backend/src/2.application/room/queries/dto/check-room-transport.dto.ts @@ -0,0 +1,9 @@ + + + +export type RoomTransportInfo = { + room_id : string; + socket_id : string; + user_id : string; + type : string; +}; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/room/queries/dto/index.ts b/rep/main_backend/src/2.application/room/queries/dto/index.ts index e69de29bb..52be5b5a4 100644 --- a/rep/main_backend/src/2.application/room/queries/dto/index.ts +++ b/rep/main_backend/src/2.application/room/queries/dto/index.ts @@ -0,0 +1 @@ +export * from "./check-room-transport.dto"; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/cache/redis/cache.ts b/rep/main_backend/src/3-1.infra/cache/redis/cache.ts index 6a00f8f40..8471866a5 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/cache.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/cache.ts @@ -10,6 +10,7 @@ import { SelectHsetDataFromRedis } from './user/user.inbound'; import { DeleteRoomDatasToRedis, InsertRoomDatasToRedis, InsertRoomDataToRedis } from './room/room.outbound'; import { SelectRoomInfoFromRedis } from './room/room.inbound'; import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis, } from "./sfu/sfu.outbound" +import { SelectSfuTransportDataFromRedis } from './sfu/sfu.inbound'; @Global() @@ -54,7 +55,8 @@ import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis, } from " InsertRoomDatasToRedis, // room data들을 저장할때 사용 DeleteRoomDatasToRedis, // room정보들을 삭제하기 위해 사용 CreateSfuTransportInfoToRedis, // transport들의 정보를 저장하기 위한 로직 - DeleteSfuTransportInfoToRedis // transport가 만약 에러가 발생하거나 삭제될때 발동하는 로직 + DeleteSfuTransportInfoToRedis, // transport가 만약 에러가 발생하거나 삭제될때 발동하는 로직 + SelectSfuTransportDataFromRedis // transport의 정보를 체크하기 위해서 필요한 로직 ], exports: [ REDIS_SERVER, @@ -66,7 +68,8 @@ import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis, } from " InsertRoomDatasToRedis, DeleteRoomDatasToRedis, CreateSfuTransportInfoToRedis, - DeleteSfuTransportInfoToRedis + DeleteSfuTransportInfoToRedis, + SelectSfuTransportDataFromRedis ], }) export class RedisModule {} diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts index e69de29bb..fb3ae9742 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts @@ -0,0 +1,34 @@ +import { SelectDataFromCache } from "@app/ports/cache/cache.inbound"; +import { Inject, Injectable } from "@nestjs/common"; +import { type RedisClientType } from "redis"; +import { CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; +import { RoomTransportInfo } from "@app/room/queries/dto"; + + +@Injectable() +export class SelectSfuTransportDataFromRedis extends SelectDataFromCache> { + + constructor( + @Inject(REDIS_SERVER) cache : RedisClientType, + ) { super(cache); }; + + // namespace 부분만 있고 keyname은 사용 안될 예정이다. + async select({ namespace, keyName, }: { namespace: string; keyName: string; }): Promise { + + const data = await this.cache.hGetAll(namespace); + + // 값이 하나라도 없다면 문제가 있는 것이다. + if ( !data || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.SOCKET_ID] || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.USER_ID] || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.ROOM_ID] || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.TYPE] ) return undefined; + + + // 이러한 형태가 아니라면 에러가 발생할것이니 여기서 잡는다. + if (data[CACHE_SFU_TRANSPORTS_KEY_NAME.TYPE] !== "send" && data[CACHE_SFU_TRANSPORTS_KEY_NAME.TYPE] !== "recv") return undefined; + + return { + socket_id : data[CACHE_SFU_TRANSPORTS_KEY_NAME.SOCKET_ID], + user_id : data[CACHE_SFU_TRANSPORTS_KEY_NAME.USER_ID], + room_id : data[CACHE_SFU_TRANSPORTS_KEY_NAME.ROOM_ID], + type : data[CACHE_SFU_TRANSPORTS_KEY_NAME.TYPE] + } + } +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts index 8376f908e..5c933a973 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts @@ -2,7 +2,7 @@ import { DeleteDataToCache, InsertDataToCache } from "@app/ports/cache/cache.out import { Inject, Injectable } from "@nestjs/common"; import { type RedisClientType } from "redis"; import { CACHE_SFU_NAMESPACE_NAME, CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; -import { CreateRoomTransportDto } from "@app/room/commands/dto/create-room-transport.dto"; +import { CreateRoomTransportDto } from "@app/room/commands/dto"; diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index a8d6dfb5c..020f57b30 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -5,8 +5,10 @@ import { SfuError, SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; import { mediaSoupRouterConfig, listenIps } from "@infra/media/mediasoup/config"; import { Router, Transport, WebRtcTransport } from "mediasoup/types"; import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; -import { CreateRoomTransportDto, CreateTransportDto } from "@app/room/commands/dto/create-room-transport.dto"; +import { CreateRoomTransportDto, CreateTransportDto } from "@app/room/commands/dto"; import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; +import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; +import { RoomTransportInfo } from "@/2.application/room/queries/dto"; @Injectable() @@ -25,7 +27,8 @@ export class SfuService { constructor( private readonly mediaSoupService : MediasoupService, private readonly insertTranportInfoToRedis : CreateSfuTransportInfoToRedis, - private readonly deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis + private readonly deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis, + private readonly selectSfuTransportInfoFromRedis : SelectSfuTransportDataFromRedis ) {} private async createRoomRouting(room_id : string) : Promise { @@ -213,7 +216,11 @@ export class SfuService { // 3. transport connect 연결 ( 이때 부터는 이제 sfu와 webrtc 통신이 가능해졌다고 생각하면 된다. ) async connectTransport(dto : ConnectTransportType) : Promise { // 검증하기 - + const namespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${dto.transport_id}`; + const transportInfo : RoomTransportInfo | undefined = await this.selectSfuTransportInfoFromRedis.select({ namespace, keyName : "" }); + if ( !transportInfo ) throw new SfuErrorMessage("transport_id를 다시 확인해주세요. - 데이터 찾는데 문제 발생"); + + if ( dto.room_id !== transportInfo.room_id || dto.socket_id !== transportInfo.socket_id || dto.type !== transportInfo.type || dto.user_id !== transportInfo.user_id ) throw new SfuErrorMessage("잘못된 transport_id에 연결하고자 합니다 다시 확인해주시길 발반디ㅏ."); // transport 가져오기 const transport : Transport | undefined = this.transports.get(dto.transport_id); diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts index b5ce4c1aa..be84af4b6 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts @@ -20,6 +20,10 @@ export type TransportEntry = { }; export type ConnectTransportType = { + room_id : string; + socket_id : string; + user_id : string; + type : "send" | "recv"; transport_id : string; dtlsParameters: DtlsParameters }; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts index 5003774e8..2c2adca23 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.gateway.ts @@ -165,11 +165,12 @@ export class SignalingWebsocketGateway implements OnGatewayInit, OnGatewayConnec transform : true })) async dtlsHandshakeGateway( + @ConnectedSocket() client : Socket, @MessageBody() validate : DtlsHandshakeValidate ) { try { // 1. dtls 핸드세이크를 거칠것이다. - await this.signalingService.dtlsHandshake(validate); + await this.signalingService.dtlsHandshake(client, validate); return { ok : true }; } catch (err){ diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index d6e376de4..80aa26dd7 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -12,6 +12,7 @@ import { SfuService } from "@present/webrtc/sfu/sfu.service"; import { NotConnectSignalling } from "@error/presentation/signalling/signalling.error"; import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; import { CreateRoomTransportDto, CreateTransportDto } from "@/2.application/room/commands/dto/create-room-transport.dto"; +import { ConnectTransportType } from "@/3-2.presentation/webrtc/sfu/sfu.validate"; @Injectable() @@ -130,8 +131,16 @@ export class SignalingWebsocketService { }; // dtls 핸드 세이크를 위한 - async dtlsHandshake( validate : DtlsHandshakeValidate) : Promise { - await this.sfuServer.connectTransport(validate); + async dtlsHandshake( client: Socket, validate : DtlsHandshakeValidate) : Promise { + const room_id : string = client.data.room_id; + const payload : SocketPayload = client.data.user; + const dto : ConnectTransportType = { + ...validate, + room_id, + socket_id : payload.socket_id, + user_id : payload.user_id, + } + await this.sfuServer.connectTransport(dto); } }; \ No newline at end of file From 0a769da21e781c5800f6405486e0429abd2b5d5c Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 06:17:15 +0900 Subject: [PATCH 17/24] =?UTF-8?q?=F0=9F=A4=96=20Refactor:=20application=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=20sfu=EC=99=80=20room=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rep/main_backend/src/2.application/room/commands/dto/index.ts | 1 - rep/main_backend/src/2.application/room/queries/dto/index.ts | 1 - .../commands/dto/create-transport.dto.ts} | 0 rep/main_backend/src/2.application/sfu/commands/dto/index.ts | 1 + .../src/2.application/sfu/commands/usecase/index.ts | 0 .../queries/dto/check-transport.dto.ts} | 0 rep/main_backend/src/2.application/sfu/queries/dto/index.ts | 1 + .../src/2.application/sfu/queries/usecase/index.ts | 0 .../src/3-1.infra/cache/redis/room/room.outbound.ts | 3 +-- rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts | 2 +- .../src/3-1.infra/cache/redis/sfu/sfu.outbound.ts | 2 +- .../src/3-2.presentation/webrtc/sfu/sfu.service.ts | 4 ++-- .../3-2.presentation/websocket/signaling/signaling.service.ts | 2 +- 13 files changed, 8 insertions(+), 9 deletions(-) rename rep/main_backend/src/2.application/{room/commands/dto/create-room-transport.dto.ts => sfu/commands/dto/create-transport.dto.ts} (100%) create mode 100644 rep/main_backend/src/2.application/sfu/commands/dto/index.ts create mode 100644 rep/main_backend/src/2.application/sfu/commands/usecase/index.ts rename rep/main_backend/src/2.application/{room/queries/dto/check-room-transport.dto.ts => sfu/queries/dto/check-transport.dto.ts} (100%) create mode 100644 rep/main_backend/src/2.application/sfu/queries/dto/index.ts create mode 100644 rep/main_backend/src/2.application/sfu/queries/usecase/index.ts diff --git a/rep/main_backend/src/2.application/room/commands/dto/index.ts b/rep/main_backend/src/2.application/room/commands/dto/index.ts index 47ae6d90f..2dad4fe15 100644 --- a/rep/main_backend/src/2.application/room/commands/dto/index.ts +++ b/rep/main_backend/src/2.application/room/commands/dto/index.ts @@ -1,4 +1,3 @@ export * from "./create-room.dto"; export * from "./connect-room.dto"; export * from "./disconnect-room.dto" -export * from "./create-room-transport.dto"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/room/queries/dto/index.ts b/rep/main_backend/src/2.application/room/queries/dto/index.ts index 52be5b5a4..e69de29bb 100644 --- a/rep/main_backend/src/2.application/room/queries/dto/index.ts +++ b/rep/main_backend/src/2.application/room/queries/dto/index.ts @@ -1 +0,0 @@ -export * from "./check-room-transport.dto"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/room/commands/dto/create-room-transport.dto.ts b/rep/main_backend/src/2.application/sfu/commands/dto/create-transport.dto.ts similarity index 100% rename from rep/main_backend/src/2.application/room/commands/dto/create-room-transport.dto.ts rename to rep/main_backend/src/2.application/sfu/commands/dto/create-transport.dto.ts diff --git a/rep/main_backend/src/2.application/sfu/commands/dto/index.ts b/rep/main_backend/src/2.application/sfu/commands/dto/index.ts new file mode 100644 index 000000000..52ddf802f --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/commands/dto/index.ts @@ -0,0 +1 @@ +export * from "./create-transport.dto"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/index.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/2.application/room/queries/dto/check-room-transport.dto.ts b/rep/main_backend/src/2.application/sfu/queries/dto/check-transport.dto.ts similarity index 100% rename from rep/main_backend/src/2.application/room/queries/dto/check-room-transport.dto.ts rename to rep/main_backend/src/2.application/sfu/queries/dto/check-transport.dto.ts diff --git a/rep/main_backend/src/2.application/sfu/queries/dto/index.ts b/rep/main_backend/src/2.application/sfu/queries/dto/index.ts new file mode 100644 index 000000000..9d300cb42 --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/queries/dto/index.ts @@ -0,0 +1 @@ +export * from "./check-transport.dto" \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/queries/usecase/index.ts b/rep/main_backend/src/2.application/sfu/queries/usecase/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts index 8e4d12d73..c24b9437c 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/room/room.outbound.ts @@ -1,11 +1,10 @@ import { DeleteDatasToCache, DeleteDataToCache, InsertDataToCache } from "@app/ports/cache/cache.outbound"; import { Inject, Injectable } from "@nestjs/common"; import { type RedisClientType } from "redis"; -import { CACHE_ROOM_INFO_KEY_NAME, CACHE_ROOM_MEMBERS_KEY_PROPS_NAME, CACHE_ROOM_NAMESPACE_NAME, CACHE_ROOM_SOCKETS_KEY_PROPS_NAME, CACHE_ROOM_SUB_NAMESPACE_NAME, CACHE_SFU_NAMESPACE_NAME, CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; +import { CACHE_ROOM_INFO_KEY_NAME, CACHE_ROOM_MEMBERS_KEY_PROPS_NAME, CACHE_ROOM_NAMESPACE_NAME, CACHE_ROOM_SOCKETS_KEY_PROPS_NAME, CACHE_ROOM_SUB_NAMESPACE_NAME, REDIS_SERVER } from "../../cache.constants"; import { RoomProps } from "@domain/room/vo"; import { InsertRoomDataDto } from "@app/room/commands/dto"; import { CacheError } from "@error/infra/infra.error"; -import { CreateRoomTransportDto } from "@app/room/commands/dto/create-room-transport.dto"; @Injectable() diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts index fb3ae9742..4e48e6e0e 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts @@ -2,7 +2,7 @@ import { SelectDataFromCache } from "@app/ports/cache/cache.inbound"; import { Inject, Injectable } from "@nestjs/common"; import { type RedisClientType } from "redis"; import { CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; -import { RoomTransportInfo } from "@app/room/queries/dto"; +import { RoomTransportInfo } from "@app/sfu/queries/dto"; @Injectable() diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts index 5c933a973..d64defc99 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts @@ -2,7 +2,7 @@ import { DeleteDataToCache, InsertDataToCache } from "@app/ports/cache/cache.out import { Inject, Injectable } from "@nestjs/common"; import { type RedisClientType } from "redis"; import { CACHE_SFU_NAMESPACE_NAME, CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; -import { CreateRoomTransportDto } from "@app/room/commands/dto"; +import { CreateRoomTransportDto } from "@app/sfu/commands/dto"; diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 020f57b30..69d1fbf46 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -5,10 +5,10 @@ import { SfuError, SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; import { mediaSoupRouterConfig, listenIps } from "@infra/media/mediasoup/config"; import { Router, Transport, WebRtcTransport } from "mediasoup/types"; import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; -import { CreateRoomTransportDto, CreateTransportDto } from "@app/room/commands/dto"; +import { CreateRoomTransportDto, CreateTransportDto } from "@app/sfu/commands/dto"; import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; -import { RoomTransportInfo } from "@/2.application/room/queries/dto"; +import { RoomTransportInfo } from "@app/sfu/queries/dto"; @Injectable() diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index 80aa26dd7..a586a41e0 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -11,7 +11,7 @@ import { UnthorizedError } from "@error/application/user/user.error"; import { SfuService } from "@present/webrtc/sfu/sfu.service"; import { NotConnectSignalling } from "@error/presentation/signalling/signalling.error"; import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; -import { CreateRoomTransportDto, CreateTransportDto } from "@/2.application/room/commands/dto/create-room-transport.dto"; +import { CreateRoomTransportDto, CreateTransportDto } from "@app/sfu/commands/dto"; import { ConnectTransportType } from "@/3-2.presentation/webrtc/sfu/sfu.validate"; From da2689111cb4e01390d64f81e36929e16ac1d50d Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 06:33:46 +0900 Subject: [PATCH 18/24] =?UTF-8?q?=F0=9F=9A=9A=20Chore:=20app=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=EC=97=90=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B5=AC=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/2.application/sfu/commands/dto/create-router.dto.ts | 0 rep/main_backend/src/2.application/sfu/commands/dto/index.ts | 3 ++- .../sfu/commands/usecase/create-router.usecase.ts | 0 .../sfu/commands/usecase/create-transport.usecase.ts | 0 .../src/2.application/sfu/commands/usecase/index.ts | 2 ++ .../dto/{check-transport.dto.ts => connect-transport.dto.ts} | 0 rep/main_backend/src/2.application/sfu/queries/dto/index.ts | 2 +- .../sfu/queries/usecase/connect-transport.usecase.ts | 0 .../src/2.application/sfu/queries/usecase/index.ts | 1 + 9 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts create mode 100644 rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts create mode 100644 rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts rename rep/main_backend/src/2.application/sfu/queries/dto/{check-transport.dto.ts => connect-transport.dto.ts} (100%) create mode 100644 rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts diff --git a/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts b/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/2.application/sfu/commands/dto/index.ts b/rep/main_backend/src/2.application/sfu/commands/dto/index.ts index 52ddf802f..db4feeb52 100644 --- a/rep/main_backend/src/2.application/sfu/commands/dto/index.ts +++ b/rep/main_backend/src/2.application/sfu/commands/dto/index.ts @@ -1 +1,2 @@ -export * from "./create-transport.dto"; \ No newline at end of file +export * from "./create-transport.dto"; +export * from "./create-router.dto"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/index.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/index.ts index e69de29bb..26c631926 100644 --- a/rep/main_backend/src/2.application/sfu/commands/usecase/index.ts +++ b/rep/main_backend/src/2.application/sfu/commands/usecase/index.ts @@ -0,0 +1,2 @@ +export * from "./create-router.usecase"; +export * from "./create-transport.usecase"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/queries/dto/check-transport.dto.ts b/rep/main_backend/src/2.application/sfu/queries/dto/connect-transport.dto.ts similarity index 100% rename from rep/main_backend/src/2.application/sfu/queries/dto/check-transport.dto.ts rename to rep/main_backend/src/2.application/sfu/queries/dto/connect-transport.dto.ts diff --git a/rep/main_backend/src/2.application/sfu/queries/dto/index.ts b/rep/main_backend/src/2.application/sfu/queries/dto/index.ts index 9d300cb42..d9913bce0 100644 --- a/rep/main_backend/src/2.application/sfu/queries/dto/index.ts +++ b/rep/main_backend/src/2.application/sfu/queries/dto/index.ts @@ -1 +1 @@ -export * from "./check-transport.dto" \ No newline at end of file +export * from "./connect-transport.dto" \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts b/rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts new file mode 100644 index 000000000..e69de29bb diff --git a/rep/main_backend/src/2.application/sfu/queries/usecase/index.ts b/rep/main_backend/src/2.application/sfu/queries/usecase/index.ts index e69de29bb..484ceebc5 100644 --- a/rep/main_backend/src/2.application/sfu/queries/usecase/index.ts +++ b/rep/main_backend/src/2.application/sfu/queries/usecase/index.ts @@ -0,0 +1 @@ +export * from "./connect-transport.usecase"; \ No newline at end of file From 1546964b03ae4df763b0fd21a2e6e37a4c99f26f Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 07:08:02 +0900 Subject: [PATCH 19/24] =?UTF-8?q?=F0=9F=9A=9A=20Chore:=20router=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20por?= =?UTF-8?q?t,=20infra=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sfu/commands/dto/create-router.dto.ts | 11 +++++++++++ .../src/2.application/sfu/ports/index.ts | 2 ++ .../sfu/ports/room-create-lock-repo.port.ts | 8 ++++++++ .../sfu/ports/room-router-repo.port.ts | 8 ++++++++ .../src/3-1.infra/memory/memory.module.ts | 15 +++++++++++++++ .../src/3-1.infra/memory/sfu/index.ts | 2 ++ .../3-1.infra/memory/sfu/room-create-lock.repo.ts | 13 +++++++++++++ .../src/3-1.infra/memory/sfu/room-router.repo.ts | 13 +++++++++++++ rep/main_backend/src/app.module.ts | 4 +++- 9 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 rep/main_backend/src/2.application/sfu/ports/index.ts create mode 100644 rep/main_backend/src/2.application/sfu/ports/room-create-lock-repo.port.ts create mode 100644 rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts create mode 100644 rep/main_backend/src/3-1.infra/memory/memory.module.ts create mode 100644 rep/main_backend/src/3-1.infra/memory/sfu/index.ts create mode 100644 rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts create mode 100644 rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts diff --git a/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts b/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts index e69de29bb..5f644848a 100644 --- a/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts +++ b/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts @@ -0,0 +1,11 @@ +import { Router } from "mediasoup/types"; + + +export type RoomEntry = { + room_id : string; + worker_idx : number; // 이 라우터를 운영하기 위해서 필요한 정보 + worker_pid : number; // worker를 디버깅 하기 위해서 필요한 정보 + router : Router; // 실질적으로 기능구현에 사용되는 라우터 -> 여기에 있으면 안되고 domain에서 가져오도록 해야한다. ( 나중에 수정 모먼트 ) + transport_ids : Set; // transport들이 저장된 라우터 + created_at : Date; +}; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/ports/index.ts b/rep/main_backend/src/2.application/sfu/ports/index.ts new file mode 100644 index 000000000..dfd266239 --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/ports/index.ts @@ -0,0 +1,2 @@ +export * from "./room-router-repo.port"; +export * from "./room-create-lock-repo.port" \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/ports/room-create-lock-repo.port.ts b/rep/main_backend/src/2.application/sfu/ports/room-create-lock-repo.port.ts new file mode 100644 index 000000000..92a97e8bc --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/ports/room-create-lock-repo.port.ts @@ -0,0 +1,8 @@ +import { RoomEntry } from "../commands/dto"; + + +export interface RoomCreateLockPort { + get(roomId: string): Promise | undefined; + set(roomId: string, inflight: Promise): void; + delete(roomId: string): void; +} diff --git a/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts b/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts new file mode 100644 index 000000000..7fdcfff2a --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts @@ -0,0 +1,8 @@ +import { RoomEntry } from "../commands/dto"; + + +export interface RoomRouterRepositoryPort { + get(roomId: string): RoomEntry | undefined; + set(roomId: string, entry: RoomEntry): void; + delete(roomId: string): void; +} diff --git a/rep/main_backend/src/3-1.infra/memory/memory.module.ts b/rep/main_backend/src/3-1.infra/memory/memory.module.ts new file mode 100644 index 000000000..44eed1b9c --- /dev/null +++ b/rep/main_backend/src/3-1.infra/memory/memory.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; +import { InMemoryRoomCreateLock, RoomRouterRepository } from "./sfu"; + + +@Module({ + providers : [ + RoomRouterRepository, + InMemoryRoomCreateLock + ], + exports : [ + RoomRouterRepository, + InMemoryRoomCreateLock + ] +}) +export class MemoryModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/index.ts b/rep/main_backend/src/3-1.infra/memory/sfu/index.ts new file mode 100644 index 000000000..10c77eb30 --- /dev/null +++ b/rep/main_backend/src/3-1.infra/memory/sfu/index.ts @@ -0,0 +1,2 @@ +export * from "./room-create-lock.repo"; +export * from "./room-router.repo" \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts b/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts new file mode 100644 index 000000000..342dc5d6d --- /dev/null +++ b/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts @@ -0,0 +1,13 @@ +import { RoomEntry } from '@app/sfu/commands/dto'; +import { RoomCreateLockPort } from '@app/sfu/ports'; +import { Injectable } from '@nestjs/common'; + + +@Injectable() +export class InMemoryRoomCreateLock implements RoomCreateLockPort { + private readonly creating = new Map>(); + + get(roomId: string) { return this.creating.get(roomId); } + set(roomId: string, inflight: Promise) { this.creating.set(roomId, inflight); } + delete(roomId: string) { this.creating.delete(roomId); } +} diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts b/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts new file mode 100644 index 000000000..2efaad8dd --- /dev/null +++ b/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts @@ -0,0 +1,13 @@ +import { RoomEntry } from '@present/webrtc/sfu/sfu.validate'; +import { Injectable } from '@nestjs/common'; +import { RoomRouterRepositoryPort } from '@app/sfu/ports'; + + +@Injectable() +export class RoomRouterRepository implements RoomRouterRepositoryPort { + private readonly roomRouters = new Map(); + + get(roomId: string) { return this.roomRouters.get(roomId); } + set(roomId: string, entry: RoomEntry) { this.roomRouters.set(roomId, entry); } + delete(roomId: string) { this.roomRouters.delete(roomId); } +} diff --git a/rep/main_backend/src/app.module.ts b/rep/main_backend/src/app.module.ts index f3d4ac970..f9fbd3deb 100644 --- a/rep/main_backend/src/app.module.ts +++ b/rep/main_backend/src/app.module.ts @@ -17,7 +17,8 @@ import { RedisChannelModule } from '@infra/channel/redis/channel'; import { RoomModule } from "@present/http/room/room.module"; import { SignalingWebsocketModule } from "@present/websocket/signaling/signaling.module"; import { MediaModule } from "@infra/media/media.module"; -import { SfuModule } from "./3-2.presentation/webrtc/sfu/sfu.module"; +import { SfuModule } from "@present/webrtc/sfu/sfu.module"; +import { MemoryModule } from "@infra/memory/memory.module"; @Module({ @@ -32,6 +33,7 @@ import { SfuModule } from "./3-2.presentation/webrtc/sfu/sfu.module"; S3DiskModule, // s3를 사용하기 위한 모듈 RedisChannelModule, // redis를 활용한 pub sub을 이용하기 위한 모듈 MediaModule, // media를 다루기 위한 모듈 + MemoryModule, // in-memory를 사용하기 위해 필요한 모듈 // 우리가 집적 만든 모듈 SettingModule, // 헬스 체크를 위한 모듈 From 55fc481f74ae7e7e3ef5652b5cba0539389f35c0 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 07:57:27 +0900 Subject: [PATCH 20/24] =?UTF-8?q?=F0=9F=8C=88=20Update:=20presentation?= =?UTF-8?q?=EC=97=90=20=EC=A7=91=EC=A4=91=EB=90=9C=EA=B1=B8=20app=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=EC=9C=BC=EB=A1=9C=20=EC=98=AE=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/application/sfu/sfu.error.ts | 19 +++++ .../sfu/commands/dto/create-router.dto.ts | 1 + .../commands/usecase/create-router.usecase.ts | 78 +++++++++++++++++++ .../src/2.application/sfu/ports/index.ts | 3 +- .../sfu/ports/router-factory.port.ts | 11 +++ .../src/3-1.infra/media/mediasoup/media.ts | 12 ++- .../src/3-1.infra/memory/memory.module.ts | 9 ++- .../memory/sfu/room-create-lock.repo.ts | 2 +- .../3-1.infra/memory/sfu/room-router.repo.ts | 2 +- .../webrtc/sfu/sfu.interface.ts | 13 ++++ .../3-2.presentation/webrtc/sfu/sfu.module.ts | 28 +++++++ .../webrtc/sfu/sfu.service.ts | 76 +++--------------- .../webrtc/sfu/sfu.validate.ts | 12 +-- 13 files changed, 179 insertions(+), 87 deletions(-) create mode 100644 rep/main_backend/src/0.common/error/application/sfu/sfu.error.ts create mode 100644 rep/main_backend/src/2.application/sfu/ports/router-factory.port.ts create mode 100644 rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.interface.ts diff --git a/rep/main_backend/src/0.common/error/application/sfu/sfu.error.ts b/rep/main_backend/src/0.common/error/application/sfu/sfu.error.ts new file mode 100644 index 000000000..bfe316932 --- /dev/null +++ b/rep/main_backend/src/0.common/error/application/sfu/sfu.error.ts @@ -0,0 +1,19 @@ +import { BaseError } from '../../error'; + +export class SfuError extends BaseError { + constructor(err: Error) { + super({ + message: `${err}`, + status: 500, + }); + } +} + +export class SfuErrorMessage extends BaseError { + constructor(message : string) { + super({ + message, + status: 500, + }); + } +} \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts b/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts index 5f644848a..31810b1f3 100644 --- a/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts +++ b/rep/main_backend/src/2.application/sfu/commands/dto/create-router.dto.ts @@ -1,6 +1,7 @@ import { Router } from "mediasoup/types"; +// room_id에 대해서 worker, router, room_id, created_at으로 메모리에서 worker에서 router를 어떻게 관리하는지 작성한다. export type RoomEntry = { room_id : string; worker_idx : number; // 이 라우터를 운영하기 위해서 필요한 정보 diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts index e69de29bb..12f16a247 100644 --- a/rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts +++ b/rep/main_backend/src/2.application/sfu/commands/usecase/create-router.usecase.ts @@ -0,0 +1,78 @@ +import { Injectable, Logger } from "@nestjs/common"; +import type { RoomCreateLockPort, RoomRouterRepositoryPort, RouterFactoryPort } from "../../ports"; +import { RoomEntry } from "../dto"; +import { SfuError } from "@error/application/sfu/sfu.error"; + + +@Injectable() +export class CreateRouterUsecase { + private readonly logger = new Logger(CreateRouterUsecase.name); + + // 기존의 in-memory를 그대로 사용해야 하기때문에 의존성을 그대로 부여 + constructor( + private readonly roomRepo : RoomRouterRepositoryPort, + private readonly roomCreateLockRepo : RoomCreateLockPort, + private readonly routerFactory : RouterFactoryPort + ) {} + + async execute(room_id : string) : Promise { + + // 이미 생성된 router라면 + const roomRouterExist = this.roomRepo.get(room_id); + if ( roomRouterExist && !roomRouterExist.router.closed ) { + return roomRouterExist; + }; + + // 생성 중이라면 + const createRoomRoutingExist = this.roomCreateLockRepo.get(room_id); + if ( createRoomRoutingExist ) { + return createRoomRoutingExist; + }; + + // 아무것도 없다면 새롭게 생성할 수 있다. + const roomEntry = this.createRoomRouting(room_id); + this.roomCreateLockRepo.set(room_id, roomEntry); + + return roomEntry; + + } + + private async createRoomRouting(room_id : string) : Promise { + try { + // 여기서 router까지 생성 + const { router, workerIdx, workerPid } = await this.routerFactory.createRouter(); + + // 기본 설정 + const roomEntry : RoomEntry = { + worker_idx : workerIdx, + room_id, + router, + worker_pid : workerPid, + transport_ids : new Set(), + created_at : new Date() + }; + + // 메모리에 저장 + this.roomRepo.set(room_id, roomEntry); + + // router가 만약 내려간다면? ( 근데 worker가 내려가면 애초에 그 방은 내려가니까 worker에 죽음은 그 프로세스 전체에 삭제 ) + router.observer.on("close", () => { + const closedRouterEntry = this.roomRepo.get(room_id); + if ( closedRouterEntry?.router === router ) { + // 방이랑 worker_id 기록한거 삭제 + this.roomRepo.delete(room_id); + }; + }); + + // 여기에서 대표 producer transport도 나중에 등록 예정 + + return roomEntry; + } catch (err) { + this.logger.error(err); // error가 발생한다면 + throw new SfuError(err); + } finally { + this.roomCreateLockRepo.delete(room_id); + }; + }; + +}; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/ports/index.ts b/rep/main_backend/src/2.application/sfu/ports/index.ts index dfd266239..3d3dcc4ce 100644 --- a/rep/main_backend/src/2.application/sfu/ports/index.ts +++ b/rep/main_backend/src/2.application/sfu/ports/index.ts @@ -1,2 +1,3 @@ export * from "./room-router-repo.port"; -export * from "./room-create-lock-repo.port" \ No newline at end of file +export * from "./room-create-lock-repo.port"; +export * from "./router-factory.port"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/ports/router-factory.port.ts b/rep/main_backend/src/2.application/sfu/ports/router-factory.port.ts new file mode 100644 index 000000000..c023ae790 --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/ports/router-factory.port.ts @@ -0,0 +1,11 @@ +import type { Router } from "mediasoup/types"; + +export type CreateRouterResult = { + router: Router; + workerIdx: number; + workerPid: number; +}; + +export interface RouterFactoryPort { + createRouter(): Promise; +} \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts index 8405f9af2..7a19aff4e 100644 --- a/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/media.ts @@ -1,8 +1,8 @@ import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; -import { type Worker } from "mediasoup/types"; +import { Router, type Worker } from "mediasoup/types"; import * as mediasoup from "mediasoup"; import * as os from "os"; -import { mediaSoupWorkerConfig } from "./config"; +import { mediaSoupRouterConfig, mediaSoupWorkerConfig } from "./config"; @Injectable() @@ -56,4 +56,12 @@ export class MediasoupService implements OnModuleInit, OnModuleDestroy { getWorker(worker_idx : number) : Worker | undefined { return this.workers[worker_idx]; }; + + // router 생성해주는 메서드 + async createRouterOnPickedWorker(): Promise<{ router: Router; workerIdx: number; workerPid: number }> { + const { worker, workerIdx } = this.picWorker(); + const router = await worker.createRouter(mediaSoupRouterConfig); + return { router, workerIdx, workerPid: worker.pid }; + } + }; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/memory/memory.module.ts b/rep/main_backend/src/3-1.infra/memory/memory.module.ts index 44eed1b9c..cc984e2c3 100644 --- a/rep/main_backend/src/3-1.infra/memory/memory.module.ts +++ b/rep/main_backend/src/3-1.infra/memory/memory.module.ts @@ -1,15 +1,16 @@ -import { Module } from "@nestjs/common"; -import { InMemoryRoomCreateLock, RoomRouterRepository } from "./sfu"; +import { Global, Module } from "@nestjs/common"; +import { RoomCreateLockRepo, RoomRouterRepository } from "./sfu"; +@Global() @Module({ providers : [ RoomRouterRepository, - InMemoryRoomCreateLock + RoomCreateLockRepo ], exports : [ RoomRouterRepository, - InMemoryRoomCreateLock + RoomCreateLockRepo ] }) export class MemoryModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts b/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts index 342dc5d6d..1310f0a81 100644 --- a/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts +++ b/rep/main_backend/src/3-1.infra/memory/sfu/room-create-lock.repo.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class InMemoryRoomCreateLock implements RoomCreateLockPort { +export class RoomCreateLockRepo implements RoomCreateLockPort { private readonly creating = new Map>(); get(roomId: string) { return this.creating.get(roomId); } diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts b/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts index 2efaad8dd..533e4bd3d 100644 --- a/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts +++ b/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts @@ -1,6 +1,6 @@ -import { RoomEntry } from '@present/webrtc/sfu/sfu.validate'; import { Injectable } from '@nestjs/common'; import { RoomRouterRepositoryPort } from '@app/sfu/ports'; +import { RoomEntry } from '@app/sfu/commands/dto'; @Injectable() diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.interface.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.interface.ts new file mode 100644 index 000000000..43c02df34 --- /dev/null +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.interface.ts @@ -0,0 +1,13 @@ +import { RouterFactoryPort } from "@app/sfu/ports"; +import { MediasoupService } from "@infra/media/mediasoup/media"; +import { Injectable } from "@nestjs/common"; + + +@Injectable() +export class MediasoupRouterFactory implements RouterFactoryPort { + constructor(private readonly mediasoupService: MediasoupService) {} + + async createRouter() { + return this.mediasoupService.createRouterOnPickedWorker(); + }; +}; diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts index 64bcf7401..fdda08f0c 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts @@ -1,10 +1,38 @@ import { Module } from "@nestjs/common"; import { SfuService } from "./sfu.service"; +import { MediasoupRouterFactory } from "./sfu.interface"; +import { CreateRouterUsecase } from "@app/sfu/commands/usecase"; +import { RoomCreateLockRepo, RoomRouterRepository } from "@infra/memory/sfu"; +import { RoomCreateLockPort, RoomRouterRepositoryPort, RouterFactoryPort } from "@app/sfu/ports"; @Module({ providers : [ SfuService, + + // 메모리 사용을 위한 의존성 주입 + MediasoupRouterFactory, + + // usecase들 + // router를 생성하기 위한 usecase + { + provide : CreateRouterUsecase, + useFactory : ( + roomRepo : RoomRouterRepositoryPort, + roomCreateLockRepo : RoomCreateLockPort, + routerFactory : RouterFactoryPort + ) => { + return new CreateRouterUsecase( + roomRepo, roomCreateLockRepo, routerFactory + ) + }, + inject : [ + RoomRouterRepository, + RoomCreateLockRepo, + MediasoupRouterFactory + ] + } + ], exports : [ SfuService, diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 69d1fbf46..bba7fb589 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,14 +1,14 @@ -import { MediasoupService } from "@infra/media/mediasoup/media"; import { Injectable, Logger } from "@nestjs/common"; -import { ConnectTransportType, RoomEntry, TransportEntry } from "./sfu.validate"; -import { SfuError, SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; -import { mediaSoupRouterConfig, listenIps } from "@infra/media/mediasoup/config"; +import { ConnectTransportType, TransportEntry } from "./sfu.validate"; +import { SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; +import { listenIps } from "@infra/media/mediasoup/config"; import { Router, Transport, WebRtcTransport } from "mediasoup/types"; import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; -import { CreateRoomTransportDto, CreateTransportDto } from "@app/sfu/commands/dto"; +import { CreateRoomTransportDto, CreateTransportDto, RoomEntry } from "@app/sfu/commands/dto"; import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; import { RoomTransportInfo } from "@app/sfu/queries/dto"; +import { CreateRouterUsecase } from "@app/sfu/commands/usecase"; @Injectable() @@ -19,80 +19,22 @@ export class SfuService { // wokre들과 router 정리 private readonly roomRouters = new Map(); // room_id : RoomEntry - private readonly createRoomRoutings = new Map>(); // room_id : Promise -> 생성하고 있는 중인 룸 // transport가 저장이 되야 한다. private readonly transports = new Map(); // transport_id : transport 저장 ( 나중에 transport를 ) constructor( - private readonly mediaSoupService : MediasoupService, + // usecase + private readonly createRouterUsecase : CreateRouterUsecase, + // private readonly insertTranportInfoToRedis : CreateSfuTransportInfoToRedis, private readonly deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis, private readonly selectSfuTransportInfoFromRedis : SelectSfuTransportDataFromRedis ) {} - private async createRoomRouting(room_id : string) : Promise { - try { - const { worker, workerIdx } = this.mediaSoupService.picWorker(); - - // 여기서 허용 가능한 router에 config를 부여할 수 있다. - const router = await worker.createRouter( - mediaSoupRouterConfig - ); - - // 기본 설정 - const roomEntry : RoomEntry = { - worker_idx : workerIdx, - room_id, - router, - worker_pid : worker.pid, - transport_ids : new Set(), - created_at : new Date() - }; - - // 메모리에 저장 - this.roomRouters.set(room_id, roomEntry); - - // router가 만약 내려간다면? ( 근데 worker가 내려가면 애초에 그 방은 내려가니까 worker에 죽음은 그 프로세스 전체에 삭제 ) - router.observer.on("close", () => { - const closedRouterEntry = this.roomRouters.get(room_id); - if ( closedRouterEntry?.router === router ) { - // 방이랑 worker_id 기록한거 삭제 - this.roomRouters.delete(room_id); - }; - }); - - // 여기에서 대표 producer transport도 나중에 등록 예정 - - return roomEntry; - } catch (err) { - this.logger.error(err); // error가 발생한다면 - throw new SfuError(err); - } finally { - this.createRoomRoutings.delete(room_id); - }; - }; - // 1. router 생성 관련 함수 -> 생성 혹은 얻는 이유는 방을 만들었다고 무조건 router를 부여하면 비어있는 방에 낭비가 심할 수 있기에 들어와야 활성화가 된다. async getOrCreateRoomRouter(room_id : string) : Promise { - - // 이미 생성된 router라면 - const roomRouterExist = this.roomRouters.get(room_id); - if ( roomRouterExist && !roomRouterExist.router.closed ) { - return roomRouterExist; - }; - - // 생성 중이라면 - const createRoomRoutingExist = this.createRoomRoutings.get(room_id); - if ( createRoomRoutingExist ) { - return createRoomRoutingExist; - }; - - // 아무것도 없다면 새롭게 생성할 수 있다. - const roomEntry = this.createRoomRouting(room_id); - this.createRoomRoutings.set(room_id, roomEntry); - - return roomEntry; + return this.createRouterUsecase.execute(room_id); }; // 방이 닫혔을때 로직도 구현해두자 diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts index be84af4b6..5b7fdfa1e 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts @@ -1,16 +1,6 @@ -import { DtlsParameters, IceCandidate, IceParameters, Router } from "mediasoup/types"; +import { DtlsParameters, IceCandidate, IceParameters } from "mediasoup/types"; -// room_id에 대해서 worker, router, room_id, created_at으로 메모리에서 worker에서 router를 어떻게 관리하는지 작성한다. -export type RoomEntry = { - room_id : string; - worker_idx : number; // 이 라우터를 운영하기 위해서 필요한 정보 - worker_pid : number; // worker를 디버깅 하기 위해서 필요한 정보 - router : Router; // 실질적으로 기능구현에 사용되는 라우터 - transport_ids : Set; // transport들이 저장된 라우터 - created_at : Date; -}; - // transport에 대해서 정보를 저장한다. export type TransportEntry = { transportId: string; From 3d8e98cdc02cb255c2da1750d53fcb2abe086122 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 08:03:22 +0900 Subject: [PATCH 21/24] =?UTF-8?q?=F0=9F=94=A8=20Fix:=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20repo=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/3-2.presentation/webrtc/sfu/sfu.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index bba7fb589..a266e14b0 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -9,6 +9,7 @@ import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; import { RoomTransportInfo } from "@app/sfu/queries/dto"; import { CreateRouterUsecase } from "@app/sfu/commands/usecase"; +import { RoomRouterRepository } from "@infra/memory/sfu"; @Injectable() @@ -17,16 +18,15 @@ export class SfuService { // sfu 서버가 관리하는 로직 ( 메모리 낭비가 있는데 어떻게 하면 좀 효율적으로 저장이 가능할까? ) private readonly logger = new Logger(SfuService.name); - // wokre들과 router 정리 - private readonly roomRouters = new Map(); // room_id : RoomEntry - // transport가 저장이 되야 한다. private readonly transports = new Map(); // transport_id : transport 저장 ( 나중에 transport를 ) constructor( // usecase private readonly createRouterUsecase : CreateRouterUsecase, - // + // infra + private readonly roomRouters : RoomRouterRepository, + private readonly insertTranportInfoToRedis : CreateSfuTransportInfoToRedis, private readonly deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis, private readonly selectSfuTransportInfoFromRedis : SelectSfuTransportDataFromRedis From 1ad3e08684750b8db794c4ad8ac9f29561f63dc2 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 08:37:51 +0900 Subject: [PATCH 22/24] =?UTF-8?q?=F0=9F=8C=88=20Update:=20present=20usecas?= =?UTF-8?q?e=20transport=20=EB=B6=80=EB=B6=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sfu/commands/dto/create-transport.dto.ts | 8 ++ .../usecase/create-transport.usecase.ts | 78 ++++++++++++ .../src/2.application/sfu/ports/index.ts | 4 +- .../sfu/ports/room-router-repo.port.ts | 1 + .../sfu/ports/transport-factory.port.ts | 7 ++ .../sfu/ports/transport-repo.port.ts | 8 ++ .../3-1.infra/cache/redis/sfu/sfu.outbound.ts | 4 +- .../src/3-1.infra/media/media.module.ts | 3 + .../media/mediasoup/sfu/sfu.outbound.ts | 40 ++++++ .../src/3-1.infra/memory/memory.module.ts | 7 +- .../src/3-1.infra/memory/sfu/index.ts | 3 +- .../3-1.infra/memory/sfu/room-router.repo.ts | 8 ++ .../3-1.infra/memory/sfu/transport-repo.ts | 12 ++ .../3-2.presentation/webrtc/sfu/sfu.module.ts | 33 ++++- .../webrtc/sfu/sfu.service.ts | 118 ++---------------- .../webrtc/sfu/sfu.validate.ts | 8 -- 16 files changed, 216 insertions(+), 126 deletions(-) create mode 100644 rep/main_backend/src/2.application/sfu/ports/transport-factory.port.ts create mode 100644 rep/main_backend/src/2.application/sfu/ports/transport-repo.port.ts create mode 100644 rep/main_backend/src/3-1.infra/media/mediasoup/sfu/sfu.outbound.ts create mode 100644 rep/main_backend/src/3-1.infra/memory/sfu/transport-repo.ts diff --git a/rep/main_backend/src/2.application/sfu/commands/dto/create-transport.dto.ts b/rep/main_backend/src/2.application/sfu/commands/dto/create-transport.dto.ts index f12aecebe..b3c715fb1 100644 --- a/rep/main_backend/src/2.application/sfu/commands/dto/create-transport.dto.ts +++ b/rep/main_backend/src/2.application/sfu/commands/dto/create-transport.dto.ts @@ -1,5 +1,13 @@ +import { DtlsParameters, IceCandidate, IceParameters } from "mediasoup/types"; // 나중에 domain으로 빼야 함 +export type TransportEntry = { + transportId: string; + iceParameters: IceParameters; + iceCandidates: Array; + dtlsParameters: DtlsParameters; +}; + export type CreateTransportDto = { room_id : string; socket_id : string; diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts index e69de29bb..178a238b8 100644 --- a/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts +++ b/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts @@ -0,0 +1,78 @@ +import { Injectable, Logger } from "@nestjs/common"; +import type { Router } from "mediasoup/types"; +import { SfuError, SfuErrorMessage } from "@error/application/sfu/sfu.error"; +import type { + RoomRouterRepositoryPort, + TransportRepositoryPort, + TransportFactoryPort, +} from "../../ports"; +import { CreateTransportDto, CreateRoomTransportDto, RoomEntry, TransportEntry } from "../dto"; +import { DeleteDataToCache, InsertDataToCache } from "@app/ports/cache/cache.outbound"; + + +@Injectable() +export class CreateTransportUsecase { + private readonly logger = new Logger(CreateTransportUsecase.name); + + constructor( + private readonly roomRepo: RoomRouterRepositoryPort, + private readonly transportRepo: TransportRepositoryPort, + private readonly transportFactory: TransportFactoryPort, + private readonly insertTranportInfoToRedis : InsertDataToCache, + private readonly deleteTransportInfoToRedis : DeleteDataToCache, + ) {} + + async execute(dto: CreateTransportDto): Promise { + + // 1. roomEntry 찾기 + const roomEntry: RoomEntry | undefined = this.roomRepo.get(dto.room_id); + if (!roomEntry || roomEntry.router.closed) { + throw new SfuErrorMessage("room_id를 다시 확인해주세요"); + } + const router: Router = roomEntry.router; + + // transport 설정 + let transport: any; // WebRtcTransport + try { + + // 2. transport 생성 + transport = await this.transportFactory.createWebRtcTransport(router); + + // transport 로깅 및 transport_id 설정 + this.transportFactory.attachDebugHooks(dto.room_id, transport); + const transportId = transport.id; + + // 3. 메모리 저장 + this.transportRepo.set(transportId, transport); + + // 4. router 갱신 + this.roomRepo.patch(dto.room_id, (e) => e.transport_ids.add(transportId)); + + // 5. cache에 transport info 정보 저장 + const validate: CreateRoomTransportDto = { ...dto, transport_id: transportId }; + await this.insertTranportInfoToRedis.insert(validate); + + // 6. transport 없어졌을때 이벤트 생성 + transport.observer.on("close", () => { + this.transportRepo.delete(transportId); + + // router 갱신 + this.roomRepo.patch(dto.room_id, (e) => e.transport_ids.delete(transportId)); + + // cache에 삭제 - transport_id를 전달하고 namespace를 잘 정리해야 한다. + this.deleteTransportInfoToRedis.deleteNamespace(transportId); + }); + + return { + transportId, + iceParameters: transport.iceParameters, + iceCandidates: transport.iceCandidates, + dtlsParameters: transport.dtlsParameters, + }; + } catch (err) { + this.logger.error(err); + if (transport && !transport.closed) transport.close(); + throw err; + } + } +} diff --git a/rep/main_backend/src/2.application/sfu/ports/index.ts b/rep/main_backend/src/2.application/sfu/ports/index.ts index 3d3dcc4ce..64eea5c73 100644 --- a/rep/main_backend/src/2.application/sfu/ports/index.ts +++ b/rep/main_backend/src/2.application/sfu/ports/index.ts @@ -1,3 +1,5 @@ export * from "./room-router-repo.port"; export * from "./room-create-lock-repo.port"; -export * from "./router-factory.port"; \ No newline at end of file +export * from "./router-factory.port"; +export * from "./transport-repo.port"; +export * from "./transport-factory.port"; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts b/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts index 7fdcfff2a..34df819d6 100644 --- a/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts +++ b/rep/main_backend/src/2.application/sfu/ports/room-router-repo.port.ts @@ -5,4 +5,5 @@ export interface RoomRouterRepositoryPort { get(roomId: string): RoomEntry | undefined; set(roomId: string, entry: RoomEntry): void; delete(roomId: string): void; + patch(roomId: string, patch: (entry: RoomEntry) => void): void; } diff --git a/rep/main_backend/src/2.application/sfu/ports/transport-factory.port.ts b/rep/main_backend/src/2.application/sfu/ports/transport-factory.port.ts new file mode 100644 index 000000000..baab9c8ea --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/ports/transport-factory.port.ts @@ -0,0 +1,7 @@ +import type { Router, WebRtcTransport } from "mediasoup/types"; + + +export interface TransportFactoryPort { + createWebRtcTransport(router: Router): Promise; + attachDebugHooks(roomId: string, transport: WebRtcTransport): void; +} \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/ports/transport-repo.port.ts b/rep/main_backend/src/2.application/sfu/ports/transport-repo.port.ts new file mode 100644 index 000000000..42398e562 --- /dev/null +++ b/rep/main_backend/src/2.application/sfu/ports/transport-repo.port.ts @@ -0,0 +1,8 @@ +import type { WebRtcTransport } from "mediasoup/types"; // 나중에 domain으로 빼야하 + + +export interface TransportRepositoryPort { + get(id: string): WebRtcTransport | undefined; + set(id: string, transport: WebRtcTransport): void; + delete(id: string): void; +} diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts index d64defc99..ebb6a79fd 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.outbound.ts @@ -38,9 +38,9 @@ export class DeleteSfuTransportInfoToRedis extends DeleteDataToCache 잘 봐주어야 함 async deleteNamespace(namespace: string): Promise { // 문자 검증? 이것도 필요해 보인다. - + const transportNamespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${namespace}`; - await this.cache.del(namespace); + await this.cache.del(transportNamespace); return true; } diff --git a/rep/main_backend/src/3-1.infra/media/media.module.ts b/rep/main_backend/src/3-1.infra/media/media.module.ts index 7ac1cb6b5..a7496999c 100644 --- a/rep/main_backend/src/3-1.infra/media/media.module.ts +++ b/rep/main_backend/src/3-1.infra/media/media.module.ts @@ -1,14 +1,17 @@ import { Global, Module } from "@nestjs/common"; import { MediasoupService } from "./mediasoup/media"; +import { MediasoupTransportFactory } from "./mediasoup/sfu/sfu.outbound"; @Global() @Module({ providers : [ MediasoupService, + MediasoupTransportFactory ], exports : [ MediasoupService, + MediasoupTransportFactory ] }) export class MediaModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/media/mediasoup/sfu/sfu.outbound.ts b/rep/main_backend/src/3-1.infra/media/mediasoup/sfu/sfu.outbound.ts new file mode 100644 index 000000000..4b3ea28d9 --- /dev/null +++ b/rep/main_backend/src/3-1.infra/media/mediasoup/sfu/sfu.outbound.ts @@ -0,0 +1,40 @@ +import { Injectable, Logger } from "@nestjs/common"; +import type { Router, WebRtcTransport } from "mediasoup/types"; +import { listenIps } from "@infra/media/mediasoup/config"; +import type { TransportFactoryPort } from "@app/sfu/ports"; + + +@Injectable() +export class MediasoupTransportFactory implements TransportFactoryPort { + private readonly logger = new Logger(MediasoupTransportFactory.name); + + async createWebRtcTransport(router: Router): Promise { + + // ip 설정을 해두고 + const transport = await router.createWebRtcTransport({ + listenIps, + enableUdp: true, // udp 허용 + enableTcp: true, // tcp 허용 + preferUdp: true, // udp가 최우선 + initialAvailableOutgoingBitrate: 1_000_000, // 첫 바이트는 이정도 + }); + + // 최대 전송율 정하기 + try { + await transport.setMaxIncomingBitrate(1_500_000); + } catch (e) { + this.logger.warn(e); // 에러 메시지만 이유는 화면공유, 음성, 비디오등 다 다른데 하나로 고정할수는 없음 ( 상황에 따라서 나중에 개선해야 함 ) + } + + return transport; + } + + attachDebugHooks(roomId: string, transport: WebRtcTransport) { + transport.on("icestatechange", (state) => { + this.logger.debug({ roomId, transportId: transport.id, state }, "ICE state changed"); + }); + transport.on("dtlsstatechange", (state) => { + this.logger.debug({ roomId, transportId: transport.id, state }, "DTLS state changed"); + }); + } +} diff --git a/rep/main_backend/src/3-1.infra/memory/memory.module.ts b/rep/main_backend/src/3-1.infra/memory/memory.module.ts index cc984e2c3..5f2174498 100644 --- a/rep/main_backend/src/3-1.infra/memory/memory.module.ts +++ b/rep/main_backend/src/3-1.infra/memory/memory.module.ts @@ -1,16 +1,19 @@ import { Global, Module } from "@nestjs/common"; import { RoomCreateLockRepo, RoomRouterRepository } from "./sfu"; +import { TransportRepository } from "./sfu/transport-repo"; @Global() @Module({ providers : [ RoomRouterRepository, - RoomCreateLockRepo + RoomCreateLockRepo, + TransportRepository ], exports : [ RoomRouterRepository, - RoomCreateLockRepo + RoomCreateLockRepo, + TransportRepository ] }) export class MemoryModule {}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/index.ts b/rep/main_backend/src/3-1.infra/memory/sfu/index.ts index 10c77eb30..af4a714d1 100644 --- a/rep/main_backend/src/3-1.infra/memory/sfu/index.ts +++ b/rep/main_backend/src/3-1.infra/memory/sfu/index.ts @@ -1,2 +1,3 @@ export * from "./room-create-lock.repo"; -export * from "./room-router.repo" \ No newline at end of file +export * from "./room-router.repo" +export * from "./transport-repo"; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts b/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts index 533e4bd3d..b78638436 100644 --- a/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts +++ b/rep/main_backend/src/3-1.infra/memory/sfu/room-router.repo.ts @@ -10,4 +10,12 @@ export class RoomRouterRepository implements RoomRouterRepositoryPort { get(roomId: string) { return this.roomRouters.get(roomId); } set(roomId: string, entry: RoomEntry) { this.roomRouters.set(roomId, entry); } delete(roomId: string) { this.roomRouters.delete(roomId); } + + patch(room_id : string, patchEntry : (entry : RoomEntry) => void) : void { + const entry = this.roomRouters.get(room_id); + if ( !entry ) return; + patchEntry(entry); + + this.roomRouters.set(room_id, entry); +}; } diff --git a/rep/main_backend/src/3-1.infra/memory/sfu/transport-repo.ts b/rep/main_backend/src/3-1.infra/memory/sfu/transport-repo.ts new file mode 100644 index 000000000..3762591d9 --- /dev/null +++ b/rep/main_backend/src/3-1.infra/memory/sfu/transport-repo.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import type { WebRtcTransport } from "mediasoup/types"; +import type { TransportRepositoryPort } from "@app/sfu/ports"; + + +@Injectable() +export class TransportRepository implements TransportRepositoryPort { + private readonly transports = new Map(); + get(id: string) { return this.transports.get(id); } + set(id: string, t: WebRtcTransport) { this.transports.set(id, t); } + delete(id: string) { this.transports.delete(id); } +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts index fdda08f0c..4255f9b9e 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts @@ -1,9 +1,11 @@ import { Module } from "@nestjs/common"; import { SfuService } from "./sfu.service"; import { MediasoupRouterFactory } from "./sfu.interface"; -import { CreateRouterUsecase } from "@app/sfu/commands/usecase"; -import { RoomCreateLockRepo, RoomRouterRepository } from "@infra/memory/sfu"; -import { RoomCreateLockPort, RoomRouterRepositoryPort, RouterFactoryPort } from "@app/sfu/ports"; +import { CreateRouterUsecase, CreateTransportUsecase } from "@app/sfu/commands/usecase"; +import { RoomCreateLockRepo, RoomRouterRepository, TransportRepository } from "@infra/memory/sfu"; +import { RoomCreateLockPort, RoomRouterRepositoryPort, RouterFactoryPort, TransportFactoryPort, TransportRepositoryPort } from "@app/sfu/ports"; +import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; +import { MediasoupTransportFactory } from "@infra/media/mediasoup/sfu/sfu.outbound"; @Module({ @@ -31,6 +33,31 @@ import { RoomCreateLockPort, RoomRouterRepositoryPort, RouterFactoryPort } from RoomCreateLockRepo, MediasoupRouterFactory ] + }, + { + provide : CreateTransportUsecase, + useFactory : ( + roomRepo : RoomRouterRepositoryPort, + transportRepo: TransportRepositoryPort, + transportFactory: TransportFactoryPort, + insertTranportInfoToRedis : CreateSfuTransportInfoToRedis, + deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis, + ) => { + return new CreateTransportUsecase( + roomRepo, + transportRepo, + transportFactory, + insertTranportInfoToRedis, + deleteTransportInfoToRedis + ) + }, + inject : [ + RoomRouterRepository, + TransportRepository, + MediasoupTransportFactory, + CreateSfuTransportInfoToRedis, + DeleteSfuTransportInfoToRedis, + ] } ], diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index a266e14b0..82e2a00f3 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,34 +1,25 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { ConnectTransportType, TransportEntry } from "./sfu.validate"; +import { Injectable } from "@nestjs/common"; +import { ConnectTransportType } from "./sfu.validate"; import { SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; -import { listenIps } from "@infra/media/mediasoup/config"; -import { Router, Transport, WebRtcTransport } from "mediasoup/types"; -import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; -import { CreateRoomTransportDto, CreateTransportDto, RoomEntry } from "@app/sfu/commands/dto"; +import { Transport } from "mediasoup/types"; +import { CreateTransportDto, RoomEntry, TransportEntry } from "@app/sfu/commands/dto"; import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; import { RoomTransportInfo } from "@app/sfu/queries/dto"; -import { CreateRouterUsecase } from "@app/sfu/commands/usecase"; -import { RoomRouterRepository } from "@infra/memory/sfu"; +import { CreateRouterUsecase, CreateTransportUsecase } from "@app/sfu/commands/usecase"; +import { RoomRouterRepository, TransportRepository } from "@infra/memory/sfu"; @Injectable() export class SfuService { - - // sfu 서버가 관리하는 로직 ( 메모리 낭비가 있는데 어떻게 하면 좀 효율적으로 저장이 가능할까? ) - private readonly logger = new Logger(SfuService.name); - // transport가 저장이 되야 한다. - private readonly transports = new Map(); // transport_id : transport 저장 ( 나중에 transport를 ) - constructor( // usecase private readonly createRouterUsecase : CreateRouterUsecase, + private readonly createTransportUsecase : CreateTransportUsecase, // infra private readonly roomRouters : RoomRouterRepository, - - private readonly insertTranportInfoToRedis : CreateSfuTransportInfoToRedis, - private readonly deleteTransportInfoToRedis : DeleteSfuTransportInfoToRedis, + private readonly transports : TransportRepository, private readonly selectSfuTransportInfoFromRedis : SelectSfuTransportDataFromRedis ) {} @@ -58,101 +49,10 @@ export class SfuService { }; }; - // room의 정보를 최신으로 바꾸는 로직이다. - private patchRoomEntry(room_id : string, patch : (entry : RoomEntry) => void) : void { - const entry = this.roomRouters.get(room_id); - if ( !entry ) return; - patch(entry); - - this.roomRouters.set(room_id, entry); - }; - - private transPortLogging(room_id : string, transport : WebRtcTransport) : void { - // transport에 대해서 내부 디버깅을 위해서 추가 - - // ice 상태에 대해서 로깅 - transport.on("icestatechange", (state) => { - this.logger.debug({room_id, transportId : transport.id, state}, "transport에 ice부분에 상태가 변했습니다."); - }); - - // dtls 핸드세이킹 과정중 상태변화 로깅 - transport.on("dtlsstatechange", (state) => { - this.logger.debug({room_id, transportId : transport.id, state}, "transport에 dtls부분에 상태가 변했습니다."); - }); - }; // 2. transport 부분 생성 ( 나중에 전체적인 부분 usecase로 빼자고 ) async createTransPort(dto : CreateTransportDto) : Promise { - - const roomEntry = this.roomRouters.get(dto.room_id); - if ( !roomEntry ) throw new SfuErrorMessage("room_id를 다시 확인해주세요"); - const router : Router = roomEntry.router; - - let transport : WebRtcTransport | undefined; - - try { - // ip 설정을 해두고 - transport = await router.createWebRtcTransport({ - listenIps, - enableUdp : true, // udp 허용 - enableTcp : true, // tcp 허용 - preferUdp : true, // udp가 최우선 - initialAvailableOutgoingBitrate: 1_000_000, // 첫 바이트는 이정도 - }); - - // transport 로깅 - this.transPortLogging(dto.room_id, transport); - - // 최대 전송율 정하기 - try { - await transport.setMaxIncomingBitrate(1_500_000); // 최대 - } catch (err) { - this.logger.warn(err); // 에러 메시지만 이유는 화면공유, 음성, 비디오등 다 다른데 하나로 고정할수는 없음 ( 상황에 따라서 나중에 개선해야 함 ) - } - - // transport id를 가져온다. - const transportId : string = transport.id; - - // 메모리에 저장 - this.transports.set(transportId, transport); - - // router 갱신 - this.patchRoomEntry(dto.room_id, (entry) => { - entry.transport_ids.add(transportId); - }); - - // cache에 정보 저장 - const validate : CreateRoomTransportDto = { - ...dto, - transport_id : transportId - } - await this.insertTranportInfoToRedis.insert(validate); - - // transport 없어졌을때 이벤트 생성 - transport.observer.on("close", () => { - this.transports.delete(transportId); - - // router 갱신 - this.patchRoomEntry(dto.room_id, (entry) => { - entry.transport_ids.delete(transportId); - }); - - // cache에 삭제 - const namespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${transportId}`; - this.deleteTransportInfoToRedis.deleteNamespace(namespace); - }); - - return { - transportId: transportId, - iceParameters: transport.iceParameters, // ice에 파라미터 정보 - iceCandidates: transport.iceCandidates, // ice 후보들 전달 - dtlsParameters: transport.dtlsParameters // dtls 핸드세이크를 위한 파라미터들 - }; - } catch (err) { - if ( transport && !transport.closed ) transport.close(); - throw err; - } - + return this.createTransportUsecase.execute(dto); }; // 3. transport connect 연결 ( 이때 부터는 이제 sfu와 webrtc 통신이 가능해졌다고 생각하면 된다. ) diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts index 5b7fdfa1e..8365cab61 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts @@ -1,14 +1,6 @@ import { DtlsParameters, IceCandidate, IceParameters } from "mediasoup/types"; -// transport에 대해서 정보를 저장한다. -export type TransportEntry = { - transportId: string; - iceParameters: IceParameters; - iceCandidates: Array; - dtlsParameters: DtlsParameters; -}; - export type ConnectTransportType = { room_id : string; socket_id : string; From b347e74fc5ac5cf548a8f1eedf7ef5b1548abd56 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 08:51:09 +0900 Subject: [PATCH 23/24] =?UTF-8?q?=F0=9F=8C=88=20Update:=20connect=20transp?= =?UTF-8?q?ort=20usecase,=20present=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/create-transport.usecase.ts | 2 +- .../sfu/queries/dto/connect-transport.dto.ts | 11 ++++++- .../usecase/connect-transport.usecase.ts | 30 +++++++++++++++++++ .../3-1.infra/cache/redis/sfu/sfu.inbound.ts | 8 +++-- .../3-2.presentation/webrtc/sfu/sfu.module.ts | 21 +++++++++++++ .../webrtc/sfu/sfu.service.ts | 23 ++------------ 6 files changed, 70 insertions(+), 25 deletions(-) diff --git a/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts b/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts index 178a238b8..d624e5b89 100644 --- a/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts +++ b/rep/main_backend/src/2.application/sfu/commands/usecase/create-transport.usecase.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from "@nestjs/common"; import type { Router } from "mediasoup/types"; -import { SfuError, SfuErrorMessage } from "@error/application/sfu/sfu.error"; +import { SfuErrorMessage } from "@error/application/sfu/sfu.error"; import type { RoomRouterRepositoryPort, TransportRepositoryPort, diff --git a/rep/main_backend/src/2.application/sfu/queries/dto/connect-transport.dto.ts b/rep/main_backend/src/2.application/sfu/queries/dto/connect-transport.dto.ts index 015b61c42..b0204a4f0 100644 --- a/rep/main_backend/src/2.application/sfu/queries/dto/connect-transport.dto.ts +++ b/rep/main_backend/src/2.application/sfu/queries/dto/connect-transport.dto.ts @@ -1,4 +1,4 @@ - +import { DtlsParameters } from "mediasoup/types"; // 마찬가지 domain으로 빼야함 export type RoomTransportInfo = { @@ -6,4 +6,13 @@ export type RoomTransportInfo = { socket_id : string; user_id : string; type : string; +}; + +export type ConnectTransportType = { + room_id : string; + socket_id : string; + user_id : string; + type : "send" | "recv"; + transport_id : string; + dtlsParameters: DtlsParameters }; \ No newline at end of file diff --git a/rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts b/rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts index e69de29bb..062b48452 100644 --- a/rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts +++ b/rep/main_backend/src/2.application/sfu/queries/usecase/connect-transport.usecase.ts @@ -0,0 +1,30 @@ +import { Injectable } from "@nestjs/common"; +import type { TransportRepositoryPort } from "../../ports"; +import { SelectDataFromCache } from "@app/ports/cache/cache.inbound"; +import { ConnectTransportType, RoomTransportInfo } from "../dto"; +import { SfuErrorMessage } from "@error/application/sfu/sfu.error"; + + +@Injectable() +export class ConnectTransportUsecase { + + constructor( + private readonly transportRepo: TransportRepositoryPort, + private readonly selectSfuTransportInfoFromRedis : SelectDataFromCache + ) {} + + async execute( dto : ConnectTransportType ) { + + // 1. transport에 정합성에 대해서 검증 + const transportInfo : RoomTransportInfo | undefined = await this.selectSfuTransportInfoFromRedis.select({ namespace : dto.transport_id, keyName : "" }); + if ( !transportInfo ) throw new SfuErrorMessage("transport_id를 다시 확인해주세요. - 데이터 찾는데 문제 발생"); + + if ( dto.room_id !== transportInfo.room_id || dto.socket_id !== transportInfo.socket_id || dto.type !== transportInfo.type || dto.user_id !== transportInfo.user_id ) throw new SfuErrorMessage("잘못된 transport_id에 연결하고자 합니다 다시 확인해주시길 발반디ㅏ."); + + // 2. transport 가져온 후 연결 + const transport = this.transportRepo.get(dto.transport_id); + if ( !transport ) throw new SfuErrorMessage("transport_id를 다시 확인해주세요."); + + await transport.connect({ dtlsParameters : dto.dtlsParameters }); + }; +}; \ No newline at end of file diff --git a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts index 4e48e6e0e..8d84204a6 100644 --- a/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts +++ b/rep/main_backend/src/3-1.infra/cache/redis/sfu/sfu.inbound.ts @@ -1,7 +1,7 @@ import { SelectDataFromCache } from "@app/ports/cache/cache.inbound"; import { Inject, Injectable } from "@nestjs/common"; import { type RedisClientType } from "redis"; -import { CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; +import { CACHE_SFU_NAMESPACE_NAME, CACHE_SFU_TRANSPORTS_KEY_NAME, REDIS_SERVER } from "../../cache.constants"; import { RoomTransportInfo } from "@app/sfu/queries/dto"; @@ -14,8 +14,10 @@ export class SelectSfuTransportDataFromRedis extends SelectDataFromCache { - - const data = await this.cache.hGetAll(namespace); + + const transportNamespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${namespace}`; + + const data = await this.cache.hGetAll(transportNamespace); // 값이 하나라도 없다면 문제가 있는 것이다. if ( !data || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.SOCKET_ID] || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.USER_ID] || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.ROOM_ID] || !data[CACHE_SFU_TRANSPORTS_KEY_NAME.TYPE] ) return undefined; diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts index 4255f9b9e..18b11b48c 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.module.ts @@ -6,6 +6,8 @@ import { RoomCreateLockRepo, RoomRouterRepository, TransportRepository } from "@ import { RoomCreateLockPort, RoomRouterRepositoryPort, RouterFactoryPort, TransportFactoryPort, TransportRepositoryPort } from "@app/sfu/ports"; import { CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis } from "@infra/cache/redis/sfu/sfu.outbound"; import { MediasoupTransportFactory } from "@infra/media/mediasoup/sfu/sfu.outbound"; +import { ConnectTransportUsecase } from "@app/sfu/queries/usecase"; +import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; @Module({ @@ -34,6 +36,8 @@ import { MediasoupTransportFactory } from "@infra/media/mediasoup/sfu/sfu.outbou MediasoupRouterFactory ] }, + + // transport 생성 usecase { provide : CreateTransportUsecase, useFactory : ( @@ -58,6 +62,23 @@ import { MediasoupTransportFactory } from "@infra/media/mediasoup/sfu/sfu.outbou CreateSfuTransportInfoToRedis, DeleteSfuTransportInfoToRedis, ] + }, + + // transport 연결 usecase + { + provide : ConnectTransportUsecase, + useFactory : ( + transportRepo: TransportRepositoryPort, + selectSfuTransportInfoFromRedis : SelectSfuTransportDataFromRedis, + ) => { + return new ConnectTransportUsecase( + transportRepo, selectSfuTransportInfoFromRedis + ) + }, + inject : [ + TransportRepository, + SelectSfuTransportDataFromRedis + ] } ], diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 82e2a00f3..824d60294 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,13 +1,9 @@ import { Injectable } from "@nestjs/common"; import { ConnectTransportType } from "./sfu.validate"; -import { SfuErrorMessage } from "@error/presentation/sfu/sfu.error"; -import { Transport } from "mediasoup/types"; import { CreateTransportDto, RoomEntry, TransportEntry } from "@app/sfu/commands/dto"; -import { CACHE_SFU_NAMESPACE_NAME } from "@infra/cache/cache.constants"; -import { SelectSfuTransportDataFromRedis } from "@infra/cache/redis/sfu/sfu.inbound"; -import { RoomTransportInfo } from "@app/sfu/queries/dto"; import { CreateRouterUsecase, CreateTransportUsecase } from "@app/sfu/commands/usecase"; import { RoomRouterRepository, TransportRepository } from "@infra/memory/sfu"; +import { ConnectTransportUsecase } from "@app/sfu/queries/usecase"; @Injectable() @@ -17,10 +13,10 @@ export class SfuService { // usecase private readonly createRouterUsecase : CreateRouterUsecase, private readonly createTransportUsecase : CreateTransportUsecase, + private readonly connectTransportUsecase : ConnectTransportUsecase, // infra private readonly roomRouters : RoomRouterRepository, private readonly transports : TransportRepository, - private readonly selectSfuTransportInfoFromRedis : SelectSfuTransportDataFromRedis ) {} // 1. router 생성 관련 함수 -> 생성 혹은 얻는 이유는 방을 만들었다고 무조건 router를 부여하면 비어있는 방에 낭비가 심할 수 있기에 들어와야 활성화가 된다. @@ -49,7 +45,6 @@ export class SfuService { }; }; - // 2. transport 부분 생성 ( 나중에 전체적인 부분 usecase로 빼자고 ) async createTransPort(dto : CreateTransportDto) : Promise { return this.createTransportUsecase.execute(dto); @@ -57,19 +52,7 @@ export class SfuService { // 3. transport connect 연결 ( 이때 부터는 이제 sfu와 webrtc 통신이 가능해졌다고 생각하면 된다. ) async connectTransport(dto : ConnectTransportType) : Promise { - // 검증하기 - const namespace : string = `${CACHE_SFU_NAMESPACE_NAME.TRANSPORT_INFO}:${dto.transport_id}`; - const transportInfo : RoomTransportInfo | undefined = await this.selectSfuTransportInfoFromRedis.select({ namespace, keyName : "" }); - if ( !transportInfo ) throw new SfuErrorMessage("transport_id를 다시 확인해주세요. - 데이터 찾는데 문제 발생"); - - if ( dto.room_id !== transportInfo.room_id || dto.socket_id !== transportInfo.socket_id || dto.type !== transportInfo.type || dto.user_id !== transportInfo.user_id ) throw new SfuErrorMessage("잘못된 transport_id에 연결하고자 합니다 다시 확인해주시길 발반디ㅏ."); - - // transport 가져오기 - const transport : Transport | undefined = this.transports.get(dto.transport_id); - - if ( !transport ) throw new SfuErrorMessage("transport_id를 다시 확인해주세요."); - - await transport.connect({ dtlsParameters : dto.dtlsParameters }); + await this.connectTransportUsecase.execute(dto); }; }; \ No newline at end of file From 56953f5fd91384ed66772e2019da68585bb5d5d5 Mon Sep 17 00:00:00 2001 From: KimDwDev Date: Fri, 9 Jan 2026 11:50:55 +0900 Subject: [PATCH 24/24] =?UTF-8?q?=E2=9C=85=20Test:=20webrtc=20=EB=94=94?= =?UTF-8?q?=EB=B2=84=EA=B9=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rep/main_backend/package.json | 8 +- rep/main_backend/pnpm-lock.yaml | 1100 +++++++++-------- .../webrtc/sfu/sfu.service.ts | 2 +- .../webrtc/sfu/sfu.validate.ts | 11 - .../websocket/signaling/signaling.service.ts | 9 +- 5 files changed, 584 insertions(+), 546 deletions(-) delete mode 100644 rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts diff --git a/rep/main_backend/package.json b/rep/main_backend/package.json index 5673348cd..bb44ed44d 100644 --- a/rep/main_backend/package.json +++ b/rep/main_backend/package.json @@ -23,13 +23,13 @@ "@aws-sdk/s3-request-presigner": "^3.957.0", "@nestjs/apollo": "^13.2.3", "@nestjs/axios": "^4.0.1", - "@nestjs/common": "^10.0.0", + "@nestjs/common": "^10", "@nestjs/config": "^4.0.2", - "@nestjs/core": "^10.0.0", + "@nestjs/core": "^10", "@nestjs/graphql": "^13.2.3", "@nestjs/platform-express": "^10.0.0", - "@nestjs/platform-socket.io": "^11.1.11", - "@nestjs/websockets": "^11.1.11", + "@nestjs/platform-socket.io": "^10.4.21", + "@nestjs/websockets": "^10.4.21", "@socket.io/redis-adapter": "^8.3.0", "argon2": "^0.44.0", "axios": "^1.13.2", diff --git a/rep/main_backend/pnpm-lock.yaml b/rep/main_backend/pnpm-lock.yaml index f688ce7cb..f32354e70 100644 --- a/rep/main_backend/pnpm-lock.yaml +++ b/rep/main_backend/pnpm-lock.yaml @@ -13,40 +13,40 @@ importers: version: 5.2.0(graphql@16.12.0) '@as-integrations/express5': specifier: ^1.1.2 - version: 1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.21.2) + version: 1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.22.1) '@aws-sdk/client-s3': specifier: ^3.957.0 - version: 3.957.0 + version: 3.965.0 '@aws-sdk/s3-request-presigner': specifier: ^3.957.0 - version: 3.957.0 + version: 3.965.0 '@nestjs/apollo': specifier: ^13.2.3 - version: 13.2.3(@apollo/server@5.2.0(graphql@16.12.0))(@as-integrations/express5@1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.21.2))(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/graphql@13.2.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14))(graphql@16.12.0) + version: 13.2.3(@apollo/server@5.2.0(graphql@16.12.0))(@as-integrations/express5@1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.22.1))(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/graphql@13.2.3(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14))(graphql@16.12.0) '@nestjs/axios': specifier: ^4.0.1 - version: 4.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) + version: 4.0.1(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) '@nestjs/common': - specifier: ^10.0.0 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + specifier: ^10 + version: 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.0.0 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + specifier: ^10 + version: 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/graphql': specifier: ^13.2.3 - version: 13.2.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14) + version: 13.2.3(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14) '@nestjs/platform-express': specifier: ^10.0.0 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20) + version: 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21) '@nestjs/platform-socket.io': - specifier: ^11.1.11 - version: 11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2) + specifier: ^10.4.21 + version: 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@10.4.21)(rxjs@7.8.2) '@nestjs/websockets': - specifier: ^11.1.11 - version: 11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + specifier: ^10.4.21 + version: 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/platform-socket.io@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.6) @@ -110,7 +110,7 @@ importers: version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20) + version: 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/platform-express@10.4.21) '@types/cookie-parser': specifier: ^1.4.10 version: 1.4.10(@types/express@4.17.25) @@ -128,10 +128,10 @@ importers: version: 2.0.16 '@typescript-eslint/eslint-plugin': specifier: ^8.50.0 - version: 8.50.0(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + version: 8.52.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^8.50.0 - version: 8.50.0(eslint@8.57.1)(typescript@5.9.3) + version: 8.52.0(eslint@8.57.1)(typescript@5.9.3) eslint: specifier: ^8.42.0 version: 8.57.1 @@ -143,7 +143,7 @@ importers: version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) + version: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) eslint-plugin-prettier: specifier: ^5.0.0 version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.7.4) @@ -318,139 +318,139 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.957.0': - resolution: {integrity: sha512-UvKQ9CpqzJxMjSjFpBaeWFAnR2sQChQH2jJEhsCFkAvrz1KDrU7sjOqUAPUH+ytsP5xghx1AZhfK2rVE/+JS0Q==} + '@aws-sdk/client-s3@3.965.0': + resolution: {integrity: sha512-BTeaaU1iK0BfatTCrtYjNkIHCoZH256qOI18l9bK4z6mVOgpHkYN4RvOu+NnKgyX58n+HWfOuhtKUD4OE33Vdw==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.957.0': - resolution: {integrity: sha512-iRdRjd+IpOogqRPt8iNRcg30J53z4rRfMviGwpKgsEa/fx3inCUPOuca3Ap7ZDES0atnEg3KGSJ3V/NQiEJ4BA==} + '@aws-sdk/client-sso@3.965.0': + resolution: {integrity: sha512-iv2tr+n4aZ+nPUFFvG00hISPuEd4DU+1/Q8rPAYKXsM+vEPJ2nAnP5duUOa2fbOLIUCRxX3dcQaQaghVHDHzQw==} engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.957.0': - resolution: {integrity: sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==} + '@aws-sdk/core@3.965.0': + resolution: {integrity: sha512-aq9BhQxdHit8UUJ9C0im9TtuKeK0pT6NXmNJxMTCFeStI7GG7ImIsSislg3BZTIifVg1P6VLdzMyz9de85iutQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/crc64-nvme@3.957.0': - resolution: {integrity: sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA==} + '@aws-sdk/crc64-nvme@3.965.0': + resolution: {integrity: sha512-9FbIyJ/Zz1AdEIrb0+Pn7wRi+F/0Y566ooepg0hDyHUzRV3ZXKjOlu3wJH3YwTz2UkdwQmldfUos2yDJps7RyA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-env@3.957.0': - resolution: {integrity: sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==} + '@aws-sdk/credential-provider-env@3.965.0': + resolution: {integrity: sha512-mdGnaIjMxTIjsb70dEj3VsWPWpoq1V5MWzBSfJq2H8zgMBXjn6d5/qHP8HMf53l9PrsgqzMpXGv3Av549A2x1g==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.957.0': - resolution: {integrity: sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==} + '@aws-sdk/credential-provider-http@3.965.0': + resolution: {integrity: sha512-YuGQel9EgA/z25oeLM+GYYQS750+8AESvr7ZEmVnRPL0sg+K3DmGqdv+9gFjFd0UkLjTlC/jtbP2cuY6UcPiHQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.957.0': - resolution: {integrity: sha512-YuoZmIeE91YIeUfihh8SiSu546KtTvU+4rG5SaL30U9+nGq6P11GRRgqF0ANUyRseLC9ONHt+utar4gbO3++og==} + '@aws-sdk/credential-provider-ini@3.965.0': + resolution: {integrity: sha512-xRo72Prer5s0xYVSCxCymVIRSqrVlevK5cmU0GWq9yJtaBNpnx02jwdJg80t/Ni7pgbkQyFWRMcq38c1tc6M/w==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-login@3.957.0': - resolution: {integrity: sha512-XcD5NEQDWYk8B4gs89bkwf2d+DNF8oS2NR5RoHJEbX4l8KErVATUjpEYVn6/rAFEktungxlYTnQ5wh0cIQvP5w==} + '@aws-sdk/credential-provider-login@3.965.0': + resolution: {integrity: sha512-43/H8Qku8LHyugbhLo8kjD+eauhybCeVkmrnvWl8bXNHJP7xi1jCdtBQJKKJqiIHZws4MOEwkji8kFdAVRCe6g==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.957.0': - resolution: {integrity: sha512-b9FT/7BQcJ001w+3JbTiJXfxHrWvPb7zDvvC1i1FKcNOvyCt3BGu04n4nO/b71a3iBnbfBXI89hCIZQsuLcEgw==} + '@aws-sdk/credential-provider-node@3.965.0': + resolution: {integrity: sha512-cRxmMHF+Zh2lkkkEVduKl+8OQdtg/DhYA69+/7SPSQURlgyjFQGlRQ58B7q8abuNlrGT3sV+UzeOylZpJbV61Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.957.0': - resolution: {integrity: sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==} + '@aws-sdk/credential-provider-process@3.965.0': + resolution: {integrity: sha512-gmkPmdiR0yxnTzLPDb7rwrDhGuCUjtgnj8qWP+m0gSz/W43rR4jRPVEf6DUX2iC+ImQhxo3NFhuB3V42Kzo3TQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.957.0': - resolution: {integrity: sha512-gTLPJFOkGtn3tVGglRhCar2oOobK1YctZRAT8nfJr17uaSRoAP46zIIHNYBZZUMqImb0qAHD9Ugm+Zd9sIqxyA==} + '@aws-sdk/credential-provider-sso@3.965.0': + resolution: {integrity: sha512-N01AYvtCqG3Wo/s/LvYt19ity18/FqggiXT+elAs3X9Om/Wfx+hw9G+i7jaDmy+/xewmv8AdQ2SK5Q30dXw/Fw==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.957.0': - resolution: {integrity: sha512-x17xMeD7c+rKEsWachGIMifACqkugskrETWz18QDWismFcrmUuOcZu5rUa8s9y1pnITLKUQ1xU/qDLPH52jLlA==} + '@aws-sdk/credential-provider-web-identity@3.965.0': + resolution: {integrity: sha512-T4gMZ2JzXnfxe1oTD+EDGLSxFfk1+WkLZdiHXEMZp8bFI1swP/3YyDFXI+Ib9Uq1JhnAmrCXtOnkicKEhDkdhQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-bucket-endpoint@3.957.0': - resolution: {integrity: sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA==} + '@aws-sdk/middleware-bucket-endpoint@3.965.0': + resolution: {integrity: sha512-gbdv3Dl8l8xmg4oH60fXvfDyTxfx28w5/Hxdymx3vurM07tAyd4qld8zEXejnSpraTo45QcHRtk5auELIMfeag==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-expect-continue@3.957.0': - resolution: {integrity: sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg==} + '@aws-sdk/middleware-expect-continue@3.965.0': + resolution: {integrity: sha512-UBxVytsmhEmFwkBnt+aV0eAJ7uc+ouNokCqMBrQ7Oc5A77qhlcHfOgXIKz2SxqsiYTsDq+a0lWFM/XpyRWraqA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.957.0': - resolution: {integrity: sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA==} + '@aws-sdk/middleware-flexible-checksums@3.965.0': + resolution: {integrity: sha512-5rzEW08trcpHMe6jkQyYc4PL1KG/H7BbnySFSzhih+r/gktQEiE36sb1BNf7av9I0Vk2Ccmt7wocB5PIT7GDkQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-host-header@3.957.0': - resolution: {integrity: sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==} + '@aws-sdk/middleware-host-header@3.965.0': + resolution: {integrity: sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-location-constraint@3.957.0': - resolution: {integrity: sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ==} + '@aws-sdk/middleware-location-constraint@3.965.0': + resolution: {integrity: sha512-07T1rwAarQs33mVg5U28AsSdLB5JUXu9yBTBmspFGajKVsEahIyntf53j9mAXF1N2KR0bNdP0J4A0kst4t43UQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-logger@3.957.0': - resolution: {integrity: sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==} + '@aws-sdk/middleware-logger@3.965.0': + resolution: {integrity: sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-recursion-detection@3.957.0': - resolution: {integrity: sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==} + '@aws-sdk/middleware-recursion-detection@3.965.0': + resolution: {integrity: sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-sdk-s3@3.957.0': - resolution: {integrity: sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg==} + '@aws-sdk/middleware-sdk-s3@3.965.0': + resolution: {integrity: sha512-dXEgnojaaVRl+OlOx35mg3rYEbfffIN4X6tLmIfDnaKz0hMaDMvsE9jJXb/vBvokbdO1sVB27/2FEM4ttLSLnw==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-ssec@3.957.0': - resolution: {integrity: sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ==} + '@aws-sdk/middleware-ssec@3.965.0': + resolution: {integrity: sha512-dke++CTw26y+a2D1DdVuZ4+2TkgItdx6TeuE0zOl4lsqXGvTBUG4eaIZalt7ZOAW5ys2pbDOk1bPuh4opoD3pQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.957.0': - resolution: {integrity: sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==} + '@aws-sdk/middleware-user-agent@3.965.0': + resolution: {integrity: sha512-RBEYVGgu/WeAt+H/qLrGc+t8LqAUkbyvh3wBfTiuAD+uBcWsKnvnB1iSBX75FearC0fmoxzXRUc0PMxMdqpjJQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.957.0': - resolution: {integrity: sha512-PZUFtaUTSZWO+mbgQGWSiwz3EqedsuKNb7Xoxjzh5rfJE352DD4/jScQEhVPxvdLw62IK9b5UDu5kZlxzBs9Ow==} + '@aws-sdk/nested-clients@3.965.0': + resolution: {integrity: sha512-muNVUjUEU+/KLFrLzQ8PMXyw4+a/MP6t4GIvwLtyx/kH0rpSy5s0YmqacMXheuIe6F/5QT8uksXGNAQenitkGQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/region-config-resolver@3.957.0': - resolution: {integrity: sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==} + '@aws-sdk/region-config-resolver@3.965.0': + resolution: {integrity: sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==} engines: {node: '>=18.0.0'} - '@aws-sdk/s3-request-presigner@3.957.0': - resolution: {integrity: sha512-8U3c4Bmq1W0nQBZ1zbXQmbr4AH2iQIChRXy+fhrwjHHWPL3ek/2gDQR/cAiDAtlvXVIjWMqLJ1Ku45dGhOIbaw==} + '@aws-sdk/s3-request-presigner@3.965.0': + resolution: {integrity: sha512-aQ9vvXjeoQsAaRHS18l8doY+E/6mmNMSDMU6eJsSUDgvgGRMHhsKjiVh7DJGbZRRogdrES4KAfx6raIB4kBz5Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/signature-v4-multi-region@3.957.0': - resolution: {integrity: sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg==} + '@aws-sdk/signature-v4-multi-region@3.965.0': + resolution: {integrity: sha512-hgbAThbsUrWtNpFBQxzXevIfd5Qgr4TLbXY1AIbmpSX9fPVC114pdieRMpopJ0fYaJ7v5/blTiS6wzVdXleZ/w==} engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.957.0': - resolution: {integrity: sha512-oSwo3BZ6gcvhjTg036V0UQmtENUeNwfCU35iDckX961CdI1alQ3TKRWLzKrwvXCbrOx+bZsuA1PHsTbNhI/+Fw==} + '@aws-sdk/token-providers@3.965.0': + resolution: {integrity: sha512-aR0qxg0b8flkXJVE+CM1gzo7uJ57md50z2eyCwofC0QIz5Y0P7/7vvb9/dmUQt6eT9XRN5iRcUqq2IVxVDvJOw==} engines: {node: '>=18.0.0'} - '@aws-sdk/types@3.957.0': - resolution: {integrity: sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==} + '@aws-sdk/types@3.965.0': + resolution: {integrity: sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-arn-parser@3.957.0': - resolution: {integrity: sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g==} + '@aws-sdk/util-arn-parser@3.965.0': + resolution: {integrity: sha512-bNGKr5Tct28jGLkL8xIkGu7swpDgBpkTVbGaofhzr/X80iclbOv656RGxhMpDvmc4S9UuQnqLRXyceNFNF2V7Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-endpoints@3.957.0': - resolution: {integrity: sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==} + '@aws-sdk/util-endpoints@3.965.0': + resolution: {integrity: sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-format-url@3.957.0': - resolution: {integrity: sha512-Yyo/tlc0iGFGTPPkuxub1uRAv6XrnVnvSNjslZh5jIYA8GZoeEFPgJa3Qdu0GUS/YwoK8GOLnnaL9h/eH5LDJQ==} + '@aws-sdk/util-format-url@3.965.0': + resolution: {integrity: sha512-KiplV4xYGXdNCcz5eRP8WfAejT5EkE2gQxC4IY6WsuxYprzQKsnGaAzEQ+giR5GgQLIRBkPaWT0xHEYkMiCQ1Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-locate-window@3.957.0': - resolution: {integrity: sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw==} + '@aws-sdk/util-locate-window@3.965.0': + resolution: {integrity: sha512-9LJFand4bIoOjOF4x3wx0UZYiFZRo4oUauxQSiEX2dVg+5qeBOJSjp2SeWykIE6+6frCZ5wvWm2fGLK8D32aJw==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-user-agent-browser@3.957.0': - resolution: {integrity: sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==} + '@aws-sdk/util-user-agent-browser@3.965.0': + resolution: {integrity: sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==} - '@aws-sdk/util-user-agent-node@3.957.0': - resolution: {integrity: sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==} + '@aws-sdk/util-user-agent-node@3.965.0': + resolution: {integrity: sha512-kokIHUfNT3/P55E4fUJJrFHuuA9BbjFKUIxoLrd3UaRfdafT0ScRfg2eaZie6arf60EuhlUIZH0yALxttMEjxQ==} engines: {node: '>=18.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -458,12 +458,12 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.957.0': - resolution: {integrity: sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==} + '@aws-sdk/xml-builder@3.965.0': + resolution: {integrity: sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==} engines: {node: '>=18.0.0'} - '@aws/lambda-invoke-store@0.2.2': - resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} '@babel/code-frame@7.27.1': @@ -631,8 +631,8 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@borewit/text-codec@0.1.1': - resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -642,11 +642,11 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -654,8 +654,8 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -678,18 +678,36 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/merge@9.1.7': + resolution: {integrity: sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/schema@10.0.30': resolution: {integrity: sha512-yPXU17uM/LR90t92yYQqn9mAJNOVZJc0nQtYeZyZeQZeQjwIGlTubvvoDL0fFVk+wZzs4YQOgds2NwSA4npodA==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/schema@10.0.31': + resolution: {integrity: sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@10.11.0': resolution: {integrity: sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@11.0.0': + resolution: {integrity: sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: @@ -865,8 +883,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@10.4.20': - resolution: {integrity: sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==} + '@nestjs/common@10.4.21': + resolution: {integrity: sha512-2nabPCrq6HAc6PlZQsdDaV16ur7rs8Z8SH/rewS0SqbrvV6hgC/D5IPjVt4NvX7UjWKapqq+bymicuiZjP5WlQ==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -884,8 +902,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@10.4.20': - resolution: {integrity: sha512-kRdtyKA3+Tu70N3RQ4JgmO1E3LzAMs/eppj7SfjabC7TgqNWoS4RLhWl4BqmsNVmjj6D5jgfPVtHtgYkU3AfpQ==} + '@nestjs/core@10.4.21': + resolution: {integrity: sha512-MhiSGplB4TkadceA7opn/NaZmJhwYYNdB8nS8I29nLNx3vU+8aGHBiueZgcphEVDETZJSfc2VA5Mn/FC3JcsrA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/microservices': ^10.0.0 @@ -935,17 +953,17 @@ packages: class-validator: optional: true - '@nestjs/platform-express@10.4.20': - resolution: {integrity: sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==} + '@nestjs/platform-express@10.4.21': + resolution: {integrity: sha512-xIsa4h+oKf4zrHpTWN2i0gYkGaXewDqv4+KCatI1+aWoZKScFdoI82MFfuzq+z/EBpnVP2ABGqvPJWu+ZhKYvQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 - '@nestjs/platform-socket.io@11.1.11': - resolution: {integrity: sha512-0z6pLg9CuTXtz7q2lRZoPOU94DN28OTa39f4cQrlZysKA6QrKM7w7z6xqb4g32qjF+LQHFNRmMJtE/pLrxBaig==} + '@nestjs/platform-socket.io@10.4.21': + resolution: {integrity: sha512-3nIwx0Bi7I4yTrmqLi2rEnk2PsGyLoA8JanP7TGyoUazs4QelWeIkcxDlgiD7WphJAv0BsLCNkfnfS/3zOFfSA==} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/websockets': ^11.0.0 + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 rxjs: ^7.1.0 '@nestjs/schematics@10.2.3': @@ -953,8 +971,8 @@ packages: peerDependencies: typescript: '>=4.8.2' - '@nestjs/testing@10.4.20': - resolution: {integrity: sha512-nMkRDukDKskdPruM6EsgMq7yJua+CPZM6I6FrLP8yXw8BiVSPv9Nm0CtcGGwt3kgZF9hfxKjGqLjsvVBsv6Vfw==} + '@nestjs/testing@10.4.21': + resolution: {integrity: sha512-mQyJvrJ4mA9nukx+zXafh0iLtbGmwalnGWdoTih6cKtANGewIVsqgfSpuxwzyR4d42uc5jqgBulEZszmUFQ/5A==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -966,12 +984,12 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/websockets@11.1.11': - resolution: {integrity: sha512-apuP7C/gtMBIYNgA8IWt75GTZeWya5JQCnrLZFcOu+IZt00j9Xd/Bm7hbj/Qr/JVoM/7q6c/4p4oOZtBGx4aeA==} + '@nestjs/websockets@10.4.21': + resolution: {integrity: sha512-LOAF7q2c9Aa7mpSzVwCNBb7DzXfXfjUTtqKxz0xaIefHUw+y1Uhpxw39zXa3ki1SlsitNVdEohIazfRXcrLDJQ==} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io': ^11.0.0 + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-socket.io': ^10.0.0 reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -1100,8 +1118,8 @@ packages: resolution: {integrity: sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==} engines: {node: '>=18.0.0'} - '@smithy/core@3.20.0': - resolution: {integrity: sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==} + '@smithy/core@3.20.1': + resolution: {integrity: sha512-wOboSEdQ85dbKAJ0zL+wQ6b0HTSBRhtGa0PYKysQXkRg+vK0tdCRRVruiFM2QMprkOQwSYOnwF4og96PAaEGag==} engines: {node: '>=18.0.0'} '@smithy/credential-provider-imds@4.2.7': @@ -1164,12 +1182,12 @@ packages: resolution: {integrity: sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.1': - resolution: {integrity: sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==} + '@smithy/middleware-endpoint@4.4.2': + resolution: {integrity: sha512-mqpAdux0BNmZu/SqkFhQEnod4fX23xxTvU2LUpmKp0JpSI+kPYCiHJMmzREr8yxbNxKL2/DU1UZm9i++ayU+2g==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.17': - resolution: {integrity: sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==} + '@smithy/middleware-retry@4.4.18': + resolution: {integrity: sha512-E5hulijA59nBk/zvcwVMaS7FG7Y4l6hWA9vrW018r+8kiZef4/ETQaPI4oY+3zsy9f6KqDv3c4VKtO4DwwgpCg==} engines: {node: '>=18.0.0'} '@smithy/middleware-serde@4.2.8': @@ -1216,8 +1234,8 @@ packages: resolution: {integrity: sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.10.2': - resolution: {integrity: sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==} + '@smithy/smithy-client@4.10.3': + resolution: {integrity: sha512-EfECiO/0fAfb590LBnUe7rI5ux7XfquQ8LBzTe7gxw0j9QW/q8UT/EHWHlxV/+jhQ3+Ssga9uUYXCQgImGMbNg==} engines: {node: '>=18.0.0'} '@smithy/types@4.11.0': @@ -1252,12 +1270,12 @@ packages: resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.16': - resolution: {integrity: sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==} + '@smithy/util-defaults-mode-browser@4.3.17': + resolution: {integrity: sha512-dwN4GmivYF1QphnP3xJESXKtHvkkvKHSZI8GrSKMVoENVSKW2cFPRYC4ZgstYjUHdR3zwaDkIaTDIp26JuY7Cw==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.19': - resolution: {integrity: sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==} + '@smithy/util-defaults-mode-node@4.2.20': + resolution: {integrity: sha512-VD/I4AEhF1lpB3B//pmOIMBNLMrtdMXwy9yCOfa2QkJGDr63vH3RqPbSAKzoGMov3iryCxTXCxSsyGmEB8PDpg==} engines: {node: '>=18.0.0'} '@smithy/util-endpoints@3.2.7': @@ -1447,63 +1465,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.50.0': - resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} + '@typescript-eslint/eslint-plugin@8.52.0': + resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.50.0 + '@typescript-eslint/parser': ^8.52.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.50.0': - resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} + '@typescript-eslint/parser@8.52.0': + resolution: {integrity: sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.50.0': - resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} + '@typescript-eslint/project-service@8.52.0': + resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.50.0': - resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} + '@typescript-eslint/scope-manager@8.52.0': + resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.50.0': - resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + '@typescript-eslint/tsconfig-utils@8.52.0': + resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.50.0': - resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} + '@typescript-eslint/type-utils@8.52.0': + resolution: {integrity: sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.50.0': - resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} + '@typescript-eslint/types@8.52.0': + resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.50.0': - resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} + '@typescript-eslint/typescript-estree@8.52.0': + resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.50.0': - resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} + '@typescript-eslint/utils@8.52.0': + resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.50.0': - resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + '@typescript-eslint/visitor-keys@8.52.0': + resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -1844,8 +1862,8 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.9.8: - resolution: {integrity: sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==} + baseline-browser-mapping@2.9.13: + resolution: {integrity: sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==} hasBin: true binary-extensions@2.3.0: @@ -1859,8 +1877,8 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.1: - resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} bowser@2.13.1: @@ -1926,8 +1944,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001763: + resolution: {integrity: sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -2065,9 +2083,8 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} @@ -2170,8 +2187,8 @@ packages: supports-color: optional: true - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -2270,10 +2287,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2450,8 +2463,8 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -2493,8 +2506,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} external-editor@3.1.0: @@ -2527,8 +2540,8 @@ packages: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -2565,8 +2578,8 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} finalhandler@2.1.1: @@ -2827,8 +2840,8 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.1: - resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -3259,8 +3272,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.12.31: - resolution: {integrity: sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==} + libphonenumber-js@1.12.33: + resolution: {integrity: sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3676,8 +3689,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} prettier@3.7.4: @@ -3711,8 +3724,8 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -3868,8 +3881,8 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} seq-queue@0.0.5: @@ -3878,8 +3891,8 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} set-function-length@1.2.2: @@ -3947,6 +3960,10 @@ packages: resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} engines: {node: '>=10.0.0'} + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + socket.io@4.8.3: resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} engines: {node: '>=10.2.0'} @@ -4164,8 +4181,8 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - token-types@6.1.1: - resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} tr46@0.0.3: @@ -4175,8 +4192,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4366,8 +4383,8 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} - validator@13.15.23: - resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} vary@1.1.2: @@ -4377,8 +4394,8 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + watchpack@2.5.0: + resolution: {integrity: sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==} engines: {node: '>=10.13.0'} wcwidth@1.0.1: @@ -4601,9 +4618,9 @@ snapshots: '@apollo/utils.logger': 3.0.0 '@apollo/utils.usagereporting': 2.1.0(graphql@16.12.0) '@apollo/utils.withrequired': 3.0.0 - '@graphql-tools/schema': 10.0.30(graphql@16.12.0) + '@graphql-tools/schema': 10.0.31(graphql@16.12.0) async-retry: 1.3.3 - body-parser: 2.2.1 + body-parser: 2.2.2 cors: 2.8.5 finalhandler: 2.1.1 graphql: 16.12.0 @@ -4672,29 +4689,29 @@ snapshots: dependencies: xss: 1.0.15 - '@as-integrations/express5@1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.21.2)': + '@as-integrations/express5@1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.22.1)': dependencies: '@apollo/server': 5.2.0(graphql@16.12.0) - express: 4.21.2 + express: 4.22.1 '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-locate-window': 3.957.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-locate-window': 3.965.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -4703,15 +4720,15 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-locate-window': 3.957.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-locate-window': 3.965.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -4720,35 +4737,35 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.957.0': + '@aws-sdk/client-s3@3.965.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/credential-provider-node': 3.957.0 - '@aws-sdk/middleware-bucket-endpoint': 3.957.0 - '@aws-sdk/middleware-expect-continue': 3.957.0 - '@aws-sdk/middleware-flexible-checksums': 3.957.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-location-constraint': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-sdk-s3': 3.957.0 - '@aws-sdk/middleware-ssec': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/signature-v4-multi-region': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/credential-provider-node': 3.965.0 + '@aws-sdk/middleware-bucket-endpoint': 3.965.0 + '@aws-sdk/middleware-expect-continue': 3.965.0 + '@aws-sdk/middleware-flexible-checksums': 3.965.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-location-constraint': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-sdk-s3': 3.965.0 + '@aws-sdk/middleware-ssec': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/signature-v4-multi-region': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.965.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.1 '@smithy/eventstream-serde-browser': 4.2.7 '@smithy/eventstream-serde-config-resolver': 4.3.7 '@smithy/eventstream-serde-node': 4.2.7 @@ -4759,21 +4776,21 @@ snapshots: '@smithy/invalid-dependency': 4.2.7 '@smithy/md5-js': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-retry': 4.4.18 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.17 + '@smithy/util-defaults-mode-node': 4.2.20 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -4784,41 +4801,41 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.957.0': + '@aws-sdk/client-sso@3.965.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.965.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.1 '@smithy/fetch-http-handler': 5.3.8 '@smithy/hash-node': 4.2.7 '@smithy/invalid-dependency': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-retry': 4.4.18 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.17 + '@smithy/util-defaults-mode-node': 4.2.20 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -4827,59 +4844,59 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.957.0': + '@aws-sdk/core@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@aws-sdk/xml-builder': 3.957.0 - '@smithy/core': 3.20.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/xml-builder': 3.965.0 + '@smithy/core': 3.20.1 '@smithy/node-config-provider': 4.3.7 '@smithy/property-provider': 4.2.7 '@smithy/protocol-http': 5.3.7 '@smithy/signature-v4': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/util-base64': 4.3.0 '@smithy/util-middleware': 4.2.7 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/crc64-nvme@3.957.0': + '@aws-sdk/crc64-nvme@3.965.0': dependencies: '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.957.0': + '@aws-sdk/credential-provider-env@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.957.0': + '@aws-sdk/credential-provider-http@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/fetch-http-handler': 5.3.8 '@smithy/node-http-handler': 4.4.7 '@smithy/property-provider': 4.2.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/util-stream': 4.5.8 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.957.0': - dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/credential-provider-env': 3.957.0 - '@aws-sdk/credential-provider-http': 3.957.0 - '@aws-sdk/credential-provider-login': 3.957.0 - '@aws-sdk/credential-provider-process': 3.957.0 - '@aws-sdk/credential-provider-sso': 3.957.0 - '@aws-sdk/credential-provider-web-identity': 3.957.0 - '@aws-sdk/nested-clients': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/credential-provider-ini@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/credential-provider-env': 3.965.0 + '@aws-sdk/credential-provider-http': 3.965.0 + '@aws-sdk/credential-provider-login': 3.965.0 + '@aws-sdk/credential-provider-process': 3.965.0 + '@aws-sdk/credential-provider-sso': 3.965.0 + '@aws-sdk/credential-provider-web-identity': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/credential-provider-imds': 4.2.7 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -4888,11 +4905,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.957.0': + '@aws-sdk/credential-provider-login@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/nested-clients': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/protocol-http': 5.3.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -4901,15 +4918,15 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.957.0': + '@aws-sdk/credential-provider-node@3.965.0': dependencies: - '@aws-sdk/credential-provider-env': 3.957.0 - '@aws-sdk/credential-provider-http': 3.957.0 - '@aws-sdk/credential-provider-ini': 3.957.0 - '@aws-sdk/credential-provider-process': 3.957.0 - '@aws-sdk/credential-provider-sso': 3.957.0 - '@aws-sdk/credential-provider-web-identity': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/credential-provider-env': 3.965.0 + '@aws-sdk/credential-provider-http': 3.965.0 + '@aws-sdk/credential-provider-ini': 3.965.0 + '@aws-sdk/credential-provider-process': 3.965.0 + '@aws-sdk/credential-provider-sso': 3.965.0 + '@aws-sdk/credential-provider-web-identity': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/credential-provider-imds': 4.2.7 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -4918,21 +4935,21 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.957.0': + '@aws-sdk/credential-provider-process@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.957.0': + '@aws-sdk/credential-provider-sso@3.965.0': dependencies: - '@aws-sdk/client-sso': 3.957.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/token-providers': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/client-sso': 3.965.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/token-providers': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 @@ -4940,11 +4957,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.957.0': + '@aws-sdk/credential-provider-web-identity@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/nested-clients': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 @@ -4952,31 +4969,31 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/middleware-bucket-endpoint@3.957.0': + '@aws-sdk/middleware-bucket-endpoint@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-arn-parser': 3.957.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-arn-parser': 3.965.0 '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 '@smithy/util-config-provider': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.957.0': + '@aws-sdk/middleware-expect-continue@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.957.0': + '@aws-sdk/middleware-flexible-checksums@3.965.0': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/crc64-nvme': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/crc64-nvme': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/is-array-buffer': 4.2.0 '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 @@ -4986,43 +5003,43 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.957.0': + '@aws-sdk/middleware-host-header@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-location-constraint@3.957.0': + '@aws-sdk/middleware-location-constraint@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.957.0': + '@aws-sdk/middleware-logger@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.957.0': + '@aws-sdk/middleware-recursion-detection@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 - '@aws/lambda-invoke-store': 0.2.2 + '@aws-sdk/types': 3.965.0 + '@aws/lambda-invoke-store': 0.2.3 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.957.0': + '@aws-sdk/middleware-sdk-s3@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-arn-parser': 3.957.0 - '@smithy/core': 3.20.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-arn-parser': 3.965.0 + '@smithy/core': 3.20.1 '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 '@smithy/signature-v4': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/util-config-provider': 4.2.0 '@smithy/util-middleware': 4.2.7 @@ -5030,57 +5047,57 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-ssec@3.957.0': + '@aws-sdk/middleware-ssec@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.957.0': + '@aws-sdk/middleware-user-agent@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@smithy/core': 3.20.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@smithy/core': 3.20.1 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.957.0': + '@aws-sdk/nested-clients@3.965.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.957.0 - '@aws-sdk/middleware-host-header': 3.957.0 - '@aws-sdk/middleware-logger': 3.957.0 - '@aws-sdk/middleware-recursion-detection': 3.957.0 - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/region-config-resolver': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-endpoints': 3.957.0 - '@aws-sdk/util-user-agent-browser': 3.957.0 - '@aws-sdk/util-user-agent-node': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.965.0 '@smithy/config-resolver': 4.4.5 - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.1 '@smithy/fetch-http-handler': 5.3.8 '@smithy/hash-node': 4.2.7 '@smithy/invalid-dependency': 4.2.7 '@smithy/middleware-content-length': 4.2.7 - '@smithy/middleware-endpoint': 4.4.1 - '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-retry': 4.4.18 '@smithy/middleware-serde': 4.2.8 '@smithy/middleware-stack': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/node-http-handler': 4.4.7 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.16 - '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-defaults-mode-browser': 4.3.17 + '@smithy/util-defaults-mode-node': 4.2.20 '@smithy/util-endpoints': 3.2.7 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -5089,39 +5106,39 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.957.0': + '@aws-sdk/region-config-resolver@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/config-resolver': 4.4.5 '@smithy/node-config-provider': 4.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/s3-request-presigner@3.957.0': + '@aws-sdk/s3-request-presigner@3.965.0': dependencies: - '@aws-sdk/signature-v4-multi-region': 3.957.0 - '@aws-sdk/types': 3.957.0 - '@aws-sdk/util-format-url': 3.957.0 - '@smithy/middleware-endpoint': 4.4.1 + '@aws-sdk/signature-v4-multi-region': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-format-url': 3.965.0 + '@smithy/middleware-endpoint': 4.4.2 '@smithy/protocol-http': 5.3.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.957.0': + '@aws-sdk/signature-v4-multi-region@3.965.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/middleware-sdk-s3': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/protocol-http': 5.3.7 '@smithy/signature-v4': 5.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.957.0': + '@aws-sdk/token-providers@3.965.0': dependencies: - '@aws-sdk/core': 3.957.0 - '@aws-sdk/nested-clients': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/property-provider': 4.2.7 '@smithy/shared-ini-file-loader': 4.4.2 '@smithy/types': 4.11.0 @@ -5129,56 +5146,56 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.957.0': + '@aws-sdk/types@3.965.0': dependencies: '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.957.0': + '@aws-sdk/util-arn-parser@3.965.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.957.0': + '@aws-sdk/util-endpoints@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 '@smithy/url-parser': 4.2.7 '@smithy/util-endpoints': 3.2.7 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.957.0': + '@aws-sdk/util-format-url@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/querystring-builder': 4.2.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.957.0': + '@aws-sdk/util-locate-window@3.965.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.957.0': + '@aws-sdk/util-user-agent-browser@3.965.0': dependencies: - '@aws-sdk/types': 3.957.0 + '@aws-sdk/types': 3.965.0 '@smithy/types': 4.11.0 bowser: 2.13.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.957.0': + '@aws-sdk/util-user-agent-node@3.965.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.957.0 - '@aws-sdk/types': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/types': 3.965.0 '@smithy/node-config-provider': 4.3.7 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.957.0': + '@aws-sdk/xml-builder@3.965.0': dependencies: '@smithy/types': 4.11.0 fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws/lambda-invoke-store@0.2.2': {} + '@aws/lambda-invoke-store@0.2.3': {} '@babel/code-frame@7.27.1': dependencies: @@ -5369,7 +5386,7 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@borewit/text-codec@0.1.1': {} + '@borewit/text-codec@0.2.1': {} '@colors/colors@1.5.0': optional: true @@ -5378,13 +5395,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.7.1': + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true @@ -5396,7 +5413,7 @@ snapshots: '@epic-web/invariant@1.0.0': {} - '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 @@ -5425,6 +5442,12 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 + '@graphql-tools/merge@9.1.7(graphql@16.12.0)': + dependencies: + '@graphql-tools/utils': 11.0.0(graphql@16.12.0) + graphql: 16.12.0 + tslib: 2.8.1 + '@graphql-tools/schema@10.0.30(graphql@16.12.0)': dependencies: '@graphql-tools/merge': 9.1.6(graphql@16.12.0) @@ -5432,6 +5455,13 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 + '@graphql-tools/schema@10.0.31(graphql@16.12.0)': + dependencies: + '@graphql-tools/merge': 9.1.7(graphql@16.12.0) + '@graphql-tools/utils': 11.0.0(graphql@16.12.0) + graphql: 16.12.0 + tslib: 2.8.1 + '@graphql-tools/utils@10.11.0(graphql@16.12.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) @@ -5440,6 +5470,14 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 + '@graphql-tools/utils@11.0.0(graphql@16.12.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.12.0 + tslib: 2.8.1 + '@graphql-typed-document-node/core@3.2.0(graphql@16.12.0)': dependencies: graphql: 16.12.0 @@ -5678,28 +5716,28 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs/apollo@13.2.3(@apollo/server@5.2.0(graphql@16.12.0))(@as-integrations/express5@1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.21.2))(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/graphql@13.2.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14))(graphql@16.12.0)': + '@nestjs/apollo@13.2.3(@apollo/server@5.2.0(graphql@16.12.0))(@as-integrations/express5@1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.22.1))(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/graphql@13.2.3(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14))(graphql@16.12.0)': dependencies: '@apollo/server': 5.2.0(graphql@16.12.0) '@apollo/server-plugin-landing-page-graphql-playground': 4.0.1(@apollo/server@5.2.0(graphql@16.12.0)) - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/graphql': 13.2.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/graphql': 13.2.3(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14) graphql: 16.12.0 iterall: 1.3.0 lodash.omit: 4.5.0 tslib: 2.8.1 optionalDependencies: - '@as-integrations/express5': 1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.21.2) + '@as-integrations/express5': 1.1.2(@apollo/server@5.2.0(graphql@16.12.0))(express@4.22.1) - '@nestjs/axios@4.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)': + '@nestjs/axios@4.0.1(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) axios: 1.13.2 rxjs: 7.8.2 @@ -5729,7 +5767,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: file-type: 20.4.1 iterare: 1.2.1 @@ -5743,17 +5781,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.2(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) dotenv: 16.4.7 dotenv-expand: 12.0.1 lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/core@10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -5763,19 +5801,19 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@nestjs/websockets': 11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/platform-express': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21) + '@nestjs/websockets': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/platform-socket.io@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) transitivePeerDependencies: - encoding - '@nestjs/graphql@13.2.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14)': + '@nestjs/graphql@13.2.3(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.1.14)': dependencies: '@graphql-tools/merge': 9.1.6(graphql@16.12.0) '@graphql-tools/schema': 10.0.30(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14) chokidar: 4.0.3 fast-glob: 3.3.3 graphql: 16.12.0 @@ -5797,32 +5835,32 @@ snapshots: - uWebSockets.js - utf-8-validate - '@nestjs/mapped-types@2.1.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/platform-express@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)': + '@nestjs/platform-express@10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) body-parser: 1.20.3 cors: 2.8.5 - express: 4.21.2 + express: 4.22.1 multer: 2.0.2 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@10.4.21)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/websockets': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/platform-socket.io@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) rxjs: 7.8.2 - socket.io: 4.8.3 + socket.io: 4.8.1 tslib: 2.8.1 transitivePeerDependencies: - bufferutil @@ -5851,25 +5889,25 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20)': + '@nestjs/testing@10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/platform-express@10.4.21)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/platform-express': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21) - '@nestjs/websockets@11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/websockets@10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.21)(@nestjs/platform-socket.io@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@11.1.11)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.1.14)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.1.14 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.11(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 10.4.21(@nestjs/common@10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@10.4.21)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -5883,7 +5921,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@nuxtjs/opencollective@0.3.2': dependencies: @@ -5982,7 +6020,7 @@ snapshots: '@smithy/util-middleware': 4.2.7 tslib: 2.8.1 - '@smithy/core@3.20.0': + '@smithy/core@3.20.1': dependencies: '@smithy/middleware-serde': 4.2.8 '@smithy/protocol-http': 5.3.7 @@ -6086,9 +6124,9 @@ snapshots: '@smithy/types': 4.11.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.1': + '@smithy/middleware-endpoint@4.4.2': dependencies: - '@smithy/core': 3.20.0 + '@smithy/core': 3.20.1 '@smithy/middleware-serde': 4.2.8 '@smithy/node-config-provider': 4.3.7 '@smithy/shared-ini-file-loader': 4.4.2 @@ -6097,12 +6135,12 @@ snapshots: '@smithy/util-middleware': 4.2.7 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.17': + '@smithy/middleware-retry@4.4.18': dependencies: '@smithy/node-config-provider': 4.3.7 '@smithy/protocol-http': 5.3.7 '@smithy/service-error-classification': 4.2.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 '@smithy/util-middleware': 4.2.7 '@smithy/util-retry': 4.2.7 @@ -6176,10 +6214,10 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/smithy-client@4.10.2': + '@smithy/smithy-client@4.10.3': dependencies: - '@smithy/core': 3.20.0 - '@smithy/middleware-endpoint': 4.4.1 + '@smithy/core': 3.20.1 + '@smithy/middleware-endpoint': 4.4.2 '@smithy/middleware-stack': 4.2.7 '@smithy/protocol-http': 5.3.7 '@smithy/types': 4.11.0 @@ -6224,20 +6262,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.16': + '@smithy/util-defaults-mode-browser@4.3.17': dependencies: '@smithy/property-provider': 4.2.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.19': + '@smithy/util-defaults-mode-node@4.2.20': dependencies: '@smithy/config-resolver': 4.4.5 '@smithy/credential-provider-imds': 4.2.7 '@smithy/node-config-provider': 4.3.7 '@smithy/property-provider': 4.2.7 - '@smithy/smithy-client': 4.10.2 + '@smithy/smithy-client': 4.10.3 '@smithy/types': 4.11.0 tslib: 2.8.1 @@ -6312,7 +6350,7 @@ snapshots: dependencies: debug: 4.4.3(supports-color@10.2.2) fflate: 0.8.2 - token-types: 6.1.1 + token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -6474,95 +6512,95 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.50.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/type-utils': 8.50.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/type-utils': 8.52.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.52.0 eslint: 8.57.1 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.52.0 debug: 4.4.3(supports-color@10.2.2) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 debug: 4.4.3(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.50.0': + '@typescript-eslint/scope-manager@8.52.0': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 - '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.50.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.52.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@8.57.1)(typescript@5.9.3) debug: 4.4.3(supports-color@10.2.2) eslint: 8.57.1 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/types@8.52.0': {} - '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 debug: 4.4.3(supports-color@10.2.2) minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.50.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.52.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.50.0': + '@typescript-eslint/visitor-keys@8.52.0': dependencies: - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/types': 8.52.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -6944,7 +6982,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.9.8: {} + baseline-browser-mapping@2.9.13: {} binary-extensions@2.3.0: {} @@ -6971,15 +7009,15 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.1: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3(supports-color@10.2.2) - http-errors: 2.0.0 - iconv-lite: 0.7.1 + http-errors: 2.0.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.14.1 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -7002,8 +7040,8 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.8 - caniuse-lite: 1.0.30001760 + baseline-browser-mapping: 2.9.13 + caniuse-lite: 1.0.30001763 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -7052,7 +7090,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001763: {} chalk@4.1.2: dependencies: @@ -7094,8 +7132,8 @@ snapshots: class-validator@0.14.3: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.12.31 - validator: 13.15.23 + libphonenumber-js: 1.12.33 + validator: 13.15.26 cli-cursor@3.1.0: dependencies: @@ -7177,7 +7215,7 @@ snapshots: cookie-signature@1.0.6: {} - cookie@0.7.1: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} @@ -7273,7 +7311,7 @@ snapshots: optionalDependencies: supports-color: 10.2.2 - dedent@1.7.0: {} + dedent@1.7.1: {} deep-is@0.1.4: {} @@ -7348,8 +7386,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} engine.io-parser@5.2.3: {} @@ -7503,22 +7539,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.50.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7529,7 +7565,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7541,7 +7577,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.50.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -7551,7 +7587,7 @@ snapshots: dependencies: eslint: 8.57.1 prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 + prettier-linter-helpers: 1.0.1 synckit: 0.11.11 optionalDependencies: '@types/eslint': 9.6.1 @@ -7573,7 +7609,7 @@ snapshots: eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) '@eslint-community/regexpp': 4.12.2 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.57.1 @@ -7590,7 +7626,7 @@ snapshots: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -7622,7 +7658,7 @@ snapshots: esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -7664,36 +7700,36 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express@4.21.2: + express@4.22.1: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 + cookie: 0.7.2 + cookie-signature: 1.0.7 debug: 2.6.9 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 + finalhandler: 1.3.2 fresh: 0.5.2 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.1 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 + send: 0.19.2 + serve-static: 1.16.3 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -7730,7 +7766,7 @@ snapshots: dependencies: strnum: 2.1.2 - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -7761,7 +7797,7 @@ snapshots: dependencies: '@tokenizer/inflate': 0.2.7 strtok3: 10.3.4 - token-types: 6.1.1 + token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color @@ -7770,14 +7806,14 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: + finalhandler@1.3.2: dependencies: debug: 2.6.9 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -7789,7 +7825,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -7858,7 +7894,7 @@ snapshots: '@paralleldrive/cuid2': 2.3.1 dezalgo: 1.0.4 once: 1.4.0 - qs: 6.14.0 + qs: 6.14.1 forwarded@0.2.0: {} @@ -8055,7 +8091,7 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.1: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -8332,7 +8368,7 @@ snapshots: '@types/node': 20.19.27 chalk: 4.1.2 co: 4.6.0 - dedent: 1.7.0 + dedent: 1.7.1 is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -8686,7 +8722,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.31: {} + libphonenumber-js@1.12.33: {} lines-and-columns@1.2.4: {} @@ -8840,7 +8876,7 @@ snapshots: aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 long: 5.3.2 lru.min: 1.1.3 named-placeholders: 1.1.6 @@ -9045,7 +9081,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 @@ -9077,7 +9113,7 @@ snapshots: dependencies: side-channel: 1.1.0 - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -9100,7 +9136,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 react-is@18.3.1: {} @@ -9240,21 +9276,21 @@ snapshots: semver@7.7.3: {} - send@0.19.0: + send@0.19.2: dependencies: debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 0.5.2 - http-errors: 2.0.0 + http-errors: 2.0.1 mime: 1.6.0 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -9264,12 +9300,12 @@ snapshots: dependencies: randombytes: 2.1.0 - serve-static@1.16.2: + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 0.19.2 transitivePeerDependencies: - supports-color @@ -9361,6 +9397,20 @@ snapshots: transitivePeerDependencies: - supports-color + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.5 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socket.io@4.8.3: dependencies: accepts: 1.3.8 @@ -9500,7 +9550,7 @@ snapshots: formidable: 2.1.5 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.14.1 semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -9591,9 +9641,9 @@ snapshots: toidentifier@1.0.1: {} - token-types@6.1.1: + token-types@6.1.2: dependencies: - '@borewit/text-codec': 0.1.1 + '@borewit/text-codec': 0.2.1 '@tokenizer/token': 0.3.0 ieee754: 1.2.1 @@ -9601,7 +9651,7 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -9811,7 +9861,7 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - validator@13.15.23: {} + validator@13.15.26: {} vary@1.1.2: {} @@ -9819,7 +9869,7 @@ snapshots: dependencies: makeerror: 1.0.12 - watchpack@2.4.4: + watchpack@2.5.0: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -9859,7 +9909,7 @@ snapshots: schema-utils: 3.3.0 tapable: 2.3.0 terser-webpack-plugin: 5.3.16(webpack@5.97.1) - watchpack: 2.4.4 + watchpack: 2.5.0 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts index 824d60294..618f98602 100644 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts +++ b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@nestjs/common"; -import { ConnectTransportType } from "./sfu.validate"; import { CreateTransportDto, RoomEntry, TransportEntry } from "@app/sfu/commands/dto"; import { CreateRouterUsecase, CreateTransportUsecase } from "@app/sfu/commands/usecase"; import { RoomRouterRepository, TransportRepository } from "@infra/memory/sfu"; import { ConnectTransportUsecase } from "@app/sfu/queries/usecase"; +import { ConnectTransportType } from "@app/sfu/queries/dto"; @Injectable() diff --git a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts b/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts deleted file mode 100644 index 8365cab61..000000000 --- a/rep/main_backend/src/3-2.presentation/webrtc/sfu/sfu.validate.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DtlsParameters, IceCandidate, IceParameters } from "mediasoup/types"; - - -export type ConnectTransportType = { - room_id : string; - socket_id : string; - user_id : string; - type : "send" | "recv"; - transport_id : string; - dtlsParameters: DtlsParameters -}; \ No newline at end of file diff --git a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts index a586a41e0..dd13d2a80 100644 --- a/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts +++ b/rep/main_backend/src/3-2.presentation/websocket/signaling/signaling.service.ts @@ -7,12 +7,11 @@ import { ConnectRoomUsecase, DisconnectRoomUsecase } from "@app/room/commands/us import { v7 as uuidV7 } from "uuid"; import { DtlsHandshakeValidate, SocketPayload } from "./signaling.validate"; import { PayloadRes } from "@app/auth/queries/dto"; -import { UnthorizedError } from "@error/application/user/user.error"; import { SfuService } from "@present/webrtc/sfu/sfu.service"; import { NotConnectSignalling } from "@error/presentation/signalling/signalling.error"; import { CHANNEL_NAMESPACE } from "@infra/channel/channel.constants"; -import { CreateRoomTransportDto, CreateTransportDto } from "@app/sfu/commands/dto"; -import { ConnectTransportType } from "@/3-2.presentation/webrtc/sfu/sfu.validate"; +import { CreateTransportDto } from "@app/sfu/commands/dto"; +import { ConnectTransportType } from "@app/sfu/queries/dto"; @Injectable() @@ -48,8 +47,7 @@ export class SignalingWebsocketService { refresh_token = cookies["refresh_token"]; }; - if ( !access_token && !refresh_token ) return undefined; - if ( !access_token || !refresh_token ) throw new UnthorizedError("토큰이 존재하지 않습니다."); + if ( !access_token || !refresh_token ) return undefined; return { access_token, refresh_token @@ -97,6 +95,7 @@ export class SignalingWebsocketService { // 방에 나갈때 사용하는 함수 async disconnectRoomService(dto : DisconnectRoomDto) : Promise { await this.disconnectRoomUsecase.execute(dto); + this.sfuServer.closeRoomRouter(dto.room_id); // sfu 서버에 내용도 정리 }; // 방에 가입할때 사용하는 함수