Skip to content

Commit d0706f2

Browse files
committed
feat: 프로젝트 참여요청 API 구현
- 엔티티 - 프로젝트 참여요청 엔티티 추가 - 프로젝트 엔티티에 프로젝트 참여요청 프로퍼티 추가 - 레포지토리에서 DB에 프로젝트 참여요청 생성 메서드 추가 - 서비스에서 프로젝트 참여요청 생성 메서드 추가 - 컨트롤러에서 프로젝트 참여요청 제출 메서드 추가 - 프로젝트 참여요청 E2E 테스트 추가 - 성공 상황 - 인증실패 상황 - 유효하지 않은 프로젝트 링크 상황 - 이미 회원인 유저가 요청한 상황 - 이미 참여요청한 유저가 다시 참여요청한 상황
1 parent c346071 commit d0706f2

File tree

9 files changed

+309
-2
lines changed

9 files changed

+309
-2
lines changed

backend/src/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Link } from './project/entity/link.entity.';
3232
import { Epic } from './project/entity/epic.entity';
3333
import { Story } from './project/entity/story.entity';
3434
import { Task } from './project/entity/task.entity';
35+
import { ProjectJoinRequest } from './project/entity/project-join-request.entity';
3536

3637
@Module({
3738
imports: [
@@ -50,11 +51,12 @@ import { Task } from './project/entity/task.entity';
5051
LoginMember,
5152
Project,
5253
ProjectToMember,
54+
ProjectJoinRequest,
5355
Memo,
5456
Link,
5557
Epic,
5658
Story,
57-
Task
59+
Task,
5860
],
5961
synchronize: ConfigService.get('NODE_ENV') == 'PROD' ? false : true,
6062
}),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IsNotEmpty, IsUUID } from 'class-validator';
2+
3+
export class ProjectJoinRequestRequestDto {
4+
@IsNotEmpty()
5+
@IsUUID(4, { message: 'not uuid' })
6+
inviteLinkId: string;
7+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
Column,
3+
CreateDateColumn,
4+
Entity,
5+
JoinColumn,
6+
ManyToOne,
7+
PrimaryGeneratedColumn,
8+
Unique,
9+
UpdateDateColumn,
10+
} from 'typeorm';
11+
import { Project } from './project.entity';
12+
import { Member } from 'src/member/entity/member.entity';
13+
14+
@Entity()
15+
@Unique('PROJECT_JOIN_REQUEST_UQ_PROJECT_ID_AND_MEMBER_ID', [
16+
'projectId',
17+
'memberId',
18+
])
19+
export class ProjectJoinRequest {
20+
@PrimaryGeneratedColumn('increment', { type: 'int' })
21+
id: number;
22+
23+
@ManyToOne(() => Project, (project) => project.joinRequestList, {
24+
nullable: false,
25+
onDelete: 'CASCADE',
26+
})
27+
@Column({ type: 'int', name: 'project_id' })
28+
projectId: number;
29+
30+
@JoinColumn({ name: 'project_id' })
31+
project: Project;
32+
33+
@ManyToOne(() => Member)
34+
@Column({ type: 'int', name: 'member_id' })
35+
memberId: number;
36+
37+
@ManyToOne(() => Member, (member) => member.id, {
38+
nullable: false,
39+
onDelete: 'CASCADE',
40+
})
41+
@JoinColumn({ name: 'member_id' })
42+
member: Member;
43+
44+
@CreateDateColumn({ type: 'timestamp' })
45+
created_at: Date;
46+
47+
@UpdateDateColumn({ type: 'timestamp' })
48+
updated_at: Date;
49+
50+
static of(projectId: number, memberId: number) {
51+
const projectJoinRequest = new ProjectJoinRequest();
52+
projectJoinRequest.projectId = projectId;
53+
projectJoinRequest.memberId = memberId;
54+
return projectJoinRequest;
55+
}
56+
}

backend/src/project/entity/project.entity.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
UpdateDateColumn,
77
OneToMany,
88
Generated,
9-
JoinColumn,
109
} from 'typeorm';
1110
import { Epic } from './epic.entity';
1211
import { Link } from './link.entity.';
1312
import { Memo } from './memo.entity';
1413
import { ProjectToMember } from './project-member.entity';
14+
import { ProjectJoinRequest } from './project-join-request.entity';
1515

1616
@Entity()
1717
export class Project {
@@ -52,6 +52,9 @@ export class Project {
5252
@OneToMany(() => Epic, (epic) => epic.project)
5353
epicList: Epic[];
5454

55+
@OneToMany(() => ProjectJoinRequest, (JoinRequest) => JoinRequest.project)
56+
joinRequestList: ProjectJoinRequest[];
57+
5558
static of(title: string, subject: string) {
5659
const newProject = new Project();
5760
newProject.title = title;

backend/src/project/project.controller.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { JoinProjectRequestDto } from './dto/JoinProjectRequest.dto';
1616
import { Response } from 'express';
1717
import { ProjectWebsocketGateway } from './websocket.gateway';
1818
import { ProjectInvitePreviewResponseDto } from './dto/ProjectInvitePreviewResponse.dto';
19+
import { ProjectJoinRequestRequestDto } from './dto/ProjectJoinRequest-Request.dto';
1920

2021
@Controller('project')
2122
export class ProjectController {
@@ -85,6 +86,30 @@ export class ProjectController {
8586
return response.status(201).send();
8687
}
8788

89+
@Post('/join-request')
90+
async submitProjectJoinRequest(
91+
@Req() request: MemberRequest,
92+
@Body() body: ProjectJoinRequestRequestDto,
93+
@Res() response: Response,
94+
) {
95+
try {
96+
await this.projectService.createProjectJoinRequest(
97+
body.inviteLinkId,
98+
request.member,
99+
);
100+
} catch (e) {
101+
if (e.message === 'Join request already submitted') {
102+
throw new ConflictException(e.message);
103+
} else if (e.message === 'Already a project member') {
104+
throw new ConflictException(e.message);
105+
} else if (e.message === 'Project not found') {
106+
throw new NotFoundException(e.message);
107+
}
108+
}
109+
110+
return response.status(201).send();
111+
}
112+
88113
@Get('/invite-preview/:inviteLinkId')
89114
async getProjectInvitePreview(
90115
@Param('inviteLinkId') inviteLinkId: string,

backend/src/project/project.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Task } from './entity/task.entity';
2424
import { WsProjectTaskController } from './ws-controller/ws-project-task.controller';
2525
import { WsProjectInfoController } from './ws-controller/ws-project-info.controller';
2626
import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller';
27+
import { ProjectJoinRequest } from './entity/project-join-request.entity';
2728

2829
@Module({
2930
imports: [
@@ -37,6 +38,7 @@ import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite
3738
Epic,
3839
Story,
3940
Task,
41+
ProjectJoinRequest,
4042
]),
4143
],
4244
controllers: [ProjectController],

backend/src/project/project.repository.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Epic, EpicColor } from './entity/epic.entity';
1010
import { Story, StoryStatus } from './entity/story.entity';
1111
import { Task, TaskStatus } from './entity/task.entity';
1212
import { MemberRole } from './enum/MemberRole.enum';
13+
import { ProjectJoinRequest } from './entity/project-join-request.entity';
1314

1415
@Injectable()
1516
export class ProjectRepository {
@@ -30,6 +31,8 @@ export class ProjectRepository {
3031
private readonly storyRepository: Repository<Story>,
3132
@InjectRepository(Task)
3233
private readonly taskRepository: Repository<Task>,
34+
@InjectRepository(ProjectJoinRequest)
35+
private readonly projectJoinRequestRepository: Repository<ProjectJoinRequest>,
3336
private readonly dataSource: DataSource,
3437
) {}
3538

@@ -103,6 +106,23 @@ export class ProjectRepository {
103106
return !!result.affected;
104107
}
105108

109+
async createProjectJoinRequest(
110+
projectJoinRequest: ProjectJoinRequest,
111+
): Promise<ProjectJoinRequest> {
112+
try {
113+
return await this.projectJoinRequestRepository.save(projectJoinRequest);
114+
} catch (e) {
115+
if (
116+
e.code === 'ER_DUP_ENTRY' &&
117+
e.sqlMessage.includes(
118+
'PROJECT_JOIN_REQUEST_UQ_PROJECT_ID_AND_MEMBER_ID',
119+
)
120+
)
121+
throw new Error('DUPLICATED PROJECT ID AND MEMBER ID');
122+
throw e;
123+
}
124+
}
125+
106126
getProject(projectId: number): Promise<Project | null> {
107127
return this.projectRepository.findOne({ where: { id: projectId } });
108128
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LexoRank } from 'lexorank';
1212
import { MemberRole } from '../enum/MemberRole.enum';
1313
import { v4 as uuidv4 } from 'uuid';
1414
import { ProjectBriefInfoDto } from '../dto/service/ProjectBriefInfo.dto';
15+
import { ProjectJoinRequest } from '../entity/project-join-request.entity';
1516

1617
@Injectable()
1718
export class ProjectService {
@@ -150,6 +151,29 @@ export class ProjectService {
150151
return this.projectRepository.getProjectByLinkId(projectLinkId);
151152
}
152153

154+
async createProjectJoinRequest(
155+
inviteLinkId: string,
156+
member: Member,
157+
): Promise<void> {
158+
const project = await this.getProjectByLinkId(inviteLinkId);
159+
if (!project) throw new Error('Project not found');
160+
const isProjectMember = await this.isProjectMember(project.id, member);
161+
if (isProjectMember) throw new Error('Already a project member');
162+
try {
163+
const newProjectJoinRequest = ProjectJoinRequest.of(
164+
project.id,
165+
member.id,
166+
);
167+
await this.projectRepository.createProjectJoinRequest(
168+
newProjectJoinRequest,
169+
);
170+
} catch (e) {
171+
if (e.message === 'DUPLICATED PROJECT ID AND MEMBER ID') {
172+
throw new Error('Join request already submitted');
173+
}
174+
}
175+
}
176+
153177
async createMemo(project: Project, member: Member, color: memoColor) {
154178
const newMemo = Memo.of(project, member, '', '', color);
155179
return this.projectRepository.createMemo(newMemo);

0 commit comments

Comments
 (0)