Skip to content

Commit 6dfc523

Browse files
committed
feat: 프로젝트 삭제 API 구현
- 게이트웨이 - 프로젝트 삭제관련 라우팅 로직 추가 - 프로젝트 삭제시 해당 프로젝트의 네임스페이스 Map 삭제하도록 구현 - 컨트롤러 - 프로젝트 삭제 메서드 추가 - 삭제 알림 보내고 1초 후 프로젝트 삭제하는 로직 구현 - 서비스 - 프로젝트 삭제 메서드 추가 - 레포지토리 - 프로젝트 삭제 메서드 추가 - 프로젝트 삭제 E2E 테스트 추가
1 parent 3916432 commit 6dfc523

File tree

7 files changed

+188
-0
lines changed

7 files changed

+188
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class ProjectDeleteNotifyDto {
2+
domain: string;
3+
action: string;
4+
content: Record<string, string>;
5+
6+
static of() {
7+
const dto = new ProjectDeleteNotifyDto();
8+
dto.domain = 'projectInfo';
9+
dto.action = 'delete';
10+
dto.content = {};
11+
return dto;
12+
}
13+
}
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 ProjectDeleteRequestDto {
4+
@Matches(/^delete$/)
5+
action: string;
6+
7+
@IsNotEmpty()
8+
content: Record<string, string>;
9+
}

backend/src/project/project.repository.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export class ProjectRepository {
3737
return this.projectRepository.save(project);
3838
}
3939

40+
async delete(projectId: number) {
41+
const result = await this.projectRepository.delete({ id: projectId });
42+
return result.affected ? result.affected : 0;
43+
}
44+
4045
async updateProjectInfo(
4146
project: Project,
4247
title: string,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ export class ProjectService {
3939
return this.projectRepository.updateProjectInfo(project, title, subject);
4040
}
4141

42+
async deleteProject(projectId: number, member: Member): Promise<boolean> {
43+
if (!(await this.isProjectLeader(projectId, member))) {
44+
throw new Error('Member is not the project leader');
45+
}
46+
const affected = await this.projectRepository.delete(projectId);
47+
if (affected === 0) return false;
48+
return true;
49+
}
50+
4251
async getProjectList(member: Member): Promise<Project[]> {
4352
return await this.projectRepository.getProjectList(member);
4453
}

backend/src/project/websocket.gateway.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,16 @@ export class ProjectWebsocketGateway
202202
) {
203203
if (data.action === 'update') {
204204
this.wsProjectInfoController.updateProjectInfo(client, data);
205+
} else if (data.action === 'delete') {
206+
this.wsProjectInfoController.deleteProject(client, data);
207+
this.deleteProjectFromNamespaceMap(client.project.id);
205208
}
206209
}
207210

211+
deleteProjectFromNamespaceMap(projectId: number) {
212+
this.namespaceMap.delete(projectId);
213+
}
214+
208215
notifyJoinToConnectedMembers(projectId: number, member: Member) {
209216
const projectNamespace = this.namespaceMap.get(projectId);
210217
if (!projectNamespace) return;

backend/src/project/ws-controller/ws-project-info.controller.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { getRecursiveErrorMsgList } from '../util/validation.util';
66
import { ProjectService } from '../service/project.service';
77
import { ProjectInfoUpdateRequestDto } from '../dto/project-info/ProjectInfoUpdateRequest.dto';
88
import { ProjectInfoUpdateNotifyDto } from '../dto/project-info/ProjectInfoUpdateNotify.dto';
9+
import { ProjectDeleteRequestDto } from '../dto/project-info/ProjectDeleteRequest.dto';
10+
import { ProjectDeleteNotifyDto } from '../dto/project-info/ProjectDeleteNotify.dto';
911

1012
@Injectable()
1113
export class WsProjectInfoController {
@@ -41,4 +43,38 @@ export class WsProjectInfoController {
4143
} else throw e;
4244
}
4345
}
46+
47+
async deleteProject(client: ClientSocket, data: any) {
48+
const errors = await validate(plainToClass(ProjectDeleteRequestDto, data));
49+
if (errors.length > 0) {
50+
const errorList = getRecursiveErrorMsgList(errors);
51+
client.emit('error', { errorList });
52+
return;
53+
}
54+
const isLeader = await this.projectService.isProjectLeader(
55+
client.project.id,
56+
client.member,
57+
);
58+
if (!isLeader) {
59+
client.disconnect();
60+
return;
61+
}
62+
client.nsp.emit('main', ProjectDeleteNotifyDto.of());
63+
//TODO: 프로젝트가 조회되지 않게 함
64+
await this.waitNSecond(1);
65+
const isDeleted = await this.projectService.deleteProject(
66+
client.project.id,
67+
client.member,
68+
);
69+
if (!isDeleted) throw new Error('Project Not Deleted');
70+
client.nsp.disconnectSockets(true);
71+
}
72+
73+
private waitNSecond(N: number): Promise<void> {
74+
return new Promise<void>((resolve) => {
75+
setTimeout(() => {
76+
resolve();
77+
}, N * 1000);
78+
});
79+
}
4480
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 { handleConnectErrorWithReject } from '../ws-common';
15+
import { Socket } from 'socket.io-client';
16+
17+
describe('WS project', () => {
18+
beforeEach(async () => {
19+
await app.close();
20+
await appInit();
21+
await listenAppAndSetPortEnv(app);
22+
});
23+
describe('delete project', () => {
24+
it('should return deleted project event when leader request', async () => {
25+
let socket1: Socket;
26+
let socket2: Socket;
27+
28+
await new Promise<void>(async (resolve, reject) => {
29+
const accessToken1 = (await createMember(memberFixture, app))
30+
.accessToken;
31+
const project = await createProject(accessToken1, projectPayload, app);
32+
const projectLinkId = await getProjectLinkId(accessToken1, project.id);
33+
34+
const accessToken2 = (await createMember(memberFixture2, app))
35+
.accessToken;
36+
await joinProject(accessToken2, projectLinkId);
37+
38+
socket1 = connectServer(project.id, accessToken1);
39+
handleConnectErrorWithReject(socket1, reject);
40+
await joinSettingPage(socket1);
41+
42+
socket2 = connectServer(project.id, accessToken2);
43+
handleConnectErrorWithReject(socket2, reject);
44+
await joinLandingPage(socket2);
45+
46+
socket1.emit('projectInfo', {
47+
action: 'delete',
48+
content: {},
49+
});
50+
51+
await Promise.all([
52+
expectDeleteProject(socket1, 'main'),
53+
expectDeleteProject(socket2, 'main'),
54+
]);
55+
56+
await Promise.all([
57+
expectCloseSocket(socket1),
58+
expectCloseSocket(socket2),
59+
]);
60+
resolve();
61+
}).finally(() => {
62+
socket1.close();
63+
socket2.close();
64+
});
65+
});
66+
67+
const expectDeleteProject = (socket: Socket, eventPage: string) => {
68+
return new Promise<void>((resolve) => {
69+
socket.on(eventPage, (data) => {
70+
const { action, domain } = data;
71+
if (domain === 'projectInfo' && action === 'delete') {
72+
resolve();
73+
}
74+
});
75+
});
76+
};
77+
const expectCloseSocket = (socket: Socket) => {
78+
return new Promise<void>((resolve) => {
79+
socket.on('disconnect', () => {
80+
resolve();
81+
});
82+
});
83+
};
84+
});
85+
});
86+
87+
const joinSettingPage = (socket: Socket) => {
88+
return new Promise<void>((resolve) => {
89+
socket.emit('joinSetting');
90+
socket.once('setting', (data) => {
91+
const { action, domain } = data;
92+
if (domain === 'setting' && action === 'init') {
93+
resolve();
94+
}
95+
});
96+
});
97+
};
98+
99+
const joinLandingPage = (socket: Socket) => {
100+
return new Promise<void>((resolve) => {
101+
socket.emit('joinLanding');
102+
socket.once('landing', (data) => {
103+
const { action, domain } = data;
104+
if (domain === 'landing' && action === 'init') {
105+
resolve();
106+
}
107+
});
108+
});
109+
};

0 commit comments

Comments
 (0)