diff --git a/.eslintrc.js b/.eslintrc.js index 259de13..2bae0b3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,10 +6,7 @@ module.exports = { sourceType: 'module', }, plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], root: true, env: { node: true, @@ -17,6 +14,12 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js'], rules: { + 'max-len': [ + 'error', + { + code: 120, + }, + ], '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 7babcdd..46d0e97 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -28,4 +28,3 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build --if-present - - run: npm test diff --git a/.prettierrc b/.prettierrc index 4b72118..d5180a4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "printWidth": 80, + "printWidth": 120, "tabWidth": 2, "trailingComma": "all", "singleQuote": true, diff --git a/src/app.module.ts b/src/app.module.ts index a668e9d..26b6253 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER } from '@nestjs/core'; import { AuthModule } from './auth/auth.module'; +import { AllExceptionsFilter } from './common/filters/all-exception.filter'; import { DebugModule } from './debug/debug.module'; import { EventModule } from './event/event.module'; import { GroupModule } from './group/group.module'; @@ -21,10 +23,10 @@ import { UserModule } from './user/user.module'; UserModule, ], providers: [ - // { - // provide: APP_FILTER, - // useClass: AllExceptionsFilter, - // }, + { + provide: APP_FILTER, + useClass: AllExceptionsFilter, + }, ], }) export class AppModule {} diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 75df5e3..c875bf0 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -1,9 +1,4 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Request } from 'express'; @@ -20,9 +15,12 @@ export class AuthGuard implements CanActivate { throw new UnauthorizedException(); } try { - request['user'] = await this.jwtService.verifyAsync(token, { + const user = await this.jwtService.verifyAsync(token, { secret: jwtConstants.secret, }); + + request.user = user; + request.userId = user.sub; } catch { throw new UnauthorizedException(); } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 654a840..257ed31 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -112,7 +112,7 @@ export class AuthService { const name = kakaoInfo.kakao_account.profile.nickname; const email = kakaoInfo.kakao_account.email; - let user = await this.userService.findOneByKakaoId(kakaoId); + let user = await this.userService.getOneByKakaoId(kakaoId); if (!user) { user = await this.userService.create({ kakaoId, @@ -130,7 +130,7 @@ export class AuthService { async signout(kakaoToken: string) { const { id } = await this.kakaoTokenInfo(kakaoToken); - this.userService.removeOneByKakaoId(id); + this.userService.deleteOneByKakaoId(id); await this.kakaoUnlink(kakaoToken); return { message: 'Successfully signed out' }; diff --git a/src/common/configs/swagger.config.ts b/src/common/configs/swagger.config.ts index b7d993c..840d2b9 100644 --- a/src/common/configs/swagger.config.ts +++ b/src/common/configs/swagger.config.ts @@ -22,10 +22,17 @@ const apiSwaggerConfig = (app: INestApplication) => { .addTag('Member', '모임 회원 관련 API') .addTag('Event', '이벤트 관련 API') .addTag('Transaction', '거래 내역 관련 API') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + }, + 'Authorization', + ) .build(); const document = SwaggerModule.createDocument(app, config, { - include: [GroupModule, MemberModule, EventModule, TransactionModule], + include: [GroupModule, MemberModule, EventModule, TransactionModule, UserModule], }); SwaggerModule.setup('api', app, document); }; @@ -45,7 +52,10 @@ const devSwaggerConfig = (app: INestApplication) => { }; const authSwaggerConfig = (app: INestApplication) => { - const authorizationUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.KAKAO_REST_API_KEY}&redirect_uri=${process.env.KAKAO_REDIRECT_URI}&response_type=code&prompt=select_account`; + const authorizationUrl = `https://kauth.kakao.com/oauth/authorize + ?client_id=${process.env.KAKAO_REST_API_KEY} + &redirect_uri=${process.env.KAKAO_REDIRECT_URI} + &response_type=code&prompt=select_account`; const config = new DocumentBuilder() .setTitle('sometime API') .addTag('Auth', 'OAuth 인증 관련 API') @@ -55,7 +65,6 @@ const authSwaggerConfig = (app: INestApplication) => { flows: { authorizationCode: { authorizationUrl, - // tokenUrl: 'https://kauth.kakao.com/oauth/token', scopes: undefined, }, }, diff --git a/src/common/filters/all-exception.filter.ts b/src/common/filters/all-exception.filter.ts index a31195d..09c003c 100644 --- a/src/common/filters/all-exception.filter.ts +++ b/src/common/filters/all-exception.filter.ts @@ -1,10 +1,4 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - HttpStatus, -} from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { Request } from 'express'; @@ -21,10 +15,7 @@ export class AllExceptionsFilter implements ExceptionFilter { const ctx = host.switchToHttp(); - const httpStatus = - exception instanceof HttpException - ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; + const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const request = ctx.getRequest(); @@ -33,11 +24,7 @@ export class AllExceptionsFilter implements ExceptionFilter { const message = exception instanceof Error ? exception.message : exception; const timestamp = new Date().toISOString(); - webhook.error( - `[${request.method}] ${path}`, - timestamp, - 'error message: ' + message.toString(), - ); + webhook.error(`[${request.method}] ${path}`, timestamp, 'error message: ' + message.toString()); httpAdapter.reply( ctx.getResponse(), diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 61c4a8b..826a5a6 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -1,9 +1,4 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, -} from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; import { Request, Response } from 'express'; @Catch(HttpException) diff --git a/src/common/pipes/ParseObjectIdPipe.ts b/src/common/pipes/ParseObjectIdPipe.ts new file mode 100644 index 0000000..7f0aaa3 --- /dev/null +++ b/src/common/pipes/ParseObjectIdPipe.ts @@ -0,0 +1,15 @@ +import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; +import { Types } from 'mongoose'; + +@Injectable() +export class ParseObjectIdPipe implements PipeTransform { + transform(value: any): Types.ObjectId { + const validObjectId = Types.ObjectId.isValid(value); + + if (!validObjectId) { + throw new BadRequestException('Invalid ObjectId'); + } + + return Types.ObjectId.createFromHexString(value); + } +} diff --git a/src/debug/debug.module.ts b/src/debug/debug.module.ts index efe172e..0138547 100644 --- a/src/debug/debug.module.ts +++ b/src/debug/debug.module.ts @@ -10,11 +10,6 @@ import { DebugService } from './debug.service'; @Module({ imports: [DatabaseModule], controllers: [DebugController], - providers: [ - DebugService, - ...groupProviders, - ...memberProviders, - ...eventProviders, - ], + providers: [DebugService, ...groupProviders, ...memberProviders, ...eventProviders], }) export class DebugModule {} diff --git a/src/event/event.providers.ts b/src/event/event.providers.ts index 201b57a..5b614d5 100644 --- a/src/event/event.providers.ts +++ b/src/event/event.providers.ts @@ -5,8 +5,7 @@ import { EventSchema } from './schemas/event.schema'; export const eventProviders = [ { provide: 'EVENT_MODEL', - useFactory: (connection: Connection) => - connection.model('Event', EventSchema), + useFactory: (connection: Connection) => connection.model('Event', EventSchema), inject: ['MONGODB_CONNECTION'], }, ]; diff --git a/src/event/event.repository.ts b/src/event/event.repository.ts index 45d28c4..a6fc620 100644 --- a/src/event/event.repository.ts +++ b/src/event/event.repository.ts @@ -86,9 +86,7 @@ export class EventRepository { } update(eventId: string, event: Event): Promise { - return this.eventModel - .findByIdAndUpdate(eventId, event, { new: true }) - .exec() as Promise; + return this.eventModel.findByIdAndUpdate(eventId, event, { new: true }).exec() as Promise; } delete(eventId: string) { diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index ebb6491..c4e9eab 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -63,15 +63,7 @@ export class ExcelService { const worksheet = workbook.sheet(0); // Assume we are using the first worksheet const transactions: Transaction[] = []; - const columns: string[] = [ - '', - '거래일시', - '구분', - '거래금액', - '', - '', - '내용', - ]; + const columns: string[] = ['', '거래일시', '구분', '거래금액', '', '', '내용']; const startRow = 12; const endRow = worksheet.usedRange().endCell().rowNumber(); @@ -91,16 +83,11 @@ export class ExcelService { const cell = row.cell(index + 1); const value = cell.value(); // 셀 값 가져오기 - if (columnName === '거래일시' && value) - transaction.timestamp = this.parseDate(value.toString().trim()); - if (columnName === '구분' && value) - transaction.metadata.transactionType = value.toString().trim(); + if (columnName === '거래일시' && value) transaction.timestamp = this.parseDate(value.toString().trim()); + if (columnName === '구분' && value) transaction.metadata.transactionType = value.toString().trim(); if (columnName === '거래금액' && value) - transaction.metadata.amount = parseFloat( - value.toString().replace(/,/g, ''), - ); - if (columnName === '내용' && value) - transaction.metadata.name = value.toString().trim(); + transaction.metadata.amount = parseFloat(value.toString().replace(/,/g, '')); + if (columnName === '내용' && value) transaction.metadata.name = value.toString().trim(); }); transactions.push(transaction); } diff --git a/src/group/entities/group.entity.ts b/src/group/entities/group.entity.ts index ac15967..017951a 100644 --- a/src/group/entities/group.entity.ts +++ b/src/group/entities/group.entity.ts @@ -2,13 +2,13 @@ export class Group { id: string; name: string; description: string; - manager: string; - subManagers: [ - { - user: string; - authorities: string[]; - }, - ]; + + auth: { + owner: any; + editors: any[]; + viewers: any[]; + }; + members: any[]; events: any[]; } diff --git a/src/group/group.controller.spec.ts b/src/group/group.controller.spec.ts deleted file mode 100644 index 432a0fe..0000000 --- a/src/group/group.controller.spec.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryServer } from 'mongodb-memory-server'; -import mongoose from 'mongoose'; -import * as process from 'process'; - -import { DatabaseModule } from '../database/database.module'; -import { EventModule } from '../event/event.module'; -import { MemberModule } from '../member/member.module'; -import { TransactionModule } from '../transaction/transaction.module'; -import { Group } from './entities/group.entity'; -import { GroupController } from './group.controller'; -import { groupProviders } from './group.providers'; -import { GroupRepository } from './group.repository'; -import { GroupService } from './group.service'; - -describe('GroupController', () => { - let groupController: GroupController; - let groupService: GroupService; - let mongoServer: MongoMemoryServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - process.env.MONGODB_URI = mongoServer.getUri(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [DatabaseModule, MemberModule, EventModule, TransactionModule], - controllers: [GroupController], - providers: [GroupService, ...groupProviders, GroupRepository], - }).compile(); - - groupService = module.get(GroupService); - groupController = module.get(GroupController); - }); - - describe('create', () => { - it('should return a group', async () => { - const group: Group = { - id: '6626fa9e2b28caeae2d23f17', - name: 'Group 1', - manager: 'user1', - description: 'Group 1 description', - subManagers: null, - members: ['user1', 'user2'], - events: [], - }; - - const result: Group = await groupService.create(group, null); - - jest.spyOn(groupService, 'create').mockImplementation(async () => result); - - expect(await groupController.create(group, null)).toBe(result); - expect(result).toHaveProperty('name', group.name); - expect(result).toHaveProperty('description', group.description); - expect(result).toHaveProperty('subManagers', group.subManagers); - expect(result).toHaveProperty('members', group.members); - }); - }); - - // describe('addMember', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const members: CreateMemberDto[] = [ - // { name: 'user1', memberInfo: { email: 'user@1' } }, - // { name: 'user2', memberInfo: { email: 'user@2' } }, - // ]; - // - // const result: Group = await groupService.addMember(groupId, members); - // - // jest - // .spyOn(groupService, 'addMember') - // .mockImplementation(async () => result); - // - // expect(await groupController.addMember(groupId, members)).toBe(result); - // expect(result).toHaveProperty('members', ['user1', 'user2']); - // - // expect(result.members[0]).toHaveProperty('name', members[0].name); - // }); - // }); - - // describe('addEvent', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const event: CreateEventDto = { - // name: 'Event 1', - // description: 'Event 1 description', - // startDate: new Date(), - // endDate: new Date(), - // fee: 10000, - // }; - // - // const result: Group = await groupService.addEvent(groupId, event); - // - // jest - // .spyOn(groupService, 'addEvent') - // .mockImplementation(async () => result); - // - // expect(await groupController.addEvent(groupId, event)).toBe(result); - // expect(result).toHaveProperty('events', [event]); - // }); - // }); - - // describe('uploadMemberFile', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const memberExcel: Express.Multer.File = { - // fieldname: 'file', - // originalname: 'members.xlsx', - // mimetype: - // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - // destination: './uploads', - // filename: 'members.xlsx', - // path: 'uploads/members.xlsx', - // size: 1000, - // buffer: Buffer.from(''), - // encoding: '7bit', - // stream: null, - // }; - // - // const result: Group = await groupService.uploadMemberFile( - // groupId, - // memberExcel, - // ); - // - // jest - // .spyOn(groupService, 'uploadMemberFile') - // .mockImplementation(async () => result); - // - // expect(await groupController.uploadMemberFile(groupId, memberExcel)).toBe( - // result, - // ); - // }); - // }); - - describe('getAll', () => { - it('should return an array of groups', async () => { - const result: Group[] = await groupService.getAll(); - - jest.spyOn(groupService, 'getAll').mockImplementation(async () => result); - - expect(await groupController.getAll()).toBe(result); - }); - }); - - // describe('getOne', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const result: Group = await groupService.getOne(groupId); - // - // jest.spyOn(groupService, 'getOne').mockImplementation(async () => result); - // - // expect(await groupController.getOne(groupId)).toBe(result); - // }); - // }); - - // describe('getMembers', () => { - // it('should return an array of members', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const result: CreateMemberDto[] = await groupService.getMembers(groupId); - // - // jest - // .spyOn(groupService, 'getMembers') - // .mockImplementation(async () => result); - // - // expect(await groupController.getMembers(groupId)).toBe(result); - // }); - // }); - - // describe('getEvents', () => { - // it('should return an array of events', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const result: CreateEventDto[] = await groupService.getEvents(groupId); - // - // jest - // .spyOn(groupService, 'getEvents') - // .mockImplementation(async () => result); - // - // expect(await groupController.getEvents(groupId)).toBe(result); - // }); - // }); - - // describe('update', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const group: UpdateGroupDto = { - // name: 'Updated Group 1', - // description: 'Updated Group 1 description', - // }; - // - // const result: Group = await groupService.update(groupId, group); - // - // jest.spyOn(groupService, 'update').mockImplementation(async () => result); - // - // expect(await groupController.update(groupId, group)).toBe(result); - // expect(result).toHaveProperty('name', group.name); - // expect(result).toHaveProperty('description', group.description); - // }); - // }); - - // describe('updateMember', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const memberId = '6626fc126cbd16603120107a'; - // const member: CreateMemberDto = { - // name: 'Updated User', - // memberInfo: { email: 'updated@user' }, - // }; - // - // const result: Member = await groupService.updateMember( - // groupId, - // memberId, - // member, - // ); - // - // jest - // .spyOn(groupService, 'updateMember') - // .mockImplementation(async () => result); - // - // expect( - // await groupController.updateMember(groupId, memberId, member), - // ).toBe(result); - // }); - // }); - - // describe('updateEvent', () => { - // it('should return a group', async () => { - // const groupId = '6626fa9e2b28caeae2d23f17'; - // const eventId = '6626fc126cbd16603120107a'; - // const event: Event = { - // id: '6626fc126cbd16603120107a', - // name: 'Updated Event 1', - // description: 'Updated Event 1 description', - // startDate: new Date(), - // endDate: new Date(), - // fee: 20000, - // attendees: ['user1', 'user2'], - // }; - // - // const result: Event = await groupService.updateEvent( - // groupId, - // eventId, - // event, - // ); - // - // jest - // .spyOn(groupService, 'updateEvent') - // .mockImplementation(async () => result); - // - // expect(await groupController.updateEvent(groupId, eventId, event)).toBe( - // result, - // ); - // }); - // }); - - afterAll(async () => { - await mongoose.connection.dropDatabase(); - await mongoose.connection.close(); - await mongoServer.stop(); - }); -}); diff --git a/src/group/group.controller.ts b/src/group/group.controller.ts index 674b4fb..92e804b 100644 --- a/src/group/group.controller.ts +++ b/src/group/group.controller.ts @@ -4,14 +4,19 @@ import { Delete, Get, Param, + ParseArrayPipe, Patch, Post, Query, + Req, UploadedFile, + UseGuards, } from '@nestjs/common'; -import { ApiBody, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { AuthGuard } from '../auth/auth.guard'; import { ApiFile } from '../common/decorators/api-file.decorator'; +import { ParseObjectIdPipe } from '../common/pipes/ParseObjectIdPipe'; import { CreateEventDto } from '../event/dto/create-event.dto'; import { UpdateEventDto } from '../event/dto/update-event.dto'; import { Event } from '../event/event.decorators'; @@ -21,6 +26,7 @@ import { Member } from '../member/member.decorators'; import { GetTransactionsPeriodDto } from '../transaction/dto/get-transaction-period-dto'; import { UploadTransactionDto } from '../transaction/dto/upload-transaction-dto'; import { Transaction } from '../transaction/transaction.decorators'; +import { User } from '../user/user.decorator'; import { CreateGroupDto } from './dto/create-group.dto'; import { UpdateGroupDto } from './dto/update-group.dto'; import { UploadGroupDto } from './dto/upload-group.dto'; @@ -28,6 +34,8 @@ import { Group } from './group.decorators'; import { GroupService } from './group.service'; @Controller('group') +@ApiBearerAuth('Authorization') +@UseGuards(AuthGuard) export class GroupController { constructor(private readonly groupService: GroupService) {} @@ -39,11 +47,8 @@ export class GroupController { }) @ApiFile('memberFile') @ApiBody({ type: UploadGroupDto }) - create( - @Body() createGroupDto: CreateGroupDto, - @UploadedFile() memberExcel: Express.Multer.File, - ) { - return this.groupService.create(createGroupDto, memberExcel); + create(@Req() req: any, @Body() createGroupDto: CreateGroupDto, @UploadedFile() memberExcel: Express.Multer.File) { + return this.groupService.create(req.userId, createGroupDto, memberExcel); } @Post(':groupId/member') @@ -54,10 +59,11 @@ export class GroupController { }) @ApiBody({ type: [CreateMemberDto] }) addMember( - @Param('groupId') groupId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, @Body() createMemberDto: CreateMemberDto[], ) { - return this.groupService.addMember(groupId, createMemberDto); + return this.groupService.addMember(req.userId, groupId, createMemberDto); } @Post(':groupId/event') @@ -67,10 +73,11 @@ export class GroupController { description: '특정 모임에 이벤트를 추가합니다.', }) addEvent( - @Param('groupId') groupId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, @Body() createEventDto: CreateEventDto, ) { - return this.groupService.addEvent(groupId, createEventDto); + return this.groupService.addEvent(req.userId, groupId, createEventDto); } @Post(':groupId/event/:eventId/member') @@ -81,11 +88,12 @@ export class GroupController { }) @ApiBody({ type: [String] }) addMemberToEvent( - @Param('groupId') groupId: string, - @Param('eventId') eventId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Param('eventId', new ParseObjectIdPipe()) eventId: string, @Body() memberIds: string[], ) { - return this.groupService.addMemberToEvent(groupId, eventId, memberIds); + return this.groupService.addMemberToEvent(req.userId, groupId, eventId, memberIds); } @Post(':groupId/member/excel') @@ -96,10 +104,11 @@ export class GroupController { }) @ApiFile('memberFile') uploadMemberFile( - @Param('groupId') groupId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, @UploadedFile() memberExcel: Express.Multer.File, ) { - return this.groupService.uploadMemberFile(groupId, memberExcel); + return this.groupService.uploadMemberFile(req.userId, groupId, memberExcel); } @Post(':groupId/transaction/excel') @@ -111,11 +120,13 @@ export class GroupController { @ApiFile('transactionFile') @ApiBody({ type: UploadTransactionDto }) uploadTransactionFile( - @Param('groupId') groupId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, @Body() uploadTransactionDto: UploadTransactionDto, @UploadedFile() transactionExcel: Express.Multer.File, ) { return this.groupService.uploadTransactionFile( + req.userId, groupId, transactionExcel, uploadTransactionDto.password, @@ -128,8 +139,8 @@ export class GroupController { summary: '모든 모임 조회', description: '모든 모임을 조회합니다.', }) - getAll() { - return this.groupService.getAll(); + getAll(@Req() req: any) { + return this.groupService.getAll(req.userId); } @Get(':groupId') @@ -143,8 +154,8 @@ export class GroupController { summary: '모임 상세 조회', description: '특정 모임을 조회합니다. (멤버, 이벤트 포함)', }) - getOne(@Param('groupId') groupId: string) { - return this.groupService.getOne(groupId); + getOne(@Req() req: any, @Param('groupId', new ParseObjectIdPipe()) groupId: string) { + return this.groupService.getOne(req.userId, groupId); } @Get(':groupId/member') @@ -153,8 +164,8 @@ export class GroupController { summary: '모임 멤버 조회', description: '특정 모임의 멤버를 조회합니다.', }) - getMembers(@Param('groupId') groupId: string) { - return this.groupService.getMembers(groupId); + getMembers(@Req() req: any, @Param('groupId', new ParseObjectIdPipe()) groupId: string) { + return this.groupService.getMembers(req.userId, groupId); } @Get(':groupId/event') @@ -163,8 +174,8 @@ export class GroupController { summary: '모임 이벤트 조회', description: '특정 모임의 이벤트를 조회합니다.', }) - getEvents(@Param('groupId') groupId: string) { - return this.groupService.getEvents(groupId); + getEvents(@Req() req: any, @Param('groupId', new ParseObjectIdPipe()) groupId: string) { + return this.groupService.getEvents(req.userId, groupId); } @Get(':groupId/event/:eventId') @@ -173,11 +184,8 @@ export class GroupController { summary: '모임 이벤트 상세 조회', description: '특정 모임의 특정 이벤트를 조회합니다.', }) - getEvent( - @Param('groupId') groupId: string, - @Param('eventId') eventId: string, - ) { - return this.groupService.getEvent(groupId, eventId); + getEvent(@Req() req: any, @Param('groupId') groupId: string, @Param('eventId') eventId: string) { + return this.groupService.getEvent(req.userId, groupId, eventId); } @Get(':groupId/transaction') @@ -186,8 +194,8 @@ export class GroupController { summary: '모임 거래내역 조회', description: '특정 모임의 거래내역을 조회합니다.', }) - getTransactions(@Param('groupId') groupId: string) { - return this.groupService.getTransactions(groupId); + getTransactions(@Req() req: any, @Param('groupId', new ParseObjectIdPipe()) groupId: string) { + return this.groupService.getTransactions(req.userId, groupId); } @Get(':groupId/transaction/period') @@ -197,13 +205,11 @@ export class GroupController { description: '특정 모임의 거래내역을 기간을 설정하여 조회합니다.', }) getTransactionsByPeriod( - @Param('groupId') groupId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, @Query() getTransactionsPeriodDto: GetTransactionsPeriodDto, ) { - return this.groupService.getTransactionsByPeriod( - groupId, - getTransactionsPeriodDto, - ); + return this.groupService.getTransactionsByPeriod(req.userId, groupId, getTransactionsPeriodDto); } @Get(':groupId/event/:eventId/transaction') @@ -224,10 +230,11 @@ export class GroupController { description: '이벤트 ID', }) getTransactionsByEvent( - @Param('groupId') groupId: string, - @Param('eventId') eventId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Param('eventId', new ParseObjectIdPipe()) eventId: string, ) { - return this.groupService.getTransactionsByEvent(groupId, eventId); + return this.groupService.getTransactionsByEvent(req.userId, groupId, eventId); } @Patch(':groupId') @@ -242,10 +249,11 @@ export class GroupController { description: '특정 모임을 수정합니다.', }) update( - @Param('groupId') groupId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, @Body() updateGroupDto: UpdateGroupDto, ) { - return this.groupService.update(groupId, updateGroupDto); + return this.groupService.update(req.userId, groupId, updateGroupDto); } @Patch(':groupId/member/:memberId') @@ -255,11 +263,12 @@ export class GroupController { description: '특정 모임의 멤버를 수정합니다.', }) updateMember( - @Param('groupId') groupId: string, - @Param('memberId') memberId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Param('memberId', new ParseObjectIdPipe()) memberId: string, @Body() updateMemberDto: UpdateMemberDto, ) { - return this.groupService.updateMember(groupId, memberId, updateMemberDto); + return this.groupService.updateMember(req.userId, groupId, memberId, updateMemberDto); } @Patch(':groupId/event/:eventId') @@ -269,11 +278,12 @@ export class GroupController { description: '특정 모임의 이벤트를 수정합니다.', }) updateEvent( - @Param('groupId') groupId: string, - @Param('eventId') eventId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Param('eventId', new ParseObjectIdPipe()) eventId: string, @Body() updateEventDto: UpdateEventDto, ) { - return this.groupService.updateEvent(groupId, eventId, updateEventDto); + return this.groupService.updateEvent(req.userId, groupId, eventId, updateEventDto); } @Delete(':groupId') @@ -287,8 +297,8 @@ export class GroupController { summary: '모임 삭제', description: '특정 모임을 삭제합니다.', }) - delete(@Param('groupId') groupId: string) { - return this.groupService.delete(groupId); + delete(@Req() req: any, @Param('groupId', new ParseObjectIdPipe()) groupId: string) { + return this.groupService.delete(req.userId, groupId); } @Delete(':groupId/member') @@ -298,10 +308,11 @@ export class GroupController { description: '특정 모임의 멤버를 삭제합니다.', }) deleteMembers( - @Param('groupId') groupId: string, - @Body() memberIds: string[], + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Body(new ParseArrayPipe({ items: String, separator: ',' })) memberIds: string[], ) { - return this.groupService.deleteMembers(groupId, memberIds); + return this.groupService.deleteMembers(req.userId, groupId, memberIds); } @Delete(':groupId/event/:eventId') @@ -311,9 +322,38 @@ export class GroupController { description: '특정 모임의 이벤트를 삭제합니다.', }) deleteEvent( - @Param('groupId') groupId: string, - @Param('eventId') eventId: string, + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Param('eventId', new ParseObjectIdPipe()) eventId: string, + ) { + return this.groupService.deleteEvent(req.userId, groupId, eventId); + } + + @Post(':groupId/invite/editor') + @User() + @ApiOperation({ + summary: '모임 편집자 초대', + description: '특정 모임에 편집자를 초대합니다.', + }) + inviteEditor( + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Query('userId', new ParseObjectIdPipe()) userId: string, + ) { + return this.groupService.inviteEditor(req.userId, groupId, userId); + } + + @Post(':groupId/invite/viewer') + @User() + @ApiOperation({ + summary: '모임 조회자 초대', + description: '특정 모임에 조회자를 초대합니다.', + }) + inviteViewer( + @Req() req: any, + @Param('groupId', new ParseObjectIdPipe()) groupId: string, + @Query('userId', new ParseObjectIdPipe()) userId: string, ) { - return this.groupService.deleteEvent(groupId, eventId); + return this.groupService.inviteViewer(req.userId, groupId, userId); } } diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 60584ee..a3ecb31 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -4,15 +4,17 @@ import { DatabaseModule } from '../database/database.module'; import { EventModule } from '../event/event.module'; import { MemberModule } from '../member/member.module'; import { TransactionModule } from '../transaction/transaction.module'; +import { UserModule } from '../user/user.module'; import { GroupController } from './group.controller'; import { groupProviders } from './group.providers'; import { GroupRepository } from './group.repository'; import { GroupService } from './group.service'; +import { GroupValidator } from './group.validator'; @Module({ - imports: [DatabaseModule, MemberModule, EventModule, TransactionModule], + imports: [DatabaseModule, MemberModule, EventModule, TransactionModule, UserModule], controllers: [GroupController], - providers: [GroupService, ...groupProviders, GroupRepository], + providers: [GroupService, ...groupProviders, GroupRepository, GroupValidator], exports: [GroupService], }) export class GroupModule {} diff --git a/src/group/group.providers.ts b/src/group/group.providers.ts index 97130c7..25029ef 100644 --- a/src/group/group.providers.ts +++ b/src/group/group.providers.ts @@ -5,8 +5,7 @@ import { GroupSchema } from './schemas/group.schema'; export const groupProviders = [ { provide: 'GROUP_MODEL', - useFactory: (connection: Connection) => - connection.model('Group', GroupSchema), + useFactory: (connection: Connection) => connection.model('Group', GroupSchema), inject: ['MONGODB_CONNECTION'], }, ]; diff --git a/src/group/group.repository.ts b/src/group/group.repository.ts index fae995d..c179367 100644 --- a/src/group/group.repository.ts +++ b/src/group/group.repository.ts @@ -21,14 +21,16 @@ export class GroupRepository { return this.groupModel.find().exec() as Promise; } + findByIds(groupIds: string[]): Promise { + return this.groupModel.find({ _id: { $in: groupIds } }).exec() as Promise; + } + findOne(groupId: string): Promise { return this.groupModel.findById(groupId).exec() as Promise; } update(groupId: string, updateGroupDto: UpdateGroupDto): Promise { - return this.groupModel - .findByIdAndUpdate(groupId, updateGroupDto, { new: true }) - .exec() as Promise; + return this.groupModel.findByIdAndUpdate(groupId, updateGroupDto, { new: true }).exec() as Promise; } delete(groupId: string) { diff --git a/src/group/group.service.ts b/src/group/group.service.ts index 16ed6e0..4a61e51 100644 --- a/src/group/group.service.ts +++ b/src/group/group.service.ts @@ -1,18 +1,20 @@ import { Injectable } from '@nestjs/common'; +import mongoose from 'mongoose'; import { CreateEventDto } from '../event/dto/create-event.dto'; import { UpdateEventDto } from '../event/dto/update-event.dto'; -import { Event } from '../event/entities/event.entity'; import { EventService } from '../event/event.service'; import { CreateMemberDto } from '../member/dto/create-member.dto'; import { UpdateMemberDto } from '../member/dto/update-member.dto'; import { MemberService } from '../member/member.service'; import { GetTransactionsPeriodDto } from '../transaction/dto/get-transaction-period-dto'; import { TransactionService } from '../transaction/transaction.service'; +import { UserService } from '../user/user.service'; import { CreateGroupDto } from './dto/create-group.dto'; import { UpdateGroupDto } from './dto/update-group.dto'; import { Group } from './entities/group.entity'; import { GroupRepository } from './group.repository'; +import { GroupValidator } from './group.validator'; @Injectable() export class GroupService { @@ -21,26 +23,31 @@ export class GroupService { private readonly memberService: MemberService, private readonly eventService: EventService, private readonly transactionService: TransactionService, + private readonly userService: UserService, + private readonly groupValidator: GroupValidator, ) {} - async create( - createGroupDto: CreateGroupDto, - memberExcel: Express.Multer.File, - ): Promise { + async create(userId: string, createGroupDto: CreateGroupDto, memberExcel: Express.Multer.File): Promise { const group: Group = createGroupDto as Group; + group.auth = { + owner: userId, + editors: [], + viewers: [], + }; if (memberExcel) { group.members = await this.memberService.uploadMemberFile(memberExcel); } - return await this.groupRepository.create(group); + const createdGroup = await this.groupRepository.create(group); + await this.userService.pushOwner(userId, createdGroup.id); + + return createdGroup; } - async addMember( - groupId: string, - createMemberDto: CreateMemberDto[], - ): Promise { + async addMember(userId: string, groupId: string, createMemberDto: CreateMemberDto[]): Promise { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); for (const member of createMemberDto) { group.members.push(await this.memberService.create(member)); @@ -51,12 +58,10 @@ export class GroupService { return group; } - async addMemberToEvent( - groupId: string, - eventId: string, - memberIds: string[], - ) { + async addMemberToEvent(userId: string, groupId: string, eventId: string, memberIds: string[]) { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + const event = await this.eventService.getOne(eventId); for (const member of memberIds) { @@ -71,11 +76,9 @@ export class GroupService { return event; } - async uploadMemberFile( - groupId: string, - memberExcel: Express.Multer.File, - ): Promise { - const group: Group = await this.groupRepository.findOne(groupId); + async uploadMemberFile(userId: string, groupId: string, memberExcel: Express.Multer.File): Promise { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); group.members = await this.memberService.uploadMemberFile(memberExcel); @@ -83,22 +86,21 @@ export class GroupService { } async uploadTransactionFile( + userId: string, groupId: string, transactionExcel: Express.Multer.File, password: string, ): Promise { - await this.transactionService.uploadTransactionFile( - groupId, - transactionExcel, - password, - ); + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + + await this.transactionService.uploadTransactionFile(groupId, transactionExcel, password); } - async addEvent( - groupId: string, - createEventDto: CreateEventDto, - ): Promise { + async addEvent(userId: string, groupId: string, createEventDto: CreateEventDto): Promise { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + const eventId = await this.eventService.create(createEventDto); group.events.push(eventId); @@ -107,12 +109,16 @@ export class GroupService { return { eventId }; } - async getAll(): Promise { - return await this.groupRepository.findAll(); + async getAll(userId: string): Promise { + const user = await this.userService.getOne(userId); + const groupIds = user.auth.owner.concat(user.auth.editor).concat(user.auth.viewer); + + return await this.groupRepository.findByIds(groupIds); } - async getOne(groupId: string): Promise { + async getOne(userId: string, groupId: string): Promise { const group: Group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); const members = await this.getAllMembers(group.members); const events = await this.getAllEvents(group.events); @@ -123,8 +129,10 @@ export class GroupService { return group; } - async getMembers(groupId: string) { + async getMembers(userId: string, groupId: string) { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); + const members = await this.getAllMembers(group.members); const memberInfo = []; if (group.members.length > 0) { @@ -136,51 +144,55 @@ export class GroupService { return { memberInfo, members }; } - async getEvents(groupId: string) { - return this.groupRepository.findOne(groupId).then((group) => { - return this.getAllEvents(group.events); - }); + async getEvents(userId: string, groupId: string) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); + + return await this.getAllEvents(group.events); } - async getEvent(groupId: string, eventId: string) { + async getEvent(userId: string, groupId: string, eventId: string) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); + return this.eventService.getOne(eventId); } - async getTransactions(groupId: string) { + async getTransactions(userId: string, groupId: string) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); + return this.transactionService.getTransactions(groupId); } - async getTransactionsByEvent(groupId: string, eventId: string) { + async getTransactionsByEvent(userId: string, groupId: string, eventId: string) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); + return this.eventService.compareEventTransactions(groupId, eventId); } - async getTransactionsByPeriod( - groupId: string, - periodDto: GetTransactionsPeriodDto, - ) { - return this.transactionService.getTransactionsByPeriod( - groupId, - periodDto.startDate, - periodDto.endDate, - ); + async getTransactionsByPeriod(userId: string, groupId: string, periodDto: GetTransactionsPeriodDto) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, userId); + + return this.transactionService.getTransactionsByPeriod(groupId, periodDto.startDate, periodDto.endDate); } - async update( - groupId: string, - updateGroupDto: UpdateGroupDto, - ): Promise { + async update(userId: string, groupId: string, updateGroupDto: UpdateGroupDto): Promise { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + return await this.groupRepository.update(groupId, { name: updateGroupDto.name, description: updateGroupDto.description, } as UpdateGroupDto); } - async updateMember( - groupId: string, - memberId: string, - updateMemberDto: UpdateMemberDto, - ) { + async updateMember(userId: string, groupId: string, memberId: string, updateMemberDto: UpdateMemberDto) { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + const member = await this.memberService.update(memberId, updateMemberDto); group.members = group.members.map((m) => (m === memberId ? member.id : m)); @@ -190,16 +202,11 @@ export class GroupService { return member; } - async updateEvent( - groupId: string, - eventId: string, - updateEventDto: UpdateEventDto, - ) { - const group: Group = await this.groupRepository.findOne(groupId); - const event: Event = await this.eventService.update( - eventId, - updateEventDto, - ); + async updateEvent(userId: string, groupId: string, eventId: string, updateEventDto: UpdateEventDto) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + + const event = await this.eventService.update(eventId, updateEventDto); group.events = group.events.map((e) => (e === eventId ? event.id : e)); @@ -208,24 +215,37 @@ export class GroupService { return event; } - async delete(groupId: string): Promise { + async delete(userId: string, groupId: string): Promise { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupOwner(group, userId); + return this.groupRepository.delete(groupId); } - async deleteMembers(groupId: string, memberIds: string[]): Promise { + async deleteMembers(userId: string, groupId: string, memberIds: string[]): Promise { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + const groupMembers = group.members; + for (const memberId of memberIds) { + if (!mongoose.Types.ObjectId.isValid(memberId)) { + throw new Error('유효하지 않은 ObjectId입니다.'); + } + if (!groupMembers.includes(memberId)) { + throw new Error('멤버가 존재하지 않습니다.'); + } + } - group.members = groupMembers.filter( - (member) => !memberIds.includes(member), - ); + group.members = groupMembers.filter((member) => !memberIds.includes(member)); this.memberService.deleteMany(memberIds); await this.groupRepository.update(groupId, group); } - async deleteEvent(groupId: string, eventId: string): Promise { + async deleteEvent(userId: string, groupId: string, eventId: string): Promise { const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupEditor(group, userId); + const groupEvents = group.events; group.events = groupEvents.filter((event) => event !== eventId); @@ -234,6 +254,28 @@ export class GroupService { await this.groupRepository.update(groupId, group); } + async inviteEditor(senderId: string, groupId: string, receiverId: string) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupOwner(group, senderId); + this.groupValidator.validateGroupInvite(group, receiverId); + + group.auth.editors.push(receiverId); + await this.userService.pushEditor(receiverId, groupId); + + return await this.groupRepository.update(groupId, group); + } + + async inviteViewer(senderId: string, groupId: string, receiverId: string) { + const group = await this.groupRepository.findOne(groupId); + this.groupValidator.validateGroupViewer(group, senderId); + this.groupValidator.validateGroupInvite(group, receiverId); + + group.auth.viewers.push(receiverId); + await this.userService.pushViewer(receiverId, groupId); + + return await this.groupRepository.update(groupId, group); + } + private async getAllMembers(memberIds: string[]) { const members = []; for (const memberId of memberIds) { diff --git a/src/group/group.validator.ts b/src/group/group.validator.ts new file mode 100644 index 0000000..6070816 --- /dev/null +++ b/src/group/group.validator.ts @@ -0,0 +1,48 @@ +import { ConflictException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; + +import { Group } from './entities/group.entity'; + +@Injectable() +export class GroupValidator { + validateGroupOwner(group: Group, userId: string) { + this.validateGroup(group); + + if (group.auth.owner !== userId) { + throw new UnauthorizedException('소유자 권한이 없습니다.'); + } + } + + validateGroupEditor(group: Group, userId: string) { + this.validateGroup(group); + + if (group.auth.owner !== userId && !group.auth.editors.includes(userId)) { + throw new UnauthorizedException('편집 권한이 없습니다.'); + } + } + + validateGroupViewer(group: Group, userId: string) { + this.validateGroup(group); + + if (group.auth.owner !== userId && !group.auth.editors.includes(userId) && !group.auth.viewers.includes(userId)) { + throw new UnauthorizedException('조회 권한이 없습니다.'); + } + } + + validateGroupInvite(group: Group, receiverId: string) { + this.validateGroup(group); + + if ( + group.auth.owner === receiverId || + group.auth.editors.includes(receiverId) || + group.auth.viewers.includes(receiverId) + ) { + throw new ConflictException('이미 초대된 사용자입니다.'); + } + } + + private validateGroup(group: Group) { + if (!group) { + throw new NotFoundException('모임을 찾을 수 없습니다.'); + } + } +} diff --git a/src/group/interfaces/group.interface.ts b/src/group/interfaces/group.interface.ts index 130e7b0..9b14a8b 100644 --- a/src/group/interfaces/group.interface.ts +++ b/src/group/interfaces/group.interface.ts @@ -3,13 +3,13 @@ import { Document } from 'mongoose'; export interface GroupInterface extends Document { name: string; description: string; - manager: string; - subManagers: [ - { - user: string; - authorities: string[]; - }, - ]; + + auth: { + owner: string; + editors: string[]; + viewers: string[]; + }; + members: any[]; events: any[]; } diff --git a/src/group/schemas/group.schema.ts b/src/group/schemas/group.schema.ts index 30ad9b0..81110f8 100644 --- a/src/group/schemas/group.schema.ts +++ b/src/group/schemas/group.schema.ts @@ -3,16 +3,13 @@ import { Schema } from 'mongoose'; export const GroupSchema = new Schema({ name: String, description: String, - manager: String, - subManagers: { - type: [ - { - user: String, - authorities: [String], - }, - ], - default: [], + + auth: { + owner: String, + editors: [String], + viewers: [String], }, + members: { type: [String], default: [] }, events: { type: [String], default: [] }, }); diff --git a/src/main.ts b/src/main.ts index 26debf1..b348dcb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,13 +14,7 @@ async function bootstrap() { } bootstrap().then(() => { - console.log( - `Server running on http://localhost:${process.env.SERVER_PORT}/api`, - ); - console.log( - `Dev server running on http://localhost:${process.env.SERVER_PORT}/dev`, - ); - console.log( - `Auth server running on http://localhost:${process.env.SERVER_PORT}/auth`, - ); + console.log(`Server running on http://localhost:${process.env.SERVER_PORT}/api`); + console.log(`Dev server running on http://localhost:${process.env.SERVER_PORT}/dev`); + console.log(`Auth server running on http://localhost:${process.env.SERVER_PORT}/auth`); }); diff --git a/src/member/member.module.ts b/src/member/member.module.ts index f3873e6..4505acd 100644 --- a/src/member/member.module.ts +++ b/src/member/member.module.ts @@ -8,12 +8,7 @@ import { MemberService } from './member.service'; @Module({ imports: [DatabaseModule], - providers: [ - MemberService, - ...memberProviders, - MemberRepository, - ExcelService, - ], + providers: [MemberService, ...memberProviders, MemberRepository, ExcelService], exports: [MemberService], }) export class MemberModule {} diff --git a/src/member/member.providers.ts b/src/member/member.providers.ts index c56fe1a..280b7d3 100644 --- a/src/member/member.providers.ts +++ b/src/member/member.providers.ts @@ -5,8 +5,7 @@ import { MemberSchema } from './schemas/member.schema'; export const memberProviders = [ { provide: 'MEMBER_MODEL', - useFactory: (connection: Connection) => - connection.model('Member', MemberSchema), + useFactory: (connection: Connection) => connection.model('Member', MemberSchema), inject: ['MONGODB_CONNECTION'], }, ]; diff --git a/src/member/member.repository.ts b/src/member/member.repository.ts index f813666..8f776f7 100644 --- a/src/member/member.repository.ts +++ b/src/member/member.repository.ts @@ -22,9 +22,7 @@ export class MemberRepository { } update(memberId: string, updateMemberDto: UpdateMemberDto): Promise { - return this.memberModel - .findByIdAndUpdate(memberId, updateMemberDto, { new: true }) - .exec() as Promise; + return this.memberModel.findByIdAndUpdate(memberId, updateMemberDto, { new: true }).exec() as Promise; } deleteMany(memberIds: string[]) { diff --git a/src/member/member.service.ts b/src/member/member.service.ts index 0a34171..816c1c4 100644 --- a/src/member/member.service.ts +++ b/src/member/member.service.ts @@ -18,8 +18,7 @@ export class MemberService { } async uploadMemberFile(excel: Express.Multer.File): Promise { - const members: Member[] = - await this.excelService.convertMemberExcelToMembers(excel); + const members: Member[] = await this.excelService.convertMemberExcelToMembers(excel); return await this.createGroupMembers(members); } @@ -28,10 +27,7 @@ export class MemberService { return await this.memberRepository.findOne(memberId); } - async update( - memberId: string, - updateMemberDto: UpdateMemberDto, - ): Promise { + async update(memberId: string, updateMemberDto: UpdateMemberDto): Promise { return (await this.memberRepository.update(memberId, { name: updateMemberDto.name, memberInfo: updateMemberDto.memberInfo, diff --git a/src/transaction/transaction.module.ts b/src/transaction/transaction.module.ts index 8af84c1..fefdf89 100644 --- a/src/transaction/transaction.module.ts +++ b/src/transaction/transaction.module.ts @@ -8,12 +8,7 @@ import { TransactionService } from './transaction.service'; @Module({ imports: [DatabaseModule], - providers: [ - TransactionService, - ...transactionProviders, - TransactionRepository, - ExcelService, - ], + providers: [TransactionService, ...transactionProviders, TransactionRepository, ExcelService], exports: [TransactionService], }) export class TransactionModule {} diff --git a/src/transaction/transaction.providers.ts b/src/transaction/transaction.providers.ts index 8f358bb..a2874c2 100644 --- a/src/transaction/transaction.providers.ts +++ b/src/transaction/transaction.providers.ts @@ -5,8 +5,7 @@ import { TransactionSchema } from './schemas/transaction.schema'; export const transactionProviders = [ { provide: 'TRANSACTION_MODEL', - useFactory: (connection: Connection) => - connection.model('Transaction', TransactionSchema), + useFactory: (connection: Connection) => connection.model('Transaction', TransactionSchema), inject: ['MONGODB_CONNECTION'], }, ]; diff --git a/src/transaction/transaction.repository.ts b/src/transaction/transaction.repository.ts index 0b6c3a2..e366b57 100644 --- a/src/transaction/transaction.repository.ts +++ b/src/transaction/transaction.repository.ts @@ -19,11 +19,7 @@ export class TransactionRepository { return this.transactionModel.find({ 'metadata.groupId': groupId }); } - async getTransactionsByPeriod( - groupId: string, - startDate: Date, - endDate: Date, - ): Promise { + async getTransactionsByPeriod(groupId: string, startDate: Date, endDate: Date): Promise { return this.transactionModel .find({ 'metadata.groupId': groupId, diff --git a/src/transaction/transaction.service.ts b/src/transaction/transaction.service.ts index d4b2486..bd13c58 100644 --- a/src/transaction/transaction.service.ts +++ b/src/transaction/transaction.service.ts @@ -11,18 +11,13 @@ export class TransactionService { private readonly excelService: ExcelService, ) {} - async uploadTransactionFile( - groupId: string, - transactionFile: Express.Multer.File, - password: string, - ): Promise { + async uploadTransactionFile(groupId: string, transactionFile: Express.Multer.File, password: string): Promise { await this.transactionRepository.deleteMany(groupId); - const transactions: Transaction[] = - await this.excelService.convertTransactionFileToTransactionArr( - groupId, - transactionFile, - password, - ); + const transactions: Transaction[] = await this.excelService.convertTransactionFileToTransactionArr( + groupId, + transactionFile, + password, + ); await this.transactionRepository.createMany(transactions); } @@ -30,15 +25,7 @@ export class TransactionService { return this.transactionRepository.getTransactions(groupId); } - async getTransactionsByPeriod( - groupId: string, - startDate: Date, - endDate: Date, - ) { - return this.transactionRepository.getTransactionsByPeriod( - groupId, - startDate, - endDate, - ); + async getTransactionsByPeriod(groupId: string, startDate: Date, endDate: Date) { + return this.transactionRepository.getTransactionsByPeriod(groupId, startDate, endDate); } } diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index c20f5c4..96df6bc 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -3,4 +3,9 @@ export class User { kakaoId: number; name: string; email: string; + auth: { + owner: string[]; + editor: string[]; + viewer: string[]; + }; } diff --git a/src/user/interfaces/user.interfaces.ts b/src/user/interfaces/user.interfaces.ts index 6bb250f..749399c 100644 --- a/src/user/interfaces/user.interfaces.ts +++ b/src/user/interfaces/user.interfaces.ts @@ -2,4 +2,9 @@ export interface UserInterface extends Document { kakaoId: number; name: string; email: string; + auth: { + owner: string[]; + editor: string[]; + viewer: string[]; + }; } diff --git a/src/user/schemas/user.schema.ts b/src/user/schemas/user.schema.ts index c10ded3..dea0aa2 100644 --- a/src/user/schemas/user.schema.ts +++ b/src/user/schemas/user.schema.ts @@ -4,4 +4,9 @@ export const UserSchema = new Schema({ kakaoId: Number, name: String, email: String, + auth: { + owner: [String], + editor: [String], + viewer: [String], + }, }); diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 2e35fb6..5046241 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -1,68 +1,15 @@ -import { - Body, - Controller, - Delete, - Get, - Param, - Patch, - Post, - Request, - UseGuards, -} from '@nestjs/common'; +import { Controller, Get, Request, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from '../auth/auth.guard'; -import { CreateUserDto } from './dto/create-user.dto'; -import { UpdateUserDto } from './dto/update-user.dto'; -import { UserService } from './user.service'; @ApiTags('User') @Controller('user') export class UserController { - constructor(private readonly userService: UserService) {} - - @Post() - create(@Body() createUserDto: CreateUserDto) { - return this.userService.create(createUserDto); - } - - @Get() - findAll() { - return this.userService.findAll(); - } - @UseGuards(AuthGuard) @ApiBearerAuth('Authorization') @Get('me') me(@Request() req: any) { return req.user; } - - @Get(':userId') - findOne(@Param('userId') userId: string) { - return this.userService.findOne(userId); - } - - @Get('kakao/:kakaoId') - findOneByKakaoId(@Param('kakaoId') kakaoId: string) { - return this.userService.findOneByKakaoId(kakaoId); - } - - @Patch(':userId') - update( - @Param('userId') userId: string, - @Body() updateUserDto: UpdateUserDto, - ) { - return this.userService.update(userId, updateUserDto); - } - - @Delete() - removeAll() { - return this.userService.removeAll(); - } - - @Delete(':userId') - remove(@Param('userId') userId: string) { - return this.userService.remove(userId); - } } diff --git a/src/user/user.decorator.ts b/src/user/user.decorator.ts new file mode 100644 index 0000000..1acabb6 --- /dev/null +++ b/src/user/user.decorator.ts @@ -0,0 +1,6 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +export function User() { + return applyDecorators(ApiTags('User')); +} diff --git a/src/user/user.providers.ts b/src/user/user.providers.ts index a8b729d..c941b5b 100644 --- a/src/user/user.providers.ts +++ b/src/user/user.providers.ts @@ -5,8 +5,7 @@ import { UserSchema } from './schemas/user.schema'; export const userProviders = [ { provide: 'USER_MODEL', - useFactory: (connection: Connection) => - connection.model('User', UserSchema), + useFactory: (connection: Connection) => connection.model('User', UserSchema), inject: ['MONGODB_CONNECTION'], }, ]; diff --git a/src/user/user.repository.ts b/src/user/user.repository.ts index 53a36d5..1cc0a70 100644 --- a/src/user/user.repository.ts +++ b/src/user/user.repository.ts @@ -29,9 +29,7 @@ export class UserRepository { } update(userId: string, updateUserDto: CreateUserDto) { - return this.userModel - .findByIdAndUpdate(userId, updateUserDto, { new: true }) - .exec() as Promise; + return this.userModel.findByIdAndUpdate(userId, updateUserDto, { new: true }).exec() as Promise; } deleteAll() { diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 281dde8..2e7e0ae 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -13,15 +13,15 @@ export class UserService { return await this.userRepository.create(createUserDto); } - async findAll() { + async getAll() { return await this.userRepository.findAll(); } - async findOne(userId: string): Promise { + async getOne(userId: string): Promise { return await this.userRepository.findOne(userId); } - async findOneByKakaoId(kakaoId: string) { + async getOneByKakaoId(kakaoId: string) { return await this.userRepository.findOneByKakaoId(kakaoId); } @@ -29,15 +29,51 @@ export class UserService { return { userId, updateUserDto }; } - removeAll() { + async pushOwner(userId: string, groupId: string) { + const user = await this.userRepository.findOne(userId); + user.auth.owner.push(groupId); + return this.userRepository.update(userId, user); + } + + async pushEditor(userId: string, groupId: string) { + const user = await this.userRepository.findOne(userId); + user.auth.editor.push(groupId); + return this.userRepository.update(userId, user); + } + + async pushViewer(userId: string, groupId: string) { + const user = await this.userRepository.findOne(userId); + user.auth.viewer.push(groupId); + return this.userRepository.update(userId, user); + } + + async popOwner(userId: string, groupId: string) { + const user = await this.userRepository.findOne(userId); + user.auth.owner = user.auth.owner.filter((id) => id !== groupId); + return this.userRepository.update(userId, user); + } + + async popEditor(userId: string, groupId: string) { + const user = await this.userRepository.findOne(userId); + user.auth.editor = user.auth.editor.filter((id) => id !== groupId); + return this.userRepository.update(userId, user); + } + + async popViewer(userId: string, groupId: string) { + const user = await this.userRepository.findOne(userId); + user.auth.viewer = user.auth.viewer.filter((id) => id !== groupId); + return this.userRepository.update(userId, user); + } + + deleteAll() { return this.userRepository.deleteAll(); } - remove(userId: string) { + delete(userId: string) { return this.userRepository.delete(userId); } - removeOneByKakaoId(kakaoId: string) { + deleteOneByKakaoId(kakaoId: string) { return this.userRepository.deleteOneByKakaoId(kakaoId); } } diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 22e885e..bebdb74 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -17,9 +17,6 @@ describe('AppController (e2e)', () => { }); it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); }); });