Skip to content

Commit

Permalink
feat: 초대링크로 프로젝트의 프리뷰 조회하는 API 구현
Browse files Browse the repository at this point in the history
- 컨트롤러
  - 프로젝트 초대 브리뷰 반환하는 메서드 구현
- 서비스
  - 초대링크로 프로젝트의 일부 정보를 반환하는 메서드 구현
- 레포지토리
  - 프로젝트, 프로젝트 회원을 조인해서 반환하는 메서드 구현
- 프로젝트 프리뷰 조회 E2E 테스트 추가
  • Loading branch information
choyoungwoo9 committed Sep 23, 2024
1 parent f39bfed commit a40ede1
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 0 deletions.
20 changes: 20 additions & 0 deletions backend/src/project/dto/ProjectInvitePreviewResponse.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class ProjectInvitePreviewResponseDto {
id: number;
title: string;
subject: string;
leaderUsername: string;

static of(
id: number,
title: string,
subject: string,
leaderUsername: string,
) {
const dto = new ProjectInvitePreviewResponseDto();
dto.id = id;
dto.title = title;
dto.subject = subject;
dto.leaderUsername = leaderUsername;
return dto;
}
}
20 changes: 20 additions & 0 deletions backend/src/project/dto/service/ProjectBriefInfo.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class ProjectBriefInfoDto {
id: number;
title: string;
subject: string;
leaderUsername: string;

static of(
id: number,
title: string,
subject: string,
leaderUsername: string,
) {
const dto = new ProjectBriefInfoDto();
dto.id = id;
dto.title = title;
dto.subject = subject;
dto.leaderUsername = leaderUsername;
return dto;
}
}
29 changes: 29 additions & 0 deletions backend/src/project/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Controller,
Get,
NotFoundException,
Param,
Post,
Req,
Res,
Expand All @@ -14,6 +15,7 @@ import { MemberRequest } from 'src/common/guard/authentication.guard';
import { JoinProjectRequestDto } from './dto/JoinProjectRequest.dto';
import { Response } from 'express';
import { ProjectWebsocketGateway } from './websocket.gateway';
import { ProjectInvitePreviewResponseDto } from './dto/ProjectInvitePreviewResponse.dto';

@Controller('project')
export class ProjectController {
Expand Down Expand Up @@ -82,4 +84,31 @@ export class ProjectController {
);
return response.status(201).send();
}

@Get('/invite-preview/:inviteLinkId')
async getProjectInvitePreview(
@Param('inviteLinkId') inviteLinkId: string,
@Res() response: Response,
) {
let projectPublicInfo;
try {
projectPublicInfo =
await this.projectService.getProjectBriefInfoByInviteLinkId(
inviteLinkId,
);
} catch (err) {
if (err.message === 'Project Not Found') throw new NotFoundException();
throw err;
}
return response
.status(200)
.send(
ProjectInvitePreviewResponseDto.of(
projectPublicInfo.id,
projectPublicInfo.title,
projectPublicInfo.subject,
projectPublicInfo.leaderUsername,
),
);
}
}
11 changes: 11 additions & 0 deletions backend/src/project/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ export class ProjectRepository {
);
}

async getProjectWithMemberListByLinkId(
inviteLinkId: string,
): Promise<Project> {
const projectWithMemberList = await this.projectRepository.findOne({
where: { inviteLinkId },
relations: { projectToMember: { member: true } },
});
if (!projectWithMemberList) throw new Error('Project Not Found');
return projectWithMemberList;
}

getProjectByLinkId(projectLinkId: string): Promise<Project | null> {
return this.projectRepository.findOne({
where: { inviteLinkId: projectLinkId },
Expand Down
22 changes: 22 additions & 0 deletions backend/src/project/service/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Task, TaskStatus } from '../entity/task.entity';
import { LexoRank } from 'lexorank';
import { MemberRole } from '../enum/MemberRole.enum';
import { v4 as uuidv4 } from 'uuid';
import { ProjectBriefInfoDto } from '../dto/service/ProjectBriefInfo.dto';

@Injectable()
export class ProjectService {
Expand Down Expand Up @@ -124,6 +125,27 @@ export class ProjectService {
return projectToMember?.role === MemberRole.LEADER;
}

async getProjectBriefInfoByInviteLinkId(
inviteLinkId: string,
): Promise<ProjectBriefInfoDto> {
const project =
await this.projectRepository.getProjectWithMemberListByLinkId(
inviteLinkId,
);
const leader = project.projectToMember.find(
(member) => member.role === MemberRole.LEADER,
);
if (!leader) {
throw new Error('Project does not have a leader');
}
return ProjectBriefInfoDto.of(
project.id,
project.title,
project.subject,
leader.member.username,
);
}

getProjectByLinkId(projectLinkId: string): Promise<Project | null> {
return this.projectRepository.getProjectByLinkId(projectLinkId);
}
Expand Down
91 changes: 91 additions & 0 deletions backend/test/project/get-project-preview.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as request from 'supertest';
import {
app,
appInit,
createMember,
createProject,
getProjectLinkId,
listenAppAndSetPortEnv,
memberFixture,
memberFixture2,
projectPayload,
} from 'test/setup';

describe('GET /project/invite-preview', () => {
beforeEach(async () => {
await app.close();
await appInit();
await listenAppAndSetPortEnv(app);
});

it('should return 200 when given valid invite link id', async () => {
const { accessToken } = await createMember(memberFixture, app);
const { id: projectId } = await createProject(
accessToken,
projectPayload,
app,
);
const inviteLinkId = await getProjectLinkId(accessToken, projectId);
const { accessToken: newAccessToken } = await createMember(
memberFixture2,
app,
);

const response = await request(app.getHttpServer())
.get(`/api/project/invite-preview/${inviteLinkId}`)
.set('Authorization', `Bearer ${newAccessToken}`);

expect(response.status).toBe(200);
expect(typeof response.body.id).toBe('number');
expect(response.body.title).toBe(projectPayload.title);
expect(response.body.subject).toBe(projectPayload.subject);
expect(response.body.leaderUsername).toBe(memberFixture.username);
});

it('should return 404 when project link ID is not found', async () => {
const invalidUUID = 'c93a87e8-a0a4-4b55-bdf2-59bf691f5c37';
const { accessToken: newAccessToken } = await createMember(
memberFixture2,
app,
);

const response = await request(app.getHttpServer())
.get(`/api/project/invite-preview/${invalidUUID}`)
.set('Authorization', `Bearer ${newAccessToken}`);

expect(response.status).toBe(404);
});

it('should return 401 (Bearer Token is missing)', async () => {
const { accessToken } = await createMember(memberFixture, app);
const { id: projectId } = await createProject(
accessToken,
projectPayload,
app,
);
const projectLinkId = await getProjectLinkId(accessToken, projectId);

const response = await request(app.getHttpServer()).get(
`/api/project/invite-preview/${projectLinkId}`,
);

expect(response.status).toBe(401);
});

it('should return 401 (Expired:accessToken) when given invalid access token', async () => {
const { accessToken } = await createMember(memberFixture, app);
const { id: projectId } = await createProject(
accessToken,
projectPayload,
app,
);
const projectLinkId = await getProjectLinkId(accessToken, projectId);

const response = await request(app.getHttpServer())
.get(`/api/project/invite-preview/${projectLinkId}`)
.set('Authorization', `Bearer invalidToken`);

expect(response.status).toBe(401);
expect(response.body.message).toBe('Expired:accessToken');
});
});

0 comments on commit a40ede1

Please sign in to comment.