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] 모각코 참가자 관련 로직 구현 및 Swagger 설정 #143

Merged
merged 4 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions app/backend/src/member/dto/member.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ export class MemberDto {
@ApiProperty({ description: 'Provider ID of the user', example: '123456' })
providerId: string;

@ApiProperty({ description: 'Email address of the user', example: 'user@example.com' })
@ApiProperty({ description: 'Email address of the user', example: 'bcwm.morak@gmail.com' })
email: string;

@ApiProperty({ description: 'Nickname of the user', example: 'john_doe' })
@ApiProperty({ description: 'Nickname of the user', example: 'morak morak' })
nickname: string;

@ApiProperty({ description: "URL of the user's profile picture", example: 'https://example.com/profile.jpg' })
Expand Down
3 changes: 2 additions & 1 deletion app/backend/src/member/member.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Controller, Get, Req, Res, UnauthorizedException } from '@nestjs/common';
import { MemberService } from './member.service';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { MemberDto } from './dto/member.dto';

@ApiTags('Member Infomation API')
@Controller('member')
export class MemberController {
constructor(private readonly memberService: MemberService) {}
Expand Down
8 changes: 8 additions & 0 deletions app/backend/src/mogaco/dto/create-mogaco.dto.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { IsDateString, IsEnum, IsInt, IsNotEmpty, IsOptional } from 'class-validator';
import { MogacoStatus } from './mogaco-status.enum';
import { ApiProperty } from '@nestjs/swagger';

export class CreateMogacoDto {
@ApiProperty({ description: 'Group ID', example: '1' })
@IsNotEmpty()
@IsInt()
groupId: number;

@ApiProperty({ description: 'Title of the Mogaco', example: '사당역 모각코' })
@IsNotEmpty()
title: string;

@ApiProperty({ description: 'Contents of the Mogaco', example: '사당역에서 모각코를 열려고 합니다.' })
@IsNotEmpty()
contents: string;

@ApiProperty({ description: 'Date of the Mogaco', example: '2023-11-25T12:00:00.000Z' })
@IsNotEmpty()
@IsDateString()
date: string;

@ApiProperty({ description: 'Maximum number of participants', example: 5 })
@IsNotEmpty()
maxHumanCount: number;

@ApiProperty({ description: 'Address of the Mogaco', example: '서울특별시 관악구 어디길 22 모락 카페' })
@IsNotEmpty()
address: string;

@ApiProperty({ description: 'Status of the Mogaco', example: '모집 중' })
@IsOptional()
@IsEnum(MogacoStatus, { message: 'Invalid status' })
status?: string;
Expand Down
22 changes: 22 additions & 0 deletions app/backend/src/mogaco/dto/mogaco.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';

export class MogacoDto {
@ApiProperty({ description: 'ID of the Mogaco', example: 1 })
id: bigint;

@ApiProperty({ description: 'Group ID', example: 1 })
groupId: bigint;

@ApiProperty({ description: 'Title of the Mogaco', example: '사당역 모각코' })
title: string;

@ApiProperty({ description: 'Contents of the Mogaco', example: '사당역에서 모각코를 열려고 합니다.' })
contents: string;

@ApiProperty({ description: 'Date of the Mogaco', example: '2023-11-22T12:00:00Z' })
date: Date;

@ApiProperty({ description: 'Maximum number of participants', example: 5 })
maxHumanCount: number;

@ApiProperty({ description: 'Address of the Mogaco', example: '서울특별시 관악구 어디길 22 모락 카페' })
address: string;

@ApiProperty({ description: 'Status of the Mogaco', example: '모집 중' })
status: string;
}

export class StatusDto {
@ApiProperty({ description: 'Status of the Mogaco', example: '모집 마감' })
status: string;
}
40 changes: 40 additions & 0 deletions app/backend/src/mogaco/dto/response-mogaco.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ApiProperty } from '@nestjs/swagger';
import { MemberDto } from 'src/member/dto/member.dto';

export class MogacoWithMemberDto {
@ApiProperty({ description: 'ID of the Mogaco', example: '3' })
id: string;

@ApiProperty({ description: 'Group ID', example: '1' })
groupId: string;

@ApiProperty({ description: 'Title of the Mogaco', example: '사당역 모각코' })
title: string;

@ApiProperty({ description: 'Contents of the Mogaco', example: '사당역에서 모각코를 열려고 합니다.' })
contents: string;

@ApiProperty({ description: 'Date of the Mogaco', example: '2023-11-25T12:00:00.000Z' })
date: string;

@ApiProperty({ description: 'Maximum number of participants', example: 5 })
maxHumanCount: number;

@ApiProperty({ description: 'Address of the Mogaco', example: '서울특별시 관악구 어디길 22 모락 카페' })
address: string;

@ApiProperty({ description: 'Status of the Mogaco', example: '모집 마감' })
status: string;

@ApiProperty({ description: 'Date of Mogaco creation', example: '2023-11-22T12:16:08.913Z' })
createdAt: string;

@ApiProperty({ description: 'Date of Mogaco update', example: '2023-11-22T12:16:08.913Z' })
updatedAt: string;

@ApiProperty({ description: 'Date of Mogaco deletion', example: null })
deletedAt: string | null;

@ApiProperty({ description: 'Member information', type: MemberDto })
member: MemberDto;
}
18 changes: 18 additions & 0 deletions app/backend/src/mogaco/dto/response-participants.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';

export class ParticipantResponseDto {
@ApiProperty({ description: 'ID of the Member', example: '1' })
id: string;

@ApiProperty({ description: 'Provider ID', example: '117187214221556274884' })
providerId: string;

@ApiProperty({ description: 'Email of the Member', example: '[email protected]' })
email: string;

@ApiProperty({ description: 'Social Type', example: 'google' })
socialType: string;

@ApiProperty({ description: 'Date of Member creation', example: '2023-11-22T04:55:02.988Z' })
createdAt: string;
}
79 changes: 78 additions & 1 deletion app/backend/src/mogaco/mogaco.controller.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,119 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';
import { MogacoService } from './mogaco.service';
import { Member, Mogaco } from '@prisma/client';
import { CreateMogacoDto, MogacoDto } from './dto';
import { CreateMogacoDto, MogacoDto, StatusDto } from './dto';
import { MogacoStatusValidationPipe } from './pipes/mogaco-status-validation.pipe';
import { MogacoStatus } from './dto/mogaco-status.enum';
import { GetUser } from 'libs/decorators/get-user.decorator';
import { AtGuard } from 'src/auth/guards/at.guard';
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MogacoWithMemberDto } from './dto/response-mogaco.dto';
import { ParticipantResponseDto } from './dto/response-participants.dto';

@ApiTags('Mogaco API')
@Controller('mogaco')
@UseGuards(AtGuard)
export class MogacoController {
constructor(private readonly mogacoService: MogacoService) {}

@Get('/')
@ApiOperation({
summary: '모든 모각코 조회',
description: '존재하는 모든 모각코를 조회합니다.\n 배열로 여러 개를 반환합니다.',
})
@ApiResponse({ status: 200, description: 'Successfully retrieved', type: [MogacoDto] })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getAllMogaco(): Promise<Mogaco[]> {
return this.mogacoService.getAllMogaco();
}

@Get('/:id')
@ApiOperation({
summary: '특정 게시물 조회',
description: '특정 게시물의 Id 값으로 해당 게시물을 조회합니다.',
})
@ApiParam({ name: 'id', description: '조회할 모각코의 Id' })
@ApiResponse({ status: 200, description: 'Successfully retrieved', type: MogacoDto })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getMogacoById(@Param('id', ParseIntPipe) id: number): Promise<MogacoDto> {
return this.mogacoService.getMogacoById(id);
}

@Post('/')
@ApiOperation({
summary: '모각코 개설',
description: '새로운 모각코를 개설합니다.',
})
@ApiBody({ type: CreateMogacoDto })
@ApiResponse({ status: 201, description: 'Successfully created', type: MogacoWithMemberDto })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async createMogaco(@Body() createMogacoDto: CreateMogacoDto, @GetUser() member: Member): Promise<Mogaco> {
return this.mogacoService.createMogaco(createMogacoDto, member);
}

@Delete('/:id')
@ApiOperation({
summary: '모각코 삭제',
description: '특정 모각코를 삭제합니다.',
})
@ApiParam({ name: 'id', description: '삭제할 모각코의 Id' })
@ApiResponse({ status: 200, description: 'Successfully deleted' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' })
async deleteMogaco(@Param('id', ParseIntPipe) id: number, @GetUser() member: Member): Promise<void> {
return this.mogacoService.deleteMogaco(id, member);
}

@Patch('/:id/status')
@ApiOperation({
summary: '모각코 상태 업데이트',
description: '특정 모각코의 상태를 업데이트합니다.',
})
@ApiParam({ name: 'id', description: '상태를 업데이트할 모각코의 Id' })
@ApiBody({ type: StatusDto })
@ApiResponse({ status: 200, description: 'Successfully Updated', type: MogacoWithMemberDto })
@ApiResponse({ status: 401, description: 'Unauthorized' })
updateMogacoStatus(
@Param('id', ParseIntPipe) id: number,
@Body('status', MogacoStatusValidationPipe) status: MogacoStatus,
): Promise<Mogaco> {
return this.mogacoService.updateMogacoStatus(id, status);
}

@Post('/:id/join')
@ApiOperation({
summary: '모각코 참가',
description: '특정 모각코에 참가합니다.',
})
@ApiParam({ name: 'id', description: '참가할 모각코의 Id' })
@ApiResponse({ status: 201, description: 'Successfully join' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async joinMogaco(@Param('id', ParseIntPipe) id: number, @GetUser() member: Member): Promise<void> {
return this.mogacoService.joinMogaco(id, member);
}

@Get('/:id/participants')
@ApiOperation({
summary: '참가자 목록 조회',
description: '특정 모각코에 참가한 모든 참가자 목록을 조회합니다.',
})
@ApiParam({ name: 'id', description: '참가자 목록을 조회할 모각코의 Id' })
@ApiResponse({ status: 200, description: 'Successfully retrieved', type: [ParticipantResponseDto] })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getParticipants(@Param('id', ParseIntPipe) id: number): Promise<Member[]> {
return this.mogacoService.getParticipants(id);
}

@Delete('/:id/join')
@ApiOperation({
summary: '모각코 참가 취소',
description: '특정 모각코 참가를 취소합니다.',
})
@ApiParam({ name: 'id', description: '참가를 취소할 모각코의 Id' })
@ApiResponse({ status: 200, description: 'Successfully cancelled join' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' })
async cancelMogacoJoin(@Param('id', ParseIntPipe) id: number, @GetUser() member: Member): Promise<void> {
return this.mogacoService.cancelMogacoJoin(id, member);
}
}
83 changes: 83 additions & 0 deletions app/backend/src/mogaco/mogaco.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,87 @@ export class MogacoRepository {
throw new Error(`Failed to update Mogaco status: ${error.message}`);
}
}

async joinMogaco(id: number, member: Member): Promise<void> {
const mogaco = await this.prisma.mogaco.findUnique({
where: { id, deletedAt: null },
});

if (!mogaco) {
throw new NotFoundException(`Mogaco with id ${id} not found`);
}

const existingParticipant = await this.prisma.participant.findUnique({
where: {
postId_userId: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

얜 다른 where랑 구조가 좀 다르네요? postId_userId 이유가 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where 옵션은 조회 조건을 지정하는데, 해당 부분은 중첩된 객체로 구성되어 있습니다. 구체적으로는 postId_userId라는 객체 속성을 가지고 있습니다.

처음에 where로 코드를 작성했는데 오류가 나서 chatGPT한테 물어봤는데 다음과 같이 수정해줘서 코드를 작성했습니다.

postId: mogaco.id,
userId: member.id,
},
},
});

if (existingParticipant) {
throw new ForbiddenException(`Member with id ${member.id} is already participating in Mogaco with id ${id}`);
}

await this.prisma.participant.create({
data: {
postId: mogaco.id,
userId: member.id,
},
});
}

async getParticipants(id: number): Promise<Member[]> {
const participants = await this.prisma.participant.findMany({
where: {
postId: id,
},
include: {
member: true,
},
});

// 가져온 참가자 목록의 각 참가자의 member 속성을 통해 실제 멤버 객체로 매핑하여 반환
return participants.map((participant) => participant.member);
}

async cancelMogacoJoin(id: number, member: Member): Promise<void> {
const mogaco = await this.prisma.mogaco.findUnique({
where: { id, deletedAt: null },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 라인은 조건 두개여도 잘되는거 같은데요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postId_userId는 Prisma에서 사용되는 복합 키(composite key)의 일부입니다. 이 키는 participant 테이블에서 두 개의 컬럼을 합쳐서 하나의 고유한 식별자로 사용하는 방식입니다.

여기서 postId는 Mogaco 게시물의 ID이고, userId는 참가자(사용자)의 ID입니다. 이 두 값이 결합되어서 postId_userId로 사용되고, 이를 통해 특정 Mogaco 게시물에 대한 특정 사용자의 참가 여부를 확인할 수 있습니다.

예를 들어, 특정 Mogaco 게시물에 참가한 사용자의 ID가 1이라면, 이를 postId_userId로 표현하면 { postId: 특정게시물ID, userId: 1 }이 됩니다. 이렇게 복합 키를 사용함으로써 특정 Mogaco 게시물과 사용자의 조합이 고유하게 식별되도록 합니다.

저도 확실한 이유에 대해 몰라서 좀 더 찾아봤습니다.
질문 감사합니다 백지님!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저게 where의 and 조건이라는 얘길까요? 전 이해가 잘 안되네요

include: {
member: true,
},
});

if (!mogaco) {
throw new NotFoundException(`Mogaco with id ${id} not found`);
}

const participant = await this.prisma.participant.findUnique({
where: {
postId_userId: {
postId: mogaco.id,
userId: member.id,
},
},
});

if (!participant) {
throw new NotFoundException(`Member with id ${member.id} is not participating in Mogaco with id ${id}`);
}

if (mogaco.memberId !== member.id) {
throw new ForbiddenException(`You do not have permission to cancel participation in this Mogaco`);
}

await this.prisma.participant.delete({
where: {
postId_userId: {
postId: mogaco.id,
userId: member.id,
},
},
});
}
}
12 changes: 12 additions & 0 deletions app/backend/src/mogaco/mogaco.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@ export class MogacoService {
mogaco.status = status;
return this.mogacoRepository.updateMogacoStatus(mogaco);
}

async joinMogaco(id: number, member: Member): Promise<void> {
return await this.mogacoRepository.joinMogaco(id, member);
}

async getParticipants(id: number): Promise<Member[]> {
return this.mogacoRepository.getParticipants(id);
}

async cancelMogacoJoin(id: number, member: Member): Promise<void> {
return this.mogacoRepository.cancelMogacoJoin(id, member);
}
}