Skip to content

Commit 36a2016

Browse files
wlgh1553shl0501
and
shl0501
authored
feat(be): user registration (#31)
* 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]>
1 parent 364ce36 commit 36a2016

20 files changed

+900
-108
lines changed

Diff for: apps/server/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@
2222
"dependencies": {
2323
"@nestjs/common": "^10.0.0",
2424
"@nestjs/core": "^10.0.0",
25+
"@nestjs/mapped-types": "*",
2526
"@nestjs/platform-express": "^10.0.0",
2627
"@prisma/client": "^5.21.1",
28+
"bcrypt": "^5.1.1",
29+
"class-transformer": "^0.5.1",
30+
"class-validator": "^0.14.1",
31+
"express": "^4.21.1",
2732
"prisma": "^5.21.1",
2833
"reflect-metadata": "^0.2.0",
2934
"rxjs": "^7.8.1"
@@ -32,6 +37,7 @@
3237
"@nestjs/cli": "^10.0.0",
3338
"@nestjs/schematics": "^10.0.0",
3439
"@nestjs/testing": "^10.0.0",
40+
"@types/bcrypt": "^5.0.2",
3541
"@types/express": "^5.0.0",
3642
"@types/jest": "^29.5.2",
3743
"@types/node": "^20.3.1",

Diff for: apps/server/prisma/schema.prisma

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
generator client {
2+
provider = "prisma-client-js"
3+
}
4+
5+
datasource db {
6+
provider = "postgresql"
7+
url = env("DATABASE_URL")
8+
}
9+
10+
model User {
11+
user_id Int @id @default(autoincrement())
12+
email String @unique
13+
password String
14+
nickname String @unique
15+
created_at DateTime @default(now())
16+
}

Diff for: apps/server/src/app.controller.spec.ts

-23
This file was deleted.

Diff for: apps/server/src/app.controller.ts

-13
This file was deleted.

Diff for: apps/server/src/app.module.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Module } from '@nestjs/common';
22

3-
import { AppController } from './app.controller';
4-
import { AppService } from './app.service';
3+
import { PrismaModule } from './prisma/prisma.module';
4+
import { UsersModule } from './users/users.module';
55

66
@Module({
7-
imports: [],
8-
controllers: [AppController],
9-
providers: [AppService],
7+
imports: [UsersModule, PrismaModule],
8+
controllers: [],
9+
providers: [],
1010
})
1111
export class AppModule {}

Diff for: apps/server/src/app.service.ts

-8
This file was deleted.

Diff for: apps/server/src/filters/prisma-exception.filter.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
2+
import {
3+
PrismaClientInitializationError,
4+
PrismaClientKnownRequestError,
5+
PrismaClientRustPanicError,
6+
PrismaClientUnknownRequestError,
7+
PrismaClientValidationError,
8+
} from '@prisma/client/runtime/library';
9+
import { Response } from 'express';
10+
11+
@Catch(
12+
PrismaClientKnownRequestError,
13+
PrismaClientUnknownRequestError,
14+
PrismaClientRustPanicError,
15+
PrismaClientInitializationError,
16+
PrismaClientValidationError,
17+
)
18+
export class PrismaExceptionFilter implements ExceptionFilter {
19+
catch(exception: any, host: ArgumentsHost) {
20+
const ctx = host.switchToHttp();
21+
const response = ctx.getResponse<Response>();
22+
const status = HttpStatus.BAD_REQUEST;
23+
24+
const errorResponse = {
25+
type: 'fail',
26+
error: {
27+
message: '알 수 없는 오류가 발생했습니다.',
28+
},
29+
};
30+
31+
if (exception instanceof PrismaClientKnownRequestError) {
32+
if (exception.code === 'P2002') errorResponse.error.message = '이미 존재하는 값입니다. 다른 값을 사용하세요.';
33+
else errorResponse.error.message = `Prisma 오류 발생: ${exception.message}`;
34+
} else if (exception instanceof PrismaClientUnknownRequestError) {
35+
errorResponse.error.message = `알 수 없는 Prisma 오류 발생: ${exception.message}`;
36+
} else if (exception instanceof PrismaClientRustPanicError) {
37+
errorResponse.error.message = 'Prisma 엔진에서 패닉이 발생했습니다. 관리자에게 문의하세요.';
38+
} else if (exception instanceof PrismaClientInitializationError) {
39+
errorResponse.error.message = 'Prisma 초기화 오류가 발생했습니다. 설정을 확인하세요.';
40+
} else if (exception instanceof PrismaClientValidationError) {
41+
errorResponse.error.message = `유효성 검사 오류: ${exception.message}`;
42+
}
43+
44+
response.status(status).json(errorResponse);
45+
}
46+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
2+
import { Request, Response } from 'express';
3+
4+
@Catch(BadRequestException)
5+
export class ValidationExceptionFilter implements ExceptionFilter {
6+
catch(exception: BadRequestException, host: ArgumentsHost) {
7+
const ctx = host.switchToHttp();
8+
const response = ctx.getResponse<Response>();
9+
const request = ctx.getRequest<Request>();
10+
const status = exception.getStatus();
11+
const exceptionResponse = exception.getResponse();
12+
13+
const errorMessage = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message;
14+
15+
response.status(status).json({
16+
type: 'fail',
17+
error: {
18+
statusCode: status,
19+
timestamp: new Date().toISOString(),
20+
path: request.url,
21+
messages: Array.isArray(errorMessage) ? errorMessage : [errorMessage],
22+
},
23+
});
24+
}
25+
}

Diff for: apps/server/src/main.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1+
import { ValidationPipe } from '@nestjs/common';
12
import { NestFactory } from '@nestjs/core';
23

34
import { AppModule } from './app.module';
4-
5+
import { PrismaExceptionFilter } from './filters/prisma-exception.filter';
6+
import { ValidationExceptionFilter } from './filters/validation-exception.filter';
57
async function bootstrap() {
68
const app = await NestFactory.create(AppModule);
9+
10+
app.setGlobalPrefix('api');
11+
12+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
13+
14+
app.useGlobalFilters(new ValidationExceptionFilter());
15+
16+
app.useGlobalFilters(new PrismaExceptionFilter());
17+
718
await app.listen(process.env.PORT ?? 3000);
819
}
920
bootstrap();

Diff for: apps/server/src/prisma/prisma.module.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { PrismaService } from './prisma.service';
4+
5+
@Module({
6+
providers: [PrismaService],
7+
exports: [PrismaService],
8+
})
9+
export class PrismaModule {}

Diff for: apps/server/src/prisma/prisma.service.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
2+
import { PrismaClient } from '@prisma/client';
3+
4+
@Injectable()
5+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
6+
async onModuleInit() {
7+
await this.$connect();
8+
}
9+
10+
async onModuleDestroy() {
11+
await this.$disconnect();
12+
}
13+
}

Diff for: apps/server/src/users/dto/create-user.dto.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength, NotContains } from 'class-validator';
2+
3+
export class CreateUserDto {
4+
@IsEmail()
5+
@IsNotEmpty()
6+
@NotContains(' ')
7+
email: string;
8+
9+
@IsString()
10+
@IsNotEmpty()
11+
@MinLength(3)
12+
@MaxLength(20)
13+
@NotContains(' ')
14+
nickname: string;
15+
16+
@IsString()
17+
@MinLength(8)
18+
@MaxLength(20)
19+
@NotContains(' ')
20+
password: string;
21+
}

Diff for: apps/server/src/users/dto/validate-user.dto.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IsEmail, IsOptional, IsString, MaxLength, MinLength, NotContains } from 'class-validator';
2+
3+
export class ValidateUserDto {
4+
@IsOptional()
5+
@IsEmail()
6+
@NotContains(' ')
7+
email: string;
8+
9+
@IsOptional()
10+
@IsString()
11+
@MinLength(3)
12+
@MaxLength(20)
13+
@NotContains(' ')
14+
nickname: string;
15+
}

Diff for: apps/server/src/users/users.controller.spec.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
3+
import { UsersController } from './users.controller';
4+
import { UsersService } from './users.service';
5+
6+
describe('UsersController', () => {
7+
let controller: UsersController;
8+
9+
beforeEach(async () => {
10+
const module: TestingModule = await Test.createTestingModule({
11+
controllers: [UsersController],
12+
providers: [UsersService],
13+
}).compile();
14+
15+
controller = module.get<UsersController>(UsersController);
16+
});
17+
18+
it('should be defined', () => {
19+
expect(controller).toBeDefined();
20+
});
21+
});

Diff for: apps/server/src/users/users.controller.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { BadRequestException, Body, Controller, Get, Post, Query } from '@nestjs/common';
2+
3+
import { CreateUserDto } from './dto/create-user.dto';
4+
import { ValidateUserDto } from './dto/validate-user.dto';
5+
import { UsersService } from './users.service';
6+
7+
@Controller('users')
8+
export class UsersController {
9+
constructor(private readonly usersService: UsersService) {}
10+
11+
@Post()
12+
async create(@Body() createUserDto: CreateUserDto) {
13+
await this.usersService.create(createUserDto);
14+
return {
15+
type: 'success',
16+
data: {},
17+
};
18+
}
19+
20+
@Get()
21+
async checkDuplication(@Query() validateUserDto: ValidateUserDto) {
22+
if (!validateUserDto.email && !validateUserDto.nickname) throw new BadRequestException('중복확인을 위한 email 또는 nickname을 제공해주세요.');
23+
24+
if (validateUserDto.email && validateUserDto.nickname) throw new BadRequestException('이메일 또는 닉네임 하나만 요청해 주세요.');
25+
return {
26+
type: 'success',
27+
data: {
28+
exists: await this.usersService.exist(validateUserDto),
29+
},
30+
};
31+
}
32+
}

Diff for: apps/server/src/users/users.module.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { UsersController } from './users.controller';
4+
import { UserRepository } from './users.repository';
5+
import { UsersService } from './users.service';
6+
import { PrismaModule } from '../prisma/prisma.module';
7+
8+
@Module({
9+
imports: [PrismaModule],
10+
controllers: [UsersController],
11+
providers: [UsersService, UserRepository],
12+
})
13+
export class UsersModule {}

Diff for: apps/server/src/users/users.repository.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
import { PrismaService } from '../prisma/prisma.service';
4+
import { CreateUserDto } from './dto/create-user.dto';
5+
6+
@Injectable()
7+
export class UserRepository {
8+
constructor(private readonly prisma: PrismaService) {}
9+
10+
async create(data: CreateUserDto) {
11+
await this.prisma.user.create({ data });
12+
}
13+
14+
async findByEmail(email: string) {
15+
return this.prisma.user.findUnique({
16+
where: { email },
17+
});
18+
}
19+
20+
async findByNickname(nickname: string) {
21+
return this.prisma.user.findUnique({
22+
where: { nickname },
23+
});
24+
}
25+
}

Diff for: apps/server/src/users/users.service.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
3+
import { UsersService } from './users.service';
4+
5+
describe('UsersService', () => {
6+
let service: UsersService;
7+
8+
beforeEach(async () => {
9+
const module: TestingModule = await Test.createTestingModule({
10+
providers: [UsersService],
11+
}).compile();
12+
13+
service = module.get<UsersService>(UsersService);
14+
});
15+
16+
it('should be defined', () => {
17+
expect(service).toBeDefined();
18+
});
19+
});

0 commit comments

Comments
 (0)