Skip to content

Commit

Permalink
feat: 엑셀 파일 업로드 (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
w8385 authored Apr 18, 2024
1 parent 4147688 commit 3cd38c2
Show file tree
Hide file tree
Showing 21 changed files with 902 additions and 185 deletions.
703 changes: 686 additions & 17 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@typescript-eslint/typescript-estree": "^7.5.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"exceljs": "^4.4.0",
"mongodb-memory-server": "^9.1.8",
"mongoose": "^8.3.0",
"reflect-metadata": "^0.1.13",
Expand All @@ -40,6 +41,7 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
Expand Down
21 changes: 21 additions & 0 deletions src/api-file.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiBody, ApiConsumes } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { applyDecorators, UseInterceptors } from '@nestjs/common';

export function ApiFile(fieldName: string = 'file') {
return applyDecorators(
UseInterceptors(FileInterceptor(fieldName)),
ApiConsumes('multipart/form-data'),
ApiBody({
schema: {
type: 'object',
properties: {
[fieldName]: {
type: 'string',
format: 'binary',
},
},
},
}),
);
}
6 changes: 2 additions & 4 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { GroupModule } from './group/group.module';
import { AuthModule } from './auth/auth.module';
import { TransactionModule } from './transaction/transaction.module';
import { MemberModule } from './member/member.module';
import { ConfigModule } from '@nestjs/config';
import { EventModule } from './event/event.module';
Expand All @@ -12,12 +11,11 @@ import { EventModule } from './event/event.module';
ConfigModule.forRoot({
isGlobal: true,
}),
UserModule,
GroupModule,
AuthModule,
TransactionModule,
MemberModule,
EventModule,
AuthModule,
UserModule,
],
})
export class AppModule {}
5 changes: 2 additions & 3 deletions src/config.swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export const configSwagger = (app: INestApplication<any>) => {
const config = new DocumentBuilder()
.setTitle('sometime API')
.addTag('Auth', 'OAuth 인증 관련 API')
.addTag('Group', '모임 관련 API')
.addTag('Member', '모임 회원 관련 API')
.addTag('Transaction', '거래내역 관련 API')
.addTag('User', '사용자 관련 API')
.addTag('Event', '이벤트 관련 API')
.addTag('Auth', 'OAuth 인증 관련 API')
.addTag('User', '사용자 관련 API')
.build();

const document = SwaggerModule.createDocument(app, config);
Expand Down
48 changes: 37 additions & 11 deletions src/event/event.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import {
import { EventService } from './event.service';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
import { ApiTags } from '@nestjs/swagger';
import { ApiParam, ApiTags } from '@nestjs/swagger';

@ApiTags('Event')
@Controller('event')
@Controller('group/:groupId/event')
@ApiParam({
name: 'groupId',
required: true,
description: '모임 ID',
})
export class EventController {
constructor(private readonly eventService: EventService) {}

Expand All @@ -27,18 +32,39 @@ export class EventController {
return this.eventService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.eventService.findOne(+id);
@Get(':eventId')
@ApiParam({
name: 'memberId',
required: true,
description: '모임 회원 ID',
})
findOne(
@Param('groupId') groupId: string,
@Param('eventId') eventId: string,
) {
return this.eventService.findOne(groupId, eventId);
}

@Patch(':id')
update(@Param('id') id: string, @Body() updateEventDto: UpdateEventDto) {
return this.eventService.update(+id, updateEventDto);
@Patch(':eventId')
@ApiParam({
name: 'memberId',
required: true,
description: '모임 회원 ID',
})
update(
@Param('eventId') eventId: string,
@Body() updateEventDto: UpdateEventDto,
) {
return this.eventService.update(+eventId, updateEventDto);
}

@Delete(':id')
remove(@Param('id') id: string) {
return this.eventService.remove(+id);
@Delete(':eventId')
@ApiParam({
name: 'memberId',
required: true,
description: '모임 회원 ID',
})
remove(@Param('eventId') eventId: string) {
return this.eventService.remove(+eventId);
}
}
15 changes: 9 additions & 6 deletions src/event/event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ export class EventService {
return `This action returns all event`;
}

findOne(id: number) {
return `This action returns a #${id} event`;
findOne(groupId: string, eventId: string) {
return {
groupId,
eventId,
};
}

update(id: number, updateEventDto: UpdateEventDto) {
return `This action updates a #${id} event` + updateEventDto;
update(eventId: number, updateEventDto: UpdateEventDto) {
return `This action updates a #${eventId} event` + updateEventDto;
}

remove(id: number) {
return `This action removes a #${id} event`;
remove(eventId: number) {
return `This action removes a #${eventId} event`;
}
}
8 changes: 0 additions & 8 deletions src/group/dto/create-group.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ export class CreateGroupDto {
})
readonly description: string;

@IsNotEmpty()
@ApiProperty({
required: true,
description: '모임장 사용자의 ObjectId',
example: '60f4b3b3b3b3b3b3b3b3b3b3',
})
readonly manager: string;

@ApiProperty({
required: false,
description: '부모임장 사용자의 ObjectId와 권한',
Expand Down
2 changes: 0 additions & 2 deletions src/group/group.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ describe('GroupController', () => {
const group: CreateGroupDto = {
name: 'Group 1',
description: 'Group 1 description',
manager: 'user1',
subManagers: null,
members: ['user1', 'user2'],
};
Expand All @@ -46,7 +45,6 @@ describe('GroupController', () => {
expect(await groupController.create(group)).toBe(result);
expect(result).toHaveProperty('name', group.name);
expect(result).toHaveProperty('description', group.description);
expect(result).toHaveProperty('manager', group.manager);
expect(result).toHaveProperty('subManagers', group.subManagers);
expect(result).toHaveProperty('members', group.members);
});
Expand Down
58 changes: 48 additions & 10 deletions src/group/group.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
Param,
Patch,
Post,
UploadedFile,
UseFilters,
} from '@nestjs/common';
import { GroupService } from './group.service';
import { CreateGroupDto } from './dto/create-group.dto';
import { UpdateGroupDto } from './dto/update-group.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { HttpExceptionFilter } from '../http-exception.filter';
import { ApiFile } from '../api-file.decorator';

@ApiTags('Group')
@Controller('group')
Expand All @@ -38,22 +40,35 @@ export class GroupController {
return this.groupService.findAll();
}

@Get(':id')
@Get(':groupId')
@ApiParam({
name: 'groupId',
required: true,
description: '모임 ID',
})
@ApiOperation({
summary: '모임 상세 조회',
description: '특정 모임을 조회합니다.',
})
findOne(@Param('id') id: string) {
return this.groupService.findOne(id);
findOne(@Param('groupId') groupId: string) {
return this.groupService.findOne(groupId);
}

@Patch(':id')
@Patch(':groupId')
@ApiParam({
name: 'groupId',
required: true,
description: '모임 ID',
})
@ApiOperation({
summary: '모임 수정',
description: '특정 모임을 수정합니다.',
})
update(@Param('id') id: string, @Body() updateGroupDto: UpdateGroupDto) {
return this.groupService.update(id, updateGroupDto);
update(
@Param('groupId') groupId: string,
@Body() updateGroupDto: UpdateGroupDto,
) {
return this.groupService.update(groupId, updateGroupDto);
}

@Delete()
Expand All @@ -65,12 +80,35 @@ export class GroupController {
return this.groupService.deleteAll();
}

@Delete(':id')
@Delete(':groupId')
@ApiParam({
name: 'groupId',
required: true,
description: '모임 ID',
})
@ApiOperation({
summary: '모임 삭제',
description: '특정 모임을 삭제합니다.',
})
delete(@Param('id') id: string) {
return this.groupService.delete(id);
delete(@Param('groupId') groupId: string) {
return this.groupService.delete(groupId);
}

@Post(':groupId/member/excel')
@ApiParam({
name: 'groupId',
required: true,
description: '모임 ID',
})
@ApiOperation({
summary: '모임 회원 엑셀 파일 업로드',
description: '모임 회원 엑셀 파일을 업로드합니다.',
})
@ApiFile('excel')
async uploadMemberFile(
@Param('groupId') groupId: string,
@UploadedFile() excel: Express.Multer.File,
) {
return await this.groupService.convertExcelToJSON(groupId, excel);
}
}
12 changes: 6 additions & 6 deletions src/group/group.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ export class GroupRepository {
return this.groupModel.find().exec();
}

findOne(id: string): Promise<Group> {
return this.groupModel.findById(id).exec();
findOne(groupId: string): Promise<Group> {
return this.groupModel.findById(groupId).exec();
}

update(id: string, updateGroupDto: UpdateGroupDto): Promise<Group> {
update(groupId: string, updateGroupDto: UpdateGroupDto): Promise<Group> {
return this.groupModel
.findByIdAndUpdate(id, updateGroupDto, { new: true })
.findByIdAndUpdate(groupId, updateGroupDto, { new: true })
.exec();
}

delete(id: string) {
this.groupModel.findByIdAndDelete(id);
delete(groupId: string) {
this.groupModel.findByIdAndDelete(groupId);
}

deleteAll() {
Expand Down
45 changes: 39 additions & 6 deletions src/group/group.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GroupRepository } from './group.repository';
import { CreateGroupDto } from './dto/create-group.dto';
import { Group } from './entities/group.entity';
import { UpdateGroupDto } from './dto/update-group.dto';
import * as XLSX from 'exceljs';

@Injectable()
export class GroupService {
Expand All @@ -16,22 +17,54 @@ export class GroupService {
return (await this.groupRepository.findAll()) as Group[];
}

async findOne(id: string): Promise<Group> {
return (await this.groupRepository.findOne(id)) as Group;
async findOne(groupId: string): Promise<Group> {
return (await this.groupRepository.findOne(groupId)) as Group;
}

async update(id: string, updateGroupDto: UpdateGroupDto): Promise<Group> {
return (await this.groupRepository.update(id, {
async update(
groupId: string,
updateGroupDto: UpdateGroupDto,
): Promise<Group> {
return (await this.groupRepository.update(groupId, {
name: updateGroupDto.name,
description: updateGroupDto.description,
} as UpdateGroupDto)) as Group;
}

async delete(id: string): Promise<void> {
return this.groupRepository.delete(id);
async delete(groupId: string): Promise<void> {
return this.groupRepository.delete(groupId);
}

async deleteAll(): Promise<void> {
return this.groupRepository.deleteAll();
}

async convertExcelToJSON(groupId: string, excel: Express.Multer.File) {
const workbook = new XLSX.Workbook();
await workbook.xlsx.load(excel.buffer);

const worksheet = workbook.getWorksheet(1);

const columns = [];
worksheet.getRow(1).eachCell((cell) => {
columns.push(cell.value);
});

const data = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber !== 1) {
const rowObject = {};
row.eachCell((cell, colNumber) => {
const col = columns[colNumber - 1];
rowObject[col] = cell.value;
});
data.push(rowObject);
}
});

return {
groupId,
data,
};
}
}
Loading

0 comments on commit 3cd38c2

Please sign in to comment.