From ec70416fe9f11b592aa5b0e90b24832fdc6b4805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 8 Oct 2024 16:36:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=EC=9A=94=EC=B2=AD=EC=8B=9C=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=EC=9D=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=A0=91=EC=86=8D=ED=95=9C=20=ED=9A=8C=EC=9B=90=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=EC=9A=94=EC=B2=AD=20=EC=95=8C=EB=A6=BC=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 - 서비스 - 프로젝트 참여 요청 메서드가 참여요청 엔티티를 반환하도록 수정 - 컨트롤러 - 프로젝트 참여요청 후 참여알림 메서드를 호출하도록 수정 - 게이트웨이 - 프로젝트 참여 알림 메서드 구현 - E2E 테스트 - 프로젝트 참여 요청 시 설정 페이지에 접속한 회원에게 알림이 가는지 확인하는 E2E 테스트 추가 --- .../CreateJoinRequestNotify.dto.ts | 48 ++++++++ backend/src/project/project.controller.ts | 9 +- .../src/project/service/project.service.ts | 10 +- backend/src/project/websocket.gateway.ts | 18 +++ ...ws-notify-project-join-request.e2e-spec.ts | 112 ++++++++++++++++++ 5 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts create mode 100644 backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts diff --git a/backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts b/backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts new file mode 100644 index 00000000..908d4024 --- /dev/null +++ b/backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts @@ -0,0 +1,48 @@ +import { Member } from 'src/member/entity/member.entity'; +import { ProjectJoinRequest } from 'src/project/entity/project-join-request.entity'; + +class JoinRequestDto { + id: number; + memberId: number; + username: string; + imageUrl: string; + + static of( + projectJoinRequest: ProjectJoinRequest, + member: Member, + ): JoinRequestDto { + const dto = new JoinRequestDto(); + dto.id = projectJoinRequest.id; + dto.memberId = member.id; + dto.username = member.username; + dto.imageUrl = member.github_image_url; + return dto; + } +} + +class ContentDto { + joinRequest: JoinRequestDto; + + static of(joinRequest: ProjectJoinRequest, member: Member): ContentDto { + const dto = new ContentDto(); + dto.joinRequest = JoinRequestDto.of(joinRequest, member); + return dto; + } +} + +export class CreateJoinRequestNotifyDto { + domain: string; + action: string; + content: ContentDto; + + static of( + joinRequest: ProjectJoinRequest, + member: Member, + ): CreateJoinRequestNotifyDto { + const dto = new CreateJoinRequestNotifyDto(); + dto.domain = 'joinRequest'; + dto.action = 'create'; + dto.content = ContentDto.of(joinRequest, member); + return dto; + } +} diff --git a/backend/src/project/project.controller.ts b/backend/src/project/project.controller.ts index 640108f3..834aea68 100644 --- a/backend/src/project/project.controller.ts +++ b/backend/src/project/project.controller.ts @@ -93,8 +93,13 @@ export class ProjectController { @Res() response: Response, ) { try { - await this.projectService.createProjectJoinRequest( - body.inviteLinkId, + const projectJoinRequest = + await this.projectService.createProjectJoinRequest( + body.inviteLinkId, + request.member, + ); + this.projectWebsocketGateway.notifyCreateJoinRequestToSettingPage( + projectJoinRequest, request.member, ); } catch (e) { diff --git a/backend/src/project/service/project.service.ts b/backend/src/project/service/project.service.ts index 9596427e..9b10543f 100644 --- a/backend/src/project/service/project.service.ts +++ b/backend/src/project/service/project.service.ts @@ -154,7 +154,7 @@ export class ProjectService { async createProjectJoinRequest( inviteLinkId: string, member: Member, - ): Promise { + ): Promise { const project = await this.getProjectByLinkId(inviteLinkId); if (!project) throw new Error('Project not found'); const isProjectMember = await this.isProjectMember(project.id, member); @@ -164,9 +164,11 @@ export class ProjectService { project.id, member.id, ); - await this.projectRepository.createProjectJoinRequest( - newProjectJoinRequest, - ); + const projectJoinRequest = + await this.projectRepository.createProjectJoinRequest( + newProjectJoinRequest, + ); + return projectJoinRequest; } catch (e) { if (e.message === 'DUPLICATED PROJECT ID AND MEMBER ID') { throw new Error('Join request already submitted'); diff --git a/backend/src/project/websocket.gateway.ts b/backend/src/project/websocket.gateway.ts index 13c59096..d4183678 100644 --- a/backend/src/project/websocket.gateway.ts +++ b/backend/src/project/websocket.gateway.ts @@ -23,6 +23,8 @@ import { WsProjectStoryController } from './ws-controller/ws-project-story.contr import { WsProjectTaskController } from './ws-controller/ws-project-task.controller'; import { WsProjectInfoController } from './ws-controller/ws-project-info.controller'; import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller'; +import { ProjectJoinRequest } from './entity/project-join-request.entity'; +import { CreateJoinRequestNotifyDto } from './dto/setting-page/CreateJoinRequestNotify.dto'; @WebSocketGateway({ namespace: /project-\d+/, @@ -212,6 +214,22 @@ export class ProjectWebsocketGateway this.namespaceMap.delete(projectId); } + notifyCreateJoinRequestToSettingPage( + projectJoinRequest: ProjectJoinRequest, + member: Member, + ) { + const projectNamespace = this.namespaceMap.get( + projectJoinRequest.projectId, + ); + if (!projectNamespace) return; + projectNamespace + .to('setting') + .emit( + 'setting', + CreateJoinRequestNotifyDto.of(projectJoinRequest, member), + ); + } + notifyJoinToConnectedMembers(projectId: number, member: Member) { const projectNamespace = this.namespaceMap.get(projectId); if (!projectNamespace) return; diff --git a/backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts b/backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts new file mode 100644 index 00000000..3c45f334 --- /dev/null +++ b/backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts @@ -0,0 +1,112 @@ +import { Socket } from 'socket.io-client'; +import * as request from 'supertest'; +import { + app, + appInit, + connectServer, + createMember, + createProject, + getMemberByAccessToken, + getProjectLinkId, + listenAppAndSetPortEnv, + memberFixture, + memberFixture2, + projectPayload, +} from 'test/setup'; +import { Member } from 'src/member/entity/member.entity'; + +describe('WS Setting', () => { + beforeEach(async () => { + await app.close(); + await appInit(); + await listenAppAndSetPortEnv(app); + }); + + it('should notify join request in setting page when project join request is submitted', async () => { + const { accessToken: leaderAccessToken } = await createMember( + memberFixture, + app, + ); + + const { id: projectId } = await createProject( + leaderAccessToken, + projectPayload, + app, + ); + const leaderSocket = await enterSettingPage(projectId, leaderAccessToken); + + const { accessToken: requestingAccessToken } = await createMember( + memberFixture2, + app, + ); + + const requestingMember = await getMemberByAccessToken( + requestingAccessToken, + ); + + const expectPromise = expectNotifyJoinRequest( + leaderSocket, + requestingMember, + ); + const submitPromise = submitJoinRequest( + leaderAccessToken, + projectId, + requestingAccessToken, + ); + await Promise.all([expectPromise, submitPromise]); + closePage(leaderSocket); + }); + + async function enterSettingPage(projectId: number, accessToken: string) { + const socket = connectServer(projectId, accessToken); + socket.emit('joinSetting'); + + await new Promise((resolve) => { + socket.once('setting', (data) => { + const { action, domain } = data; + expect(domain).toBe('setting'); + expect(action).toBe('init'); + resolve(); + }); + }); + + return socket; + } + + function closePage(socket: Socket) { + socket.close(); + } +}); + +async function submitJoinRequest( + leaderAccessToken: string, + projectId: number, + requestingAccessToken: string, +): Promise { + const inviteLinkId = await getProjectLinkId(leaderAccessToken, projectId); + return request(app.getHttpServer()) + .post('/api/project/join-request') + .set('Authorization', `Bearer ${requestingAccessToken}`) + .send({ inviteLinkId }); +} + +async function expectNotifyJoinRequest( + socket: Socket, + requestingMember: Member, +) { + return new Promise((resolve) => { + socket.on('setting', (data) => { + const { action, domain, content } = data; + expect(domain).toBe('joinRequest'); + expect(action).toBe('create'); + expect(content.joinRequest).toBeDefined(); + expect(content.joinRequest.id).toBeDefined(); + expect(content.joinRequest.memberId).toBe(requestingMember.id); + expect(content.joinRequest.username).toBe(requestingMember.username); + expect(content.joinRequest.imageUrl).toBe( + requestingMember.github_image_url, + ); + resolve(); + }); + }); +}