From ad2145a9ebc10832e57512ece6746aeb8c8aebc5 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 02:03:25 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20producerResu?= =?UTF-8?q?med=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/hooks/mediasoup/useMediasoup.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/web/src/hooks/mediasoup/useMediasoup.ts b/apps/web/src/hooks/mediasoup/useMediasoup.ts index 2852e4cd..f6716f5c 100644 --- a/apps/web/src/hooks/mediasoup/useMediasoup.ts +++ b/apps/web/src/hooks/mediasoup/useMediasoup.ts @@ -19,8 +19,7 @@ const useMediasoup = () => { disconnect, } = useMediasoupAction(); const { startCameraStream, startMicStream } = useLocalStreamAction(); - const { consume, filterRemoteStream, pauseRemoteStream, resumeRemoteStream } = - useRemoteStreamAction(); + const { consume, filterRemoteStream, pauseRemoteStream } = useRemoteStreamAction(); const initSocketEvent = () => { const socket = socketRef.current; @@ -45,10 +44,6 @@ const useMediasoup = () => { pauseRemoteStream(producerId); }); - socket.on(SOCKET_EVENTS.producerResumed, ({ producerId }) => { - resumeRemoteStream(producerId); - }); - socket.on(SOCKET_EVENTS.newProducer, ({ peerId, producerId, kind, paused }) => { if (socket.id === peerId) return; From 647fd4f0a8a419a13378239aae1fc9a0b01ebb10 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 13:37:28 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20consume=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B0=84=EC=86=8C=ED=99=94=20=EB=B0=8F=20createCon?= =?UTF-8?q?sumers=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20=EB=A7=A4?= =?UTF-8?q?=EA=B0=9C=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/media/src/signaling/signaling.gateway.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/media/src/signaling/signaling.gateway.ts b/apps/media/src/signaling/signaling.gateway.ts index 191b7a4d..68f74789 100644 --- a/apps/media/src/signaling/signaling.gateway.ts +++ b/apps/media/src/signaling/signaling.gateway.ts @@ -4,7 +4,6 @@ import { OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, - WsException, } from '@nestjs/websockets'; import { Socket } from 'socket.io'; import { SOCKET_EVENTS, STREAM_STATUS } from '@repo/mediasoup'; @@ -13,6 +12,7 @@ import type { client, server } from '@repo/mediasoup'; import { MediasoupService } from '@/mediasoup/mediasoup.service'; import { UseFilters } from '@nestjs/common'; import { WSExceptionFilter } from '@/wsException.filter'; +import { types } from 'mediasoup'; @WebSocketGateway() @UseFilters(WSExceptionFilter) @@ -63,8 +63,9 @@ export class SignalingGateway implements OnGatewayDisconnect { async handleProduce( @ConnectedSocket() client: Socket, @MessageBody() createProducerDto: server.CreateProducerDto - ): Promise { + ) { const { transportId, kind, rtpParameters, roomId, appData } = createProducerDto; + const producerData = await this.mediasoupService.produce( client.id, kind, @@ -137,23 +138,25 @@ export class SignalingGateway implements OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() createConsumerDto: server.CreateConsumerDto ): Promise { - const { transportId, producerId, roomId, rtpCapabilities } = createConsumerDto; - - return this.mediasoupService.consume( - client.id, - producerId, - roomId, - transportId, - rtpCapabilities - ); + return this.mediasoupService.consume(client.id, createConsumerDto); } @SubscribeMessage(SOCKET_EVENTS.createConsumers) async createConsumers( @ConnectedSocket() client: Socket, - @MessageBody() createConsumersDto: server.CreateConsumerDto[] + @MessageBody('roomId') roomId: string, + @MessageBody('transportId') transportId: string, + @MessageBody('rtpCapabilities') rtpCapabilities: types.RtpCapabilities ) { - await this.mediasoupService.createConsumers(client.id, createConsumersDto); + const producers = this.mediasoupService.getProducers(roomId, client.id); + + return this.mediasoupService.createConsumers({ + roomId, + socketId: client.id, + producers, + rtpCapabilities, + transportId, + }); } @SubscribeMessage(SOCKET_EVENTS.pauseConsumers) @@ -162,7 +165,7 @@ export class SignalingGateway implements OnGatewayDisconnect { @MessageBody('roomId') roomId: string, @MessageBody('consumerIds') consumerIds: string[] ) { - this.mediasoupService.pauseConsumers(client.id, roomId, consumerIds); + return this.mediasoupService.pauseConsumers(client.id, roomId, consumerIds); } @SubscribeMessage(SOCKET_EVENTS.resumeConsumers) @@ -171,6 +174,6 @@ export class SignalingGateway implements OnGatewayDisconnect { @MessageBody('roomId') roomId: string, @MessageBody('consumerIds') consumerIds: string[] ) { - this.mediasoupService.resumeConsumers(client.id, roomId, consumerIds); + return this.mediasoupService.resumeConsumers(client.id, roomId, consumerIds); } } From 8b51324ab81501cd9681b507c44ad0c7b6650470 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 13:38:07 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20consume=20=EB=B0=8F=20createConsu?= =?UTF-8?q?mers=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=A7=A4=EA=B0=9C?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20appData=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/media/src/mediasoup/mediasoup.service.ts | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/media/src/mediasoup/mediasoup.service.ts b/apps/media/src/mediasoup/mediasoup.service.ts index 80210375..f22f1002 100644 --- a/apps/media/src/mediasoup/mediasoup.service.ts +++ b/apps/media/src/mediasoup/mediasoup.service.ts @@ -145,7 +145,7 @@ export class MediasoupService implements OnModuleInit { peerId: peer.socketId, nickname: peer.nickname, kind, - appData: appData, + appData: { mediaTypes: appData.mediaTypes as MediaTypes }, paused, }; }) @@ -171,10 +171,7 @@ export class MediasoupService implements OnModuleInit { async consume( socketId: string, - producerId: string, - roomId: string, - transportId: string, - rtpCapabilities: types.RtpCapabilities + { producerId, rtpCapabilities, roomId, transportId, appData, ...rest }: server.CreateConsumerDto ) { const room = this.roomService.getRoom(roomId); const peer = room.getPeer(socketId); @@ -190,6 +187,7 @@ export class MediasoupService implements OnModuleInit { producerId, rtpCapabilities, paused: true, + appData, }); consumer.on('producerclose', () => { @@ -212,17 +210,34 @@ export class MediasoupService implements OnModuleInit { peer.addConsumer(consumer); return { + peerId: peer.socketId, + paused: consumer.paused, consumerId: consumer.id, producerId: consumer.producerId, kind: consumer.kind, + appData: consumer.appData, rtpParameters: consumer.rtpParameters, }; } - async createConsumers(socketId: string, targets: server.CreateConsumerDto[]) { + async createConsumers(data: server.CreateConsumersDto) { + const { socketId, roomId, rtpCapabilities, transportId, producers } = data; + + const targets = producers.filter((producer) => producer.peerId !== socketId); + + if (targets.length === 0) { + return []; + } + return Promise.all( - targets.map(({ producerId, roomId, transportId, rtpCapabilities }) => - this.consume(socketId, producerId, roomId, transportId, rtpCapabilities) + producers.map((producer) => + this.consume(socketId, { + appData: producer.appData, + producerId: producer.producerId, + rtpCapabilities, + roomId, + transportId, + }) ) ); } @@ -251,20 +266,20 @@ export class MediasoupService implements OnModuleInit { const peer = room.peers.get(socketId); const consumer = peer.getConsumer(consumerId); + if (consumer.producerPaused) { + return { paused: true }; + } + consumer.resume(); - return consumerId; + return { paused: false, consumerId, producerId: consumer.producerId }; } pauseConsumers(socketId: string, roomId: string, consumerIds: string[]) { - consumerIds.forEach((consumerId) => { - this.pauseConsumer(socketId, consumerId, roomId); - }); + return consumerIds.map((consumerId) => this.pauseConsumer(socketId, consumerId, roomId)); } resumeConsumers(socketId: string, roomId: string, consumerIds: string[]) { - consumerIds.forEach((consumerId) => { - this.resumeConsumer(socketId, consumerId, roomId); - }); + return consumerIds.map((consumerId) => this.resumeConsumer(socketId, consumerId, roomId)); } } From d42a025996bcd7f11e5a648955df01539994fbf7 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 13:38:36 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20resumeAudioConsumers=20=EB=B0=8F?= =?UTF-8?q?=20resumeVideoConsumers=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/contexts/remoteStream/context.ts | 7 +++++-- apps/web/src/contexts/remoteStream/provider.tsx | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/web/src/contexts/remoteStream/context.ts b/apps/web/src/contexts/remoteStream/context.ts index 0dbd108c..8ac943c2 100644 --- a/apps/web/src/contexts/remoteStream/context.ts +++ b/apps/web/src/contexts/remoteStream/context.ts @@ -7,10 +7,13 @@ interface RemoteStreamState { } interface MediasoupActionContextProps { - consume: (data: client.CreateProducerRes) => void; + consume: (data: client.CreateProducerRes) => Promise; + createConsumers: () => Promise; + resumeAudioConsumers: (consumers: client.RemoteStream[]) => void; filterRemoteStream: (cb: (remoteStream: client.RemoteStream) => boolean) => void; pauseRemoteStream: (producerId: string) => void; - resumeRemoteStream: (producerId: string) => void; + resumeVideoStream: (producerId: string) => void; + resumeAudioStream: (producerId: string) => void; } export const RemoteStreamStateContext = createContext(undefined); diff --git a/apps/web/src/contexts/remoteStream/provider.tsx b/apps/web/src/contexts/remoteStream/provider.tsx index b8b5dbf0..8cff9685 100644 --- a/apps/web/src/contexts/remoteStream/provider.tsx +++ b/apps/web/src/contexts/remoteStream/provider.tsx @@ -15,18 +15,24 @@ export const RemoteStreamProvider = ({ children }: RemoteStreamProviderProps) => audioStreams, videoStreams, consume, + createConsumers, filterRemoteStream, pauseRemoteStream, - resumeRemoteStream, + resumeVideoStream, + resumeAudioStream, + resumeAudioConsumers, } = useRemoteStream(); const state = { audioStreams, videoStreams }; const actions = { consume, + createConsumers, filterRemoteStream, pauseRemoteStream, - resumeRemoteStream, + resumeVideoStream, + resumeAudioStream, + resumeAudioConsumers, } as const; return ( From ffc3c7f827e5664fd7dc41f2053c253d88f8cc2b Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:10:08 +0900 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20pnpm-lock=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39481dd6..6bd6907d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,13 +43,13 @@ importers: version: 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6) '@nestjs/swagger': specifier: ^8.0.1 - version: 8.0.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 8.0.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/throttler': specifier: ^6.2.1 - version: 6.2.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(reflect-metadata@0.2.2) + version: 6.2.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))) + version: 10.0.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))) '@repo/types': specifier: workspace:* version: link:../../packages/types @@ -110,7 +110,7 @@ importers: version: 10.2.3(chokidar@3.6.0)(typescript@5.6.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6) + version: 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)) '@repo/lint': specifier: workspace:* version: link:../../packages/eslint @@ -243,7 +243,7 @@ importers: version: 10.2.3(chokidar@3.6.0)(typescript@5.6.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6) + version: 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)) '@types/express': specifier: ^5.0.0 version: 5.0.0 @@ -7076,7 +7076,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@8.0.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@8.0.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.0 '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7091,7 +7091,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 - '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6)': + '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6))': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7099,13 +7099,13 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6) - '@nestjs/throttler@6.2.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.2.1(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) reflect-metadata: 0.2.2 - '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))': + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -9129,7 +9129,7 @@ snapshots: debug: 4.3.7(supports-color@9.4.0) enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -9142,7 +9142,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -9164,7 +9164,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 From ead1f7e826401b4f849ffa7b99dbe9496cd38bcb Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:10:25 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20resumeAudioConsumers=20=EB=B0=8F?= =?UTF-8?q?=20resumeVideoConsumers=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/mediasoup/src/events.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mediasoup/src/events.ts b/packages/mediasoup/src/events.ts index 1c2f4236..69947f51 100644 --- a/packages/mediasoup/src/events.ts +++ b/packages/mediasoup/src/events.ts @@ -34,6 +34,8 @@ export const SOCKET_EVENTS = { consumerClosed: 'consumer-closed', consumerPaused: 'consumer-paused', pauseConsumers: 'pause-consumers', + resumeAudioConsumers: 'resume-audio-consumers', + resumeVideoConsumers: 'resume-video-consumers', resumeConsumers: 'resume-consumers', } as const; From 439ed908cdffd329c2c686462ed6bcbc295ffba6 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:11:41 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20CreateConsumerRes=20=EB=B0=8F=20R?= =?UTF-8?q?esumeConsumersRes=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20peerId=20=EB=B0=8F=20paused=20=EC=86=8D?= =?UTF-8?q?=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 --- packages/mediasoup/src/client/index.ts | 9 ++++++++- packages/mediasoup/src/server/index.ts | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/mediasoup/src/client/index.ts b/packages/mediasoup/src/client/index.ts index 24850a8d..7bc960c4 100644 --- a/packages/mediasoup/src/client/index.ts +++ b/packages/mediasoup/src/client/index.ts @@ -16,7 +16,6 @@ export interface ConsumerTransports { consumerTransport: BaseTransport; consumerTransportId: BaseTransport; } - export interface CreateProducerRes { kind: types.MediaKind; peerId: string; @@ -34,8 +33,10 @@ export interface CreateTransportRes { } export interface CreateConsumerRes { + peerId: string; consumerId: string; producerId: string; + paused: boolean; kind: types.MediaKind; rtpParameters: types.RtpParameters; } @@ -54,6 +55,12 @@ export interface GetProducersRes { peerId: string; } +export interface ResumeConsumersRes { + consumerId: string; + producerId: string; + paused: boolean; +} + export const PRODUCER_OPTIONS: ProducerOptions = { encodings: [ { diff --git a/packages/mediasoup/src/server/index.ts b/packages/mediasoup/src/server/index.ts index a9f3e225..957ae513 100644 --- a/packages/mediasoup/src/server/index.ts +++ b/packages/mediasoup/src/server/index.ts @@ -26,16 +26,35 @@ export interface CreateProducerDto { } export interface CreateConsumerDto { + peerId: string; transportId: string; producerId: string; roomId: string; rtpCapabilities: types.RtpCapabilities; + appData?: { mediaTypes: MediaTypes }; +} + +export interface CreateConsumersDto { + socketId: string; + roomId: string; + transportId: string; + rtpCapabilities: types.RtpCapabilities; + producers: GetProducersRes[]; } export interface GetProducersDto { roomId: string; } +export interface GetProducersRes { + kind: types.MediaKind; + peerId: string; + nickname: string; + producerId: string; + paused: boolean; + appData?: { mediaTypes: MediaTypes }; +} + export interface ChangeProducerStateDto { producerId: string; status: StreamStatus; From c2e73866e36cfeb6d6aafff1f5cb9c77734605cb Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:12:32 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20CreateConsumerDto=EC=97=90=20peer?= =?UTF-8?q?Id=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20cons?= =?UTF-8?q?ume=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/media/src/mediasoup/mediasoup.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/media/src/mediasoup/mediasoup.service.ts b/apps/media/src/mediasoup/mediasoup.service.ts index f22f1002..a848b0c3 100644 --- a/apps/media/src/mediasoup/mediasoup.service.ts +++ b/apps/media/src/mediasoup/mediasoup.service.ts @@ -171,7 +171,7 @@ export class MediasoupService implements OnModuleInit { async consume( socketId: string, - { producerId, rtpCapabilities, roomId, transportId, appData, ...rest }: server.CreateConsumerDto + { peerId, producerId, rtpCapabilities, roomId, transportId, appData }: server.CreateConsumerDto ) { const room = this.roomService.getRoom(roomId); const peer = room.getPeer(socketId); @@ -210,7 +210,7 @@ export class MediasoupService implements OnModuleInit { peer.addConsumer(consumer); return { - peerId: peer.socketId, + peerId, paused: consumer.paused, consumerId: consumer.id, producerId: consumer.producerId, @@ -232,6 +232,7 @@ export class MediasoupService implements OnModuleInit { return Promise.all( producers.map((producer) => this.consume(socketId, { + peerId: producer.peerId, appData: producer.appData, producerId: producer.producerId, rtpCapabilities, @@ -258,7 +259,7 @@ export class MediasoupService implements OnModuleInit { consumer.pause(); - return consumerId; + return { paused: true, consumerId, producerId: consumer.producerId }; } resumeConsumer(socketId: string, consumerId: string, roomId: string) { @@ -267,7 +268,7 @@ export class MediasoupService implements OnModuleInit { const consumer = peer.getConsumer(consumerId); if (consumer.producerPaused) { - return { paused: true }; + return { paused: true, consumerId, producerId: consumer.producerId }; } consumer.resume(); From 82561ab787b49e1de066c42fdde6b893a48e3096 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:13:01 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20RemoteStreamContext=EC=97=90=20pa?= =?UTF-8?q?useVideoConsumers=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20resumeVideoStream=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/contexts/remoteStream/context.ts | 8 ++++++-- apps/web/src/contexts/remoteStream/provider.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/web/src/contexts/remoteStream/context.ts b/apps/web/src/contexts/remoteStream/context.ts index 8ac943c2..9414df11 100644 --- a/apps/web/src/contexts/remoteStream/context.ts +++ b/apps/web/src/contexts/remoteStream/context.ts @@ -9,11 +9,15 @@ interface RemoteStreamState { interface MediasoupActionContextProps { consume: (data: client.CreateProducerRes) => Promise; createConsumers: () => Promise; + resumeAudioConsumers: (consumers: client.RemoteStream[]) => void; + resumeVideoConsumers: (consumers: client.RemoteStream[]) => void; + + pauseVideoConsumers: (consumers: client.RemoteStream[]) => void; + filterRemoteStream: (cb: (remoteStream: client.RemoteStream) => boolean) => void; pauseRemoteStream: (producerId: string) => void; - resumeVideoStream: (producerId: string) => void; - resumeAudioStream: (producerId: string) => void; + resumeRemoteStream: (producerId: string) => void; } export const RemoteStreamStateContext = createContext(undefined); diff --git a/apps/web/src/contexts/remoteStream/provider.tsx b/apps/web/src/contexts/remoteStream/provider.tsx index 8cff9685..7b187a95 100644 --- a/apps/web/src/contexts/remoteStream/provider.tsx +++ b/apps/web/src/contexts/remoteStream/provider.tsx @@ -18,9 +18,10 @@ export const RemoteStreamProvider = ({ children }: RemoteStreamProviderProps) => createConsumers, filterRemoteStream, pauseRemoteStream, - resumeVideoStream, - resumeAudioStream, + resumeRemoteStream, resumeAudioConsumers, + resumeVideoConsumers, + pauseVideoConsumers, } = useRemoteStream(); const state = { audioStreams, videoStreams }; @@ -30,9 +31,10 @@ export const RemoteStreamProvider = ({ children }: RemoteStreamProviderProps) => createConsumers, filterRemoteStream, pauseRemoteStream, - resumeVideoStream, - resumeAudioStream, + resumeRemoteStream, resumeAudioConsumers, + resumeVideoConsumers, + pauseVideoConsumers, } as const; return ( From 7529b1742e182186ec87f10c3d0caeff5a41f4a5 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:13:10 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20PinnedGrid=EC=97=90=20pinnedStrea?= =?UTF-8?q?m=20=EC=86=8D=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 --- apps/web/src/components/live/StreamView/List/Pinned.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/components/live/StreamView/List/Pinned.tsx b/apps/web/src/components/live/StreamView/List/Pinned.tsx index 694c7703..0ea0bf05 100644 --- a/apps/web/src/components/live/StreamView/List/Pinned.tsx +++ b/apps/web/src/components/live/StreamView/List/Pinned.tsx @@ -22,6 +22,7 @@ function PinnedGrid({ }: PinnedListProps) { const { paginatedItems: subPaginatedStreams, ...subPaginationControlsProps } = usePagination({ itemsPerPage: ITEMS_PER_SUB_GRID, + pinnedStream: pinnedVideoStreamData, }); return ( From d3a7c4a8003f842d3136a20ae1e26a29fca8b1a7 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:14:25 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20grid=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20grid=20resume=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/hooks/usePagination.ts | 70 +++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/apps/web/src/hooks/usePagination.ts b/apps/web/src/hooks/usePagination.ts index 7d937661..baaf8f38 100644 --- a/apps/web/src/hooks/usePagination.ts +++ b/apps/web/src/hooks/usePagination.ts @@ -1,40 +1,48 @@ import { useEffect, useMemo, useState } from 'react'; +import { client } from '@repo/mediasoup'; import { StreamData } from '@/components/live/StreamView'; import { useLocalStreamState } from '@/contexts/localStream/context'; -import { useRemoteStreamState } from '@/contexts/remoteStream/context'; +import { useMediasoupState } from '@/contexts/mediasoup/context'; +import { useRemoteStreamAction, useRemoteStreamState } from '@/contexts/remoteStream/context'; +import useDebouncedCallback from '@/hooks/useDebounce'; interface PaginationParams { itemsPerPage: number; + pinnedStream?: StreamData; } -const usePagination = ({ itemsPerPage }: PaginationParams) => { +const usePagination = ({ itemsPerPage, pinnedStream }: PaginationParams) => { + const { socketRef } = useMediasoupState(); const { video, screen } = useLocalStreamState(); const { videoStreams } = useRemoteStreamState(); - const [currentPage, setCurrentPage] = useState(0); + const { resumeVideoConsumers, pauseVideoConsumers } = useRemoteStreamAction(); + const [currentPage, setCurrentPage] = useState(0); const paginatedItems = useMemo(() => { - const totalItems: StreamData[] = [...videoStreams]; + const totalItems: StreamData[] = []; - if (screen.stream) { - totalItems.unshift({ + if (video.stream) { + totalItems.push({ socketId: 'local', kind: 'video', - stream: screen.stream, - paused: false, + stream: video.stream, + paused: video.paused, }); } - if (video.stream) { - totalItems.unshift({ + if (screen.stream) { + totalItems.push({ socketId: 'local', kind: 'video', - stream: video.stream, - paused: video.paused, + stream: screen.stream, + paused: false, }); } + totalItems.push(...videoStreams); + const startIdx = currentPage * itemsPerPage; const endIdx = startIdx + itemsPerPage; @@ -52,12 +60,46 @@ const usePagination = ({ itemsPerPage }: PaginationParams) => { const streamLength = videoStreams.length + (video.stream ? 1 : 0) + (screen.stream ? 1 : 0); const totalPages = Math.ceil(streamLength / itemsPerPage); + const resumeGridStreams = useDebouncedCallback(() => { + const gridItems = paginatedItems.filter( + (item) => item.socketId !== 'local' && item.consumer + ) as client.RemoteStream[]; + + const isExistPinned = gridItems.some((item) => item.socketId === pinnedStream?.socketId); + + if (pinnedStream && !isExistPinned) { + gridItems.push(pinnedStream as client.RemoteStream); + } + + resumeVideoConsumers(gridItems); + }, 300); + + const pauseGridStreams = () => { + const socket = socketRef.current; + + if (!socket) return; + + const gridItems = paginatedItems.filter( + (item) => + item.socketId !== 'local' && item.consumer && pinnedStream?.socketId !== item.socketId + ) as client.RemoteStream[]; + + pauseVideoConsumers(gridItems); + }; + useEffect(() => { - if (paginatedItems.length !== 0 || currentPage <= 0) return; + if (paginatedItems.length !== 0) return; - setCurrentPage(currentPage - 1); + setCurrentPage((prev) => Math.max(0, prev - 1)); }, [paginatedItems]); + useEffect(() => { + if (paginatedItems.length === 0) return; + + pauseGridStreams(); + resumeGridStreams(); + }, [paginatedItems.length, currentPage]); + return { currentPage, totalPages, From 57ca6bacb3eed53ef765d4a115b3db7d8bf8e7b9 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:14:36 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat:=20useDebouncedCallback=20=ED=9B=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/hooks/useDebounce.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 apps/web/src/hooks/useDebounce.ts diff --git a/apps/web/src/hooks/useDebounce.ts b/apps/web/src/hooks/useDebounce.ts new file mode 100644 index 00000000..e16f504b --- /dev/null +++ b/apps/web/src/hooks/useDebounce.ts @@ -0,0 +1,22 @@ +import { useRef, useCallback } from 'react'; + +function useDebouncedCallback void>( + callback: T, + delay: number +): (...args: Parameters) => void { + const timeoutRef = useRef | null>(null); + + return useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + callback(...args); + }, delay); + }, + [callback, delay] + ); +} + +export default useDebouncedCallback; From 512d033c9d29f6e47a7628d049a46207700663ea Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:15:20 +0900 Subject: [PATCH 13/17] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20getProducer=20->=20getProducers=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/hooks/mediasoup/useProducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/hooks/mediasoup/useProducer.ts b/apps/web/src/hooks/mediasoup/useProducer.ts index 2832f93b..bd2a5c3d 100644 --- a/apps/web/src/hooks/mediasoup/useProducer.ts +++ b/apps/web/src/hooks/mediasoup/useProducer.ts @@ -100,7 +100,7 @@ const useProducer = ({ socketRef, transportsRef }: UseProducerProps) => { const params = { roomId }; return new Promise((resolve) => { - socket.emit(SOCKET_EVENTS.getProducer, params, (result: client.CreateProducerRes[]) => { + socket.emit(SOCKET_EVENTS.getProducers, params, (result: client.CreateProducerRes[]) => { const producers = producersRef.current; const producerIds = Object.values(producers) From 28a65b5b18ed30b3b9b1c2a0724f4a0ee0f62d68 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:15:59 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20useRemoteStream=20consumer=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=20resume/pause=20=EB=A9=94=EC=84=9C=EB=93=9C=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 --- .../src/hooks/mediasoup/useRemoteStream.ts | 257 ++++++++++++++++-- 1 file changed, 227 insertions(+), 30 deletions(-) diff --git a/apps/web/src/hooks/mediasoup/useRemoteStream.ts b/apps/web/src/hooks/mediasoup/useRemoteStream.ts index 5bd1009b..f9e8c65a 100644 --- a/apps/web/src/hooks/mediasoup/useRemoteStream.ts +++ b/apps/web/src/hooks/mediasoup/useRemoteStream.ts @@ -1,5 +1,5 @@ import { useParams } from '@tanstack/react-router'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { client, SOCKET_EVENTS } from '@repo/mediasoup'; import { useMediasoupState } from '@/contexts/mediasoup/context'; @@ -11,7 +11,9 @@ const useRemoteStream = () => { const [videoStreams, setVideoStreams] = useState([]); const [audioStreams, setAudioStreams] = useState([]); - const consume = async ({ producerId, peerId, kind, paused }: client.CreateProducerRes) => { + const consume = async (data: client.CreateProducerRes) => { + const { peerId, producerId, kind, paused, appData, nickname } = data; + const socket = socketRef.current; const device = deviceRef.current; const { recvTransport } = transportsRef.current; @@ -20,36 +22,195 @@ const useRemoteStream = () => { const params = { roomId: ticleId, + paused, + kind, producerId, transportId: recvTransport.id, rtpCapabilities: device.rtpCapabilities, }; - socket.emit(SOCKET_EVENTS.consume, params, async (params: client.CreateConsumerRes) => { - const { consumerId, ...rest } = params; + return new Promise((resolve) => { + socket.emit(SOCKET_EVENTS.consume, params, async (params: client.CreateConsumerRes) => { + const { consumerId, paused, ...rest } = params; - const consumer = await recvTransport.consume({ id: consumerId, ...rest }); + const consumer = await recvTransport.consume({ + id: consumerId, + appData, + ...rest, + }); + const stream = new MediaStream([consumer.track]); - const stream = new MediaStream([consumer.track]); + if (paused) { + consumer.pause(); + } - const newRemoteStream: client.RemoteStream = { - consumer, - socketId: peerId, - kind, - stream, - paused, - }; + setRemoteStream({ + stream, + consumer, + socketId: peerId, + kind: consumer.kind, + paused: consumer.paused, + }); - setRemoteStream(newRemoteStream); + resolve(); + }); + }); + }; - if (!paused) { - return; - } + const createConsumers = async () => { + const socket = socketRef.current; + const recvTransport = transportsRef.current.recvTransport; + const device = deviceRef.current; - consumer.pause(); + if (!socket || !recvTransport || !device) { + throw new Error('socket, recvTransport, device is not initialized'); + } + + const params = { + roomId: ticleId, + transportId: recvTransport.id, + rtpCapabilities: device.rtpCapabilities, + }; + + return new Promise((resolve) => { + socket.emit( + SOCKET_EVENTS.createConsumers, + params, + async (result: client.CreateConsumerRes[]) => { + if (!result || !result.length) return; + + const remoteStreams = await Promise.all(result.map(createRemoteStream)); + + resolve(remoteStreams); + } + ); + }); + }; + + const resumeAudioConsumers = (consumers: client.RemoteStream[]) => { + const socket = socketRef.current; + + if (!socket) { + throw new Error('socket is not initialized'); + } + if (!consumers.length) return; + + const consumerIds = consumers + .filter((consumer) => consumer.kind === 'audio') + .map((consumer) => consumer.consumer.id); + + const params = { roomId: ticleId, consumerIds }; + + socket.emit(SOCKET_EVENTS.resumeConsumers, params, (data: client.ResumeConsumersRes[]) => { + data.forEach((item) => { + if (item.paused) return; + + resumeRemoteStream(item.producerId); + }); + }); + }; + + const resumeVideoConsumers = (consumers: client.RemoteStream[]) => { + const socket = socketRef.current; + + if (!socket) { + throw new Error('socket is not initialized'); + } + if (!consumers.length) return; + + const consumerIds = consumers + .filter((consumer) => consumer.kind === 'video') + .map((consumer) => consumer.consumer.id); + + const params = { roomId: ticleId, consumerIds }; + + socket.emit(SOCKET_EVENTS.resumeConsumers, params, (data: client.ResumeConsumersRes[]) => { + data.forEach(({ paused, consumerId }) => { + if (paused) return; + + setVideoStreams(resumeStreamByConsumerId(consumerId)); + }); + }); + }; + + const pauseVideoConsumers = (consumers: client.RemoteStream[]) => { + const socket = socketRef.current; + if (!socket) { + throw new Error('socket is not initialized'); + } + if (!consumers.length) return; + + const consumerIds = consumers + .filter((consumer) => consumer.kind === 'video') + .map((consumer) => consumer.consumer.id); + + const params = { roomId: ticleId, consumerIds }; + + socket.emit(SOCKET_EVENTS.pauseConsumers, params, (data: client.ResumeConsumersRes[]) => { + data.forEach(({ consumerId }) => setVideoStreams(pauseStreamByConsumerId(consumerId))); }); }; + const pauseStreamByConsumerId = (consumerId: string) => { + return (prevStreams: client.RemoteStream[]) => { + const newStreams = prevStreams.map((stream) => { + if (stream.consumer.id === consumerId) { + stream.consumer.pause(); + stream.paused = true; + } + + return stream; + }); + + return newStreams; + }; + }; + + const resumeStreamByConsumerId = (consumerId: string) => { + return (prevStreams: client.RemoteStream[]) => { + const newStreams = prevStreams.map((stream) => { + if (stream.consumer.id === consumerId) { + stream.consumer.resume(); + stream.paused = false; + } + + return stream; + }); + + return newStreams; + }; + }; + + const createRemoteStream = async (data: client.CreateConsumerRes) => { + const recvTransport = transportsRef.current.recvTransport; + + if (!recvTransport) { + throw new Error('recvTransport is not initialized'); + } + + const { consumerId, ...rest } = data; + + const consumer = await recvTransport.consume({ id: consumerId, ...rest }); + + const stream = new MediaStream([consumer.track]); + + if (data.paused) { + consumer.pause(); + } + + const newStream = { + stream, + consumer, + socketId: data.peerId, + kind: consumer.kind, + paused: consumer.paused, + }; + + setRemoteStream(newStream); + + return newStream; + }; + const setRemoteStream = (remoteStream: client.RemoteStream) => { const getNewStreams = (prevStreams: client.RemoteStream[]) => { const isExist = prevStreams.some( @@ -64,20 +225,36 @@ const useRemoteStream = () => { }; if (remoteStream.kind === 'video') { - setVideoStreams((prevStreams) => getNewStreams(prevStreams)); + setVideoStreams(getNewStreams); } if (remoteStream.kind === 'audio') { - setAudioStreams((prevStreams) => getNewStreams(prevStreams)); + setAudioStreams(getNewStreams); } }; const filterRemoteStream = (cb: (remoteStream: client.RemoteStream) => boolean) => { - setVideoStreams((prevStreams) => prevStreams.filter(cb)); - setAudioStreams((prevStreams) => prevStreams.filter(cb)); + const getNewStreams = (prevStreams: client.RemoteStream[]) => { + const result = prevStreams.filter(cb); + + const deletedStreams = prevStreams.filter((stream) => !cb(stream)); + + deletedStreams.forEach((stream) => stream.consumer.close()); + + return result; + }; + + setVideoStreams(getNewStreams); + setAudioStreams(getNewStreams); }; - const pauseRemoteStream = (producerId: string) => { + const pauseRemoteStream = useCallback((producerId: string) => { + const socket = socketRef.current; + + if (!socket) { + throw new Error('socket is not initialized'); + } + const getNewStreams = (prevStreams: client.RemoteStream[]) => { const newStreams = [...prevStreams]; const stream = newStreams.find((stream) => stream.consumer.producerId === producerId); @@ -86,17 +263,28 @@ const useRemoteStream = () => { return prevStreams; } + socket.emit(SOCKET_EVENTS.pauseConsumers, { + roomId: ticleId, + consumerIds: [stream.consumer.id], + }); + stream.consumer.pause(); stream.paused = true; return newStreams; }; - setVideoStreams((prevStreams) => getNewStreams(prevStreams)); - setAudioStreams((prevStreams) => getNewStreams(prevStreams)); - }; + setVideoStreams(getNewStreams); + setAudioStreams(getNewStreams); + }, []); + + const resumeRemoteStream = useCallback((producerId: string) => { + const socket = socketRef.current; + + if (!socket) { + throw new Error('socket is not initialized'); + } - const resumeRemoteStream = (producerId: string) => { const getNewStreams = (prevStreams: client.RemoteStream[]) => { const newStreams = [...prevStreams]; const stream = newStreams.find((stream) => stream.consumer.producerId === producerId); @@ -105,23 +293,32 @@ const useRemoteStream = () => { return prevStreams; } + socket.emit(SOCKET_EVENTS.resumeConsumers, { + roomId: ticleId, + consumerIds: [stream.consumer.id], + }); + stream.consumer.resume(); stream.paused = false; return newStreams; }; - setVideoStreams((prevStreams) => getNewStreams(prevStreams)); - setAudioStreams((prevStreams) => getNewStreams(prevStreams)); - }; + setVideoStreams(getNewStreams); + setAudioStreams(getNewStreams); + }, []); return { videoStreams, audioStreams, consume, + createConsumers, filterRemoteStream, pauseRemoteStream, resumeRemoteStream, + resumeAudioConsumers, + resumeVideoConsumers, + pauseVideoConsumers, }; }; From bd784fe3522da73e3ac193e8b6c8c06ff4bd402e Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:18:54 +0900 Subject: [PATCH 15/17] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EC=A0=91?= =?UTF-8?q?=EC=86=8D=EC=8B=9C=20audio=20=EC=8A=A4=ED=8A=B8=EB=A6=BC?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20resume=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=8C=90=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/hooks/mediasoup/useMediasoup.ts | 38 +++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/web/src/hooks/mediasoup/useMediasoup.ts b/apps/web/src/hooks/mediasoup/useMediasoup.ts index f6716f5c..42e2201c 100644 --- a/apps/web/src/hooks/mediasoup/useMediasoup.ts +++ b/apps/web/src/hooks/mediasoup/useMediasoup.ts @@ -11,15 +11,17 @@ const useMediasoup = () => { const { socketRef, isConnected, isError } = useMediasoupState(); const { createRoom } = useRoom(); + const { createRecvTransport, createSendTransport, createDevice, disconnect } = + useMediasoupAction(); const { - createRecvTransport, - createSendTransport, - createDevice, - connectExistProducer, - disconnect, - } = useMediasoupAction(); + consume, + createConsumers, + filterRemoteStream, + pauseRemoteStream, + resumeRemoteStream, + resumeAudioConsumers, + } = useRemoteStreamAction(); const { startCameraStream, startMicStream } = useLocalStreamAction(); - const { consume, filterRemoteStream, pauseRemoteStream } = useRemoteStreamAction(); const initSocketEvent = () => { const socket = socketRef.current; @@ -44,11 +46,14 @@ const useMediasoup = () => { pauseRemoteStream(producerId); }); - socket.on(SOCKET_EVENTS.newProducer, ({ peerId, producerId, kind, paused }) => { - if (socket.id === peerId) return; + socket.on(SOCKET_EVENTS.producerResumed, ({ producerId }) => { + resumeRemoteStream(producerId); + }); + + socket.on(SOCKET_EVENTS.newProducer, (data) => { + if (socket.id === data.peerId) return; - // TODO: nickname 추가 - consume({ producerId, kind, peerId, paused, nickname: '변경' }); + consume({ ...data, nickname: '변경' }); }); }; @@ -65,25 +70,24 @@ const useMediasoup = () => { const setRemoteStream = async (device: client.Device) => { await createRecvTransport(device); - const remoteProducers = await connectExistProducer(); + const consumers = await createConsumers(); - if (!remoteProducers || remoteProducers.length === 0) return; - - remoteProducers.forEach(consume); + resumeAudioConsumers(consumers); }; const initMediasoup = async () => { const socket = socketRef.current; if (!socket) return; + const rtpCapabilities = await createRoom(); if (!rtpCapabilities) return; const device = await createDevice(rtpCapabilities); - await setLocalStream(device); - await setRemoteStream(device); + setLocalStream(device); + setRemoteStream(device); }; useEffect(() => { From 9caa238b351974bc81eda296e6b630bfe9c8c49b Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 21:32:30 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20=EC=95=84=EB=B0=94=ED=83=80?= =?UTF-8?q?=EA=B0=80=20=ED=95=AD=EC=83=81=20=EC=A4=91=EC=95=99=EC=97=90=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/live/StreamView/List/VideoPlayer.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx b/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx index bb852732..f17426a7 100644 --- a/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx +++ b/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx @@ -86,8 +86,11 @@ function VideoPlayer({ Your browser does not support the video. ) : ( -
- +
+
))}
From bfadb07263490081f99161fadca9be26a2e01fc5 Mon Sep 17 00:00:00 2001 From: seoko97 Date: Thu, 28 Nov 2024 23:32:27 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=20=EB=B3=91=ED=95=A9=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/auth.controller.ts | 39 ++- apps/api/src/auth/github/github.strategy.ts | 10 +- apps/api/src/auth/google/google.strategy.ts | 10 + apps/api/src/entity/applicant.entity.ts | 2 +- apps/api/src/entity/summary.entity.ts | 2 +- apps/api/src/ticle/ticle.controller.ts | 8 +- apps/api/src/ticle/ticle.service.ts | 23 +- apps/media/package.json | 1 + apps/media/src/mediasoup/mediasoup.service.ts | 16 +- apps/media/src/room/peer.ts | 4 +- apps/media/src/room/room.service.ts | 2 +- apps/media/src/room/room.ts | 2 +- apps/media/src/signaling/signaling.gateway.ts | 4 +- apps/web/package.json | 3 +- apps/web/src/api/auth.ts | 12 +- apps/web/src/api/ticle.ts | 8 +- apps/web/src/assets/icons/trash.svg | 3 + apps/web/src/components/auth/GuestLogin.tsx | 7 +- apps/web/src/components/auth/OAuthLogin.tsx | 8 +- .../web/src/components/common/Header/User.tsx | 26 +- .../src/components/common/Header/index.tsx | 1 - .../src/components/common/Select/index.tsx | 93 ++++-- .../src/components/dashboard/DashboardTab.tsx | 2 +- .../dashboard/apply/TicleInfoCard.tsx | 4 +- .../dashboard/open/TicleInfoCard.tsx | 4 +- .../live/StreamView/List/Pinned.tsx | 2 + .../live/StreamView/List/SubVideoGrid.tsx | 2 + .../live/StreamView/List/VideoGrid.tsx | 2 + .../live/StreamView/List/VideoPlayer.tsx | 7 +- .../src/components/live/StreamView/index.tsx | 4 +- .../web/src/components/ticle/detail/index.tsx | 29 +- .../src/components/ticle/list/TicleCard.tsx | 14 +- .../src/components/user/UserProfileDialog.tsx | 2 +- apps/web/src/hooks/api/ticle.ts | 18 +- apps/web/src/hooks/mediasoup/useMediasoup.ts | 2 +- apps/web/src/hooks/mediasoup/useProducer.ts | 2 +- .../src/hooks/mediasoup/useRemoteStream.ts | 8 +- apps/web/src/hooks/mediasoup/useRoom.ts | 18 +- apps/web/src/hooks/mediasoup/useTransport.ts | 2 +- apps/web/src/hooks/useAuthInfo.ts | 40 +++ apps/web/src/hooks/usePagination.ts | 32 ++- apps/web/src/hooks/usePinnedVideo.ts | 1 + apps/web/src/routeTree.gen.ts | 272 ++++++++++-------- .../web/src/routes/_authenticated/_layout.tsx | 51 ++++ .../dashboard/_layout.tsx | 2 +- .../{ => _authenticated}/dashboard/apply.tsx | 2 +- .../{ => _authenticated}/dashboard/open.tsx | 2 +- .../{ => _authenticated}/live/$ticleId.tsx | 2 +- .../{ => _authenticated}/ticle/$ticleId.tsx | 2 +- .../{ => _authenticated}/ticle/open.tsx | 2 +- apps/web/src/routes/auth/oauth.tsx | 10 + apps/web/src/stores/useAuthStore.ts | 42 +++ apps/web/src/styles/global.css | 22 +- packages/mediasoup/src/client/index.ts | 7 +- packages/mediasoup/src/server/index.ts | 7 +- packages/types/src/errorMessages.ts | 1 + packages/types/src/ticle/ticleDetail.ts | 2 + packages/types/src/user/userProfile.ts | 1 + packages/types/src/user/userProfileOfMe.ts | 1 + pnpm-lock.yaml | 30 ++ 60 files changed, 673 insertions(+), 264 deletions(-) create mode 100644 apps/web/src/assets/icons/trash.svg create mode 100644 apps/web/src/hooks/useAuthInfo.ts create mode 100644 apps/web/src/routes/_authenticated/_layout.tsx rename apps/web/src/routes/{ => _authenticated}/dashboard/_layout.tsx (90%) rename apps/web/src/routes/{ => _authenticated}/dashboard/apply.tsx (72%) rename apps/web/src/routes/{ => _authenticated}/dashboard/open.tsx (72%) rename apps/web/src/routes/{ => _authenticated}/live/$ticleId.tsx (89%) rename apps/web/src/routes/{ => _authenticated}/ticle/$ticleId.tsx (84%) rename apps/web/src/routes/{ => _authenticated}/ticle/open.tsx (84%) create mode 100644 apps/web/src/stores/useAuthStore.ts diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index ce9b9d95..9cc5afb7 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Post, Res, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Post, Query, Res, UseGuards } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ThrottlerGuard } from '@nestjs/throttler'; @@ -43,17 +43,21 @@ export class AuthController { @ApiResponse({ status: 302, description: '홈으로 리다이렉션' }) @ApiResponse({ status: 401 }) @UseGuards(LocalAuthGuard) - localLogin(@GetUserId() userId: number, @Res() response: Response) { - this.loginProcess(response, userId); + localLogin( + @GetUserId() userId: number, + @Query('redirect') redirect: string, + @Res() response: Response + ) { + this.loginProcess(response, userId, redirect); } @Get('guest/login') @ApiOperation({ summary: '게스트 로그인' }) @ApiResponse({ status: 302, description: '홈으로 리다이렉션' }) @UseGuards(ThrottlerGuard) - async guestLogin(@Res() response: Response) { + async guestLogin(@Query('redirect') redirect: string, @Res() response: Response) { const guestUser = await this.authService.createGuestUser(); - this.loginProcess(response, guestUser.id); + this.loginProcess(response, guestUser.id, redirect); } @Get('google/login') @@ -65,8 +69,12 @@ export class AuthController { @Get('google/callback') @UseGuards(GoogleAuthGuard) - googleAuthCallback(@GetUserId() userId: number, @Res() response: Response) { - this.loginProcess(response, userId); + googleAuthCallback( + @GetUserId() userId: number, + @Query('state') state: string, + @Res() response: Response + ) { + this.loginProcess(response, userId, state); } @Get('github/login') @@ -78,8 +86,12 @@ export class AuthController { @Get('github/callback') @UseGuards(GitHubAuthGuard) - githubAuthCallback(@GetUserId() userId: number, @Res() response: Response) { - this.loginProcess(response, userId); + githubAuthCallback( + @GetUserId() userId: number, + @Query('state') state: string, + @Res() response: Response + ) { + this.loginProcess(response, userId, state); } @Get('logout') @@ -90,13 +102,14 @@ export class AuthController { this.redirectToHome(response); } - private loginProcess(response: Response, userId: number) { + private loginProcess(response: Response, userId: number, path?: string) { const { accessToken } = this.authService.createJWT(userId); response.cookie('accessToken', accessToken, this.cookieConfig.getAuthCookieOptions()); - this.redirectToHome(response); + this.redirectToHome(response, path); } - private redirectToHome(response: Response) { - response.redirect(this.redirectUrl); + private redirectToHome(response: Response, path?: string) { + const redirectUrl = `${this.redirectUrl}${path || ''}`; + response.redirect(redirectUrl); } } diff --git a/apps/api/src/auth/github/github.strategy.ts b/apps/api/src/auth/github/github.strategy.ts index 6fdf01c6..e56592d7 100644 --- a/apps/api/src/auth/github/github.strategy.ts +++ b/apps/api/src/auth/github/github.strategy.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import { Profile, Strategy } from 'passport-github2'; +import { Request } from 'express'; +import { Profile, Strategy, StrategyOption } from 'passport-github2'; import { Provider } from '@repo/types'; import { AuthService } from '../auth.service'; @@ -19,6 +20,13 @@ export class GitHubStrategy extends PassportStrategy(Strategy, Provider.github) scope: ['user:email'], }); } + authenticate(req: Request, options: StrategyOption) { + const returnUrl = req.query.redirect as string; + if (returnUrl) { + options.state = returnUrl; + } + return super.authenticate(req, options); + } async validate(accessToken: string, refreshToken: string, profile: Profile) { const { id, username, emails, photos } = profile; diff --git a/apps/api/src/auth/google/google.strategy.ts b/apps/api/src/auth/google/google.strategy.ts index a4dd0aa3..5252a18a 100644 --- a/apps/api/src/auth/google/google.strategy.ts +++ b/apps/api/src/auth/google/google.strategy.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; +import { Request } from 'express'; +import { StrategyOption } from 'passport-github2'; import { Profile, Strategy } from 'passport-google-oauth20'; import { Provider } from '@repo/types'; @@ -20,6 +22,14 @@ export class GoogleStrategy extends PassportStrategy(Strategy, Provider.google) }); } + authenticate(req: Request, options: StrategyOption) { + const returnUrl = req.query.redirect as string; + if (returnUrl) { + options.state = returnUrl; + } + return super.authenticate(req, options); + } + async validate(accessToken: string, refreshToken: string, profile: Profile): Promise { const { id, displayName, emails, photos } = profile; diff --git a/apps/api/src/entity/applicant.entity.ts b/apps/api/src/entity/applicant.entity.ts index f5c33823..8c215112 100644 --- a/apps/api/src/entity/applicant.entity.ts +++ b/apps/api/src/entity/applicant.entity.ts @@ -16,7 +16,7 @@ export class Applicant { @PrimaryGeneratedColumn({ type: 'bigint' }) id: number; - @ManyToOne(() => Ticle, (ticle) => ticle.applicants) + @ManyToOne(() => Ticle, (ticle) => ticle.applicants, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'ticle_id' }) ticle: Ticle; diff --git a/apps/api/src/entity/summary.entity.ts b/apps/api/src/entity/summary.entity.ts index 0986cf52..6cd3f050 100644 --- a/apps/api/src/entity/summary.entity.ts +++ b/apps/api/src/entity/summary.entity.ts @@ -26,7 +26,7 @@ export class Summary { @CreateDateColumn({ type: 'timestamp', name: 'created_at' }) createdAt: Date; - @OneToOne(() => Ticle, (ticle) => ticle.summary) + @OneToOne(() => Ticle, (ticle) => ticle.summary, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'ticle_id' }) ticle: Ticle; } diff --git a/apps/api/src/ticle/ticle.controller.ts b/apps/api/src/ticle/ticle.controller.ts index f2085ed8..4c5312d7 100644 --- a/apps/api/src/ticle/ticle.controller.ts +++ b/apps/api/src/ticle/ticle.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; import { CreateTicleSchema } from '@repo/types'; import { JwtAuthGuard } from '@/auth/jwt/jwt-auth.guard'; @@ -50,4 +50,10 @@ export class TicleController { applyToTicle(@GetUserId() userId: number, @Param('ticleId') ticleId: number) { return this.ticleService.applyTicle(ticleId, userId); } + + @Delete(':ticleId') + @UseGuards(JwtAuthGuard) + deleteTicle(@GetUserId() userId: number, @Param('ticleId') ticleId: number) { + return this.ticleService.deleteTicle(userId, ticleId); + } } diff --git a/apps/api/src/ticle/ticle.service.ts b/apps/api/src/ticle/ticle.service.ts index a2ec54c6..34f15d05 100644 --- a/apps/api/src/ticle/ticle.service.ts +++ b/apps/api/src/ticle/ticle.service.ts @@ -134,7 +134,8 @@ export class TicleService { .leftJoinAndSelect('ticle.tags', 'tags') .leftJoinAndSelect('ticle.speaker', 'speaker') .leftJoinAndSelect('ticle.applicants', 'applicants') - .select(['ticle', 'tags', 'speaker.id', 'speaker.profileImageUrl', 'applicants']) + .leftJoinAndSelect('applicants.user', 'user') + .select(['ticle', 'tags', 'speaker.id', 'speaker.profileImageUrl', 'applicants', 'user.id']) .where('ticle.id = :id', { id: ticleId }) .getOne(); @@ -143,7 +144,7 @@ export class TicleService { } const { tags, speaker, ...ticleData } = ticle; - const alreadyApplied = ticle.applicants.some((applicnat) => applicnat.id === userId); + const alreadyApplied = ticle.applicants.some((applicant) => applicant.user.id === userId); return { ...ticleData, @@ -226,4 +227,22 @@ export class TicleService { }, }; } + + async deleteTicle(userId: number, ticleId: number) { + const ticle = await this.ticleRepository.findOne({ + where: { id: ticleId }, + relations: ['speaker'], + }); + + if (!ticle) { + throw new NotFoundException(ErrorMessage.TICLE_NOT_FOUND); + } + + if (ticle.speaker.id !== userId) { + throw new BadRequestException(ErrorMessage.CANNOT_DELETE_OTHERS_TICLE); + } + + await this.ticleRepository.remove(ticle); + return; + } } diff --git a/apps/media/package.json b/apps/media/package.json index cdbe2361..6450a768 100644 --- a/apps/media/package.json +++ b/apps/media/package.json @@ -26,6 +26,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.7", "@nestjs/websockets": "^10.4.7", + "@repo/lint": "workspace:*", "@repo/tsconfig": "workspace:*", "@repo/mediasoup": "workspace:*", "@repo/types": "workspace:*", diff --git a/apps/media/src/mediasoup/mediasoup.service.ts b/apps/media/src/mediasoup/mediasoup.service.ts index a848b0c3..e2437e95 100644 --- a/apps/media/src/mediasoup/mediasoup.service.ts +++ b/apps/media/src/mediasoup/mediasoup.service.ts @@ -145,7 +145,7 @@ export class MediasoupService implements OnModuleInit { peerId: peer.socketId, nickname: peer.nickname, kind, - appData: { mediaTypes: appData.mediaTypes as MediaTypes }, + appData: appData as server.GetProducersRes['appData'], paused, }; }) @@ -171,7 +171,15 @@ export class MediasoupService implements OnModuleInit { async consume( socketId: string, - { peerId, producerId, rtpCapabilities, roomId, transportId, appData }: server.CreateConsumerDto + { + peerId, + producerId, + rtpCapabilities, + roomId, + transportId, + nickname, + appData, + }: server.CreateConsumerDto ) { const room = this.roomService.getRoom(roomId); const peer = room.getPeer(socketId); @@ -211,11 +219,12 @@ export class MediasoupService implements OnModuleInit { return { peerId, + appData, + nickname, paused: consumer.paused, consumerId: consumer.id, producerId: consumer.producerId, kind: consumer.kind, - appData: consumer.appData, rtpParameters: consumer.rtpParameters, }; } @@ -235,6 +244,7 @@ export class MediasoupService implements OnModuleInit { peerId: producer.peerId, appData: producer.appData, producerId: producer.producerId, + nickname: producer.nickname, rtpCapabilities, roomId, transportId, diff --git a/apps/media/src/room/peer.ts b/apps/media/src/room/peer.ts index ba86ec22..b3111916 100644 --- a/apps/media/src/room/peer.ts +++ b/apps/media/src/room/peer.ts @@ -1,6 +1,6 @@ import { WsException } from '@nestjs/websockets'; -import { ErrorMessage } from '@repo/types'; import { types } from 'mediasoup'; +import { ErrorMessage } from '@repo/types'; export class Peer { socketId: string; @@ -34,7 +34,7 @@ export class Peer { (consumer) => consumer.producerId === producerId ); - return !Boolean(consumer); + return !consumer; } addProducer(producer: types.Producer) { diff --git a/apps/media/src/room/room.service.ts b/apps/media/src/room/room.service.ts index d4f91ad5..d4a366d3 100644 --- a/apps/media/src/room/room.service.ts +++ b/apps/media/src/room/room.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { WsException } from '@nestjs/websockets'; import { Router } from 'mediasoup/node/lib/RouterTypes'; +import { ErrorMessage } from '@repo/types'; import { Room } from './room'; -import { ErrorMessage } from '@repo/types'; @Injectable() export class RoomService { diff --git a/apps/media/src/room/room.ts b/apps/media/src/room/room.ts index 26a87e14..e2485668 100644 --- a/apps/media/src/room/room.ts +++ b/apps/media/src/room/room.ts @@ -1,8 +1,8 @@ import { WsException } from '@nestjs/websockets'; import { Router } from 'mediasoup/node/lib/RouterTypes'; +import { ErrorMessage } from '@repo/types'; import { Peer } from './peer'; -import { ErrorMessage } from '@repo/types'; export class Room { id: string; diff --git a/apps/media/src/signaling/signaling.gateway.ts b/apps/media/src/signaling/signaling.gateway.ts index 68f74789..7c9481c0 100644 --- a/apps/media/src/signaling/signaling.gateway.ts +++ b/apps/media/src/signaling/signaling.gateway.ts @@ -1,3 +1,4 @@ +import { UseFilters } from '@nestjs/common'; import { ConnectedSocket, MessageBody, @@ -5,14 +6,13 @@ import { SubscribeMessage, WebSocketGateway, } from '@nestjs/websockets'; +import { types } from 'mediasoup'; import { Socket } from 'socket.io'; import { SOCKET_EVENTS, STREAM_STATUS } from '@repo/mediasoup'; import type { client, server } from '@repo/mediasoup'; import { MediasoupService } from '@/mediasoup/mediasoup.service'; -import { UseFilters } from '@nestjs/common'; import { WSExceptionFilter } from '@/wsException.filter'; -import { types } from 'mediasoup'; @WebSocketGateway() @UseFilters(WSExceptionFilter) diff --git a/apps/web/package.json b/apps/web/package.json index f261b92c..56eaff63 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,7 +34,8 @@ "react-hook-form": "^7.53.2", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.5.4", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^5.0.1" }, "devDependencies": { "@chromatic-com/storybook": "^3.2.2", diff --git a/apps/web/src/api/auth.ts b/apps/web/src/api/auth.ts index 9f0a5fb9..2dd11995 100644 --- a/apps/web/src/api/auth.ts +++ b/apps/web/src/api/auth.ts @@ -1,3 +1,5 @@ +import { useParams, useSearch } from '@tanstack/react-router'; + import axiosInstance from '@/api/axios'; import { ENV } from '@/constants/env'; @@ -24,12 +26,4 @@ const logOut = () => { window.location.href = `${ENV.API_URL}/auth/logout`; }; -const guestLogin = () => { - window.location.href = `${ENV.API_URL}/auth/guest/login`; -}; - -const oauthLogin = (provider: 'google' | 'github') => { - window.location.href = `${ENV.API_URL}/auth/${provider}/login`; -}; - -export { logIn, signUp, oauthLogin, guestLogin, logOut }; +export { logIn, signUp, logOut }; diff --git a/apps/web/src/api/ticle.ts b/apps/web/src/api/ticle.ts index 94103c22..f67a894b 100644 --- a/apps/web/src/api/ticle.ts +++ b/apps/web/src/api/ticle.ts @@ -47,4 +47,10 @@ const applyTicle = async (ticleId: string) => { return data; }; -export { getTitleList, getTicle, createTicle, applyTicle }; +const deleteTicle = async (ticleId: string) => { + const { data } = await axiosInstance.delete(`/ticle/${ticleId}`); + + return data; +}; + +export { getTitleList, getTicle, createTicle, applyTicle, deleteTicle }; diff --git a/apps/web/src/assets/icons/trash.svg b/apps/web/src/assets/icons/trash.svg new file mode 100644 index 00000000..755acb9d --- /dev/null +++ b/apps/web/src/assets/icons/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/auth/GuestLogin.tsx b/apps/web/src/components/auth/GuestLogin.tsx index 7595ca79..4c1bff66 100644 --- a/apps/web/src/components/auth/GuestLogin.tsx +++ b/apps/web/src/components/auth/GuestLogin.tsx @@ -1,9 +1,12 @@ -import { guestLogin } from '@/api/auth'; import ChevronRight from '@/assets/icons/chevron-right.svg?react'; +import { ENV } from '@/constants/env'; +import { Route } from '@/routes/auth/oauth'; function GuestLogin() { + const { redirect } = Route.useSearch(); + const loginUrl = `${ENV.API_URL}/auth/guest/login?redirect=${redirect || ''}`; const handleGuestLogin = () => { - guestLogin(); + window.location.href = loginUrl; }; return ( diff --git a/apps/web/src/components/auth/OAuthLogin.tsx b/apps/web/src/components/auth/OAuthLogin.tsx index a91c8351..a6e2b782 100644 --- a/apps/web/src/components/auth/OAuthLogin.tsx +++ b/apps/web/src/components/auth/OAuthLogin.tsx @@ -1,9 +1,10 @@ import { cva } from 'class-variance-authority'; import { useState } from 'react'; -import { oauthLogin } from '@/api/auth'; import GithubIc from '@/assets/icons/github.svg?react'; import GoogleIc from '@/assets/icons/google.svg?react'; +import { ENV } from '@/constants/env'; +import { Route } from '@/routes/auth/oauth'; import Loading from '../common/Loading'; @@ -32,10 +33,13 @@ interface OAuthLoginProps { function OAuthLogin({ type }: OAuthLoginProps) { const [loadingOAuthType, setLoadingOAuthType] = useState(null); + const { redirect } = Route.useSearch(); + + const loginUrl = `${ENV.API_URL}/auth/${type}/login?redirect=${redirect || ''}`; const onLoginBtnClick = (type: OAuthType) => { setLoadingOAuthType(type); - oauthLogin(type); + window.location.href = loginUrl; }; const isCurrentLoading = loadingOAuthType === type; diff --git a/apps/web/src/components/common/Header/User.tsx b/apps/web/src/components/common/Header/User.tsx index 75dafab1..06a34a29 100644 --- a/apps/web/src/components/common/Header/User.tsx +++ b/apps/web/src/components/common/Header/User.tsx @@ -1,10 +1,9 @@ /* eslint-disable react-refresh/only-export-components */ import { Link } from '@tanstack/react-router'; -import axios from 'axios'; import { Provider } from '@repo/types'; import UserProfileOfMeDialog from '@/components/user/UserProfileOfMeDialog'; -import { useUserProfileOfMe } from '@/hooks/api/user'; +import useAuthInfo from '@/hooks/useAuthInfo'; import useModal from '@/hooks/useModal'; import Avatar from '../Avatar'; @@ -18,33 +17,30 @@ export const LOGIN_TYPE: Record = { }; function User() { - const { data, error } = useUserProfileOfMe(); - const { isOpen, onOpen, onClose } = useModal(); - - const isUnauthorized = axios.isAxiosError(error) && error.response?.status === 401; + const { isLoading, isAuthenticated, authInfo } = useAuthInfo(); - const loginType = data?.provider && LOGIN_TYPE[data.provider]; + const { isOpen, onOpen, onClose } = useModal(); const AuthorizedContent = () => ( <>
- - {data?.nickname} + + {authInfo?.nickname}
- {isOpen && data && loginType && ( + {isOpen && authInfo && ( )} ); const UnauthorizedContent = () => ( - +
@@ -53,7 +49,7 @@ function User() { return ( ); } diff --git a/apps/web/src/components/common/Header/index.tsx b/apps/web/src/components/common/Header/index.tsx index 2942950d..b55db9d8 100644 --- a/apps/web/src/components/common/Header/index.tsx +++ b/apps/web/src/components/common/Header/index.tsx @@ -20,7 +20,6 @@ function Header() { 대시보드 - {/* TODO: User 로그인시 핸들링 */} ); diff --git a/apps/web/src/components/common/Select/index.tsx b/apps/web/src/components/common/Select/index.tsx index 957200ed..2d832f89 100644 --- a/apps/web/src/components/common/Select/index.tsx +++ b/apps/web/src/components/common/Select/index.tsx @@ -1,10 +1,12 @@ import { cva } from 'class-variance-authority'; -import { useRef, useState, KeyboardEvent } from 'react'; +import { useRef, useState, KeyboardEvent, useLayoutEffect } from 'react'; import ChevronDownIc from '@/assets/icons/chevron-down.svg?react'; import ChevronUpIc from '@/assets/icons/chevron-up.svg?react'; import useOutsideClick from '@/hooks/useOutsideClick'; +import Portal from '../Portal'; + const selectVariants = cva( 'flex w-full cursor-pointer items-center justify-between gap-3.5 rounded-base bg-white px-3.5 py-2.5 text-body1 text-main', { @@ -24,6 +26,12 @@ export interface Option { label: string; value: string; } + +interface Position { + top: number; + left: number; + width: number; +} interface Select { options: Option[]; placeholder?: string; @@ -33,6 +41,42 @@ interface Select { function Select({ options, placeholder, selectedOption, onChange }: Select) { const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ + top: 0, + left: 0, + width: 0, + }); + + const selectRef = useRef(null); + const optionRef = useRef(null); + + const updatePosition = () => { + if (!selectRef.current) return; + + const selectRect = selectRef.current.getBoundingClientRect(); + const scrollTop = window.scrollY || document.documentElement.scrollTop; + const scrollLeft = window.scrollX || document.documentElement.scrollLeft; + + setPosition({ + top: selectRect.bottom + scrollTop, + left: selectRect.left + scrollLeft, + width: selectRect.width, + }); + }; + + useLayoutEffect(() => { + if (!isOpen) return; + + updatePosition(); + + window.addEventListener('scroll', updatePosition); + window.addEventListener('resize', updatePosition); + + return () => { + window.removeEventListener('scroll', updatePosition); + window.removeEventListener('resize', updatePosition); + }; + }, [isOpen]); const handleOptionChange = (option: Option) => { onChange?.(option); @@ -55,8 +99,7 @@ function Select({ options, placeholder, selectedOption, onChange }: Select) { handleOptionChange(option); }; - const selectRef = useRef(null); - useOutsideClick(selectRef, handleSelectClose); + useOutsideClick(optionRef, handleSelectClose); return (
@@ -69,24 +112,32 @@ function Select({ options, placeholder, selectedOption, onChange }: Select) { {isOpen ?
); diff --git a/apps/web/src/components/dashboard/DashboardTab.tsx b/apps/web/src/components/dashboard/DashboardTab.tsx index e4014ac4..6c7abcc9 100644 --- a/apps/web/src/components/dashboard/DashboardTab.tsx +++ b/apps/web/src/components/dashboard/DashboardTab.tsx @@ -14,7 +14,7 @@ const DASHBOARD_ROUTES = { function DashboardTab() { const navigate = useNavigate(); - const isOpenedMatch = useMatch({ from: '/dashboard/open', shouldThrow: false }); + const isOpenedMatch = useMatch({ from: '/_authenticated/dashboard/open', shouldThrow: false }); const selectedTab = isOpenedMatch ? 'OPENED' : 'APPLIED'; const DASHBOARD_TAB_DATA: TabData[] = [ diff --git a/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx b/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx index cd6e75a5..b87ea007 100644 --- a/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx +++ b/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx @@ -39,7 +39,9 @@ function TicleInfoCard({

티클명

- {ticleTitle} + + {ticleTitle} +

진행 일시

diff --git a/apps/web/src/components/dashboard/open/TicleInfoCard.tsx b/apps/web/src/components/dashboard/open/TicleInfoCard.tsx index 702a941e..336d753c 100644 --- a/apps/web/src/components/dashboard/open/TicleInfoCard.tsx +++ b/apps/web/src/components/dashboard/open/TicleInfoCard.tsx @@ -43,7 +43,9 @@ function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: Ticl

티클명

- {ticleTitle} + + {ticleTitle} +

진행 일시

diff --git a/apps/web/src/components/live/StreamView/List/Pinned.tsx b/apps/web/src/components/live/StreamView/List/Pinned.tsx index 0ea0bf05..2d2d4af1 100644 --- a/apps/web/src/components/live/StreamView/List/Pinned.tsx +++ b/apps/web/src/components/live/StreamView/List/Pinned.tsx @@ -35,7 +35,9 @@ function PinnedGrid({
diff --git a/apps/web/src/components/live/StreamView/List/SubVideoGrid.tsx b/apps/web/src/components/live/StreamView/List/SubVideoGrid.tsx index 6bf7f338..b24bbdde 100644 --- a/apps/web/src/components/live/StreamView/List/SubVideoGrid.tsx +++ b/apps/web/src/components/live/StreamView/List/SubVideoGrid.tsx @@ -64,6 +64,8 @@ function SubVideoGrid({ avatarSize="sm" paused={streamData.paused} isMicOn={getAudioMutedState(streamData)} + nickname={streamData.nickname} + mediaType={streamData.consumer?.appData?.mediaTypes} />
))} diff --git a/apps/web/src/components/live/StreamView/List/VideoGrid.tsx b/apps/web/src/components/live/StreamView/List/VideoGrid.tsx index d4cb5f37..c91ed481 100644 --- a/apps/web/src/components/live/StreamView/List/VideoGrid.tsx +++ b/apps/web/src/components/live/StreamView/List/VideoGrid.tsx @@ -35,6 +35,8 @@ function VideoGrid({ videoStreamData, onVideoClick, getAudioMutedState }: VideoG stream={streamData.stream} paused={streamData.paused} isMicOn={getAudioMutedState(streamData)} + nickname={streamData.nickname} + mediaType={streamData.consumer?.appData?.mediaTypes} /> ))} diff --git a/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx b/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx index f17426a7..84b0e7af 100644 --- a/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx +++ b/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx @@ -24,7 +24,8 @@ export interface VideoPlayerProps { paused?: boolean; isMicOn?: boolean; avatarSize?: 'sm' | 'md' | 'lg'; - mediaType?: 'video' | 'screen'; + mediaType?: string; + nickname: string; } function VideoPlayer({ @@ -33,8 +34,8 @@ function VideoPlayer({ mediaType = 'video', isMicOn = false, avatarSize = 'md', + nickname, }: VideoPlayerProps) { - const NAME = '김티클'; // TODO: 이름 받아오기 const [isLoading, setIsLoading] = useState(true); const videoRef = useRef(null); @@ -65,7 +66,7 @@ function VideoPlayer({ )}
- {NAME} + {nickname}
)} diff --git a/apps/web/src/components/live/StreamView/index.tsx b/apps/web/src/components/live/StreamView/index.tsx index e6104b29..20b1082d 100644 --- a/apps/web/src/components/live/StreamView/index.tsx +++ b/apps/web/src/components/live/StreamView/index.tsx @@ -1,4 +1,5 @@ import { types } from 'mediasoup-client'; +import { MediaTypes } from '@repo/mediasoup'; import AudioStreams from '@/components/live/StreamView/AudioStreams'; import PinnedGrid from '@/components/live/StreamView/List/Pinned'; @@ -7,11 +8,12 @@ import useAudioState from '@/hooks/useAudioState'; import usePinnedVideo from '@/hooks/usePinnedVideo'; export interface StreamData { - consumer?: types.Consumer; + consumer?: types.Consumer<{ mediaTypes: MediaTypes; nickname: string }>; socketId: string; kind: types.MediaKind; stream: MediaStream | null; paused: boolean; + nickname: string; } const StreamView = () => { diff --git a/apps/web/src/components/ticle/detail/index.tsx b/apps/web/src/components/ticle/detail/index.tsx index 8b645de8..b23f7bee 100644 --- a/apps/web/src/components/ticle/detail/index.tsx +++ b/apps/web/src/components/ticle/detail/index.tsx @@ -1,22 +1,28 @@ -import { useNavigate, useParams } from '@tanstack/react-router'; +import { useParams } from '@tanstack/react-router'; import CalendarIc from '@/assets/icons/calendar.svg?react'; import ClockIc from '@/assets/icons/clock.svg?react'; +import TrashIc from '@/assets/icons/trash.svg?react'; import Avatar from '@/components/common/Avatar'; import Badge from '@/components/common/Badge'; import Button from '@/components/common/Button'; import UserProfileDialog from '@/components/user/UserProfileDialog'; -import { useApplyTicle, useTicle } from '@/hooks/api/ticle'; +import { useApplyTicle, useDeleteTicle, useTicle } from '@/hooks/api/ticle'; import useModal from '@/hooks/useModal'; import { formatDateTimeRange } from '@/utils/date'; function Detail() { - const { ticleId } = useParams({ from: '/ticle/$ticleId' }); + const { ticleId } = useParams({ from: '/_authenticated/ticle/$ticleId' }); const { data } = useTicle(ticleId); - const { mutate } = useApplyTicle(); + const { mutate: applyMutate } = useApplyTicle(); + const { mutate: deleteMutate } = useDeleteTicle(); const handleApplyButtonClick = () => { - mutate(ticleId); + applyMutate(ticleId); + }; + + const handleDeleteButtonClick = () => { + deleteMutate(ticleId); }; const { isOpen, onOpen, onClose } = useModal(); @@ -78,7 +84,18 @@ function Detail() { - + {data.isOwner ? ( + + ) : ( + + )} ); } diff --git a/apps/web/src/components/ticle/list/TicleCard.tsx b/apps/web/src/components/ticle/list/TicleCard.tsx index e53b5920..c6bedd55 100644 --- a/apps/web/src/components/ticle/list/TicleCard.tsx +++ b/apps/web/src/components/ticle/list/TicleCard.tsx @@ -22,12 +22,14 @@ const TicleCard = ({ }: TicleCardProps) => { return (
-
-

{title}

-
- {tags.map((tag) => ( - {tag} - ))} +
+
+

{title}

+
+ {tags.map((tag) => ( + {tag} + ))} +
diff --git a/apps/web/src/components/user/UserProfileDialog.tsx b/apps/web/src/components/user/UserProfileDialog.tsx index 85411fdc..831c7e42 100644 --- a/apps/web/src/components/user/UserProfileDialog.tsx +++ b/apps/web/src/components/user/UserProfileDialog.tsx @@ -34,7 +34,7 @@ function UserProfileDialog({ isOpen, onClose, speakerId, nickname }: UserProfile

개설한 티클 목록

-
+
{data.ticleInfo.map((info) => (