diff --git a/src/common/config/mongo.config.ts b/src/common/config/mongo.config.ts index baf23fd..a2101e2 100644 --- a/src/common/config/mongo.config.ts +++ b/src/common/config/mongo.config.ts @@ -9,9 +9,9 @@ export class MongoConfig implements MongooseOptionsFactory { createMongooseOptions(): MongooseModuleOptions { const host = this.configService.getOrThrow('mongo.host'); const port = this.configService.getOrThrow('mongo.port'); - const username = this.configService.getOrThrow('mongo.username'); - const password = this.configService.getOrThrow('mongo.password'); - const authSource = this.configService.getOrThrow('mongo.authSource'); + const username = this.configService.get('mongo.username'); + const password = this.configService.get('mongo.password'); + const authSource = this.configService.get('mongo.authSource'); const database = this.configService.getOrThrow('mongo.database'); return { @@ -22,9 +22,9 @@ export class MongoConfig implements MongooseOptionsFactory { private buildConnectionString( host: string, port: number, - username: string, - password: string, - authSource: string, + username: string | undefined, + password: string | undefined, + authSource: string | undefined, database: string, ): string { return 'mongodb://${credential}${host}/${database}${otherOptions}' diff --git a/src/domain/activity/project/controller/project.controller.ts b/src/domain/activity/project/controller/project.controller.ts index d20def9..064ea3e 100644 --- a/src/domain/activity/project/controller/project.controller.ts +++ b/src/domain/activity/project/controller/project.controller.ts @@ -7,6 +7,7 @@ import { GetProjectsPageResponseDto, GetProjectsRequestDto, GetProjectsResponseDto, + SearchProjectsRequestDto, } from '@wink/activity/dto'; import { ProjectNotFoundException } from '@wink/activity/exception'; import { ProjectService } from '@wink/activity/service'; @@ -34,6 +35,14 @@ export class ProjectController { return this.projectService.getProjectsPage(); } + @Get('/search') + @ApiOperation({ summary: '프로젝트 검색' }) + @ApiProperty({ type: SearchProjectsRequestDto }) + @ApiCustomResponse(GetProjectsResponseDto) + async searchProjects(@Body() request: SearchProjectsRequestDto): Promise { + return this.projectService.searchProjects(request); + } + @Get() @ApiOperation({ summary: '프로젝트 목록' }) @ApiProperty({ type: GetProjectsRequestDto }) diff --git a/src/domain/activity/project/dto/index.ts b/src/domain/activity/project/dto/index.ts index 1d24674..e8cefac 100644 --- a/src/domain/activity/project/dto/index.ts +++ b/src/domain/activity/project/dto/index.ts @@ -3,6 +3,7 @@ export * from './request/create-project.request.dto'; export * from './request/update-project.request.dto'; export * from './request/get-project.request.dto'; export * from './request/get-projects.request.dto'; +export * from './request/search-projects.request.dto'; export * from './request/delete-project.request.dto'; // Response diff --git a/src/domain/activity/project/dto/request/search-projects.request.dto.ts b/src/domain/activity/project/dto/request/search-projects.request.dto.ts new file mode 100644 index 0000000..0ec1b48 --- /dev/null +++ b/src/domain/activity/project/dto/request/search-projects.request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { CommonValidation, TypeValidation } from '@wink/validation'; + +export class SearchProjectsRequestDto { + @ApiProperty({ + description: '검색어', + example: '김', + }) + @CommonValidation.IsNotEmpty() + @TypeValidation.IsString() + query!: string; +} diff --git a/src/domain/activity/project/repository/project.repository.ts b/src/domain/activity/project/repository/project.repository.ts index f74d792..4a8c313 100644 --- a/src/domain/activity/project/repository/project.repository.ts +++ b/src/domain/activity/project/repository/project.repository.ts @@ -15,6 +15,10 @@ export class ProjectRepository { } // Read + async count(): Promise { + return this.projectModel.countDocuments().exec(); + } + async findAll(): Promise { return this.projectModel.find().sort({ createdAt: -1 }).exec(); } @@ -23,7 +27,7 @@ export class ProjectRepository { return this.projectModel .find() .sort({ createdAt: -1 }) - .skip((page - 1) * 10) + .skip((page - 1) * 15) .limit(10) .exec(); } @@ -32,6 +36,13 @@ export class ProjectRepository { return this.projectModel.findById(id).exec(); } + async findAllByContainsTitle(title: string): Promise { + return this.projectModel + .find({ title: { $regex: title, $options: 'i' } }) + .sort({ createdAt: -1 }) + .exec(); + } + // Delete async deleteById(id: string): Promise { await this.projectModel.deleteOne({ _id: id }).exec(); diff --git a/src/domain/activity/project/service/project.service.ts b/src/domain/activity/project/service/project.service.ts index 3f9813a..fdfb8d6 100644 --- a/src/domain/activity/project/service/project.service.ts +++ b/src/domain/activity/project/service/project.service.ts @@ -6,6 +6,7 @@ import { GetProjectsPageResponseDto, GetProjectsRequestDto, GetProjectsResponseDto, + SearchProjectsRequestDto, } from '@wink/activity/dto'; import { ProjectNotFoundException } from '@wink/activity/exception'; import { ProjectRepository } from '@wink/activity/repository'; @@ -25,10 +26,15 @@ export class ProjectService { } async getProjectsPage(): Promise { - const projects = await this.projectRepository.findAll(); - const page = Math.ceil(projects.length / 10); + const count = await this.projectRepository.count(); - return { page }; + return { page: Math.ceil(count / 15) }; + } + + async searchProjects({ query }: SearchProjectsRequestDto): Promise { + const projects = await this.projectRepository.findAllByContainsTitle(query); + + return { projects }; } async getProjects({ page }: GetProjectsRequestDto): Promise { diff --git a/src/domain/activity/social/controller/social.controller.ts b/src/domain/activity/social/controller/social.controller.ts index 890629f..b2fd815 100644 --- a/src/domain/activity/social/controller/social.controller.ts +++ b/src/domain/activity/social/controller/social.controller.ts @@ -4,7 +4,10 @@ import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; import { GetSocialRequestDto, GetSocialResponseDto, + GetSocialsPageResponseDto, + GetSocialsRequestDto, GetSocialsResponseDto, + SearchSocialsRequestDto, } from '@wink/activity/dto'; import { SocialNotFoundException } from '@wink/activity/exception'; import { SocialService } from '@wink/activity/service'; @@ -21,14 +24,29 @@ export class SocialController { @ApiProperty({ type: GetSocialRequestDto }) @ApiCustomResponse(GetSocialResponseDto) @ApiCustomErrorResponse([SocialNotFoundException]) - async getProject(@Body() request: GetSocialRequestDto): Promise { + async getSocial(@Body() request: GetSocialRequestDto): Promise { return this.socialService.getSocial(request); } + @Get('/max') + @ApiOperation({ summary: '친목 활동 최대 페이지' }) + @ApiCustomResponse(GetSocialsPageResponseDto) + async getSocialsPage(): Promise { + return this.socialService.getSocialsPage(); + } + + @Get('/search') + @ApiOperation({ summary: '친목 활동 검색' }) + @ApiProperty({ type: SearchSocialsRequestDto }) + @ApiCustomResponse(GetSocialsResponseDto) + async searchProjects(@Body() request: SearchSocialsRequestDto): Promise { + return this.socialService.searchSocials(request); + } + @Get() @ApiOperation({ summary: '친목 활동 목록' }) @ApiCustomResponse(GetSocialsResponseDto) - async getSocials(): Promise { - return this.socialService.getSocials(); + async getSocials(@Body() request: GetSocialsRequestDto): Promise { + return this.socialService.getSocials(request); } } diff --git a/src/domain/activity/social/dto/index.ts b/src/domain/activity/social/dto/index.ts index 6ed2c43..6e1cf00 100644 --- a/src/domain/activity/social/dto/index.ts +++ b/src/domain/activity/social/dto/index.ts @@ -2,9 +2,12 @@ export * from './request/create-social.request.dto'; export * from './request/delete-social.request.dto'; export * from './request/get-social.request.dto'; +export * from './request/get-socials.request.dto'; +export * from './request/search-socials.request.dto'; export * from './request/update-social.request.dto'; // Response export * from './response/create-social.response.dto'; export * from './response/get-social.response.dto'; export * from './response/get-socials.response.dto'; +export * from './response/get-socials-page.response.dto'; diff --git a/src/domain/activity/social/dto/request/get-socials.request.dto.ts b/src/domain/activity/social/dto/request/get-socials.request.dto.ts new file mode 100644 index 0000000..4c64249 --- /dev/null +++ b/src/domain/activity/social/dto/request/get-socials.request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { CommonValidation, TypeValidation } from '@wink/validation'; + +export class GetSocialsRequestDto { + @ApiProperty({ + description: '페이지', + example: 1, + }) + @CommonValidation.IsNotEmpty() + @TypeValidation.IsNumber() + page!: number; +} diff --git a/src/domain/activity/social/dto/request/search-socials.request.dto.ts b/src/domain/activity/social/dto/request/search-socials.request.dto.ts new file mode 100644 index 0000000..af9e807 --- /dev/null +++ b/src/domain/activity/social/dto/request/search-socials.request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { CommonValidation, TypeValidation } from '@wink/validation'; + +export class SearchSocialsRequestDto { + @ApiProperty({ + description: '검색어', + example: '김', + }) + @CommonValidation.IsNotEmpty() + @TypeValidation.IsString() + query!: string; +} diff --git a/src/domain/activity/social/dto/response/get-socials-page.response.dto.ts b/src/domain/activity/social/dto/response/get-socials-page.response.dto.ts new file mode 100644 index 0000000..4852794 --- /dev/null +++ b/src/domain/activity/social/dto/response/get-socials-page.response.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetSocialsPageResponseDto { + @ApiProperty({ + description: '최대 페이지', + example: 1, + }) + page!: number; +} diff --git a/src/domain/activity/social/repository/social.repository.ts b/src/domain/activity/social/repository/social.repository.ts index fda948c..3e2de6d 100644 --- a/src/domain/activity/social/repository/social.repository.ts +++ b/src/domain/activity/social/repository/social.repository.ts @@ -15,8 +15,28 @@ export class SocialRepository { } // Read + async count(): Promise { + return this.socialModel.countDocuments().exec(); + } + async findAll(): Promise { - return this.socialModel.find().sort({ createdAt: -1 }).limit(6).exec(); + return this.socialModel.find().sort({ createdAt: -1 }).exec(); + } + + async findAllPage(page: number): Promise { + return this.socialModel + .find() + .sort({ createdAt: -1 }) + .skip((page - 1) * 10) + .limit(10) + .exec(); + } + + async findAllByContainsTitle(title: string): Promise { + return this.socialModel + .find({ title: { $regex: title, $options: 'i' } }) + .sort({ createdAt: -1 }) + .exec(); } async findById(id: string): Promise { diff --git a/src/domain/activity/social/service/social.service.ts b/src/domain/activity/social/service/social.service.ts index c7c84c2..a1f27ea 100644 --- a/src/domain/activity/social/service/social.service.ts +++ b/src/domain/activity/social/service/social.service.ts @@ -3,7 +3,10 @@ import { Injectable } from '@nestjs/common'; import { GetSocialRequestDto, GetSocialResponseDto, + GetSocialsPageResponseDto, + GetSocialsRequestDto, GetSocialsResponseDto, + SearchSocialsRequestDto, } from '@wink/activity/dto'; import { SocialNotFoundException } from '@wink/activity/exception'; import { SocialRepository } from '@wink/activity/repository'; @@ -22,9 +25,21 @@ export class SocialService { return { social }; } - async getSocials(): Promise { - const socials = await this.socialRepository.findAll(); + async getSocials({ page }: GetSocialsRequestDto): Promise { + const socials = await this.socialRepository.findAllPage(page); return { socials }; } + + async searchSocials({ query }: SearchSocialsRequestDto): Promise { + const socials = await this.socialRepository.findAllByContainsTitle(query); + + return { socials }; + } + + async getSocialsPage(): Promise { + const count = await this.socialRepository.count(); + + return { page: Math.ceil(count / 10) }; + } } diff --git a/src/domain/activity/study/controller/study.controller.ts b/src/domain/activity/study/controller/study.controller.ts index d97962b..6c853ef 100644 --- a/src/domain/activity/study/controller/study.controller.ts +++ b/src/domain/activity/study/controller/study.controller.ts @@ -6,6 +6,7 @@ import { GetStudiesPageResponseDto, GetStudiesRequestDto, GetStudiesResponseDto, + SearchStudyRequestDto, } from '@wink/activity/dto'; import { StudyService } from '@wink/activity/service'; @@ -30,6 +31,14 @@ export class StudyController { return this.studyService.getStudiesPage(); } + @Get('/search') + @ApiOperation({ summary: '스터디 활동 검색' }) + @ApiProperty({ type: SearchStudyRequestDto }) + @ApiCustomResponse(GetStudiesResponseDto) + async searchStudied(@Body() request: SearchStudyRequestDto): Promise { + return this.studyService.searchStudies(request); + } + @Get() @ApiOperation({ summary: '스터디 목록' }) @ApiProperty({ type: GetStudiesRequestDto }) diff --git a/src/domain/activity/study/dto/index.ts b/src/domain/activity/study/dto/index.ts index 5ec39e2..2b7f834 100644 --- a/src/domain/activity/study/dto/index.ts +++ b/src/domain/activity/study/dto/index.ts @@ -4,6 +4,7 @@ export * from './request/create-study.request.dto'; export * from './request/delete-category.request.dto'; export * from './request/delete-study.request.dto'; export * from './request/get-studies.request.dto'; +export * from './request/search-study.request.dto'; export * from './request/update-category.request.dto'; // Response diff --git a/src/domain/activity/study/dto/request/search-study.request.dto.ts b/src/domain/activity/study/dto/request/search-study.request.dto.ts new file mode 100644 index 0000000..b657611 --- /dev/null +++ b/src/domain/activity/study/dto/request/search-study.request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { CommonValidation, TypeValidation } from '@wink/validation'; + +export class SearchStudyRequestDto { + @ApiProperty({ + description: '검색어', + example: '김', + }) + @CommonValidation.IsNotEmpty() + @TypeValidation.IsString() + query!: string; +} diff --git a/src/domain/activity/study/repository/study.repository.ts b/src/domain/activity/study/repository/study.repository.ts index c693178..07f6186 100644 --- a/src/domain/activity/study/repository/study.repository.ts +++ b/src/domain/activity/study/repository/study.repository.ts @@ -15,6 +15,10 @@ export class StudyRepository { } // Read + async count(): Promise { + return this.studyModel.countDocuments().exec(); + } + async findAll(): Promise { return this.studyModel.find().sort({ uploadedAt: -1 }).exec(); } @@ -32,6 +36,13 @@ export class StudyRepository { return this.studyModel.findById(id).exec(); } + async findAllByContainsTitle(title: string): Promise { + return this.studyModel + .find({ title: { $regex: title, $options: 'i' } }) + .sort({ uploadedAt: -1 }) + .exec(); + } + // Delete async deleteById(id: string): Promise { await this.studyModel.deleteOne({ _id: id }).exec(); diff --git a/src/domain/activity/study/service/study.service.ts b/src/domain/activity/study/service/study.service.ts index 917543a..1ba921b 100644 --- a/src/domain/activity/study/service/study.service.ts +++ b/src/domain/activity/study/service/study.service.ts @@ -5,6 +5,7 @@ import { GetStudiesPageResponseDto, GetStudiesRequestDto, GetStudiesResponseDto, + SearchStudyRequestDto, } from '@wink/activity/dto'; import { CategoryRepository, StudyRepository } from '@wink/activity/repository'; @@ -27,10 +28,15 @@ export class StudyService { return { studies }; } + async searchStudies({ query }: SearchStudyRequestDto): Promise { + const studies = await this.studyRepository.findAllByContainsTitle(query); + + return { studies }; + } + async getStudiesPage(): Promise { - const studies = await this.studyRepository.findAll(); - const page = Math.ceil(studies.length / 10); + const count = await this.studyRepository.count(); - return { page }; + return { page: Math.ceil(count / 10) }; } } diff --git a/src/domain/auth/exception/index.ts b/src/domain/auth/exception/index.ts index aa4547f..f5462c4 100644 --- a/src/domain/auth/exception/index.ts +++ b/src/domain/auth/exception/index.ts @@ -12,4 +12,3 @@ export * from './wrong-password.exception'; export * from './unauthorized.exception'; export * from './permission.exception'; -export * from './super-role.exception'; diff --git a/src/domain/member/controller/member.admin.controller.ts b/src/domain/member/controller/member.admin.controller.ts index 55c3b6f..4250862 100644 --- a/src/domain/member/controller/member.admin.controller.ts +++ b/src/domain/member/controller/member.admin.controller.ts @@ -1,14 +1,16 @@ import { Body, Controller, Get, Patch, Post } from '@nestjs/common'; import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; -import { SuperRoleException } from '@wink/auth/exception'; import { AuthAdminAccount, AuthAdminAccountException, ReqMember } from '@wink/auth/guard'; import { ApproveWaitingMemberRequestDto, + GetMembersForAdminPageResponseDto, + GetMembersForAdminRequestDto, GetMembersForAdminResponseDto, GetWaitingMembersResponseDto, RejectWaitingMemberRequestDto, + SearchMembersRequestDto, UpdateMemberFeeRequestDto, UpdateMemberRoleRequestDto, } from '@wink/member/dto'; @@ -58,13 +60,37 @@ export class MemberAdminController { return this.memberAdminService.rejectWaitingMember(member, request); } + @Get('/max') + @AuthAdminAccount() + @ApiOperation({ summary: '부원 목록 최대 페이지' }) + @ApiCustomResponse(GetMembersForAdminPageResponseDto) + @ApiCustomErrorResponse([...AuthAdminAccountException]) + async getMembersPage(): Promise { + return this.memberAdminService.getMembersPage(); + } + + @Get('/search') + @AuthAdminAccount() + @ApiOperation({ summary: '부원 검색' }) + @ApiProperty({ type: SearchMembersRequestDto }) + @ApiCustomResponse(GetMembersForAdminResponseDto) + @ApiCustomErrorResponse([...AuthAdminAccountException]) + async searchMembers( + @Body() request: SearchMembersRequestDto, + ): Promise { + return this.memberAdminService.searchMember(request); + } + @Get() @AuthAdminAccount() @ApiOperation({ summary: '부원 목록' }) + @ApiProperty({ type: GetMembersForAdminRequestDto }) @ApiCustomResponse(GetMembersForAdminResponseDto) @ApiCustomErrorResponse([...AuthAdminAccountException]) - async getMembers(): Promise { - return this.memberAdminService.getMembers(); + async getMembers( + @Body() request: GetMembersForAdminRequestDto, + ): Promise { + return this.memberAdminService.getMembers(request); } @Patch('/role') @@ -72,11 +98,7 @@ export class MemberAdminController { @ApiOperation({ summary: '부원 권한 수정' }) @ApiProperty({ type: UpdateMemberRoleRequestDto }) @ApiCustomResponse() - @ApiCustomErrorResponse([ - ...AuthAdminAccountException, - NotApprovedMemberException, - SuperRoleException, - ]) + @ApiCustomErrorResponse([...AuthAdminAccountException, NotApprovedMemberException]) async updateMemberRole( @ReqMember() member: Member, @Body() request: UpdateMemberRoleRequestDto, @@ -89,11 +111,7 @@ export class MemberAdminController { @ApiOperation({ summary: '부원 회비 납부 여부 수정' }) @ApiProperty({ type: UpdateMemberFeeRequestDto }) @ApiCustomResponse() - @ApiCustomErrorResponse([ - ...AuthAdminAccountException, - NotApprovedMemberException, - SuperRoleException, - ]) + @ApiCustomErrorResponse([...AuthAdminAccountException, NotApprovedMemberException]) async updateMemberFee( @ReqMember() member: Member, @Body() request: UpdateMemberFeeRequestDto, diff --git a/src/domain/member/dto/index.ts b/src/domain/member/dto/index.ts index 0dd66b6..d76d7c8 100644 --- a/src/domain/member/dto/index.ts +++ b/src/domain/member/dto/index.ts @@ -1,6 +1,8 @@ // Request export * from './request/approve-waiting-member.request.dto'; +export * from './request/get-members-for-admin.request.dto'; export * from './request/reject-waiting-member.request.dto'; +export * from './request/search-members.request.dto'; export * from './request/update-member-fee.request.dto'; export * from './request/update-member-role.request.dto'; export * from './request/update-my-info.request.dto'; @@ -8,5 +10,6 @@ export * from './request/update-my-password.request.dto'; // Response export * from './response/get-members.response.dto'; +export * from './response/get-members-for-admin-page.response.dto'; export * from './response/get-waiting-members.response.dto'; export * from './response/update-my-avatar.response.dto'; diff --git a/src/domain/member/dto/request/get-members-for-admin.request.dto.ts b/src/domain/member/dto/request/get-members-for-admin.request.dto.ts new file mode 100644 index 0000000..0b3fac5 --- /dev/null +++ b/src/domain/member/dto/request/get-members-for-admin.request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { CommonValidation, TypeValidation } from '@wink/validation'; + +export class GetMembersForAdminRequestDto { + @ApiProperty({ + description: '페이지', + example: 1, + }) + @CommonValidation.IsNotEmpty() + @TypeValidation.IsNumber() + page!: number; +} diff --git a/src/domain/member/dto/request/search-members.request.dto.ts b/src/domain/member/dto/request/search-members.request.dto.ts new file mode 100644 index 0000000..ba22e13 --- /dev/null +++ b/src/domain/member/dto/request/search-members.request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { CommonValidation, TypeValidation } from '@wink/validation'; + +export class SearchMembersRequestDto { + @ApiProperty({ + description: '검색어', + example: '김', + }) + @CommonValidation.IsNotEmpty() + @TypeValidation.IsString() + query!: string; +} diff --git a/src/domain/member/dto/response/get-members-for-admin-page.response.dto.ts b/src/domain/member/dto/response/get-members-for-admin-page.response.dto.ts new file mode 100644 index 0000000..eeed77a --- /dev/null +++ b/src/domain/member/dto/response/get-members-for-admin-page.response.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetMembersForAdminPageResponseDto { + @ApiProperty({ + description: '최대 페이지', + example: 1, + }) + page!: number; +} diff --git a/src/domain/member/repository/member.repository.ts b/src/domain/member/repository/member.repository.ts index b628aa3..84e5a5d 100644 --- a/src/domain/member/repository/member.repository.ts +++ b/src/domain/member/repository/member.repository.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Member } from '@wink/member/schema'; +import { Member, Role } from '@wink/member/schema'; import { Model } from 'mongoose'; +const roleOrder = Object.values(Role); + @Injectable() export class MemberRepository { constructor(@InjectModel(Member.name) private readonly memberModel: Model) {} @@ -15,8 +17,102 @@ export class MemberRepository { } // Read + async count(): Promise { + return this.memberModel.countDocuments().exec(); + } + async findAll(): Promise { - return this.memberModel.find({ approved: true }).exec(); + return this.memberModel.aggregate([ + { + $match: { + approved: true, + }, + }, + { + $addFields: { + roleIndex: { + $switch: { + branches: roleOrder.map((role, index) => ({ + case: { $eq: ['$role', role] }, + then: index, + })), + default: roleOrder.length, + }, + }, + }, + }, + { + $sort: { + roleIndex: 1, + name: 1, + }, + }, + ]); + } + + async findAllPage(page: number): Promise { + return this.memberModel.aggregate([ + { + $match: { + approved: true, + }, + }, + { + $addFields: { + roleIndex: { + $switch: { + branches: roleOrder.map((role, index) => ({ + case: { $eq: ['$role', role] }, + then: index, + })), + default: roleOrder.length, + }, + }, + }, + }, + { + $sort: { + roleIndex: 1, + name: 1, + }, + }, + { + $skip: 10 * (page - 1), + }, + { + $limit: 10, + }, + ]); + } + + async findByContainsName(name: string): Promise { + return this.memberModel.aggregate([ + { + $match: { + approved: true, + name: { $regex: name, $options: 'i' }, + }, + }, + { + $addFields: { + roleIndex: { + $switch: { + branches: roleOrder.map((role, index) => ({ + case: { $eq: ['$role', role] }, + then: index, + })), + default: roleOrder.length, + }, + }, + }, + }, + { + $sort: { + roleIndex: 1, + name: 1, + }, + }, + ]); } async findAllWaitingMember(): Promise { diff --git a/src/domain/member/schema/Role.ts b/src/domain/member/schema/Role.ts index 00b7e5b..41f24f6 100644 --- a/src/domain/member/schema/Role.ts +++ b/src/domain/member/schema/Role.ts @@ -9,26 +9,3 @@ export enum Role { PLANNING_ASSISTANT = 'PLANNING_ASSISTANT', MEMBER = 'MEMBER', } - -const roleHierarchy: { [key: string]: number } = { - [Role.PRESIDENT]: 1, - - [Role.VICE_PRESIDENT]: 2, - - [Role.TREASURY_HEAD]: 3, - [Role.PUBLIC_RELATIONS_HEAD]: 3, - [Role.PLANNING_HEAD]: 3, - - [Role.TREASURY_ASSISTANT]: 4, - [Role.PUBLIC_RELATIONS_ASSISTANT]: 4, - [Role.PLANNING_ASSISTANT]: 4, - - [Role.MEMBER]: 5, -}; - -export const checkRoleHierarchy = (myRole: Role, targetRole: Role): boolean => { - const myRoleIndex = roleHierarchy[myRole]; - const targetRoleIndex = roleHierarchy[targetRole]; - - return myRoleIndex < targetRoleIndex; -}; diff --git a/src/domain/member/service/member.admin.service.ts b/src/domain/member/service/member.admin.service.ts index 9ac3d56..ddf25ae 100644 --- a/src/domain/member/service/member.admin.service.ts +++ b/src/domain/member/service/member.admin.service.ts @@ -1,21 +1,24 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { MemberNotFoundException, SuperRoleException } from '@wink/auth/exception'; +import { MemberNotFoundException } from '@wink/auth/exception'; import { ApproveWaitingMemberRequestDto, EachGetMembersForAdminResponseDto, EachGetWaitingMembersResponseDto, + GetMembersForAdminPageResponseDto, + GetMembersForAdminRequestDto, GetMembersForAdminResponseDto, GetWaitingMembersResponseDto, RejectWaitingMemberRequestDto, + SearchMembersRequestDto, UpdateMemberFeeRequestDto, UpdateMemberRoleRequestDto, } from '@wink/member/dto'; import { NotApprovedMemberException, NotWaitingMemberException } from '@wink/member/exception'; import { MemberRepository } from '@wink/member/repository'; -import { Member, Role, checkRoleHierarchy, omitMember, pickMember } from '@wink/member/schema'; +import { Member, Role, omitMember, pickMember } from '@wink/member/schema'; import { ApproveWaitingMemberEvent, @@ -93,8 +96,22 @@ export class MemberAdminService { ); } - async getMembers(): Promise { - const members = (await this.memberRepository.findAll()).map((member) => { + async getMembersPage(): Promise { + const count = await this.memberRepository.count(); + + return { page: Math.ceil(count / 10) }; + } + + async searchMember({ query }: SearchMembersRequestDto): Promise { + const members = (await this.memberRepository.findByContainsName(query)).map((member) => { + return omitMember(member, ['approved']); + }); + + return { members }; + } + + async getMembers({ page }: GetMembersForAdminRequestDto): Promise { + const members = (await this.memberRepository.findAllPage(page)).map((member) => { return omitMember(member, ['approved']); }); @@ -115,10 +132,6 @@ export class MemberAdminService { throw new NotApprovedMemberException(); } - if (!checkRoleHierarchy(from.role!, to.role!)) { - throw new SuperRoleException(); - } - to.role = role; await this.memberRepository.save(to); @@ -137,10 +150,6 @@ export class MemberAdminService { throw new NotApprovedMemberException(); } - if (!checkRoleHierarchy(from.role!, to.role!)) { - throw new SuperRoleException(); - } - to.fee = fee; await this.memberRepository.save(to); diff --git a/test/member/admin-service.test.ts b/test/member/admin-service.test.ts index f4b1aad..b07b65c 100644 --- a/test/member/admin-service.test.ts +++ b/test/member/admin-service.test.ts @@ -1,5 +1,3 @@ -import { SuperRoleException } from '@wink/auth/exception'; - import { ApproveWaitingMemberRequestDto, RejectWaitingMemberRequestDto, @@ -158,7 +156,7 @@ describe('MemberAdminService', () => { // Given // When - const result = memberAdminService.getMembers(); + const result = memberAdminService.getMembers({ page: 1 }); // Then await expect(result).resolves.toStrictEqual({ members: [] }); @@ -169,7 +167,7 @@ describe('MemberAdminService', () => { memoryMemberRepository.push(...MEMBERS); // When - const result = memberAdminService.getMembers(); + const result = memberAdminService.getMembers({ page: 1 }); // Then await expect(result).resolves.toBeInstanceOf(Object); @@ -204,34 +202,6 @@ describe('MemberAdminService', () => { await expect(result).rejects.toThrow(NotApprovedMemberException); }); - it('PermissionException (1)', async () => { - // Given - const member = { ...TARGET }; - member.role = Role.PRESIDENT; - - memoryMemberRepository.push(member); - - // When - const result = memberAdminService.updateRole(ME, PARAM); - - // Then - await expect(result).rejects.toThrow(SuperRoleException); - }); - - it('PermissionException (2)', async () => { - // Given - const member = { ...TARGET }; - member.role = Role.VICE_PRESIDENT; - - memoryMemberRepository.push(member); - - // When - const result = memberAdminService.updateRole(ME, PARAM); - - // Then - await expect(result).rejects.toThrow(SuperRoleException); - }); - it('Update role', async () => { // Given const member = { ...TARGET }; @@ -275,34 +245,6 @@ describe('MemberAdminService', () => { await expect(result).rejects.toThrow(NotApprovedMemberException); }); - it('PermissionException (1)', async () => { - // Given - const member = { ...TARGET }; - member.role = Role.PRESIDENT; - - memoryMemberRepository.push(member); - - // When - const result = memberAdminService.updateFee(ME, PARAM); - - // Then - await expect(result).rejects.toThrow(SuperRoleException); - }); - - it('PermissionException (2)', async () => { - // Given - const member = { ...TARGET }; - member.role = Role.VICE_PRESIDENT; - - memoryMemberRepository.push(member); - - // When - const result = memberAdminService.updateFee(ME, PARAM); - - // Then - await expect(result).rejects.toThrow(SuperRoleException); - }); - it('Change fee', async () => { // Given const member = { ...TARGET }; diff --git a/test/mock/repository/member-repository.mock.ts b/test/mock/repository/member-repository.mock.ts index 9e4f349..bcb0497 100644 --- a/test/mock/repository/member-repository.mock.ts +++ b/test/mock/repository/member-repository.mock.ts @@ -13,10 +13,18 @@ export const mockMemberRepository = (memory: Member[]) => ({ }), // Read + count: jest.fn(async () => { + return memory.length; + }), + findAll: jest.fn(async () => { return memory.filter((member) => member.approved); }), + findAllPage: jest.fn(async () => { + return memory.filter((member) => member.approved); + }), + findAllWaitingMember: jest.fn(async () => { return memory.filter((member) => !member.approved); }),