Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 설정 페이지 API에 프로젝트 참여요청 정보 추가 #341

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
35 changes: 32 additions & 3 deletions backend/src/project/dto/setting-page/InitSettingResponse.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Member } from 'src/member/entity/member.entity';
import { ProjectJoinRequest } from 'src/project/entity/project-join-request.entity';
import { Project } from 'src/project/entity/project.entity';
import { MemberRole } from 'src/project/enum/MemberRole.enum';

Expand Down Expand Up @@ -30,14 +31,38 @@ class ProjectDto {
}
}

class JoinRequestListDto {
id: number;
memberId: number;
username: string;
imageUrl: string;

static of(projectJoinRequest: ProjectJoinRequest): JoinRequestListDto {
const dto = new JoinRequestListDto();
dto.id = projectJoinRequest.id;
dto.memberId = projectJoinRequest.memberId;
dto.username = projectJoinRequest.member.username;
dto.imageUrl = projectJoinRequest.member.github_image_url;
return dto;
}
}

class ProjectInfoDto {
project: ProjectDto;
member: ProjectMemberDto[];
joinRequestList: JoinRequestListDto[];

static of(project: Project, memberList: Member[]): ProjectInfoDto {
static of(
project: Project,
memberList: Member[],
joinRequestList: ProjectJoinRequest[],
): ProjectInfoDto {
const dto = new ProjectInfoDto();
dto.project = ProjectDto.of(project);
dto.member = memberList.map((member) => ProjectMemberDto.of(member));
dto.joinRequestList = joinRequestList.map((joinRequest) =>
JoinRequestListDto.of(joinRequest),
);
return dto;
}
}
Expand All @@ -47,11 +72,15 @@ export class InitSettingResponseDto {
action: string;
content: ProjectInfoDto;

static of(project: Project, memberList: Member[]): InitSettingResponseDto {
static of(
project: Project,
memberList: Member[],
joinRequestList: ProjectJoinRequest[],
): InitSettingResponseDto {
const dto = new InitSettingResponseDto();
dto.domain = 'setting';
dto.action = 'init';
dto.content = ProjectInfoDto.of(project, memberList);
dto.content = ProjectInfoDto.of(project, memberList, joinRequestList);
return dto;
}
}
9 changes: 7 additions & 2 deletions backend/src/project/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions backend/src/project/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ export class ProjectRepository {
}
}

async getProjectJoinRequestListWithMember(
projectId: number,
): Promise<ProjectJoinRequest[]> {
return this.projectJoinRequestRepository.find({
where: { projectId },
relations: { member: true },
});
}

getProject(projectId: number): Promise<Project | null> {
return this.projectRepository.findOne({ where: { id: projectId } });
}
Expand Down
25 changes: 21 additions & 4 deletions backend/src/project/service/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class ProjectService {
async createProjectJoinRequest(
inviteLinkId: string,
member: Member,
): Promise<void> {
): Promise<ProjectJoinRequest> {
const project = await this.getProjectByLinkId(inviteLinkId);
if (!project) throw new Error('Project not found');
const isProjectMember = await this.isProjectMember(project.id, member);
Expand All @@ -164,16 +164,33 @@ 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');
}
}
}

async getProjectJoinRequestList(
projectId: number,
member: Member,
): Promise<ProjectJoinRequest[]> {
if (!(await this.isExistProject(projectId))) {
throw new Error('Project not found');
}
if (!(await this.isProjectLeader(projectId, member))) {
throw new Error('Member is not the project leader');
}
return this.projectRepository.getProjectJoinRequestListWithMember(
projectId,
);
}

async createMemo(project: Project, member: Member, color: memoColor) {
const newMemo = Memo.of(project, member, '', '', color);
return this.projectRepository.createMemo(newMemo);
Expand Down
18 changes: 18 additions & 0 deletions backend/src/project/websocket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+/,
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions backend/src/project/ws-controller/ws-project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,18 @@ export class WsProjectController {
client.leave('backlog');
client.join('setting');

const [project, projectMemberList] = await Promise.all([
const [project, projectMemberList, joinRequestList] = await Promise.all([
this.projectService.getProject(client.projectId, client.member),
this.projectService.getProjectMemberList(client.project),
this.projectService.getProjectJoinRequestList(
client.projectId,
client.member,
),
]);

client.emit(
'setting',
InitSettingResponseDto.of(project, projectMemberList),
InitSettingResponseDto.of(project, projectMemberList, joinRequestList),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 return join request data when leader enters setting page', async () => {
const { accessToken: leaderAccessToken } = await createMember(
memberFixture,
app,
);

const { id: projectId } = await createProject(
leaderAccessToken,
projectPayload,
app,
);

const { accessToken: requestingAccessToken } = await createMember(
memberFixture2,
app,
);

await submitJoinRequest(
leaderAccessToken,
projectId,
requestingAccessToken,
);
const requestingMember = await getMemberByAccessToken(
requestingAccessToken,
);

const leaderSocket = await enterSettingPage(projectId, leaderAccessToken);
await expectJoinRequestList(leaderSocket, requestingMember);
closePage(leaderSocket);
});

async function enterSettingPage(projectId: number, accessToken: string) {
const socket = connectServer(projectId, accessToken);
socket.emit('joinSetting');
return socket;
}

async function expectJoinRequestList(
socket: Socket,
requestingMember: Member,
) {
return new Promise<void>((resolve) => {
socket.on('setting', (data) => {
const { action, domain, content } = data;
expect(domain).toBe('setting');
expect(action).toBe('init');
expect(content.joinRequestList).toBeDefined();
expect(content.joinRequestList.length).toBe(1);
expect(content.joinRequestList[0].id).toBeDefined();
expect(content.joinRequestList[0].memberId).toBe(requestingMember.id);
expect(content.joinRequestList[0].username).toBe(
requestingMember.username,
);
expect(content.joinRequestList[0].imageUrl).toBe(
requestingMember.github_image_url,
);
resolve();
});
});
}

function closePage(socket: Socket) {
socket.close();
}
});

async function submitJoinRequest(
leaderAccessToken: string,
projectId: number,
requestingAccessToken: string,
): Promise<request.Response> {
const inviteLinkId = await getProjectLinkId(leaderAccessToken, projectId);
return request(app.getHttpServer())
.post('/api/project/join-request')
.set('Authorization', `Bearer ${requestingAccessToken}`)
.send({ inviteLinkId });
}
Loading
Loading