diff --git a/app/backend/libs/utils/bigIntToJson.ts b/app/backend/libs/utils/bigIntToJson.ts new file mode 100644 index 000000000..db853b5a2 --- /dev/null +++ b/app/backend/libs/utils/bigIntToJson.ts @@ -0,0 +1,3 @@ +(BigInt.prototype as any).toJSON = function () { + return this.toString(); +}; diff --git a/app/backend/prisma/migrations/20231121065729_init/migration.sql b/app/backend/prisma/migrations/20231121065729_init/migration.sql new file mode 100644 index 000000000..d978c958d --- /dev/null +++ b/app/backend/prisma/migrations/20231121065729_init/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to alter the column `date` on the `Mogaco` table. The data in that column could be lost. The data in that column will be cast from `DateTime(0)` to `DateTime`. + - You are about to alter the column `deleted_at` on the `Mogaco` table. The data in that column could be lost. The data in that column will be cast from `DateTime(0)` to `DateTime`. + +*/ +-- AlterTable +ALTER TABLE `Mogaco` MODIFY `date` DATETIME NOT NULL, + MODIFY `updated_at` DATETIME(3) NULL, + MODIFY `deleted_at` DATETIME NULL; diff --git a/app/backend/prisma/migrations/20231121065750_morak_0_0_2/migration.sql b/app/backend/prisma/migrations/20231121065750_morak_0_0_2/migration.sql new file mode 100644 index 000000000..941ad318f --- /dev/null +++ b/app/backend/prisma/migrations/20231121065750_morak_0_0_2/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to alter the column `date` on the `Mogaco` table. The data in that column could be lost. The data in that column will be cast from `DateTime(0)` to `DateTime`. + - You are about to alter the column `deleted_at` on the `Mogaco` table. The data in that column could be lost. The data in that column will be cast from `DateTime(0)` to `DateTime`. + +*/ +-- AlterTable +ALTER TABLE `Mogaco` MODIFY `date` DATETIME NOT NULL, + MODIFY `deleted_at` DATETIME NULL; diff --git a/app/backend/prisma/schema.prisma b/app/backend/prisma/schema.prisma index b8083e51e..de37e12fb 100644 --- a/app/backend/prisma/schema.prisma +++ b/app/backend/prisma/schema.prisma @@ -31,8 +31,8 @@ model Mogaco { address String @db.VarChar(191) status String @db.VarChar(191) created_at DateTime @default(now()) - updated_at DateTime @updatedAt() - deleted_at DateTime @db.DateTime + updated_at DateTime? @updatedAt() + deleted_at DateTime? @db.DateTime mogacos Participant[] @@map("Mogaco") diff --git a/app/backend/src/app.module.ts b/app/backend/src/app.module.ts index 76d1e2687..a069d665b 100644 --- a/app/backend/src/app.module.ts +++ b/app/backend/src/app.module.ts @@ -5,6 +5,7 @@ import { AppController } from './app.controller'; import { MemberModule } from './member/member.module'; import * as redisStore from 'cache-manager-redis-store'; import { getSecret } from 'vault'; +import { MogacoModule } from './mogaco/mogaco.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { getSecret } from 'vault'; }), AuthModule, MemberModule, + MogacoModule, ], controllers: [AppController], providers: [], diff --git a/app/backend/src/auth/auth.controller.ts b/app/backend/src/auth/auth.controller.ts index 473a01a8c..38c1fe7f4 100644 --- a/app/backend/src/auth/auth.controller.ts +++ b/app/backend/src/auth/auth.controller.ts @@ -84,7 +84,7 @@ export class AuthController { res.cookie('access_token', tokens.access_token, { httpOnly: false, maxAge: getSecret('MAX_AGE_ACCESS_TOKEN') }); res.cookie('refresh_token', tokens.refresh_token, { - httpOnly: false, + httpOnly: true, maxAge: getSecret('MAX_AGE_REFRESH_TOKEN'), }); diff --git a/app/backend/src/auth/auth.service.ts b/app/backend/src/auth/auth.service.ts index 7812b9e05..3547b6b77 100644 --- a/app/backend/src/auth/auth.service.ts +++ b/app/backend/src/auth/auth.service.ts @@ -26,7 +26,7 @@ export class AuthService { generateJwt(payload: Payload): Tokens { const accessToken = this.jwtService.sign(payload, { expiresIn: getSecret('MAX_AGE_ACCESS_TOKEN'), - secret: getSecret('MAX_AGE_ACCESS_TOKEN'), + secret: getSecret('JWT_ACCESS_SECRET'), }); const refreshToken = this.jwtService.sign(payload, { diff --git a/app/backend/src/main.ts b/app/backend/src/main.ts index 75707ad60..3e9e7c8d7 100644 --- a/app/backend/src/main.ts +++ b/app/backend/src/main.ts @@ -4,6 +4,7 @@ import * as cookieParser from 'cookie-parser'; import { ValidationPipe } from '@nestjs/common'; import { setupSwagger } from '../libs/utils/swagger'; import { getSecret, loadSecrets } from 'vault'; +import '../libs/utils/bigIntToJson'; async function bootstrap() { await loadSecrets(); diff --git a/app/backend/src/member/member.service.ts b/app/backend/src/member/member.service.ts index 43d156ee7..5f5e2b066 100644 --- a/app/backend/src/member/member.service.ts +++ b/app/backend/src/member/member.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { MemberDto } from './dto/member.dto'; +import { getSecret } from 'vault'; @Injectable() export class MemberService { constructor(private jwtService: JwtService) {} async getUserData(encryptedToken: string): Promise { - const decodedAccessToken = this.jwtService.verify(encryptedToken, { secret: process.env.JWT_ACCESS_SECRET }); + const decodedAccessToken = this.jwtService.verify(encryptedToken, { secret: getSecret(`JWT_ACCESS_SECRET`) }); const { providerId, email, nickname, profilePicture } = decodedAccessToken; return { providerId, email, nickname, profilePicture }; diff --git a/app/backend/src/mogaco/dto/create-mogaco.dto.ts b/app/backend/src/mogaco/dto/create-mogaco.dto.ts new file mode 100644 index 000000000..29b58e1bc --- /dev/null +++ b/app/backend/src/mogaco/dto/create-mogaco.dto.ts @@ -0,0 +1,28 @@ +import { IsDateString, IsEnum, IsInt, IsNotEmpty, IsOptional } from 'class-validator'; +import { MogacoStatus } from './mogaco-status.enum'; + +export class CreateMogacoDto { + @IsNotEmpty() + @IsInt() + group_id: number; + + @IsNotEmpty() + title: string; + + @IsNotEmpty() + contents: string; + + @IsNotEmpty() + @IsDateString() + date: string; + + @IsNotEmpty() + max_human_count: number; + + @IsNotEmpty() + address: string; + + @IsOptional() + @IsEnum(MogacoStatus, { message: 'Invalid status' }) + status?: string; +} diff --git a/app/backend/src/mogaco/dto/index.ts b/app/backend/src/mogaco/dto/index.ts new file mode 100644 index 000000000..0156e7892 --- /dev/null +++ b/app/backend/src/mogaco/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-mogaco.dto'; +export * from './mogaco.dto'; diff --git a/app/backend/src/mogaco/dto/mogaco-status.enum.ts b/app/backend/src/mogaco/dto/mogaco-status.enum.ts new file mode 100644 index 000000000..a4ed387f7 --- /dev/null +++ b/app/backend/src/mogaco/dto/mogaco-status.enum.ts @@ -0,0 +1,5 @@ +export enum MogacoStatus { + RECRUITING = '모집 중', + CLOSED = '모집 마감', + COMPLETED = '종료', +} diff --git a/app/backend/src/mogaco/dto/mogaco.dto.ts b/app/backend/src/mogaco/dto/mogaco.dto.ts new file mode 100644 index 000000000..0e1160092 --- /dev/null +++ b/app/backend/src/mogaco/dto/mogaco.dto.ts @@ -0,0 +1,10 @@ +export class MogacoDto { + id: bigint; + group_id: bigint; + title: string; + contents: string; + date: Date; + max_human_count: number; + address: string; + status: string; +} diff --git a/app/backend/src/mogaco/mogaco.controller.ts b/app/backend/src/mogaco/mogaco.controller.ts new file mode 100644 index 000000000..3907e0c96 --- /dev/null +++ b/app/backend/src/mogaco/mogaco.controller.ts @@ -0,0 +1,39 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { MogacoService } from './mogaco.service'; +import { Mogaco } from '@prisma/client'; +import { CreateMogacoDto, MogacoDto } from './dto'; +import { MogacoStatusValidationPipe } from './pipes/mogaco-status-validation.pipe'; +import { MogacoStatus } from './dto/mogaco-status.enum'; + +@Controller('mogaco') +export class MogacoController { + constructor(private readonly mogacoService: MogacoService) {} + + @Get('/') + async getAllMogaco(): Promise { + return this.mogacoService.getAllMogaco(); + } + + @Get('/:id') + async getMogacoById(@Param('id', ParseIntPipe) id: number): Promise { + return this.mogacoService.getMogacoById(id); + } + + @Post('/') + async createMogaco(@Body() createMogacoDto: CreateMogacoDto): Promise { + return this.mogacoService.createMogaco(createMogacoDto); + } + + @Delete('/:id') + async deleteMogaco(@Param('id', ParseIntPipe) id: number): Promise { + return this.mogacoService.deleteMogaco(id); + } + + @Patch('/:id/status') + updateMogacoStatus( + @Param('id', ParseIntPipe) id: number, + @Body('status', MogacoStatusValidationPipe) status: MogacoStatus, + ): Promise { + return this.mogacoService.updateMogacoStatus(id, status); + } +} diff --git a/app/backend/src/mogaco/mogaco.module.ts b/app/backend/src/mogaco/mogaco.module.ts new file mode 100644 index 000000000..e940b8376 --- /dev/null +++ b/app/backend/src/mogaco/mogaco.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MogacoService } from './mogaco.service'; +import { MogacoController } from './mogaco.controller'; +import { MogacoRepository } from './mogaco.repository'; +import { PrismaService } from 'libs/utils/prisma.service'; + +@Module({ + controllers: [MogacoController], + providers: [MogacoService, MogacoRepository, PrismaService], +}) +export class MogacoModule {} diff --git a/app/backend/src/mogaco/mogaco.repository.ts b/app/backend/src/mogaco/mogaco.repository.ts new file mode 100644 index 000000000..2a16ed3e3 --- /dev/null +++ b/app/backend/src/mogaco/mogaco.repository.ts @@ -0,0 +1,82 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../libs/utils/prisma.service'; +import { Mogaco } from '@prisma/client'; +import { MogacoStatus } from './dto/mogaco-status.enum'; +import { CreateMogacoDto, MogacoDto } from './dto'; + +@Injectable() +export class MogacoRepository { + constructor(private prisma: PrismaService) {} + + async getAllMogaco(): Promise { + return this.prisma.mogaco.findMany(); + } + + async getMogacoById(id: number): Promise { + const mogaco = await this.prisma.mogaco.findUnique({ + where: { id }, + }); + + if (!mogaco) { + throw new NotFoundException(`Mogaco with id ${id} not found`); + } + + return { + id: mogaco.id, + group_id: mogaco.group_id, + title: mogaco.title, + contents: mogaco.contents, + date: mogaco.date, + max_human_count: mogaco.max_human_count, + address: mogaco.address, + status: mogaco.status, + }; + } + + async createMogaco(createMogacoDto: CreateMogacoDto): Promise { + try { + const { group_id, title, contents, max_human_count, address, date } = createMogacoDto; + + const mogaco = await this.prisma.mogaco.create({ + data: { + group_id, + title, + contents, + max_human_count, + address, + status: MogacoStatus.RECRUITING, + date: new Date(date), + }, + }); + + return mogaco; + } catch (error) { + throw new Error(`Failed to create Mogaco: ${error.message}`); + } + } + + async deleteMogaco(id: number): Promise { + const mogaco = await this.prisma.mogaco.findUnique({ + where: { id }, + }); + + if (!mogaco) { + throw new NotFoundException(`Mogaco with id ${id} not found`); + } + + await this.prisma.mogaco.delete({ + where: { id }, + }); + } + + async updateMogacoStatus(mogaco: MogacoDto): Promise { + try { + return await this.prisma.mogaco.update({ + where: { id: mogaco.id }, + data: { status: mogaco.status }, + }); + } catch (error) { + throw new Error(`Failed to update Mogaco status: ${error.message}`); + } + } +} diff --git a/app/backend/src/mogaco/mogaco.service.ts b/app/backend/src/mogaco/mogaco.service.ts new file mode 100644 index 000000000..a765e08ef --- /dev/null +++ b/app/backend/src/mogaco/mogaco.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { MogacoRepository } from './mogaco.repository'; +import { Mogaco } from '@prisma/client'; +import { CreateMogacoDto, MogacoDto } from './dto'; +import { MogacoStatus } from './dto/mogaco-status.enum'; + +@Injectable() +export class MogacoService { + constructor(private mogacoRepository: MogacoRepository) {} + + async getAllMogaco(): Promise { + return this.mogacoRepository.getAllMogaco(); + } + + async getMogacoById(id: number): Promise { + return this.mogacoRepository.getMogacoById(id); + } + + async createMogaco(createMogaco: CreateMogacoDto): Promise { + return this.mogacoRepository.createMogaco(createMogaco); + } + + async deleteMogaco(id: number): Promise { + return this.mogacoRepository.deleteMogaco(id); + } + + async updateMogacoStatus(id: number, status: MogacoStatus): Promise { + const mogaco = await this.getMogacoById(id); + mogaco.status = status; + return this.mogacoRepository.updateMogacoStatus(mogaco); + } +} diff --git a/app/backend/src/mogaco/pipes/mogaco-status-validation.pipe.ts b/app/backend/src/mogaco/pipes/mogaco-status-validation.pipe.ts new file mode 100644 index 000000000..d65801355 --- /dev/null +++ b/app/backend/src/mogaco/pipes/mogaco-status-validation.pipe.ts @@ -0,0 +1,21 @@ +import { BadRequestException, PipeTransform } from '@nestjs/common'; +import { MogacoStatus } from '../dto/mogaco-status.enum'; + +export class MogacoStatusValidationPipe implements PipeTransform { + readonly StatusOptions = [MogacoStatus.RECRUITING, MogacoStatus.CLOSED, MogacoStatus.COMPLETED]; + + transform(value: any) { + value = value.toUpperCase(); + + if (!this.isStatusValid(value)) { + throw new BadRequestException(`${value} isn't in the status options`); + } + + return value; + } + + private isStatusValid(status: any) { + const index = this.StatusOptions.indexOf(status); + return index !== -1; + } +}