Skip to content

Commit

Permalink
feat: 프로젝트 삭제 API 구현
Browse files Browse the repository at this point in the history
- 게이트웨이
  - 프로젝트 삭제관련 라우팅 로직 추가
  - 프로젝트 삭제시 해당 프로젝트의 네임스페이스 Map 삭제하도록 구현
- 컨트롤러
  - 프로젝트 삭제 메서드 추가
  - 삭제 알림 보내고 1초 후 프로젝트 삭제하는 로직 구현
- 서비스
  - 프로젝트 삭제 메서드 추가
- 레포지토리
  - 프로젝트 삭제 메서드 추가
- 프로젝트 삭제 E2E 테스트 추가
  • Loading branch information
choyoungwoo9 committed Sep 22, 2024
1 parent 3916432 commit 6dfc523
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 0 deletions.
13 changes: 13 additions & 0 deletions backend/src/project/dto/project-info/ProjectDeleteNotify.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class ProjectDeleteNotifyDto {
domain: string;
action: string;
content: Record<string, string>;

static of() {
const dto = new ProjectDeleteNotifyDto();
dto.domain = 'projectInfo';
dto.action = 'delete';
dto.content = {};
return dto;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsNotEmpty, Matches } from 'class-validator';

export class ProjectDeleteRequestDto {
@Matches(/^delete$/)
action: string;

@IsNotEmpty()
content: Record<string, string>;
}
5 changes: 5 additions & 0 deletions backend/src/project/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export class ProjectRepository {
return this.projectRepository.save(project);
}

async delete(projectId: number) {
const result = await this.projectRepository.delete({ id: projectId });
return result.affected ? result.affected : 0;
}

async updateProjectInfo(
project: Project,
title: string,
Expand Down
9 changes: 9 additions & 0 deletions backend/src/project/service/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export class ProjectService {
return this.projectRepository.updateProjectInfo(project, title, subject);
}

async deleteProject(projectId: number, member: Member): Promise<boolean> {
if (!(await this.isProjectLeader(projectId, member))) {
throw new Error('Member is not the project leader');
}
const affected = await this.projectRepository.delete(projectId);
if (affected === 0) return false;
return true;
}

async getProjectList(member: Member): Promise<Project[]> {
return await this.projectRepository.getProjectList(member);
}
Expand Down
7 changes: 7 additions & 0 deletions backend/src/project/websocket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,16 @@ export class ProjectWebsocketGateway
) {
if (data.action === 'update') {
this.wsProjectInfoController.updateProjectInfo(client, data);
} else if (data.action === 'delete') {
this.wsProjectInfoController.deleteProject(client, data);
this.deleteProjectFromNamespaceMap(client.project.id);
}
}

deleteProjectFromNamespaceMap(projectId: number) {
this.namespaceMap.delete(projectId);
}

notifyJoinToConnectedMembers(projectId: number, member: Member) {
const projectNamespace = this.namespaceMap.get(projectId);
if (!projectNamespace) return;
Expand Down
36 changes: 36 additions & 0 deletions backend/src/project/ws-controller/ws-project-info.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { getRecursiveErrorMsgList } from '../util/validation.util';
import { ProjectService } from '../service/project.service';
import { ProjectInfoUpdateRequestDto } from '../dto/project-info/ProjectInfoUpdateRequest.dto';
import { ProjectInfoUpdateNotifyDto } from '../dto/project-info/ProjectInfoUpdateNotify.dto';
import { ProjectDeleteRequestDto } from '../dto/project-info/ProjectDeleteRequest.dto';
import { ProjectDeleteNotifyDto } from '../dto/project-info/ProjectDeleteNotify.dto';

@Injectable()
export class WsProjectInfoController {
Expand Down Expand Up @@ -41,4 +43,38 @@ export class WsProjectInfoController {
} else throw e;
}
}

async deleteProject(client: ClientSocket, data: any) {
const errors = await validate(plainToClass(ProjectDeleteRequestDto, data));
if (errors.length > 0) {
const errorList = getRecursiveErrorMsgList(errors);
client.emit('error', { errorList });
return;
}
const isLeader = await this.projectService.isProjectLeader(
client.project.id,
client.member,
);
if (!isLeader) {
client.disconnect();
return;
}
client.nsp.emit('main', ProjectDeleteNotifyDto.of());
//TODO: 프로젝트가 조회되지 않게 함
await this.waitNSecond(1);
const isDeleted = await this.projectService.deleteProject(
client.project.id,
client.member,
);
if (!isDeleted) throw new Error('Project Not Deleted');
client.nsp.disconnectSockets(true);
}

private waitNSecond(N: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, N * 1000);
});
}
}
109 changes: 109 additions & 0 deletions backend/test/project/ws-setting-page/ws-delete-project.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
app,
appInit,
connectServer,
createMember,
createProject,
getProjectLinkId,
joinProject,
listenAppAndSetPortEnv,
memberFixture,
memberFixture2,
projectPayload,
} from 'test/setup';
import { handleConnectErrorWithReject } from '../ws-common';
import { Socket } from 'socket.io-client';

describe('WS project', () => {
beforeEach(async () => {
await app.close();
await appInit();
await listenAppAndSetPortEnv(app);
});
describe('delete project', () => {
it('should return deleted project event when leader request', async () => {
let socket1: Socket;
let socket2: Socket;

await new Promise<void>(async (resolve, reject) => {
const accessToken1 = (await createMember(memberFixture, app))
.accessToken;
const project = await createProject(accessToken1, projectPayload, app);
const projectLinkId = await getProjectLinkId(accessToken1, project.id);

const accessToken2 = (await createMember(memberFixture2, app))
.accessToken;
await joinProject(accessToken2, projectLinkId);

socket1 = connectServer(project.id, accessToken1);
handleConnectErrorWithReject(socket1, reject);
await joinSettingPage(socket1);

socket2 = connectServer(project.id, accessToken2);
handleConnectErrorWithReject(socket2, reject);
await joinLandingPage(socket2);

socket1.emit('projectInfo', {
action: 'delete',
content: {},
});

await Promise.all([
expectDeleteProject(socket1, 'main'),
expectDeleteProject(socket2, 'main'),
]);

await Promise.all([
expectCloseSocket(socket1),
expectCloseSocket(socket2),
]);
resolve();
}).finally(() => {
socket1.close();
socket2.close();
});
});

const expectDeleteProject = (socket: Socket, eventPage: string) => {
return new Promise<void>((resolve) => {
socket.on(eventPage, (data) => {
const { action, domain } = data;
if (domain === 'projectInfo' && action === 'delete') {
resolve();
}
});
});
};
const expectCloseSocket = (socket: Socket) => {
return new Promise<void>((resolve) => {
socket.on('disconnect', () => {
resolve();
});
});
};
});
});

const joinSettingPage = (socket: Socket) => {
return new Promise<void>((resolve) => {
socket.emit('joinSetting');
socket.once('setting', (data) => {
const { action, domain } = data;
if (domain === 'setting' && action === 'init') {
resolve();
}
});
});
};

const joinLandingPage = (socket: Socket) => {
return new Promise<void>((resolve) => {
socket.emit('joinLanding');
socket.once('landing', (data) => {
const { action, domain } = data;
if (domain === 'landing' && action === 'init') {
resolve();
}
});
});
};

0 comments on commit 6dfc523

Please sign in to comment.