From dcb8d5d6d276a47105f47aead8d14ddd6905210d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B7=BC=ED=98=95?= Date: Wed, 10 Apr 2024 23:19:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Group,=20Member=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Group CRUD 구현 * refactor: dto 및 스킴의 ObjectId 타입 > string * fix: group/update SQL Injection issue * feat: member crud * fix: member crud npm test --------- Co-authored-by: ssoxong --- src/group/dto/create-group.dto.ts | 34 ++++++++++++++++++++ src/group/dto/update-group.dto.ts | 17 ++++++++-- src/group/entities/group.entity.ts | 14 ++++++++- src/group/group.controller.ts | 37 +++++++++++++++++++--- src/group/group.service.ts | 25 ++++++++++----- src/group/interfaces/group.interface.ts | 11 ++++++- src/group/schemas/group.schema.ts | 9 ++++++ src/member/dto/create-member.dto.ts | 16 +++++++++- src/member/dto/update-member.dto.ts | 16 ++++++++-- src/member/entities/member.entity.ts | 6 +++- src/member/interfaces/member.interface.ts | 6 ++++ src/member/member.controller.spec.ts | 27 ++++++++++------ src/member/member.controller.ts | 37 +++++++++++++++++++--- src/member/member.module.ts | 5 ++- src/member/member.provider.ts | 11 +++++++ src/member/member.service.spec.ts | 24 ++++++++------ src/member/member.service.ts | 38 +++++++++++++++++------ src/member/schemas/member.schema.ts | 6 ++++ 18 files changed, 285 insertions(+), 54 deletions(-) create mode 100644 src/member/interfaces/member.interface.ts create mode 100644 src/member/member.provider.ts create mode 100644 src/member/schemas/member.schema.ts diff --git a/src/group/dto/create-group.dto.ts b/src/group/dto/create-group.dto.ts index 2536dde..0b11ec2 100644 --- a/src/group/dto/create-group.dto.ts +++ b/src/group/dto/create-group.dto.ts @@ -6,4 +6,38 @@ export class CreateGroupDto { example: 'SSU', }) readonly name: string; + + @ApiProperty({ + description: '모임 설명', + example: '숭실대학교 학생들의 모임', + }) + readonly description: string; + + @ApiProperty({ + description: '모임장 사용자의 ObjectId', + example: '60f4b3b3b3b3b3b3b3b3b3b3', + }) + readonly manager: string; + + @ApiProperty({ + description: '부모임장 사용자의 ObjectId와 권한', + example: [ + { + user: '60f4b3b3b3b3b3b3b3b3b3', + authorities: ['subManager'], + }, + ], + }) + readonly subManagers: [ + { + user: string; + authorities: string[]; + }, + ]; + + @ApiProperty({ + description: '모임원 사용자의 ObjectId', + example: ['60f4b3b3b3b3b3b3b3b3b3'], + }) + readonly members: string[]; } diff --git a/src/group/dto/update-group.dto.ts b/src/group/dto/update-group.dto.ts index 1acb803..19798f4 100644 --- a/src/group/dto/update-group.dto.ts +++ b/src/group/dto/update-group.dto.ts @@ -1,4 +1,15 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateGroupDto } from './create-group.dto'; +import { ApiProperty } from '@nestjs/swagger'; -export class UpdateGroupDto extends PartialType(CreateGroupDto) {} +export class UpdateGroupDto { + @ApiProperty({ + description: '모임 이름', + example: 'SSU', + }) + readonly name: string; + + @ApiProperty({ + description: '모임 설명', + example: '숭실대학교 학생들의 모임', + }) + readonly description: string; +} diff --git a/src/group/entities/group.entity.ts b/src/group/entities/group.entity.ts index f087764..d19745f 100644 --- a/src/group/entities/group.entity.ts +++ b/src/group/entities/group.entity.ts @@ -1 +1,13 @@ -export class Group {} +export class Group { + id: string; + name: string; + description: string; + manager: string; + subManagers: [ + { + user: string; + authorities: string[]; + }, + ]; + members: string[]; +} diff --git a/src/group/group.controller.ts b/src/group/group.controller.ts index 4c5f5f5..a206545 100644 --- a/src/group/group.controller.ts +++ b/src/group/group.controller.ts @@ -10,7 +10,7 @@ import { import { GroupService } from './group.service'; import { CreateGroupDto } from './dto/create-group.dto'; import { UpdateGroupDto } from './dto/update-group.dto'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; @ApiTags('Group') @Controller('group') @@ -18,27 +18,56 @@ export class GroupController { constructor(private readonly groupService: GroupService) {} @Post() + @ApiOperation({ + summary: '모임 생성', + description: '모임을 생성합니다.', + }) create(@Body() createGroupDto: CreateGroupDto) { return this.groupService.create(createGroupDto); } @Get() + @ApiOperation({ + summary: '모든 모임 조회', + description: '모든 모임을 조회합니다.', + }) findAll() { return this.groupService.findAll(); } @Get(':id') + @ApiOperation({ + summary: '모임 상세 조회', + description: '특정 모임을 조회합니다.', + }) findOne(@Param('id') id: string) { - return this.groupService.findOne(+id); + return this.groupService.findOne(id); } @Patch(':id') + @ApiOperation({ + summary: '모임 수정', + description: '특정 모임을 수정합니다.', + }) update(@Param('id') id: string, @Body() updateGroupDto: UpdateGroupDto) { - return this.groupService.update(+id, updateGroupDto); + return this.groupService.update(id, updateGroupDto); + } + + @Delete() + @ApiOperation({ + summary: '모든 모임 삭제', + description: '모든 모임을 삭제합니다.', + }) + removeAll() { + return this.groupService.removeAll(); } @Delete(':id') + @ApiOperation({ + summary: '모임 삭제', + description: '특정 모임을 삭제합니다.', + }) remove(@Param('id') id: string) { - return this.groupService.remove(+id); + return this.groupService.remove(id); } } diff --git a/src/group/group.service.ts b/src/group/group.service.ts index e518438..a61093f 100644 --- a/src/group/group.service.ts +++ b/src/group/group.service.ts @@ -12,22 +12,33 @@ export class GroupService { ) {} create(createGroupDto: CreateGroupDto) { - return this.groupModel.create(createGroupDto); + return new this.groupModel(createGroupDto).save(); } async findAll(): Promise { return this.groupModel.find().exec(); } - findOne(id: number) { - return `This action returns a #${id} group`; + removeAll() { + return this.groupModel.deleteMany({}); } - update(id: number, updateGroupDto: UpdateGroupDto) { - return { id, updateGroupDto }; + async findOne(id: string): Promise { + return this.groupModel.findById(id).exec(); } - remove(id: number) { - return `This action removes a #${id} group`; + async update(id: string, updateGroupDto: UpdateGroupDto): Promise { + try { + const group = await this.groupModel.findById(id).exec(); + group.name = updateGroupDto.name; + group.description = updateGroupDto.description; + return group.save(); + } catch (e) { + return null; + } + } + + async remove(id: string): Promise { + return this.groupModel.findByIdAndDelete(id); } } diff --git a/src/group/interfaces/group.interface.ts b/src/group/interfaces/group.interface.ts index 93620ae..aeb4422 100644 --- a/src/group/interfaces/group.interface.ts +++ b/src/group/interfaces/group.interface.ts @@ -1,5 +1,14 @@ import { Document } from 'mongoose'; export interface Group extends Document { - readonly name: string; + name: string; + description: string; + manager: string; + subManagers: [ + { + user: string; + authorities: string[]; + }, + ]; + members: string[]; } diff --git a/src/group/schemas/group.schema.ts b/src/group/schemas/group.schema.ts index 252ec68..f35f831 100644 --- a/src/group/schemas/group.schema.ts +++ b/src/group/schemas/group.schema.ts @@ -2,4 +2,13 @@ import * as mongoose from 'mongoose'; export const GroupSchema = new mongoose.Schema({ name: String, + description: String, + manager: String, + subManagers: [ + { + user: String, + authorities: [String], + }, + ], + members: [String], }); diff --git a/src/member/dto/create-member.dto.ts b/src/member/dto/create-member.dto.ts index 2885eed..d69811d 100644 --- a/src/member/dto/create-member.dto.ts +++ b/src/member/dto/create-member.dto.ts @@ -1 +1,15 @@ -export class CreateMemberDto {} +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateMemberDto { + @ApiProperty({ + description: '회원 이름', + example: '박하은', + }) + readonly name: string; + + @ApiProperty({ + description: '회원 연락처', + example: '010-1234-5678', + }) + readonly phoneNumber: string; +} diff --git a/src/member/dto/update-member.dto.ts b/src/member/dto/update-member.dto.ts index f22d88c..24ff817 100644 --- a/src/member/dto/update-member.dto.ts +++ b/src/member/dto/update-member.dto.ts @@ -1,4 +1,16 @@ -import { PartialType } from '@nestjs/swagger'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; import { CreateMemberDto } from './create-member.dto'; -export class UpdateMemberDto extends PartialType(CreateMemberDto) {} +export class UpdateMemberDto extends PartialType(CreateMemberDto) { + @ApiProperty({ + description: '회원 이름', + example: '박하은', + }) + readonly name: string; + + @ApiProperty({ + description: '회원 연락처', + example: '010-1234-5678', + }) + readonly phoneNumber: string; +} diff --git a/src/member/entities/member.entity.ts b/src/member/entities/member.entity.ts index 08ad20d..d015ab0 100644 --- a/src/member/entities/member.entity.ts +++ b/src/member/entities/member.entity.ts @@ -1 +1,5 @@ -export class Member {} +export class Member { + id: string; + name: string; + phoneNumber: string; +} diff --git a/src/member/interfaces/member.interface.ts b/src/member/interfaces/member.interface.ts new file mode 100644 index 0000000..0e4a7a3 --- /dev/null +++ b/src/member/interfaces/member.interface.ts @@ -0,0 +1,6 @@ +import { Document } from 'mongoose'; + +export interface Member extends Document { + name: string; + phoneNumber: string; +} diff --git a/src/member/member.controller.spec.ts b/src/member/member.controller.spec.ts index 428672e..cd49dd0 100644 --- a/src/member/member.controller.spec.ts +++ b/src/member/member.controller.spec.ts @@ -1,20 +1,27 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { MemberController } from './member.controller'; import { MemberService } from './member.service'; +import { Member } from './interfaces/member.interface'; +import { HydratedDocument, Model } from 'mongoose'; describe('MemberController', () => { - let controller: MemberController; + let memberController: MemberController; + let memberService: MemberService; + let memberModel: Model; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [MemberController], - providers: [MemberService], - }).compile(); - - controller = module.get(MemberController); + memberService = new MemberService(memberModel); + memberController = new MemberController(memberService); }); - it('should be defined', () => { - expect(controller).toBeDefined(); + describe('findAll', () => { + it('should return an array of members', async () => { + const result: HydratedDocument = [ + { _id: 1, name: '테스트1' }, + { _id: 2, name: '테스트2' }, + ]; + jest.spyOn(memberService, 'findAll').mockImplementation(() => result); + + expect(await memberController.findAll()).toBe(result); + }); }); }); diff --git a/src/member/member.controller.ts b/src/member/member.controller.ts index fd1b618..0f752c8 100644 --- a/src/member/member.controller.ts +++ b/src/member/member.controller.ts @@ -10,7 +10,7 @@ import { import { MemberService } from './member.service'; import { CreateMemberDto } from './dto/create-member.dto'; import { UpdateMemberDto } from './dto/update-member.dto'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; @ApiTags('Member') @Controller('member') @@ -18,27 +18,56 @@ export class MemberController { constructor(private readonly memberService: MemberService) {} @Post() + @ApiOperation({ + summary: '회원 생성', + description: '회원을 생성합니다.', + }) create(@Body() createMemberDto: CreateMemberDto) { return this.memberService.create(createMemberDto); } @Get() + @ApiOperation({ + summary: '모든 회원 조회', + description: '모든 회원을 조회합니다.', + }) findAll() { return this.memberService.findAll(); } @Get(':id') + @ApiOperation({ + summary: '회원 상세 조회', + description: '특정 회원을 조회합니다.', + }) findOne(@Param('id') id: string) { - return this.memberService.findOne(+id); + return this.memberService.findOne(id); } @Patch(':id') + @ApiOperation({ + summary: '회원 수정', + description: '특정 회원을 수정합니다.', + }) update(@Param('id') id: string, @Body() updateMemberDto: UpdateMemberDto) { - return this.memberService.update(+id, updateMemberDto); + return this.memberService.update(id, updateMemberDto); + } + + @Delete() + @ApiOperation({ + summary: '모든 회원 삭제', + description: '모든 회원을 삭제합니다.', + }) + removeAll() { + return this.memberService.removeAll(); } @Delete(':id') + @ApiOperation({ + summary: '회원 삭제', + description: '특정 회원을 삭제합니다.', + }) remove(@Param('id') id: string) { - return this.memberService.remove(+id); + return this.memberService.remove(id); } } diff --git a/src/member/member.module.ts b/src/member/member.module.ts index 1f2050a..812f9a1 100644 --- a/src/member/member.module.ts +++ b/src/member/member.module.ts @@ -1,9 +1,12 @@ import { Module } from '@nestjs/common'; import { MemberService } from './member.service'; import { MemberController } from './member.controller'; +import { DatabaseModule } from '../database/database.module'; +import { memberProvider } from './member.provider'; @Module({ + imports: [DatabaseModule], controllers: [MemberController], - providers: [MemberService], + providers: [MemberService, ...memberProvider], }) export class MemberModule {} diff --git a/src/member/member.provider.ts b/src/member/member.provider.ts new file mode 100644 index 0000000..6c18893 --- /dev/null +++ b/src/member/member.provider.ts @@ -0,0 +1,11 @@ +import { Connection } from 'mongoose'; +import { MemberSchema } from './schemas/member.schema'; + +export const memberProvider = [ + { + provide: 'MEMBER_MODEL', + useFactory: (connection: Connection) => + connection.model('Member', MemberSchema), + inject: ['MONGODB_CONNECTION'], + }, +]; diff --git a/src/member/member.service.spec.ts b/src/member/member.service.spec.ts index 234fd86..629a31f 100644 --- a/src/member/member.service.spec.ts +++ b/src/member/member.service.spec.ts @@ -1,18 +1,24 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { MemberService } from './member.service'; +import { Member } from './interfaces/member.interface'; +import { HydratedDocument, Model } from 'mongoose'; describe('MemberService', () => { - let service: MemberService; + let memberService: MemberService; + let memberModel: Model; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [MemberService], - }).compile(); - - service = module.get(MemberService); + memberService = new MemberService(memberModel); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('findAll', () => { + it('should return an array of members', async () => { + const result: HydratedDocument = [ + { _id: 1, name: '테스트1' }, + { _id: 2, name: '테스트2' }, + ]; + jest.spyOn(memberService, 'findAll').mockImplementation(() => result); + + expect(await memberService.findAll()).toBe(result); + }); }); }); diff --git a/src/member/member.service.ts b/src/member/member.service.ts index 5d18c32..1fd0c87 100644 --- a/src/member/member.service.ts +++ b/src/member/member.service.ts @@ -1,26 +1,44 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { CreateMemberDto } from './dto/create-member.dto'; import { UpdateMemberDto } from './dto/update-member.dto'; +import { Model } from 'mongoose'; +import { Member } from './interfaces/member.interface'; @Injectable() export class MemberService { + constructor( + @Inject('MEMBER_MODEL') + private readonly memberModel: Model, + ) {} + create(createMemberDto: CreateMemberDto) { - return createMemberDto; + return new this.memberModel(createMemberDto).save(); + } + + async findAll(): Promise { + return this.memberModel.find().exec(); } - findAll() { - return `This action returns all member`; + removeAll() { + return this.memberModel.deleteMany({}); } - findOne(id: number) { - return `This action returns a #${id} member`; + async findOne(id: string): Promise { + return this.memberModel.findById(id).exec(); } - update(id: number, updateMemberDto: UpdateMemberDto) { - return { id, updateMemberDto }; + async update(id: string, updateMemberDto: UpdateMemberDto): Promise { + try { + const member = await this.memberModel.findById(id).exec(); + member.name = updateMemberDto.name; + member.phoneNumber = updateMemberDto.phoneNumber; + return member.save(); + } catch (e) { + return null; + } } - remove(id: number) { - return `This action removes a #${id} member`; + async remove(id: string) { + return this.memberModel.findByIdAndDelete(id); } } diff --git a/src/member/schemas/member.schema.ts b/src/member/schemas/member.schema.ts new file mode 100644 index 0000000..9cdfafa --- /dev/null +++ b/src/member/schemas/member.schema.ts @@ -0,0 +1,6 @@ +import * as mongoose from 'mongoose'; + +export const MemberSchema = new mongoose.Schema({ + name: String, + phoneNumber: String, +});