Skip to content

Commit c346071

Browse files
committed
feat: 초대링크로 프로젝트의 프리뷰 조회하는 API 구현
- 컨트롤러 - 프로젝트 초대 브리뷰 반환하는 메서드 구현 - 서비스 - 초대링크로 프로젝트의 일부 정보를 반환하는 메서드 구현 - 레포지토리 - 프로젝트, 프로젝트 회원을 조인해서 반환하는 메서드 구현 - 프로젝트 프리뷰 조회 E2E 테스트 추가
1 parent f39bfed commit c346071

File tree

6 files changed

+193
-0
lines changed

6 files changed

+193
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export class ProjectInvitePreviewResponseDto {
2+
id: number;
3+
title: string;
4+
subject: string;
5+
leaderUsername: string;
6+
7+
static of(
8+
id: number,
9+
title: string,
10+
subject: string,
11+
leaderUsername: string,
12+
) {
13+
const dto = new ProjectInvitePreviewResponseDto();
14+
dto.id = id;
15+
dto.title = title;
16+
dto.subject = subject;
17+
dto.leaderUsername = leaderUsername;
18+
return dto;
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export class ProjectBriefInfoDto {
2+
id: number;
3+
title: string;
4+
subject: string;
5+
leaderUsername: string;
6+
7+
static of(
8+
id: number,
9+
title: string,
10+
subject: string,
11+
leaderUsername: string,
12+
) {
13+
const dto = new ProjectBriefInfoDto();
14+
dto.id = id;
15+
dto.title = title;
16+
dto.subject = subject;
17+
dto.leaderUsername = leaderUsername;
18+
return dto;
19+
}
20+
}

backend/src/project/project.controller.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Controller,
55
Get,
66
NotFoundException,
7+
Param,
78
Post,
89
Req,
910
Res,
@@ -14,6 +15,7 @@ import { MemberRequest } from 'src/common/guard/authentication.guard';
1415
import { JoinProjectRequestDto } from './dto/JoinProjectRequest.dto';
1516
import { Response } from 'express';
1617
import { ProjectWebsocketGateway } from './websocket.gateway';
18+
import { ProjectInvitePreviewResponseDto } from './dto/ProjectInvitePreviewResponse.dto';
1719

1820
@Controller('project')
1921
export class ProjectController {
@@ -82,4 +84,31 @@ export class ProjectController {
8284
);
8385
return response.status(201).send();
8486
}
87+
88+
@Get('/invite-preview/:inviteLinkId')
89+
async getProjectInvitePreview(
90+
@Param('inviteLinkId') inviteLinkId: string,
91+
@Res() response: Response,
92+
) {
93+
let projectPublicInfo;
94+
try {
95+
projectPublicInfo =
96+
await this.projectService.getProjectBriefInfoByInviteLinkId(
97+
inviteLinkId,
98+
);
99+
} catch (err) {
100+
if (err.message === 'Project Not Found') throw new NotFoundException();
101+
throw err;
102+
}
103+
return response
104+
.status(200)
105+
.send(
106+
ProjectInvitePreviewResponseDto.of(
107+
projectPublicInfo.id,
108+
projectPublicInfo.title,
109+
projectPublicInfo.subject,
110+
projectPublicInfo.leaderUsername,
111+
),
112+
);
113+
}
85114
}

backend/src/project/project.repository.ts

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

78+
async getProjectWithMemberListByLinkId(
79+
inviteLinkId: string,
80+
): Promise<Project> {
81+
const projectWithMemberList = await this.projectRepository.findOne({
82+
where: { inviteLinkId },
83+
relations: { projectToMember: { member: true } },
84+
});
85+
if (!projectWithMemberList) throw new Error('Project Not Found');
86+
return projectWithMemberList;
87+
}
88+
7889
getProjectByLinkId(projectLinkId: string): Promise<Project | null> {
7990
return this.projectRepository.findOne({
8091
where: { inviteLinkId: projectLinkId },

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Task, TaskStatus } from '../entity/task.entity';
1111
import { LexoRank } from 'lexorank';
1212
import { MemberRole } from '../enum/MemberRole.enum';
1313
import { v4 as uuidv4 } from 'uuid';
14+
import { ProjectBriefInfoDto } from '../dto/service/ProjectBriefInfo.dto';
1415

1516
@Injectable()
1617
export class ProjectService {
@@ -124,6 +125,27 @@ export class ProjectService {
124125
return projectToMember?.role === MemberRole.LEADER;
125126
}
126127

128+
async getProjectBriefInfoByInviteLinkId(
129+
inviteLinkId: string,
130+
): Promise<ProjectBriefInfoDto> {
131+
const project =
132+
await this.projectRepository.getProjectWithMemberListByLinkId(
133+
inviteLinkId,
134+
);
135+
const leader = project.projectToMember.find(
136+
(member) => member.role === MemberRole.LEADER,
137+
);
138+
if (!leader) {
139+
throw new Error('Project does not have a leader');
140+
}
141+
return ProjectBriefInfoDto.of(
142+
project.id,
143+
project.title,
144+
project.subject,
145+
leader.member.username,
146+
);
147+
}
148+
127149
getProjectByLinkId(projectLinkId: string): Promise<Project | null> {
128150
return this.projectRepository.getProjectByLinkId(projectLinkId);
129151
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as request from 'supertest';
2+
import {
3+
app,
4+
appInit,
5+
createMember,
6+
createProject,
7+
getProjectLinkId,
8+
listenAppAndSetPortEnv,
9+
memberFixture,
10+
memberFixture2,
11+
projectPayload,
12+
} from 'test/setup';
13+
14+
describe('GET /project/invite-preview', () => {
15+
beforeEach(async () => {
16+
await app.close();
17+
await appInit();
18+
await listenAppAndSetPortEnv(app);
19+
});
20+
21+
it('should return 200 when given valid invite link id', async () => {
22+
const { accessToken } = await createMember(memberFixture, app);
23+
const { id: projectId } = await createProject(
24+
accessToken,
25+
projectPayload,
26+
app,
27+
);
28+
const inviteLinkId = await getProjectLinkId(accessToken, projectId);
29+
const { accessToken: newAccessToken } = await createMember(
30+
memberFixture2,
31+
app,
32+
);
33+
34+
const response = await request(app.getHttpServer())
35+
.get(`/api/project/invite-preview/${inviteLinkId}`)
36+
.set('Authorization', `Bearer ${newAccessToken}`);
37+
38+
expect(response.status).toBe(200);
39+
expect(typeof response.body.id).toBe('number');
40+
expect(response.body.title).toBe(projectPayload.title);
41+
expect(response.body.subject).toBe(projectPayload.subject);
42+
expect(response.body.leaderUsername).toBe(memberFixture.username);
43+
});
44+
45+
it('should return 404 when project link ID is not found', async () => {
46+
const invalidUUID = 'c93a87e8-a0a4-4b55-bdf2-59bf691f5c37';
47+
const { accessToken: newAccessToken } = await createMember(
48+
memberFixture2,
49+
app,
50+
);
51+
52+
const response = await request(app.getHttpServer())
53+
.get(`/api/project/invite-preview/${invalidUUID}`)
54+
.set('Authorization', `Bearer ${newAccessToken}`);
55+
56+
expect(response.status).toBe(404);
57+
});
58+
59+
it('should return 401 (Bearer Token is missing)', async () => {
60+
const { accessToken } = await createMember(memberFixture, app);
61+
const { id: projectId } = await createProject(
62+
accessToken,
63+
projectPayload,
64+
app,
65+
);
66+
const projectLinkId = await getProjectLinkId(accessToken, projectId);
67+
68+
const response = await request(app.getHttpServer()).get(
69+
`/api/project/invite-preview/${projectLinkId}`,
70+
);
71+
72+
expect(response.status).toBe(401);
73+
});
74+
75+
it('should return 401 (Expired:accessToken) when given invalid access token', async () => {
76+
const { accessToken } = await createMember(memberFixture, app);
77+
const { id: projectId } = await createProject(
78+
accessToken,
79+
projectPayload,
80+
app,
81+
);
82+
const projectLinkId = await getProjectLinkId(accessToken, projectId);
83+
84+
const response = await request(app.getHttpServer())
85+
.get(`/api/project/invite-preview/${projectLinkId}`)
86+
.set('Authorization', `Bearer invalidToken`);
87+
88+
expect(response.status).toBe(401);
89+
expect(response.body.message).toBe('Expired:accessToken');
90+
});
91+
});

0 commit comments

Comments
 (0)