diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4b9cb7e..8db45286 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ importers: '@nestjs/platform-socket.io': specifier: ^10.4.7 version: 10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1) + '@nestjs/swagger': + specifier: ^8.0.7 + version: 8.0.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^10.4.7 version: 10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -946,6 +949,9 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@microsoft/tsdoc@0.15.0': + resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==} + '@mongodb-js/saslprep@1.1.9': resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==} @@ -1003,6 +1009,19 @@ packages: peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/mapped-types@2.0.6': + resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/mongoose@10.1.0': resolution: {integrity: sha512-1ExAnZUfh2QffEaGjqYGgVPy/sYBQCVLCLqVgkcClKx/BCd0QNgND8MB70lwyobp3nm/+nbGQqBpu9F3/hgOCw==} peerDependencies: @@ -1035,6 +1054,23 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@8.0.7': + resolution: {integrity: sha512-zaTMCEZ/CxX7QYF110nTqJsn7eCXp4VI9kv7+AdUcIlBmhhgJpggBw2Mx2p6xVjyz1EoWXGfxxWKnxEyaQwFlg==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@10.4.6': resolution: {integrity: sha512-aiDicKhlGibVGNYuew399H5qZZXaseOBT/BS+ERJxxCmco7ZdAqaujsNjSaSbTK9ojDPf27crLT0C4opjqJe3A==} peerDependencies: @@ -1299,6 +1335,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@shelf/jest-mongodb@4.3.2': resolution: {integrity: sha512-LL7NBaT04sJspoOZXqw3HGLw0+XnZNlIV72x2ymzyuloqIKXwgUl8eL1XKDUh4Ud8dUBRMrOngCQBcHKjWnrHQ==} engines: {node: '>=16'} @@ -4634,6 +4673,9 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -5850,6 +5892,8 @@ snapshots: - encoding - supports-color + '@microsoft/tsdoc@0.15.0': {} + '@mongodb-js/saslprep@1.1.9': dependencies: sparse-bitfield: 3.0.3 @@ -5919,6 +5963,11 @@ snapshots: '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + '@nestjs/mongoose@10.1.0(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(mongoose@8.8.0(socks@2.8.3))(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5966,6 +6015,18 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.15.0 + '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.18.2 + '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6)': dependencies: '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -6407,6 +6468,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@scarf/scarf@1.4.0': {} + '@shelf/jest-mongodb@4.3.2(jest-environment-node@29.7.0)(mongodb@6.10.0(socks@2.8.3))': dependencies: debug: 4.3.4 @@ -10390,6 +10453,10 @@ snapshots: svg-parser@2.0.4: {} + swagger-ui-dist@5.18.2: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} synckit@0.9.2: diff --git a/server/package.json b/server/package.json index d9c755ff..69da594c 100644 --- a/server/package.json +++ b/server/package.json @@ -31,6 +31,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.7", + "@nestjs/swagger": "^8.0.7", "@nestjs/websockets": "^10.4.7", "@noctaCrdt": "workspace:*", "bcrypt": "^5.1.1", diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts index 1088c2de..3176835d 100644 --- a/server/src/app.controller.ts +++ b/server/src/app.controller.ts @@ -1,12 +1,15 @@ -// server/src/app.controller.ts -import { Controller, Get, Injectable } from "@nestjs/common"; +import { Controller, Get } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger"; import { AppService } from "./app.service"; +@ApiTags("app") @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() + @ApiOperation({ summary: "Get a greeting message" }) + @ApiResponse({ status: 200, description: "Successfully retrieved greeting message." }) public getHello(): string { return this.appService.getHello(); } diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index f0f9a559..18a72b07 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -1,13 +1,35 @@ import { Controller, Post, Body, Request, UseGuards } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { JwtAuthGuard } from "./jwt-auth.guard"; +import { ApiTags, ApiOperation, ApiBody, ApiResponse } from "@nestjs/swagger"; +import { UserDto } from "./dto/user.dto"; +@ApiTags("auth") @Controller("auth") export class AuthController { constructor(private authService: AuthService) {} @Post("register") - async register(@Body() body: { email: string; password: string; name: string }) { + @ApiOperation({ summary: "Register a new user" }) + @ApiBody({ + schema: { + type: "object", + properties: { + email: { type: "string" }, + password: { type: "string" }, + name: { type: "string" }, + }, + }, + }) + @ApiResponse({ + status: 201, + description: "The user has been successfully created.", + type: UserDto, + }) + @ApiResponse({ status: 400, description: "Bad Request" }) + async register( + @Body() body: { email: string; password: string; name: string }, + ): Promise { const { email, password, name } = body; const user = await this.authService.register(email, password, name); return { @@ -18,6 +40,18 @@ export class AuthController { } @Post("login") + @ApiOperation({ summary: "Login a user" }) + @ApiBody({ + schema: { + type: "object", + properties: { + email: { type: "string" }, + password: { type: "string" }, + }, + }, + }) + @ApiResponse({ status: 200, description: "The user has been successfully logged in." }) + @ApiResponse({ status: 401, description: "Unauthorized" }) async login(@Body() body: { email: string; password: string }) { const user = await this.authService.validateUser(body.email, body.password); if (!user) { @@ -28,6 +62,9 @@ export class AuthController { @UseGuards(JwtAuthGuard) @Post("profile") + @ApiOperation({ summary: "Get user profile" }) + @ApiResponse({ status: 200, description: "The user profile has been successfully retrieved." }) + @ApiResponse({ status: 401, description: "Unauthorized" }) getProfile(@Request() req) { return req.user; } diff --git a/server/src/auth/dto/user.dto.ts b/server/src/auth/dto/user.dto.ts new file mode 100644 index 00000000..bfe5f0fb --- /dev/null +++ b/server/src/auth/dto/user.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class UserDto { + @ApiProperty({ + description: "The unique identifier of the user", + example: "5f8f8c44b54764421b7156c9", + }) + id: string; + + @ApiProperty({ + description: "The email address of the user", + example: "user@example.com", + }) + email: string; + + @ApiProperty({ + description: "The name of the user", + example: "John Doe", + }) + name: string; +} diff --git a/server/src/main.ts b/server/src/main.ts index 6a203d04..406efa7f 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,5 +1,6 @@ import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; +import { createSwaggerDocument } from "./swagger/swagger.config"; const bootstrap = async () => { const app = await NestFactory.create(AppModule); @@ -27,6 +28,8 @@ const bootstrap = async () => { app.setGlobalPrefix("api"); + createSwaggerDocument(app); + await app.listen(process.env.PORT ?? 3000); }; bootstrap(); diff --git a/server/src/swagger/swagger.config.ts b/server/src/swagger/swagger.config.ts new file mode 100644 index 00000000..6e2b2981 --- /dev/null +++ b/server/src/swagger/swagger.config.ts @@ -0,0 +1,17 @@ +import { INestApplication } from "@nestjs/common"; +import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; + +export const createSwaggerDocument = (app: INestApplication): void => { + if (process.env.NODE_ENV !== "development") { + return; // 개발 환경이 아니면 Swagger 비활성화 + } + + const config = new DocumentBuilder() + .setTitle("Nocta API Docs") + .setDescription("Nocta API description") + .setVersion("1.0.0") + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup("api-docs", app, document); +};