관리 메뉴

bright jazz music

[nestjs] 스웨거 설치 및 DTO 에 적용 본문

Framework/NestJS

[nestjs] 스웨거 설치 및 DTO 에 적용

bright jazz music 2024. 12. 25. 23:24

1. 먼저 스웨거를 설치한다

pnpm add @nestjs/swagger swagger-ui-express
// npm install @nestjs/swagger swagger-ui-express

원래는 기본적인 dto와 컨트롤러, 서비스를 먼저 작성하였다. 그러나 그 과정을 기록해 놓지 않았기 때문에 현재 코드는 스웨거가 적용된 상태이다. 따라서 이해를 돕기 위해 스웨거를 먼저 설치하고 진행한다.

 

2. main.ts에 적용

설치를 완료했으면 main.ts에 적용해준다.

// src/main.ts
// 애플리케이션 진입점
// 스프링부트의 @SpringBootApplication가 붙어있는 파일(메인함수가 있는 파일)에 대응되는 파일

// 유효성 검사 파이프 추가
import { ValidationPipe } from '@nestjs/common';


import { NestFactory } from '@nestjs/core';
// Swagger 모듈 추가
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  console.log('DB Connection Info:', {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    username: process.env.DB_USERNAME,
    database: process.env.DB_DATABASE,
    // password는 보안상 출력하지 않음
  });
  
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, // DTO에 정의되지 않은 속성은 제거
    forbidNonWhitelisted: true, // DTO에 정의되지 않은 속성이 있으면 요청 자체를 막음
    transform: true, // 요청 데이터를 DTO 클래스의 인스턴스로 변환
  }));
  
  // CORS 허용
  app.enableCors({
      // origin: true로 설정하면 모든 도메인에서의 요청을 허용
  // 실제 운영 환경에서는 특정 도메인만 허용하도록 설정해야 함
  });
  
  // 전역 경로 설정: 경로 앞에 /api/v1 붙이기
  app.setGlobalPrefix('api/v1');  // /api/v1/members

  // Swagger 설정
  const config = new DocumentBuilder()
    .setTitle('Members API') // 문서 제목
    .setDescription('회원 관리 API 문서') // 문서 설명
    .setVersion('1.0') // 문서 버전
    .addBearerAuth( // Bearer 인증 토큰 추가
      {
        type: 'http',
        scheme: 'bearer', // 인증 토큰 타입
        bearerFormat: 'JWT', // 인증 토큰 형식
        name: 'JWT', // 인증 토큰 이름
        description: 'Enter JWT token', // 인증 토큰 설명
        in: 'header', // 인증 토큰 위치
      },
      'access-token',
    )
    .addTag('auth', '인증 관련 API') // 태그 추가
    .addTag('members', '회원 관리 API') // 태그 추가
    .addTag('email', '이메일 관련 API') // 태그 추가
    .build(); // 문서 빌드

  const document = SwaggerModule.createDocument(app, config); // Swagger 문서 생성
  SwaggerModule.setup('api-docs', app, document, { // Swagger 문서 설정
    swaggerOptions: {
      persistAuthorization: true, // 인증 토큰 유지
    },
  });


  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

 

 

3. 디티오 작성

 

3.1. 멤버 생성 디티오

얜 일단 너무 많아서 아직 안 달았음

// src/members/dto/create-member.dto.ts
// 멤버 생성 DTO: 새로운 회원 가입 시 필요한 데이터 정의
// 필수 정보만을 포함하여 회원가입의 진입 장벽을 낮춤

import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, MinLength, IsBoolean, IsEnum } from 'class-validator';
import { AuthProvider } from '@common/enums';

export class CreateMemberDto {
  @ApiProperty({
    example: 'user@example.com',
    description: '사용자 이메일 (로그인 ID로 사용)',
    required: true,
  })
  @IsEmail()
  email: string;

  @ApiProperty({
    example: 'Password123!',
    description: '비밀번호 (최소 8자)',
    required: true,
  })
  @IsString()
  @MinLength(8)
  password: string;

  @ApiProperty({
    example: true,
    description: '이용약관 동의',
    required: true,
  })
  @IsBoolean()
  termsAgreed: boolean;

  @ApiProperty({
    example: true,
    description: '개인정보 처리방침 동의',
    required: true,
  })
  @IsBoolean()
  privacyAgreed: boolean;

  @ApiProperty({
    example: false,
    description: '마케팅 수신 동의 (선택)',
    required: false,
    default: false,
  })
  @IsBoolean()
  marketingAgreed: boolean = false;

  @ApiProperty({
    example: 'email',
    description: '인증 제공자',
    enum: AuthProvider,
    default: AuthProvider.LOCAL,
  })
  @IsEnum(AuthProvider)
  provider: AuthProvider = AuthProvider.LOCAL;
}

 

3.2. 멤버 업데이트 디티오

// src/members/dto/update-member.dto.ts
// 멤버 수정 DTO: 회원 정보 업데이트 시 사용되는 데이터 정의
// CreateMemberDto를 상속받아 모든 필드를 선택적으로 만듦

import { CreateMemberDto } from './create-member.dto';
import { ApiProperty,PartialType }from '@nestjs/swagger';
import { IsOptional, IsString, IsObject } from 'class-validator';

export class UpdateMemberDto extends PartialType(CreateMemberDto) {
    // @IsOptional()
    // @IsString()
    // @ApiProperty({ 
    //     example: "새로운닉네임", 
    //     description: "변경할 닉네임",
    //     required: false 
    // })
    // nickname?: string;

    @IsOptional()
    @IsObject()
    @ApiProperty({ 
        example: {
            "email": true,
            "push": false,
            "sms": true,
            "marketing": false,
            "inApp": true
        },
        description: "알림 설정",
        required: false 
    })
    notificationSettings?: {
        email: boolean;
        push: boolean;
        sms: boolean;
        marketing: boolean;
        inApp: boolean;
    };

    @ApiProperty({ 
        example: "newPassword123!", 
        description: "새 비밀번호 (로컬 로그인 사용자만)",
        required: false 
    })
    password?: string;
}

 

3.3. 응답 디티오

 

아직은 전체 상태를 반환하는 전체 필드 응답 디티오만 만들었음. 여긴 너무 많아서 스웨거 달지 않음. 나중에 추가 예정

// src/members/dto/member-response.dto.ts
// 멤버 응답 DTO: 멤버의 모든 공개 가능한 정보를 포함하는 응답 객체

import { MemberStatus } from "@common/enums";
export class MemberResponseDto {
    uuid: string;              // 외부 노출용 식별자
    email: string;             // 이메일
    name?: string;             // 이름 (선택)
    nickname?: string;         // 닉네임 (선택)
    phoneNumber?: string;      // 전화번호 (선택)
    provider: string;          // 인증 제공자 (kakao/google/email)
    emailVerified: boolean;    // 이메일 인증 여부
    profileImage?: string;     // 프로필 이미지 URL (선택)
    status: MemberStatus;         // 계정 상태
    lastLoginAt?: Date;        // 마지막 로그인 시간

    // 약관 동의 정보
    termsAgreed: boolean;      // 이용약관 동의
    termsAgreedAt?: Date;      // 이용약관 동의 시간
    marketingAgreed: boolean;  // 마케팅 수신 동의
    marketingAgreedAt?: Date;  // 마케팅 수신 동의 시간
    privacyAgreed: boolean;    // 개인정보 수집 동의
    privacyAgreedAt?: Date;    // 개인정보 수집 동의 시간

   

    // 알림 설정
    notificationSettings: {
        email: boolean;        // 이메일 알림
        push: boolean;         // 푸시 알림
        sms: boolean;          // SMS 알림
        marketing: boolean;    // 마케팅 알림
    };

    // 사용자 설정
    preferences: {
        language: string;      // 언어 설정
        timezone: string;      // 시간대 설정
        theme: string;         // 테마 설정
    };

    // 포인트 및 레벨 정보
    points: {
        total: number;            // 총 포인트
        purchase: number;    // 구매 포인트
        reward: number;      // 리워드 포인트
    };
    levelInfo: {
        level: number;            // 현재 레벨
        experience: number;       // 경험치
    };
    role: string;            // 사용자 역할 (ADMIN/USER/MANAGER)

    // 시스템 정보
    createdAt: Date;         // 계정 생성 시간
    updatedAt: Date;         // 정보 수정 시간
}

 

 

4. 매퍼

매퍼도 변경했는지 기억나지 않으므로 혹시 몰라 첨부함.

// src/members/mappers/member.mapper.ts
// 멤버 매퍼: Entity와 DTO 간의 변환을 담당
// 데이터 계층과 표현 계층 사이의 데이터 변환을 중앙화하여 관리

import { Member } from '../entities/member.entity';
import { MemberResponseDto } from '../dto/member-response.dto';
import { CreateMemberDto } from '../dto/create-member.dto';
import { UpdateMemberDto } from '../dto/update-member.dto';
import { AuthProvider, MemberStatus } from '@common/enums';

export class MemberMapper {
  // Entity를 ResponseDTO로 변환
  static toDto(member: Member): MemberResponseDto {
    const dto = new MemberResponseDto();
    dto.uuid = member.uuid;
    dto.email = member.email;
    dto.name = member.name;
    dto.nickname = member.nickname;
    dto.phoneNumber = member.phoneNumber;
    dto.provider = member.provider;
    dto.emailVerified = member.emailVerified;
    dto.profileImage = member.profileImage;
    dto.status = member.status;
    dto.lastLoginAt = member.lastLoginAt;
    dto.termsAgreed = member.termsAgreed;
    dto.termsAgreedAt = member.termsAgreedAt;
    dto.marketingAgreed = member.marketingAgreed;
    dto.marketingAgreedAt = member.marketingAgreedAt;
    dto.notificationSettings = member.notificationSettings;
    dto.preferences = member.preferences;
    dto.createdAt = member.createdAt;
    dto.updatedAt = member.updatedAt;
    dto.points = member.points;

    dto.levelInfo = member.levelInfo;
    dto.role = member.role;
    return dto;
  }

  // DTO 목록을 ResponseDTO 목록으로 변환
  static toDtoList(members: Member[]): MemberResponseDto[] {
    return members.map(member => this.toDto(member));
  }

  // DTO를 Entity로 변환 (생성/수정 시 사용)
  static toEntity(dto: CreateMemberDto | UpdateMemberDto): Partial<Member> {
    const entity = new Member();
    if (dto.email) entity.email = dto.email;
    if (dto.password) entity.password = dto.password;
    
    // 약관 동의 처리
    if ('termsAgreed' in dto) {
      entity.termsAgreed = dto.termsAgreed;
      entity.termsAgreedAt = new Date();
    }
    
    if ('marketingAgreed' in dto) {
      entity.marketingAgreed = dto.marketingAgreed;
      entity.marketingAgreedAt = new Date();
    }

    // 기본값 설정
    entity.provider = AuthProvider.LOCAL;
    entity.status = MemberStatus.ACTIVE;
    
    return entity;
  }
}
Comments