Skip to content

Commit

Permalink
feat(be): user registration (#31)
Browse files Browse the repository at this point in the history
* chore: configure linter (#7)

chore: configure linter

* fix: resolve eslint configuration error (#11)

* feat: make register

* feat: error exception handling

* feat: add api prefix

* feat: duplication check api

* fix: resolve conflict error

---------

Co-authored-by: shl0501 <[email protected]>
  • Loading branch information
wlgh1553 and shl0501 authored Nov 7, 2024
1 parent 364ce36 commit 36a2016
Show file tree
Hide file tree
Showing 20 changed files with 900 additions and 108 deletions.
6 changes: 6 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^5.21.1",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"express": "^4.21.1",
"prisma": "^5.21.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
Expand All @@ -32,6 +37,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
Expand Down
16 changes: 16 additions & 0 deletions apps/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model User {
user_id Int @id @default(autoincrement())
email String @unique
password String
nickname String @unique
created_at DateTime @default(now())
}
23 changes: 0 additions & 23 deletions apps/server/src/app.controller.spec.ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/server/src/app.controller.ts

This file was deleted.

10 changes: 5 additions & 5 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { UsersModule } from './users/users.module';

@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
imports: [UsersModule, PrismaModule],
controllers: [],
providers: [],
})
export class AppModule {}
8 changes: 0 additions & 8 deletions apps/server/src/app.service.ts

This file was deleted.

46 changes: 46 additions & 0 deletions apps/server/src/filters/prisma-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import {
PrismaClientInitializationError,
PrismaClientKnownRequestError,
PrismaClientRustPanicError,
PrismaClientUnknownRequestError,
PrismaClientValidationError,
} from '@prisma/client/runtime/library';
import { Response } from 'express';

@Catch(
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
)
export class PrismaExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = HttpStatus.BAD_REQUEST;

const errorResponse = {
type: 'fail',
error: {
message: '알 수 없는 오류가 발생했습니다.',
},
};

if (exception instanceof PrismaClientKnownRequestError) {
if (exception.code === 'P2002') errorResponse.error.message = '이미 존재하는 값입니다. 다른 값을 사용하세요.';
else errorResponse.error.message = `Prisma 오류 발생: ${exception.message}`;
} else if (exception instanceof PrismaClientUnknownRequestError) {
errorResponse.error.message = `알 수 없는 Prisma 오류 발생: ${exception.message}`;
} else if (exception instanceof PrismaClientRustPanicError) {
errorResponse.error.message = 'Prisma 엔진에서 패닉이 발생했습니다. 관리자에게 문의하세요.';
} else if (exception instanceof PrismaClientInitializationError) {
errorResponse.error.message = 'Prisma 초기화 오류가 발생했습니다. 설정을 확인하세요.';
} else if (exception instanceof PrismaClientValidationError) {
errorResponse.error.message = `유효성 검사 오류: ${exception.message}`;
}

response.status(status).json(errorResponse);
}
}
25 changes: 25 additions & 0 deletions apps/server/src/filters/validation-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();

const errorMessage = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message;

response.status(status).json({
type: 'fail',
error: {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
messages: Array.isArray(errorMessage) ? errorMessage : [errorMessage],
},
});
}
}
13 changes: 12 additions & 1 deletion apps/server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';

import { AppModule } from './app.module';

import { PrismaExceptionFilter } from './filters/prisma-exception.filter';
import { ValidationExceptionFilter } from './filters/validation-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.setGlobalPrefix('api');

app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));

app.useGlobalFilters(new ValidationExceptionFilter());

app.useGlobalFilters(new PrismaExceptionFilter());

await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
9 changes: 9 additions & 0 deletions apps/server/src/prisma/prisma.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';

import { PrismaService } from './prisma.service';

@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
13 changes: 13 additions & 0 deletions apps/server/src/prisma/prisma.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}

async onModuleDestroy() {
await this.$disconnect();
}
}
21 changes: 21 additions & 0 deletions apps/server/src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength, NotContains } from 'class-validator';

export class CreateUserDto {
@IsEmail()
@IsNotEmpty()
@NotContains(' ')
email: string;

@IsString()
@IsNotEmpty()
@MinLength(3)
@MaxLength(20)
@NotContains(' ')
nickname: string;

@IsString()
@MinLength(8)
@MaxLength(20)
@NotContains(' ')
password: string;
}
15 changes: 15 additions & 0 deletions apps/server/src/users/dto/validate-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsEmail, IsOptional, IsString, MaxLength, MinLength, NotContains } from 'class-validator';

export class ValidateUserDto {
@IsOptional()
@IsEmail()
@NotContains(' ')
email: string;

@IsOptional()
@IsString()
@MinLength(3)
@MaxLength(20)
@NotContains(' ')
nickname: string;
}
21 changes: 21 additions & 0 deletions apps/server/src/users/users.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing';

import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('UsersController', () => {
let controller: UsersController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();

controller = module.get<UsersController>(UsersController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
32 changes: 32 additions & 0 deletions apps/server/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BadRequestException, Body, Controller, Get, Post, Query } from '@nestjs/common';

import { CreateUserDto } from './dto/create-user.dto';
import { ValidateUserDto } from './dto/validate-user.dto';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
async create(@Body() createUserDto: CreateUserDto) {
await this.usersService.create(createUserDto);
return {
type: 'success',
data: {},
};
}

@Get()
async checkDuplication(@Query() validateUserDto: ValidateUserDto) {
if (!validateUserDto.email && !validateUserDto.nickname) throw new BadRequestException('중복확인을 위한 email 또는 nickname을 제공해주세요.');

if (validateUserDto.email && validateUserDto.nickname) throw new BadRequestException('이메일 또는 닉네임 하나만 요청해 주세요.');
return {
type: 'success',
data: {
exists: await this.usersService.exist(validateUserDto),
},
};
}
}
13 changes: 13 additions & 0 deletions apps/server/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';

import { UsersController } from './users.controller';
import { UserRepository } from './users.repository';
import { UsersService } from './users.service';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
imports: [PrismaModule],
controllers: [UsersController],
providers: [UsersService, UserRepository],
})
export class UsersModule {}
25 changes: 25 additions & 0 deletions apps/server/src/users/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';

import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UserRepository {
constructor(private readonly prisma: PrismaService) {}

async create(data: CreateUserDto) {
await this.prisma.user.create({ data });
}

async findByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
});
}

async findByNickname(nickname: string) {
return this.prisma.user.findUnique({
where: { nickname },
});
}
}
19 changes: 19 additions & 0 deletions apps/server/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';

import { UsersService } from './users.service';

describe('UsersService', () => {
let service: UsersService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();

service = module.get<UsersService>(UsersService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Loading

0 comments on commit 36a2016

Please sign in to comment.