Skip to content

Commit b17a14e

Browse files
committed
feat: 프로젝트 참여요청시 해당 프로젝트의 설정 페이지에 접속한 회원에게 프로젝트 참여요청 알림 구현
- 서비스 - 프로젝트 참여 요청 메서드가 참여요청 엔티티를 반환하도록 수정 - 컨트롤러 - 프로젝트 참여요청 후 참여알림 메서드를 호출하도록 수정 - 게이트웨이 - 프로젝트 참여 알림 메서드 구현 - E2E 테스트 - 프로젝트 참여 요청 시 설정 페이지에 접속한 회원에게 알림이 가는지 확인하는 E2E 테스트 추가
1 parent 992cb75 commit b17a14e

File tree

5 files changed

+191
-6
lines changed

5 files changed

+191
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Member } from 'src/member/entity/member.entity';
2+
import { ProjectJoinRequest } from 'src/project/entity/project-join-request.entity';
3+
4+
class JoinRequestDto {
5+
id: number;
6+
memberId: number;
7+
username: string;
8+
imageUrl: string;
9+
10+
static of(
11+
projectJoinRequest: ProjectJoinRequest,
12+
member: Member,
13+
): JoinRequestDto {
14+
const dto = new JoinRequestDto();
15+
dto.id = projectJoinRequest.id;
16+
dto.memberId = member.id;
17+
dto.username = member.username;
18+
dto.imageUrl = member.github_image_url;
19+
return dto;
20+
}
21+
}
22+
23+
class ContentDto {
24+
joinRequest: JoinRequestDto;
25+
26+
static of(joinRequest: ProjectJoinRequest, member: Member): ContentDto {
27+
const dto = new ContentDto();
28+
dto.joinRequest = JoinRequestDto.of(joinRequest, member);
29+
return dto;
30+
}
31+
}
32+
33+
export class CreateJoinRequestNotifyDto {
34+
domain: string;
35+
action: string;
36+
content: ContentDto;
37+
38+
static of(
39+
joinRequest: ProjectJoinRequest,
40+
member: Member,
41+
): CreateJoinRequestNotifyDto {
42+
const dto = new CreateJoinRequestNotifyDto();
43+
dto.domain = 'joinRequest';
44+
dto.action = 'create';
45+
dto.content = ContentDto.of(joinRequest, member);
46+
return dto;
47+
}
48+
}

backend/src/project/project.controller.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,13 @@ export class ProjectController {
9393
@Res() response: Response,
9494
) {
9595
try {
96-
await this.projectService.createProjectJoinRequest(
97-
body.inviteLinkId,
96+
const projectJoinRequest =
97+
await this.projectService.createProjectJoinRequest(
98+
body.inviteLinkId,
99+
request.member,
100+
);
101+
this.projectWebsocketGateway.notifyCreateJoinRequestToSettingPage(
102+
projectJoinRequest,
98103
request.member,
99104
);
100105
} catch (e) {

backend/src/project/service/project.service.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export class ProjectService {
154154
async createProjectJoinRequest(
155155
inviteLinkId: string,
156156
member: Member,
157-
): Promise<void> {
157+
): Promise<ProjectJoinRequest> {
158158
const project = await this.getProjectByLinkId(inviteLinkId);
159159
if (!project) throw new Error('Project not found');
160160
const isProjectMember = await this.isProjectMember(project.id, member);
@@ -164,9 +164,11 @@ export class ProjectService {
164164
project.id,
165165
member.id,
166166
);
167-
await this.projectRepository.createProjectJoinRequest(
168-
newProjectJoinRequest,
169-
);
167+
const projectJoinRequest =
168+
await this.projectRepository.createProjectJoinRequest(
169+
newProjectJoinRequest,
170+
);
171+
return projectJoinRequest;
170172
} catch (e) {
171173
if (e.message === 'DUPLICATED PROJECT ID AND MEMBER ID') {
172174
throw new Error('Join request already submitted');

backend/src/project/websocket.gateway.ts

+18
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { WsProjectStoryController } from './ws-controller/ws-project-story.contr
2323
import { WsProjectTaskController } from './ws-controller/ws-project-task.controller';
2424
import { WsProjectInfoController } from './ws-controller/ws-project-info.controller';
2525
import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller';
26+
import { ProjectJoinRequest } from './entity/project-join-request.entity';
27+
import { CreateJoinRequestNotifyDto } from './dto/setting-page/CreateJoinRequestNotify.dto';
2628

2729
@WebSocketGateway({
2830
namespace: /project-\d+/,
@@ -212,6 +214,22 @@ export class ProjectWebsocketGateway
212214
this.namespaceMap.delete(projectId);
213215
}
214216

217+
notifyCreateJoinRequestToSettingPage(
218+
projectJoinRequest: ProjectJoinRequest,
219+
member: Member,
220+
) {
221+
const projectNamespace = this.namespaceMap.get(
222+
projectJoinRequest.projectId,
223+
);
224+
if (!projectNamespace) return;
225+
projectNamespace
226+
.to('setting')
227+
.emit(
228+
'setting',
229+
CreateJoinRequestNotifyDto.of(projectJoinRequest, member),
230+
);
231+
}
232+
215233
notifyJoinToConnectedMembers(projectId: number, member: Member) {
216234
const projectNamespace = this.namespaceMap.get(projectId);
217235
if (!projectNamespace) return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Socket } from 'socket.io-client';
2+
import * as request from 'supertest';
3+
import {
4+
app,
5+
appInit,
6+
connectServer,
7+
createMember,
8+
createProject,
9+
getMemberByAccessToken,
10+
getProjectLinkId,
11+
listenAppAndSetPortEnv,
12+
memberFixture,
13+
memberFixture2,
14+
projectPayload,
15+
} from 'test/setup';
16+
import { Member } from 'src/member/entity/member.entity';
17+
18+
describe('WS Setting', () => {
19+
beforeEach(async () => {
20+
await app.close();
21+
await appInit();
22+
await listenAppAndSetPortEnv(app);
23+
});
24+
25+
it('should notify join request in setting page when project join request is submitted', async () => {
26+
const { accessToken: leaderAccessToken } = await createMember(
27+
memberFixture,
28+
app,
29+
);
30+
31+
const { id: projectId } = await createProject(
32+
leaderAccessToken,
33+
projectPayload,
34+
app,
35+
);
36+
const leaderSocket = await enterSettingPage(projectId, leaderAccessToken);
37+
38+
const { accessToken: requestingAccessToken } = await createMember(
39+
memberFixture2,
40+
app,
41+
);
42+
43+
const requestingMember = await getMemberByAccessToken(
44+
requestingAccessToken,
45+
);
46+
47+
const expectPromise = expectNotifyJoinRequest(
48+
leaderSocket,
49+
requestingMember,
50+
);
51+
const submitPromise = submitJoinRequest(
52+
leaderAccessToken,
53+
projectId,
54+
requestingAccessToken,
55+
);
56+
await Promise.all([expectPromise, submitPromise]);
57+
closePage(leaderSocket);
58+
});
59+
60+
async function enterSettingPage(projectId: number, accessToken: string) {
61+
const socket = connectServer(projectId, accessToken);
62+
socket.emit('joinSetting');
63+
64+
await new Promise<void>((resolve) => {
65+
socket.once('setting', (data) => {
66+
const { action, domain } = data;
67+
expect(domain).toBe('setting');
68+
expect(action).toBe('init');
69+
resolve();
70+
});
71+
});
72+
73+
return socket;
74+
}
75+
76+
function closePage(socket: Socket) {
77+
socket.close();
78+
}
79+
});
80+
81+
async function submitJoinRequest(
82+
leaderAccessToken: string,
83+
projectId: number,
84+
requestingAccessToken: string,
85+
): Promise<request.Response> {
86+
const inviteLinkId = await getProjectLinkId(leaderAccessToken, projectId);
87+
return request(app.getHttpServer())
88+
.post('/api/project/join-request')
89+
.set('Authorization', `Bearer ${requestingAccessToken}`)
90+
.send({ inviteLinkId });
91+
}
92+
93+
async function expectNotifyJoinRequest(
94+
socket: Socket,
95+
requestingMember: Member,
96+
) {
97+
return new Promise<void>((resolve) => {
98+
socket.on('setting', (data) => {
99+
const { action, domain, content } = data;
100+
expect(domain).toBe('joinRequest');
101+
expect(action).toBe('create');
102+
expect(content.joinRequest).toBeDefined();
103+
expect(content.joinRequest.id).toBeDefined();
104+
expect(content.joinRequest.memberId).toBe(requestingMember.id);
105+
expect(content.joinRequest.username).toBe(requestingMember.username);
106+
expect(content.joinRequest.imageUrl).toBe(
107+
requestingMember.github_image_url,
108+
);
109+
resolve();
110+
});
111+
});
112+
}

0 commit comments

Comments
 (0)