ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NestJS_온라인 공연 예매 서비스 프로젝트 5편_공연 기능 구현(수정중)
    TIL (Today I Learned) 2023. 12. 28. 21:08

    # Nest JS 프로젝트 공연 구현을 시작에 앞서

    이전 단계에 대한 궁금증이 남아 계신 분들은 아래의 포스팅을 차례로 살펴보시길 권장합니다.

     

    NestJS_온라인 공연 예매 서비스 프로젝트 1편_기본 세팅

    # Nest JS 프로젝트를 시작하며 TypeScript와 NestJS를 활용한 온라인 공연 예매 서비스 프로젝트를 자세하게 기록하는 포스팅을 시작하고자 합니다. 지금까지 다양한 과제를 수행해왔지만, 아마도 이

    k0zdevel.tistory.com

     

    NestJS_온라인 공연 예매 서비스 프로젝트 2편_프로젝트 필수 기능 구현

    # Nest JS 프로젝트 본격적으로 시작에 앞서 지난 포스팅에 이어 두 번째 이야기를 시작하겠습니다. 이전 글에서는 프로젝트의 기초 세팅에 대해 다뤘습니다. 만약 그 내용에 대해 궁금하시거나

    k0zdevel.tistory.com

     

    NestJS_온라인 공연 예매 서비스 프로젝트 3편_유저기능 구현(회원가입 및 로그인[上])

    # Nest JS 프로젝트 유저 기능 구현을 시작에 앞서 지난 글에서는 엄격한 DTO 다루기와 ValidationPipe의 활용, TypeORM을 사용한 데이터베이스 연결, 그리고 Auth 모듈을 생성해 인증 및 인가 기능을 구현

    k0zdevel.tistory.com

     

    NestJS_온라인 공연 예매 서비스 프로젝트 4편_유저기능 구현(회원가입 및 로그인[下])

    # Nest JS 프로젝트 유저 기능 구현을 시작에 앞서 이전 포스팅에서는 회원가입 및 로그인 [상]에서 user 모듈, 서비스, 컨트롤러를 기본적으로 구현했습니다. 더불어, Enum을 활용하여 user와 admin을

    k0zdevel.tistory.com

     

    NESTJS_예외처리의 중요성과 종류

    # 예외처리의 중요성 Nest.js에서는 예외를 처리하고 사용자에게 적절한 응답을 제공하는 것이 중요합니다. 이를 통해 애플리케이션의 안정성을 유지하고 클라이언트와의 통신을 원활하게 할 수

    k0zdevel.tistory.com


    # Nest JS_show기능 구현_기초 작업

     

    ***show module, servise, controller 생성하기***

    nest g mo show
    nest g s show
    nest g co show


    ***show entity 생성***

    show entity는 데이터베이스에서 공연 정보를 효과적으로 저장하고 관리하기 위한 역할을 합니다.  entity는 데이터베이스 테이블의 구조를 정의하며, 각 행은 특정 공연에 대한 정보를 나타냅니다.

     

    show entity생성을 위해 src/show 디렉토리로 이동한 후 entities 디렉토리를 만들어서 show .entity.ts 파일을새롭게 정의해줘야합니다. 

    ***show entity 작성***

    import {
      Column,
      Entity,
      PrimaryGeneratedColumn,
      CreateDateColumn,
      UpdateDateColumn,
      DeleteDateColumn,
      OneToMany,
    } from 'typeorm';
    
    @Entity({ name: 'show' })
    export class Show {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column({ type: 'integer', nullable: false, name: 'showPoster' })
      showPoster: number;
    
      @Column({ type: 'varchar', nullable: false, name: 'showTitle' })
      showTitle: string;
    
      @Column({ type: 'varchar', nullable: false, name: 'showCast' })
      showCast: string;
    
      @Column({ type: 'varchar', nullable: false, name: 'showGenre' })
      showGenre: string;
    
      @Column({ type: 'varchar', nullable: false, name: 'showDescription' })
      showDescription: string;
    
      @Column({ type: 'varchar', nullable: false, name: 'showDateTime' })
      showDateTime: string;
    
      @Column({ type: 'varchar', nullable: false, name: 'showVenue' })
      showVenue: string;
    
      @Column({ type: 'date', nullable: false, name: 'showRunTime' })
      showRunTime: Date;
    
      @Column({ type: 'integer', nullable: false, name: 'showAgeRating' })
      showAgeRating: number;
    
      @Column({ type: 'integer', nullable: false, name: 'tiketPrice' })
      tiketPrice: number;
    
      @Column({ type: 'integer', nullable: false, name: 'seat' })
      seat: number;
    
      @CreateDateColumn({ name: 'createdAt', comment: '생성일시' })
      createdAt: Date;
    
      @UpdateDateColumn({ name: 'updatedAt', comment: '수정일시' })
      updatedAt: Date;
    
      @DeleteDateColumn({ name: 'deletedAt', comment: '삭제일시' })
      deletedAt?: Date | null;
    
      @OneToMany(() => Reservation, (reservation) => reservation.show)
      reservations: Reservation[];
    
      @OneToMany(() => SetMetadata, (seat) => seat.show)
      seats: Seat[];
    }

     

    Show ntity는 공연에 관한 정보를 나타내며 제목, 출연진, 장르, 설명, 일시, 장소, 런타임, 연령 등의 세부 정보를 포함합니다. 또한 관련된 예약 및 좌석과 같은 세부 정보도 포함하고 있습니다.


    ***show.controller에 사용할 DTO 생성하기***

    Show ntity 작업이 완료되면 다음으로 show.controller에서 사용할 DTO 파일도 만들어야합니다. rc/show 디렉토리로 다시 이동하여 dto디렉토리를 생성한 후  show.dto.ts 파일을 새롭게 만들어 작성해야합니다. 

    import { IsNotEmpty, IsString, IsInt, IsDate, IsUrl } from 'class-validator';
    
    export class ShowDto {
      @IsString()
      @IsNotEmpty({ message: '포스터 이미지 URL을 입력해주세요.' })
      @IsUrl({}, { message: '올바른 URL 형식이 아닙니다.' })
      showPoster: string;
    
      @IsString()
      @IsNotEmpty({ message: '공연 제목을 입력해주세요' })
      showTitle: string;
    
      @IsString()
      @IsNotEmpty({ message: '출연진 정보를 입력해주세요.' })
      showCast: string;
    
      @IsString()
      @IsNotEmpty({ message: '장르를 입력해주세요.' })
      showGenre: string;
    
      @IsString()
      @IsNotEmpty({ message: '설명을 입력해주세요.' })
      showDescription: string;
    
      @IsString()
      @IsNotEmpty({ message: '날짜 및 시간을 입력해주세요.' })
      showDateTime: string;
    
      @IsString()
      @IsNotEmpty({ message: '장소를 입력해주세요.' })
      showVenue: string;
    
      @IsDate()
      @IsNotEmpty({ message: '런타임을 입력해주세요.' })
      showRunTime: Date;
    
      @IsInt()
      @IsNotEmpty({ message: '연령 등급을 입력해주세요.' })
      showAgeRating: number;
    
      @IsInt()
      @IsNotEmpty({ message: '티켓 가격을 입력해주세요.' })
      tiketPrice: number;
    
      @IsInt()
      @IsNotEmpty({ message: '좌석 수를 입력해주세요.' })
      seat: number;
    }

     

    위의 코드는 TypeScript에서 클래스 유효성 검사를 위해 사용되는 class-validator 라이브러리를 사용한 DTO(Data Transfer Object)입니다. 각각의 속성은 class-validator의 데코레이터를 사용하여 해당 속성에 대한 유효성 검사 규칙을 정의하고 있습니다. 이렇게 정의된 DTO는 클라이언트에서 서버로 전송되는 데이터에 대한 유효성을 검사하는데 사용됩니다.


    ***show 기능의 핵심 인가기능 roles.decorator.ts 생성하기***

    다음으로 show 기능의 핵심인 인가 기능을 본격적으로 생성하기 전에 커스텀 데코레이터를 하나 만들어야합니다. Roles라는 데코레이터를 만들어보겠습니다. src/auth 디렉토리로 이동한 후 roles.decorator.ts 파일을 생성합니다.

     

    // auth/roles.decorator.ts
    
    // 사용자 역할(role)을 정의한 타입을 가져옵니다.
    import { Role } from 'src/user/types/userRole.type';
    
    // NestJS에서 제공하는 SetMetadata 데코레이터를 가져옵니다.
    import { SetMetadata } from '@nestjs/common';
    
    // Roles 데코레이터를 정의합니다.
    // 이 데코레이터는 메타데이터를 설정하여 역할(role) 정보를 전달합니다.
    export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);

    **추가설명

    • Roles라는 함수를 내보냅니다. 이 함수는 메타데이터를 설정하는 역할을 합니다.
    • ...roles: Role[]: 가변 인자를 통해 여러 개의 역할(role)을 받습니다. Role 타입의 배열로 역할들을 나타냅니다.
    • SetMetadata('roles', roles): SetMetadata 데코레이터를 사용하여 'roles'라는 키로 메타데이터를 설정합니다. 이 메타데이터에는 전달된 역할(role)들이 배열로 저장됩니다.

    Roles는 우리가 만든 커스텀 데코레이터의 이름입니다. 이 데코레이터는 여러 역할을 매개변수로 받을 수 있도록 (...roles: Role[]) 코드를 사용합니다. 예를 들어, @Roles(Role.Admin, Role.User)와 같이 여러 역할을 동시에 지정할 수 있습니다.

     

    이데코레이터는 SetMetadata('roles', roles) 코드를 사용하여 'roles'라는 문자열 키에 역할 정보를 저장합니다. 이 부분은 메타데이터를 사용하는데, 메타데이터는 키-값 쌍으로 정보를 저장하는데 사용됩니다. 이렇게 저장된 역할 정보는 나중에 인가를 검증하는 가드에서 활용될 것입니다.

     

    코드를 통해 간결하게 역할(role) 정보를 전달하고 저장함으로써, 컨트롤러나 핸들러 메서드에서 간편하게 역할 기반의 인가 기능을 적용할 수 있습니다. 이러한 구조를 통해 코드의 가독성을 높이고 역할 기반의 접근 제어를 쉽게 구현할 수 있습니다.


    ***위에서 만든 roles.decorator.ts를 활용하여 roles.guard.ts 생성하기***

    src/auth 디렉토리로 이동한 후 roles.guard.ts 파일을 생성하고 코드를 작성합니다.

    // 필요한 모듈 및 타입 임포트
    import { Role } from 'src/user/types/userRole.type';
    import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
    import { Reflector } from '@nestjs/core';
    import { AuthGuard } from '@nestjs/passport';
    
    // AuthGuard('jwt')를 확장하여 JWT 토큰을 사용한 인증을 수행하는 RolesGuard 클래스 정의
    @Injectable()
    export class RolesGuard extends AuthGuard('jwt') implements CanActivate {
      // Reflector를 주입받는 생성자
      constructor(private reflector: Reflector) {
        // AuthGuard('jwt')의 생성자 호출
        super();
      }
    
      // CanActivate 인터페이스의 canActivate 메서드 구현
      async canActivate(context: ExecutionContext) {
        // 부모 클래스의 canActivate 메서드 호출하여 JWT 토큰을 사용한 인증 수행
        const authenticated = await super.canActivate(context);
    
        // 인증에 실패한 경우, 요청 거부
        if (!authenticated) {
          return false;
        }
    
        // 메타데이터에서 필요한 역할 정보를 가져오기
        const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
          context.getHandler(),   // 현재 핸들러 메서드에서 설정한 역할 정보
          context.getClass(),     // 현재 컨트롤러에서 설정한 역할 정보
        ]);
    
        // 만약 역할 정보가 없다면, 모든 요청을 허용
        if (!requiredRoles) {
          return true;
        }
    
        // 요청에서 사용자 정보 가져오기
        const { user } = context.switchToHttp().getRequest();
    
        // 사용자의 역할이 필요한 역할 중 하나와 일치하는지 확인
        return requiredRoles.some((role) => user.role === role);
      }
    }

    **추가설명

    • RolesGuard 클래스는 CanActivate 인터페이스를 구현하며, AuthGuard('jwt')를 확장합니다. 이렇게 함으로써, canActivate 메서드를 구현하여 특정 조건에 따라 인가를 수행할 수 있습니다.
    • canActivate 메서드에서 super.canActivate(context)를 호출하여 부모 클래스의 canActivate 메서드를 실행하고, JWT 토큰을 사용한 인증을 수행합니다.
    • 메타데이터에서 역할 정보를 가져오기 위해 this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);를 사용합니다. 이렇게 함으로써 컨트롤러나 핸들러 메서드에서 설정한 역할 정보를 효과적으로 가져올 수 있습니다.
    • 최종적으로, 사용자의 역할이 필요한 역할 중 하나와 일치하는지 확인하여 결과를 반환합니다.

    RolesGuard 클래스는 CanActivate 인터페이스를 구현하며, 동시에 AuthGuard('jwt')를 확장하여 JWT 인증 전략을 기본으로 가져가면서 부가적으로 인가 기능을 수행합니다. 즉, 로그인하지 않은 사용자는 인가 기능에 도달하지 못하게 됩니다.

     

    가드는 @Roles(Role.Admin)과 같은 데코레이터를 활용하여 역할 기반의 인가를 수행합니다. 데코레이터는 roles라는 키로 메타데이터를 설정하며, 이 메타데이터는 해당 역할을 나타내는 값을 가집니다. API를 호출하면 Nest.js는 RolesGuard의 canActivate 메서드를 호출하고, 해당 메서드에서는 Reflector를 사용하여 roles 키의 메타데이터를 읽어옵니다. 이때, 메타데이터의 값은 데코레이터에 설정된 역할 정보입니다.

     

    마지막으로, 현재 인증된 사용자의 역할과 필요한 역할을 비교하여 사용자가 해당 역할을 가지고 있다면 접근을 허용하고, 그렇지 않다면 거부합니다. 이를 통해 JWT 토큰을 통한 인증과 함께 역할 기반의 인가를 적용할 수 있습니다.


     

    # Nest JS_show.controller.ts 작업

    사전 준비를 마쳤기 때문에 본격적인 show 기능을 구현하겠습니다 .먼저 show.controller을 시작으로 service, module을 하나씩 작성해나가겠습니다. show.controller.ts 파일로 이동하여 기존에 작성되어 있던 코드는 지우고 다시 작성을 시작합니다. 

     

    // 필요한 모듈 및 타입 임포트
    import { Roles } from 'src/auth/roles.decorator'; // 사용자 역할 데코레이터
    import { RolesGuard } from 'src/auth/roles.guard'; // 역할 기반 가드
    import { Role } from 'src/user/types/userRole.type'; // 사용자 역할 타입
    
    import {
      Body,
      Controller,
      Delete,
      Get,
      Param,
      Post,
      Put,
      UploadedFile,
      UseGuards,
      UseInterceptors,
    } from '@nestjs/common';
    import { FileInterceptor } from '@nestjs/platform-express';
    
    import { ShowDto } from './dto/show.dto'; // 공연 데이터 전송 객체
    import { ShowService } from './show.service';
    
    // RolesGuard를 사용하여 역할 기반의 권한 확인을 수행하는 컨트롤러 클래스 정의
    @UseGuards(RolesGuard)
    @Controller('show')
    export class ShowController {
      // ShowService 주입
      constructor(private readonly showService: ShowService) {}
    
      // 모든 공연 조회 엔드포인트
      @Get()
      async findAll() {
        return await this.showService.findAll();
      }
    
      // 특정 공연 조회 엔드포인트
      @Get(':id')
      async findOne(@Param('id') id: number) {
        return await this.showService.findOne(id);
      }
    
      // RolesGuard를 사용하여 'admin' 역할이 있는 사용자만 접근 가능한 공연 생성 엔드포인트
      @Roles(Role.Admin)
      @Post()
      @UseInterceptors(FileInterceptor('file')) // 파일 업로드를 위한 인터셉터 사용
      async create(@UploadedFile() file: Express.Multer.File) {
        await this.showService.create(file);
      }
    
      // RolesGuard를 사용하여 'admin' 역할이 있는 사용자만 접근 가능한 공연 수정 엔드포인트
      @Roles(Role.Admin)
      @Put(':id')
      async update(@Param('id') id: number, @Body() ShowDto: ShowDto) {
        await this.showService.update(id, ShowDto);
      }
    
      // RolesGuard를 사용하여 'admin' 역할이 있는 사용자만 접근 가능한 공연 삭제 엔드포인트
      @Roles(Role.Admin)
      @Delete(':id')
      async delete(@Param('id') id: number) {
        await this.showService.delete(id);
      }
    }

    **추가설명

    • @UseGuards(RolesGuard) 데코레이터는 RolesGuard를 특정 컨트롤러에 적용하며, 이는 해당 컨트롤러의 모든 메서드에 대한 호출에 대해 사용자의 로그인 여부를 확인하는 역할을 합니다. 이는 RolesGuard가 AuthGuard('jwt')를 상속받았기 때문에 가능합니다.
    • @Roles(Role.Admin) 데코레이터는 어드민 권한이 필요한 API를 나타냅니다. 특히, 팀 생성, 수정, 삭제와 같은 행위는 어드민만 가능합니다.
    • @UseInterceptors(FileInterceptor('file')) 데코레이터는 파일 업로드를 위한 인터셉터를 적용합니다. 클라이언트는 'file'이라는 키 값으로 CSV 파일을 업로드할 수 있으며, 해당 인터셉터는 요청을 가로채어 파일을 추출하고, 추출한 파일을 이후의 핸들러에서 사용할 수 있도록 요청 객체에 첨부합니다. 인터셉터는 AOP 개념을 따르며, 요청을 중간에 가로채어 추가 작업을 수행합니다.

    # Nest JS_show.service.ts 작업

    show.controller.ts 파일 작업이 완료되었다면 show.service.ts 파일로 이동하여 기존에 작성되어 있던 코드는 지우고 service 코드를 다시 작성합니다. 

     

Designed by Tistory.