Skip to content

Commit 3916432

Browse files
committed
feat: 초대링크 변경 API 구현
- 컨트롤러, 서비스, 레포지토리에서 초대링크 업데이트 메서드 구현 - 리더인 경우, 리더가 아닌경우 각각에 대한 초대링크 업데이트 E2E 테스트 추가
1 parent c9aa172 commit 3916432

File tree

8 files changed

+198
-0
lines changed

8 files changed

+198
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IsNotEmpty, Matches } from 'class-validator';
2+
3+
export class InviteLinkUpdateRequestDto {
4+
@Matches(/^update$/)
5+
action: string;
6+
7+
@IsNotEmpty()
8+
content: Record<string, any>;
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class inviteLinkDto {
2+
inviteLinkId: string;
3+
4+
static of(inviteLinkId: string): inviteLinkDto {
5+
const dto = new inviteLinkDto();
6+
dto.inviteLinkId = inviteLinkId;
7+
return dto;
8+
}
9+
}
10+
11+
export class InviteLinkUpdateResponseDto {
12+
domain: string;
13+
action: string;
14+
content: inviteLinkDto;
15+
16+
static of(inviteLinkId: string): InviteLinkUpdateResponseDto {
17+
const dto = new InviteLinkUpdateResponseDto();
18+
dto.domain = 'inviteLink';
19+
dto.action = 'update';
20+
dto.content = inviteLinkDto.of(inviteLinkId);
21+
return dto;
22+
}
23+
}

backend/src/project/project.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { WsProjectStoryController } from './ws-controller/ws-project-story.contr
2323
import { Task } from './entity/task.entity';
2424
import { WsProjectTaskController } from './ws-controller/ws-project-task.controller';
2525
import { WsProjectInfoController } from './ws-controller/ws-project-info.controller';
26+
import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller';
2627

2728
@Module({
2829
imports: [
@@ -53,6 +54,7 @@ import { WsProjectInfoController } from './ws-controller/ws-project-info.control
5354
WsProjectStoryController,
5455
WsProjectTaskController,
5556
WsProjectInfoController,
57+
WsProjectInviteLinkController,
5658
],
5759
})
5860
export class ProjectModule {}

backend/src/project/project.repository.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ export class ProjectRepository {
7676
});
7777
}
7878

79+
async updateInviteLink(
80+
projectId: number,
81+
newInviteLinkId: string,
82+
): Promise<boolean> {
83+
const result = await this.projectRepository.update(
84+
{ id: projectId },
85+
{ inviteLinkId: newInviteLinkId },
86+
);
87+
return !!result.affected;
88+
}
89+
7990
getProject(projectId: number): Promise<Project | null> {
8091
return this.projectRepository.findOne({ where: { id: projectId } });
8192
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Story, StoryStatus } from '../entity/story.entity';
1010
import { Task, TaskStatus } from '../entity/task.entity';
1111
import { LexoRank } from 'lexorank';
1212
import { MemberRole } from '../enum/MemberRole.enum';
13+
import { v4 as uuidv4 } from 'uuid';
1314

1415
@Injectable()
1516
export class ProjectService {
@@ -51,6 +52,19 @@ export class ProjectService {
5152
return project;
5253
}
5354

55+
async updateInviteLink(projectId: number, member: Member): Promise<string> {
56+
if (!(await this.isProjectLeader(projectId, member))) {
57+
throw new Error('Member is not the project leader');
58+
}
59+
const newInviteLinkId = await uuidv4();
60+
const isUpdated = await this.projectRepository.updateInviteLink(
61+
projectId,
62+
newInviteLinkId,
63+
);
64+
if (!isUpdated) throw new Error('invite link not updated');
65+
return newInviteLinkId;
66+
}
67+
5468
async getProject(projectId: number, member: Member): Promise<Project | null> {
5569
if (!(await this.isExistProject(projectId)))
5670
throw new Error('Project not found');

backend/src/project/websocket.gateway.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { WsProjectEpicController } from './ws-controller/ws-project-epic.control
2222
import { WsProjectStoryController } from './ws-controller/ws-project-story.controller';
2323
import { WsProjectTaskController } from './ws-controller/ws-project-task.controller';
2424
import { WsProjectInfoController } from './ws-controller/ws-project-info.controller';
25+
import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller';
2526

2627
@WebSocketGateway({
2728
namespace: /project-\d+/,
@@ -42,6 +43,7 @@ export class ProjectWebsocketGateway
4243
private readonly wsProjectStoryController: WsProjectStoryController,
4344
private readonly wsProjectTaskController: WsProjectTaskController,
4445
private readonly wsProjectInfoController: WsProjectInfoController,
46+
private readonly wsProjectInviteLinkController: WsProjectInviteLinkController,
4547
) {
4648
this.namespaceMap = new Map();
4749
}
@@ -132,6 +134,16 @@ export class ProjectWebsocketGateway
132134
}
133135
}
134136

137+
@SubscribeMessage('inviteLink')
138+
async handleInviteLinkEvent(
139+
@ConnectedSocket() client: ClientSocket,
140+
@MessageBody() data: any,
141+
) {
142+
if (data.action === 'update') {
143+
this.wsProjectInviteLinkController.updateInviteLink(client, data);
144+
}
145+
}
146+
135147
@SubscribeMessage('joinBacklog')
136148
async handleJoinBacklogEvent(@ConnectedSocket() client: ClientSocket) {
137149
this.wsProjectController.joinBacklogPage(client);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ProjectService } from '../service/project.service';
3+
import { ClientSocket } from '../type/ClientSocket.type';
4+
import { validate } from 'class-validator';
5+
import { plainToClass } from 'class-transformer';
6+
import { getRecursiveErrorMsgList } from '../util/validation.util';
7+
import { InviteLinkUpdateRequestDto } from '../dto/invite-link/InviteLinkUpdateRequest.dto';
8+
import { InviteLinkUpdateResponseDto } from '../dto/invite-link/InviteLinkUpdateResponse.dto';
9+
10+
@Injectable()
11+
export class WsProjectInviteLinkController {
12+
constructor(private readonly projectService: ProjectService) {}
13+
async updateInviteLink(client: ClientSocket, data: any) {
14+
const errors = await validate(
15+
plainToClass(InviteLinkUpdateRequestDto, data),
16+
);
17+
if (errors.length > 0) {
18+
const errorList = getRecursiveErrorMsgList(errors);
19+
client.emit('error', { errorList });
20+
return;
21+
}
22+
try {
23+
const newInviteLinkId = await this.projectService.updateInviteLink(
24+
client.project.id,
25+
client.member,
26+
);
27+
client.emit('landing', InviteLinkUpdateResponseDto.of(newInviteLinkId));
28+
} catch (e) {
29+
if (e.message === 'Member is not the project leader') {
30+
client.disconnect(true);
31+
} else throw e;
32+
}
33+
}
34+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
app,
3+
appInit,
4+
connectServer,
5+
createMember,
6+
createProject,
7+
getProjectLinkId,
8+
joinProject,
9+
listenAppAndSetPortEnv,
10+
memberFixture,
11+
memberFixture2,
12+
projectPayload,
13+
} from 'test/setup';
14+
import {
15+
emitJoinLanding,
16+
handleConnectErrorWithReject,
17+
handleErrorWithReject,
18+
initLanding,
19+
} from '../ws-common';
20+
21+
describe('WS invite link', () => {
22+
beforeEach(async () => {
23+
await app.close();
24+
await appInit();
25+
await listenAppAndSetPortEnv(app);
26+
});
27+
describe('update invite link', () => {
28+
it('should return updated invite link data when project leader request', async () => {
29+
let socket;
30+
return new Promise<void>(async (resolve, reject) => {
31+
const accessToken = (await createMember(memberFixture, app))
32+
.accessToken;
33+
const project = await createProject(accessToken, projectPayload, app);
34+
socket = connectServer(project.id, accessToken);
35+
handleConnectErrorWithReject(socket, reject);
36+
handleErrorWithReject(socket, reject);
37+
await emitJoinLanding(socket);
38+
await initLanding(socket);
39+
const data = {
40+
action: 'update',
41+
content: {},
42+
};
43+
socket.emit('inviteLink', data);
44+
await expectUpdateInviteLink(socket);
45+
resolve();
46+
}).finally(() => {
47+
socket.close();
48+
});
49+
});
50+
const expectUpdateInviteLink = async (socket) => {
51+
return await new Promise<void>((res) => {
52+
socket.once('landing', async (data) => {
53+
const { content, action, domain } = data;
54+
expect(domain).toBe('inviteLink');
55+
expect(action).toBe('update');
56+
expect(content.inviteLinkId).toBeDefined();
57+
expect(typeof content.inviteLinkId).toBe('string');
58+
res();
59+
});
60+
});
61+
};
62+
63+
it('should disconnect if the requester is not the project leader', async () => {
64+
let socket;
65+
return new Promise<void>(async (resolve, reject) => {
66+
const accessToken = (await createMember(memberFixture, app))
67+
.accessToken;
68+
const project = await createProject(accessToken, projectPayload, app);
69+
const projectLinkId = await getProjectLinkId(accessToken, project.id);
70+
71+
const accessToken2 = (await createMember(memberFixture2, app))
72+
.accessToken;
73+
await joinProject(accessToken2, projectLinkId);
74+
75+
socket = connectServer(project.id, accessToken2);
76+
handleConnectErrorWithReject(socket, reject);
77+
handleErrorWithReject(socket, reject);
78+
await emitJoinLanding(socket);
79+
await initLanding(socket);
80+
const data = {
81+
action: 'update',
82+
content: {},
83+
};
84+
socket.emit('inviteLink', data);
85+
socket.on('disconnect', () => {
86+
resolve();
87+
});
88+
}).finally(() => {
89+
socket.close();
90+
});
91+
});
92+
});
93+
});

0 commit comments

Comments
 (0)