From 920f1b19c7d913a619346b861e8acbb2c46105e0 Mon Sep 17 00:00:00 2001 From: suhyeon Date: Sat, 18 Nov 2023 02:05:06 +0900 Subject: [PATCH 1/5] docs: Update readme --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f9ce765..569698c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ # 미션 - 휴대폰 인증 API + +## 구현 기능 +- [ ] POST 휴대전화 번호에 인증번호 전송 +- [ ] POST 휴대전화 번호와 인증번호를 입력받아 인증 + +## 예외처리 +- [ ] API에 요청받은 Body 값의 타입을 검증하여 올바르지 않은 타입일 경우 400 BadRequest 에러를 리턴 +- [ ] API에 요청받은 Body 값의 필수 값이 누락되거나/빈 값인 경우 400 BadRequest 에러를 리턴 + ## 🔍 진행 방식 - 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다. @@ -87,4 +96,4 @@ result : true ## ✏️ 과제 진행 요구 사항 - 미션은 [nest-phone-verify](https://github.com/eojjeoda-nest/nest-phone-verify-1) 저장소를 Fork & Clone 하고 시작한다. -- **기능을 구현하기 전 `README.md`에 구현할 기능/예외처리를 목록으로 정리**해 추가한다. \ No newline at end of file +- **기능을 구현하기 전 `README.md`에 구현할 기능/예외처리를 목록으로 정리**해 추가한다. From 771fdf14403210f2bb1fc53e505a7ec609fdc7a7 Mon Sep 17 00:00:00 2001 From: suhyeon Date: Sat, 18 Nov 2023 02:07:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=EB=A1=9C=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=ED=95=98=EB=8A=94=20api?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 11 ---------- package-lock.json | 4 ++-- src/app.module.ts | 2 ++ src/auth/auth.controller.spec.ts | 18 ++++++++++++++++ src/auth/auth.controller.ts | 14 +++++++++++++ src/auth/auth.module.ts | 9 ++++++++ src/auth/auth.service.spec.ts | 18 ++++++++++++++++ src/auth/auth.service.ts | 26 ++++++++++++++++++++++++ src/auth/dto/request-signin-token.dto.ts | 11 ++++++++++ src/main.ts | 4 ++-- 10 files changed, 102 insertions(+), 15 deletions(-) delete mode 100644 .env.example create mode 100644 src/auth/auth.controller.spec.ts create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.spec.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/dto/request-signin-token.dto.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index e084950..0000000 --- a/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -# 서버 애플리케이션 포트 관련 변수입니다. -PORT=8000 - -# DB 세팅 환경 변수입니다 -DB_HOST=localhost -DB_PORT=3306 -DB_USERNAME=user -DB_PASSWORD=password -DB_DATABASE=nest_db - -DB_SYNC=true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a31e1a7..934af5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "attendance-api", + "name": "nest-phone-verify-1", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "attendance-api", + "name": "nest-phone-verify-1", "version": "0.0.1", "license": "UNLICENSED", "dependencies": { diff --git a/src/app.module.ts b/src/app.module.ts index 8309d36..8affeac 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { addTransactionalDataSource } from 'typeorm-transactional'; import { DataSource } from 'typeorm'; +import { AuthModule } from './auth/auth.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { DataSource } from 'typeorm'; return addTransactionalDataSource(new DataSource(options)); }, }), + AuthModule, ], controllers: [], providers: [], diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..27a31e6 --- /dev/null +++ b/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..61fa94e --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,14 @@ +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { RequestSigninTokenDto } from './dto/request-signin-token.dto'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + /** SMS 인증번호 요청 */ + @Post('signin') + @HttpCode(HttpStatus.OK) + async requestSigninToken(@Body() body: RequestSigninTokenDto) { + return this.authService.requestSigninToken(body); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..27faccb --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; + +@Module({ + providers: [AuthService], + controllers: [AuthController] +}) +export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..b96800c --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { RequestSigninTokenDto } from './dto/request-signin-token.dto'; + +@Injectable() +export class AuthService { + private readonly tokenValidityDuration = 300000; + + async requestSigninToken( + body: RequestSigninTokenDto, + ): Promise<{ code: string }> { + /** + * TODO: + * 실제 휴대전화 번호로 전송되는 것이 아니라, 휴대전화 번호를 입력하면 인증번호가 전송된다고 가정한다. + * 인증번호은 인증 요청시간으로부터 5분간 유효하다고 가정한다. + * 인증번호 전송시에는 API 응답으로 인증번호를 리턴한다 + */ + const { phoneNumber } = body; + const code = this.generateToken(); + const validUntil = new Date(Date.now() + this.tokenValidityDuration); + return { code }; + } + + private generateToken(): string { + return Math.floor(100000 + Math.random() * 900000).toString(); + } +} diff --git a/src/auth/dto/request-signin-token.dto.ts b/src/auth/dto/request-signin-token.dto.ts new file mode 100644 index 0000000..ec1be92 --- /dev/null +++ b/src/auth/dto/request-signin-token.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString, Matches } from 'class-validator'; + +export class RequestSigninTokenDto { + @IsString() + @IsNotEmpty() + @Matches(/^01[0-9]-[0-9]{3,4}-[0-9]{4}$/, { + message: + 'phoneNumber must be a valid Korean phone number format (e.g., 010-1234-5678)', + }) + phoneNumber: string; +} diff --git a/src/main.ts b/src/main.ts index 620bdfe..71d2049 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,14 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { initializeTransactionalContext } from 'typeorm-transactional'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { initializeTransactionalContext(); const app = await NestFactory.create(AppModule); - // TODO: 프로그램 구현 + app.useGlobalPipes(new ValidationPipe()); await app.listen(process.env.PORT || 8000); - console.log(`Application is running on: ${await app.getUrl()}`); } From ef8463842d002b0be162cd9172e94a887af964b0 Mon Sep 17 00:00:00 2001 From: suhyeon Date: Fri, 24 Nov 2023 05:35:08 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20=EC=9D=B8=EC=A6=9D=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=A0=84=EC=86=A1=20api=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=ED=9C=B4=EB=8C=80=EC=A0=84=ED=99=94=20=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=ED=95=98=EB=8A=94=20api=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?,=20swagger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 + src/auth/auth.controller.ts | 28 +++++- src/auth/auth.module.ts | 5 +- src/auth/auth.service.ts | 86 +++++++++++++++---- src/auth/dto/request-signin-code.dto.ts | 13 +++ src/auth/dto/singin-code-response.dto.ts | 4 + src/auth/dto/sms-code.dto.ts | 9 ++ ...token.dto.ts => verify-singin-code.dto.ts} | 6 +- src/auth/dto/verify-singin-response.dto.ts | 6 ++ src/auth/entity/auth.entity.ts | 17 ++++ src/main.ts | 12 +++ src/util/consts.ts | 3 + 12 files changed, 166 insertions(+), 25 deletions(-) create mode 100644 src/auth/dto/request-signin-code.dto.ts create mode 100644 src/auth/dto/singin-code-response.dto.ts create mode 100644 src/auth/dto/sms-code.dto.ts rename src/auth/dto/{request-signin-token.dto.ts => verify-singin-code.dto.ts} (58%) create mode 100644 src/auth/dto/verify-singin-response.dto.ts create mode 100644 src/auth/entity/auth.entity.ts create mode 100644 src/util/consts.ts diff --git a/src/app.module.ts b/src/app.module.ts index 8affeac..fde5010 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { ConfigModule } from '@nestjs/config'; import { addTransactionalDataSource } from 'typeorm-transactional'; import { DataSource } from 'typeorm'; import { AuthModule } from './auth/auth.module'; +import { Auth } from './auth/entity/auth.entity'; @Module({ imports: [ @@ -17,6 +18,7 @@ import { AuthModule } from './auth/auth.module'; username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, + entities: [Auth], synchronize: process.env.DB_SYNC === 'true', timezone: 'Z', }; diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 61fa94e..292b97e 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,14 +1,34 @@ -import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { + Body, + Controller, + HttpCode, + HttpStatus, + Param, + Post, +} from '@nestjs/common'; import { AuthService } from './auth.service'; -import { RequestSigninTokenDto } from './dto/request-signin-token.dto'; +import { RequestSigninCodeDto } from './dto/request-signin-code.dto'; +import { SmsCodeDto } from './dto/sms-code.dto'; +import { VerifySinginCodeDto } from './dto/verify-singin-code.dto'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} + /** SMS 인증번호 요청 */ @Post('signin') @HttpCode(HttpStatus.OK) - async requestSigninToken(@Body() body: RequestSigninTokenDto) { - return this.authService.requestSigninToken(body); + async requestSigninToken(@Body() body: RequestSigninCodeDto) { + return this.authService.requestSigninCode(body); + } + + /** SMS 인증 */ + @Post('signin/:code') + @HttpCode(HttpStatus.OK) + async verifySigninToken( + @Param() param: SmsCodeDto, + @Body() body: VerifySinginCodeDto, + ) { + return await this.authService.verifySigninCode(param, body); } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 27faccb..b857f04 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,9 +1,12 @@ import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Auth } from './entity/auth.entity'; @Module({ + imports: [TypeOrmModule.forFeature([Auth])], providers: [AuthService], - controllers: [AuthController] + controllers: [AuthController], }) export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index b96800c..d5373a7 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,26 +1,76 @@ -import { Injectable } from '@nestjs/common'; -import { RequestSigninTokenDto } from './dto/request-signin-token.dto'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { RequestSigninCodeDto } from './dto/request-signin-code.dto'; +import { SinginCodeResponseDto } from './dto/singin-code-response.dto'; +import { SmsCodeDto } from './dto/sms-code.dto'; +import { VerifySinginResponseDto } from './dto/verify-singin-response.dto'; +import { TTL } from '../util/consts'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Auth } from './entity/auth.entity'; +import { Repository } from 'typeorm'; +import { VerifySinginCodeDto } from './dto/verify-singin-code.dto'; @Injectable() export class AuthService { - private readonly tokenValidityDuration = 300000; - - async requestSigninToken( - body: RequestSigninTokenDto, - ): Promise<{ code: string }> { - /** - * TODO: - * 실제 휴대전화 번호로 전송되는 것이 아니라, 휴대전화 번호를 입력하면 인증번호가 전송된다고 가정한다. - * 인증번호은 인증 요청시간으로부터 5분간 유효하다고 가정한다. - * 인증번호 전송시에는 API 응답으로 인증번호를 리턴한다 - */ - const { phoneNumber } = body; - const code = this.generateToken(); - const validUntil = new Date(Date.now() + this.tokenValidityDuration); + constructor( + @InjectRepository(Auth) + private readonly authRepository: Repository, + ) {} + + private generateCode(): string { + return Math.floor(100000 + Math.random() * 900000).toString(); + } + + /** + * SMS 인증번호 요청 : + * 실제 휴대전화 번호로 전송되는 것이 아니라, 휴대전화 번호를 입력하면 인증번호가 전송된다고 가정한다. + * 인증번호은 인증 요청시간으로부터 5분간 유효하다고 가정한다. + * 인증번호 전송시에는 API 응답으로 인증번호를 리턴한다 + */ + async requestSigninCode( + body: RequestSigninCodeDto, + ): Promise { + const { phone } = body; + const code = this.generateCode(); + const expires = Date.now() + TTL.VALIDITY_DURATION * 1000; + + const smsCode = this.authRepository.create({ + phone, + code, + expires, + }); + await this.authRepository.save(smsCode); + return { code }; } - private generateToken(): string { - return Math.floor(100000 + Math.random() * 900000).toString(); + /** + * SMS 인증 + * 인증번호의 유효시간이 5분이 지나면 인증번호가 만료되었다고 반환한다. + * 인증번호가 일치하는지 확인한다. + */ + async verifySigninCode( + param: SmsCodeDto, + body: VerifySinginCodeDto, + ): Promise { + const { code } = param; + const { phone } = body; + + const smsCode = await this.authRepository.findOne({ + where: { phone }, + }); + + if (!smsCode || smsCode.expires < Date.now()) { + throw new UnauthorizedException( + '인증 시간이 만료되었습니다. 다시 시도해 주세요.', + ); + } + + if (smsCode.code !== code) { + throw new UnauthorizedException( + '인증번호가 일치하지 않습니다. 다시 시도해 주세요.', + ); + } + + return { result: true }; } } diff --git a/src/auth/dto/request-signin-code.dto.ts b/src/auth/dto/request-signin-code.dto.ts new file mode 100644 index 0000000..8388f47 --- /dev/null +++ b/src/auth/dto/request-signin-code.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString, Matches } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RequestSigninCodeDto { + @ApiProperty({ description: 'phone number', example: '010-1234-5678' }) + @IsString() + @IsNotEmpty() + @Matches(/^01[0-9]-[0-9]{3,4}-[0-9]{4}$/, { + message: + 'phoneNumber must be a valid Korean phone number format (e.g., 010-1234-5678)', + }) + phone: string; +} diff --git a/src/auth/dto/singin-code-response.dto.ts b/src/auth/dto/singin-code-response.dto.ts new file mode 100644 index 0000000..5e9e102 --- /dev/null +++ b/src/auth/dto/singin-code-response.dto.ts @@ -0,0 +1,4 @@ +export class SinginCodeResponseDto { + /** 인증번호 */ + code: string; +} diff --git a/src/auth/dto/sms-code.dto.ts b/src/auth/dto/sms-code.dto.ts new file mode 100644 index 0000000..f236e45 --- /dev/null +++ b/src/auth/dto/sms-code.dto.ts @@ -0,0 +1,9 @@ +import { IsNumberString, Length } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class SmsCodeDto { + @ApiProperty({ description: 'sms code', example: '123456' }) + @Length(6, 6) + @IsNumberString() + code: string; +} diff --git a/src/auth/dto/request-signin-token.dto.ts b/src/auth/dto/verify-singin-code.dto.ts similarity index 58% rename from src/auth/dto/request-signin-token.dto.ts rename to src/auth/dto/verify-singin-code.dto.ts index ec1be92..ec49522 100644 --- a/src/auth/dto/request-signin-token.dto.ts +++ b/src/auth/dto/verify-singin-code.dto.ts @@ -1,11 +1,13 @@ import { IsNotEmpty, IsString, Matches } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; -export class RequestSigninTokenDto { +export class VerifySinginCodeDto { + @ApiProperty({ description: 'phone number', example: '010-1234-5678' }) @IsString() @IsNotEmpty() @Matches(/^01[0-9]-[0-9]{3,4}-[0-9]{4}$/, { message: 'phoneNumber must be a valid Korean phone number format (e.g., 010-1234-5678)', }) - phoneNumber: string; + phone: string; } diff --git a/src/auth/dto/verify-singin-response.dto.ts b/src/auth/dto/verify-singin-response.dto.ts new file mode 100644 index 0000000..5e7c5c5 --- /dev/null +++ b/src/auth/dto/verify-singin-response.dto.ts @@ -0,0 +1,6 @@ +import { IsBoolean } from 'class-validator'; + +export class VerifySinginResponseDto { + @IsBoolean() + result: boolean; +} diff --git a/src/auth/entity/auth.entity.ts b/src/auth/entity/auth.entity.ts new file mode 100644 index 0000000..54da79d --- /dev/null +++ b/src/auth/entity/auth.entity.ts @@ -0,0 +1,17 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Auth { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 50 }) + phone: string; + + @Column() + code: string; + + // 밀리초 단위의 타임스탬프 데이터를 정확하고 안정적으로 저장하기 위함 + @Column('bigint') + expires: number; +} diff --git a/src/main.ts b/src/main.ts index 71d2049..a0b696f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,11 +2,23 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { initializeTransactionalContext } from 'typeorm-transactional'; import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { initializeTransactionalContext(); const app = await NestFactory.create(AppModule); + + const options = new DocumentBuilder() + .setTitle('Phone verify API') + .setDescription('Phone verify API description') + .setVersion('1.0') + .addTag('API') + .build(); + + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup('api', app, document); + app.useGlobalPipes(new ValidationPipe()); await app.listen(process.env.PORT || 8000); console.log(`Application is running on: ${await app.getUrl()}`); diff --git a/src/util/consts.ts b/src/util/consts.ts new file mode 100644 index 0000000..b005d99 --- /dev/null +++ b/src/util/consts.ts @@ -0,0 +1,3 @@ +export enum TTL { + VALIDITY_DURATION = 60 * 5, // 5분을 초단위로 변경 +} From 75e33f74658cab83c5fb5ebcc5de0b4a2a77275f Mon Sep 17 00:00:00 2001 From: suhyeon Date: Fri, 24 Nov 2023 21:35:57 +0900 Subject: [PATCH 4/5] docs: Update readme --- README.md | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 569698c..166daf8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # 미션 - 휴대폰 인증 API ## 구현 기능 -- [ ] POST 휴대전화 번호에 인증번호 전송 -- [ ] POST 휴대전화 번호와 인증번호를 입력받아 인증 + +- POST 휴대전화 번호에 인증번호 전송 +- POST 휴대전화 번호와 인증번호를 입력받아 인증 ## 예외처리 -- [ ] API에 요청받은 Body 값의 타입을 검증하여 올바르지 않은 타입일 경우 400 BadRequest 에러를 리턴 -- [ ] API에 요청받은 Body 값의 필수 값이 누락되거나/빈 값인 경우 400 BadRequest 에러를 리턴 + +- API에 요청받은 Body 값의 타입을 검증하여 올바르지 않은 타입일 경우 400 BadRequest 에러를 리턴 +- API에 요청받은 Body 값의 필수 값이 누락되거나/빈 값인 경우 400 BadRequest 에러를 리턴 +- 인증번호가 만료된 경우 400 BadRequest 에러를 리턴 +- 인증번호가 다를 시 400 BadRequest 에러를 리턴 ## 🔍 진행 방식 @@ -33,26 +37,27 @@ Ran all test suites. 기본적으로 휴대전화 번호 인증을 위한 API를 구현한다. 예시) + 1. 010-1234-5678로 인증번호를 전송한다. 2. 010-1234-5678로 전송된 인증번호를 입력하면 인증이 완료된다. 총 2개의 API 엔드포인트로 구성한다. - - 휴대전화 번호를 인증번호를 전송하는 API - - 실제 휴대전화 번호로 전송되는 것이 아니라, 휴대전화 번호를 입력하면 인증번호가 전송된다고 가정한다. - - 인증번호은 인증 요청시간으로부터 5분간 유효하다고 가정한다. - - 인증번호 전송시에는 API 응답으로 인증번호를 리턴한다. - - - 휴대전화 번호와 인증번호를 입력받아 인증하는 API +- 휴대전화 번호를 인증번호를 전송하는 API + - 실제 휴대전화 번호로 전송되는 것이 아니라, 휴대전화 번호를 입력하면 인증번호가 전송된다고 가정한다. + - 인증번호은 인증 요청시간으로부터 5분간 유효하다고 가정한다. + - 인증번호 전송시에는 API 응답으로 인증번호를 리턴한다. + +- 휴대전화 번호와 인증번호를 입력받아 인증하는 API ### 공통 필수 예외처리 사항 - API에 요청받은 Body 값의 타입을 검증하여 올바르지 않은 타입일 경우 `400 BadRequest` 에러를 리턴해야한다. - API에 요청받은 Body 값의 필수 값이 누락되거나/빈 값인 경우 `400 BadRequest` 에러를 리턴해야한다. - ### API 요청/응답 요구 사항 + 1. 모든 API의 요청/응답은 DTO를 통해 TypeSafe하게 이루어져야한다. 2. DTO의 타입은 `class-validator`를 이용하여 검증한다. 3. DTO 내부 요소의 명칭은 `camelCase`로 작성한다. @@ -60,10 +65,13 @@ Ran all test suites. #### 요청 - 휴대전화 번호는 010-1234-5678과 같은 문자열 형식이다. + ``` phoneNumber : 010-1234-5678 ``` + - 인증번호는 6자리 랜덤 숫자 문자열이다. + ``` code : 612131 ``` @@ -71,10 +79,13 @@ code : 612131 #### 응답 - 인증번호 요청시 인증번호를 응답으로 리턴한다. + ``` code : 612131 ``` + - 인증 완료시 `true`를 응답으로 리턴한다. + ``` result : true ``` @@ -87,10 +98,12 @@ result : true - **Swagger**를 이용하여 API 명세를 작성한다. - **package.json**에 명시된 라이브러리만을 이용하여 구현한다. - **eslint**, **prettier** 등의 코드 포맷팅 라이브러리를 이용하여 제공된 코드 컨벤션에 맞추어 코드를 작성한다. -- `node`, `npm` 버전은 `package.json`에 명시된 버전을 사용한다. [Volta를 이용하여 node 버전을 관리한다.](https://docs.volta.sh/guide/getting-started) +- `node`, `npm` 버전은 `package.json`에 명시된 버전을 + 사용한다. [Volta를 이용하여 node 버전을 관리한다.](https://docs.volta.sh/guide/getting-started) - **(선택 사항)** API 구현이 완료되고, 유닛 테스트, E2E 테스트등 모든 테스트 코드를 작성하여 테스트를 통과하면 굿! + --- ## ✏️ 과제 진행 요구 사항 From e2db65310e7e909650e202b133d2e3c2b8752bc4 Mon Sep 17 00:00:00 2001 From: suhyeon Date: Fri, 24 Nov 2023 23:04:11 +0900 Subject: [PATCH 5/5] fix: Remove response dto validator --- src/auth/dto/verify-singin-response.dto.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/auth/dto/verify-singin-response.dto.ts b/src/auth/dto/verify-singin-response.dto.ts index 5e7c5c5..b22ee41 100644 --- a/src/auth/dto/verify-singin-response.dto.ts +++ b/src/auth/dto/verify-singin-response.dto.ts @@ -1,6 +1,3 @@ -import { IsBoolean } from 'class-validator'; - export class VerifySinginResponseDto { - @IsBoolean() result: boolean; }