diff --git a/package-lock.json b/package-lock.json index 07ce62d..83805e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/jwt": "^9.0.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", + "@nestjs/swagger": "^6.1.4", "@nestjs/typeorm": "^9.0.1", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", @@ -25,6 +26,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "swagger-ui-express": "^4.6.0", "typeorm": "^0.3.10" }, "devDependencies": { @@ -1611,6 +1613,25 @@ "@nestjs/common": "^8.0.0 || ^9.0.0" } }, + "node_modules/@nestjs/mapped-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.0.tgz", + "integrity": "sha512-NTFwPZkQWsArQH8QSyFWGZvJ08gR+R4TofglqZoihn/vU+ktHEJjMqsIsADwb7XD97DhiD+TVv5ac+jG33BHrg==", + "peerDependencies": { + "@nestjs/common": "^7.0.8 || ^8.0.0 || ^9.0.0", + "class-transformer": "^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "class-validator": "^0.11.1 || ^0.12.0 || ^0.13.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/passport": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", @@ -1736,6 +1757,37 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/@nestjs/swagger": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.1.4.tgz", + "integrity": "sha512-kE8VjR+NaoKqxg8XqM/YYfALScPh4AcoR8Wywga8/OxHsTHY+MKxqvTpWp7IhCUWSA6xT8nQUpcC9Rt7C+r7Hw==", + "dependencies": { + "@nestjs/mapped-types": "1.2.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "4.15.5" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0", + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.2.0.tgz", @@ -6161,8 +6213,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -7998,6 +8049,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, + "node_modules/swagger-ui-express": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz", + "integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==", + "dependencies": { + "swagger-ui-dist": ">=4.11.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -10265,6 +10335,12 @@ "jsonwebtoken": "8.5.1" } }, + "@nestjs/mapped-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.0.tgz", + "integrity": "sha512-NTFwPZkQWsArQH8QSyFWGZvJ08gR+R4TofglqZoihn/vU+ktHEJjMqsIsADwb7XD97DhiD+TVv5ac+jG33BHrg==", + "requires": {} + }, "@nestjs/passport": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", @@ -10361,6 +10437,18 @@ } } }, + "@nestjs/swagger": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.1.4.tgz", + "integrity": "sha512-kE8VjR+NaoKqxg8XqM/YYfALScPh4AcoR8Wywga8/OxHsTHY+MKxqvTpWp7IhCUWSA6xT8nQUpcC9Rt7C+r7Hw==", + "requires": { + "@nestjs/mapped-types": "1.2.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "4.15.5" + } + }, "@nestjs/testing": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.2.0.tgz", @@ -13729,8 +13817,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.includes": { "version": "4.3.0", @@ -15098,6 +15185,19 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, + "swagger-ui-express": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz", + "integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==", + "requires": { + "swagger-ui-dist": ">=4.11.0" + } + }, "symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 8e84a78..b9742d1 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/jwt": "^9.0.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", + "@nestjs/swagger": "^6.1.4", "@nestjs/typeorm": "^9.0.1", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", @@ -37,6 +38,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "swagger-ui-express": "^4.6.0", "typeorm": "^0.3.10" }, "devDependencies": { diff --git a/src/main.ts b/src/main.ts index eeaa1dc..c64d475 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,8 +3,25 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as config from 'config'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; + async function bootstrap() { const app = await NestFactory.create(AppModule); + + + const swaggerConfig = new DocumentBuilder() + .setTitle('API DOCS From Swagger.') + .setDescription('회원 서버 API 문서입니다.') + .setVersion('0.0.1') + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'Token' }, + 'access-token', + ) + .build(); + + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api', app, document); + const serverConfig = config.get('server'); const port = serverConfig.port; diff --git a/src/members/dto/create-member.dto.ts b/src/members/dto/create-member.dto.ts index 0f1dea2..1c2e17d 100644 --- a/src/members/dto/create-member.dto.ts +++ b/src/members/dto/create-member.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; import { MemberAlarm } from '../model/member-alarm.enum'; import { MemberStatus } from '../model/member-status.enum'; @@ -6,35 +7,44 @@ export class CreateMemberDto { @IsNotEmpty() @IsString() + @ApiProperty({ type: String, description: '유저 폰번호' }) phone: string; @IsNotEmpty() @IsString() @MinLength(2) @MaxLength(20) + @ApiProperty({ type: String, description: '유저 이름' }) memberName: string; @IsNotEmpty() @IsString() @MinLength(8) @MaxLength(20) + @ApiProperty({ type: String, description: '유저가 입력한 id 계정' }) memberId: string; @IsEmail() + @ApiProperty({ type: String, description: '유저 이메일' }) email: string; @IsNotEmpty() @Matches(/^[a-zA-Z0-9]*$/, { message: 'Password only accepts english and number', // Matches 에 적합하지 않으면 이 메세지 노출 }) + @ApiProperty({ type: String, description: '유저가 입력한 pw 계정' }) password: string; - alarmFlag: string = MemberAlarm.ACTIVE; + @ApiProperty({ type: "${MemberAlarm}", description: '유저 알람 발송 활성화 여부' }) + alarmFlag: MemberAlarm = MemberAlarm.ACTIVE; + @ApiProperty({ type: "${MemberStatus}", description: '유저 활동 활성화 상태' }) status: MemberStatus = MemberStatus.ACTIVE; + @ApiProperty({ type: Date, description: '유저 회원가입 날짜' }) createdAt: Date = new Date(); + @ApiProperty({ type: Date, description: '유저 회원정보 수정 날짜' }) modifiedAt: Date = new Date(); } \ No newline at end of file diff --git a/src/members/dto/login-info.dto.ts b/src/members/dto/login-info.dto.ts index 621b14d..2de41cc 100644 --- a/src/members/dto/login-info.dto.ts +++ b/src/members/dto/login-info.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; export class LoginInfoDto { @@ -6,6 +7,7 @@ export class LoginInfoDto { @IsString() @MinLength(4) @MaxLength(20) + @ApiProperty({ type: String, description: '유저가 입력한 id 계정' }) memberId: string; @IsNotEmpty() @@ -16,6 +18,7 @@ export class LoginInfoDto { @Matches(/^[a-zA-Z0-9]*$/, { message: 'Password only accepts english and number', // Matches 에 적합하지 않으면 이 메세지 노출 }) + @ApiProperty({ type: String, description: '유저가 입력한 pw 계정' }) password: string; } \ No newline at end of file diff --git a/src/members/dto/modify-member.dto.ts b/src/members/dto/modify-member.dto.ts index 38fa034..6da3fb8 100644 --- a/src/members/dto/modify-member.dto.ts +++ b/src/members/dto/modify-member.dto.ts @@ -1,27 +1,35 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsDate, IsNotEmpty, IsOptional, isString, IsString, Matches, MaxLength, MinLength } from 'class-validator'; +import { MemberAlarm } from '../model/member-alarm.enum'; import { MemberStatus } from '../model/member-status.enum'; export class ModifyMemberDto { @IsOptional() @IsString() + @ApiProperty({ type: String, description: '유저 폰번호' }) phone: string; @IsOptional() + @ApiProperty({ type: "${MemberStatus}", description: '유저 활동 활성화 상태' }) status: MemberStatus @IsString() - alarmFlag: string + @ApiProperty({ type: "${MemberAlarm}", description: '유저 알람 발송 활성화 상태' }) + alarmFlag: MemberAlarm + @ApiProperty({ type: Date, description: '유저 회원정보 수정 날짜' }) modifiedAt: Date = new Date(); // 수정날짜는 현재날짜 @IsOptional() + @ApiProperty({ type: Date, description: '유저 회원 삭제 날짜' }) deletedAt: Date = new Date(); // status 가 SLP 이면 insert @IsOptional() @Matches(/^[a-zA-Z0-9]*$/, { message: 'Password only accepts english and number', // Matches 에 적합하지 않으면 이 메세지 노출 }) + @ApiProperty({ type: String, description: '유저가 입력한 pw 계정' }) password: string; } \ No newline at end of file diff --git a/src/members/dto/signin-member.dto.ts b/src/members/dto/signin-member.dto.ts index 21fd944..384ba90 100644 --- a/src/members/dto/signin-member.dto.ts +++ b/src/members/dto/signin-member.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; export class SignInMemberDto { @@ -6,11 +7,13 @@ export class SignInMemberDto { @IsString() @MinLength(8) @MaxLength(20) + @ApiProperty({ type: String, description: '유저가 입력한 id 계정' }) memberId: string; @IsNotEmpty() @Matches(/^[a-zA-Z0-9]*$/, { message: 'Password only accepts english and number', // Matches 에 적합하지 않으면 이 메세지 노출 }) + @ApiProperty({ type: String, description: '유저가 입력한 pw 계정' }) password: string; } \ No newline at end of file diff --git a/src/members/entity/loginlog.entity.ts b/src/members/entity/loginlog.entity.ts index 8c560cc..702bbf9 100644 --- a/src/members/entity/loginlog.entity.ts +++ b/src/members/entity/loginlog.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity, Column, @@ -8,22 +9,26 @@ import { import { Members } from './members.entity'; @Entity() -export class LoginLog extends BaseEntity { +export class LoginLog extends BaseEntity { // api 게이트웨이나 인증 서버로 옮기기 // pk @PrimaryGeneratedColumn() + @ApiProperty({ type: Number }) id: number; // 휴대폰번호 @Column() + @ApiProperty({ type: Date }) loginAt: Date; // 로그인 로그 테이블과 회원의 연관관계 설정 @ManyToOne((type) => Members, (member) => member.id, { eager: false }) + @ApiProperty({ type: Members }) member: Members; // 디바이스 정보 @Column() + @ApiProperty({ type: String }) device: string; } diff --git a/src/members/entity/members.entity.ts b/src/members/entity/members.entity.ts index c651689..50a5dbe 100644 --- a/src/members/entity/members.entity.ts +++ b/src/members/entity/members.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity, Column, @@ -5,6 +6,7 @@ import { PrimaryGeneratedColumn, Unique, } from 'typeorm'; +import { MemberAlarm } from '../model/member-alarm.enum'; import { MemberStatus } from '../model/member-status.enum'; @Entity() @@ -13,21 +15,26 @@ import { // 회원번호 - pk @PrimaryGeneratedColumn() + @ApiProperty({ type: Number }) id: number; // 휴대폰번호 @Column({nullable: true}) + @ApiProperty({ type: String }) phone: string; @Column() + @ApiProperty({ type: String }) email: string; // 사용자 입력 계정 id @Column() + @ApiProperty({ type: String }) memberId: string; // 비밀번호 @Column() + @ApiProperty({ type: String }) password: string; // 이름 @@ -36,23 +43,28 @@ import { // 활성상태 @Column({nullable: true}) + @ApiProperty({ type: MemberStatus }) status: MemberStatus; // 알림설정 @Column({nullable: true}) - alarmFlag: string; + @ApiProperty({ type: MemberAlarm }) + alarmFlag: MemberAlarm; // 생성일자 @Column({type: "timestamp", nullable: true}) + @ApiProperty({ type: Date }) createdAt: Date; // 수정일자 // @Column({type: "timestamp", default: () => "CURRENT_TIMESTAMP", nullable: true}) @Column({type: "timestamp", nullable: true}) + @ApiProperty({ type: Date }) modifiedAt: Date; // 삭제일자 @Column({type: "timestamp", nullable: true}) + @ApiProperty({ type: Date }) deletedAt: Date; } diff --git a/src/members/members.controller.ts b/src/members/members.controller.ts index 03269d1..aa98a30 100644 --- a/src/members/members.controller.ts +++ b/src/members/members.controller.ts @@ -3,6 +3,12 @@ import { CreateMemberDto } from './dto/create-member.dto'; import { ModifyMemberDto } from './dto/modify-member.dto'; import { Members } from './entity/members.entity'; import { MembersService } from './members.service'; +import { + ApiResponse, + ApiOkResponse, + ApiUnauthorizedResponse, + ApiBody, + } from '@nestjs/swagger'; @Controller('members') @@ -14,6 +20,9 @@ export class MembersController { } // 회원 가입 (회원 활성 상태 default ACT) + @ApiResponse({ description: '회원가입 API' }) + @ApiBody({ type: CreateMemberDto }) + @ApiOkResponse({ description: '멤버 회원가입' }) // 200 @Post('/signup') signUp( @Body(new ValidationPipe({transform: true})) createMemberDto: CreateMemberDto, // ValidationPipe true로 설정해야 dto 오버라이드 돼서 디폴트값 세팅됨 @@ -23,23 +32,23 @@ export class MembersController { } // 회원 조회 + @ApiResponse({ description: '회원조회 API' }) + @ApiUnauthorizedResponse({ description: 'Invalid Credential' }) // 401 @Get('/:id') - getMemberById(@Param('id', ParseIntPipe) id: number): Promise { + getMemberById(@Param('id', ParseIntPipe) id: number): Promise { this.log.verbose(`find Member id: ${id}`); return this.membersService.getMemberById(id); } // 회원 정보 수정(탈퇴, 활성상태) + @ApiResponse({ description: '회원 정보 수정 API' }) @Patch('/:id/status') modifyMemberById( @Param('id', ParseIntPipe) id: number, @Body(ValidationPipe) modifyMemberDto: ModifyMemberDto ) { this.log.verbose(`Mod Member id: ${id}`); this.log.verbose(`Modify target data: ${JSON.stringify(modifyMemberDto)}`); - + return this.membersService.modifyMemberById(id, modifyMemberDto); } - - // 회원 활성 상태 조회 (ACT / SLP) -> 회원 조회 - }