DAY19

yejichoi·2022년 11월 30일
0
post-thumbnail

1. Algorithm class

2. Backend class

CRUD API 구현

ProductCategory Create

데이터베이스에 저장하고 저장한 데이터를 읽어와서 수정까지 진행
productCategory.module.ts, productCategory.resolver.ts, productCategory.service.ts(핵심로직) 파일 총 3개 필요

// productCategory.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductCategory } from './entities/productCategory.entity';
import { ProductCategoryResolver } from './productCategory.resolver';
import { ProductCategoryService } from './productCategory.service';

@Module({
  imports: [TypeOrmModule.forFeature([ProductCategory])],
  providers: [
    ProductCategoryResolver, //
    ProductCategoryService,
  ],
})
export class ProductCategoryModule {}
  • import 옵션TypeOrmModule.forFeature([ProductCategory])을 추가
    TypeOrmModule.forFeature메서드는 엔티티를 전달.
    만약 import 하지 않으면 typeorm을 이용한 데이터베이스 연동이 안됨
// productCategory.resolver.ts

import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { ProductCategory } from './entities/productCategory.entity';
import { ProductCategoryService } from './productCategory.service';

@Resolver()
export class ProductCategoryResolver {
 constructor(
   private readonly productCategoryService: ProductCategoryService,
 ) {}

 @Mutation(() => ProductCategory)
 createProductCategory(
   @Args('name') name: string, // 이름을 만들겠다는것? 
 ) {
   return this.productCategoryService.create({ name }); //구조분해할당처럼 
 }
}
  • ProductCategoryResolver 클래스에 생성자로 비즈니스 로직을 의존성주입
  • graphql의 @Mutation 을 사용해서 return 받을 graphql의 결과 타입을 알려줌
  • graphql의 @Args 을 사용해서 받고싶은 graphql의 인자 타입을 알려줌
  • 의존성 주입을 해 놓은 비즈니스 로직의 create 메서드를 this.productCategoryService.create() 를 이용하여 연결시켜서 함수를 실행시켜 줌
  • return : 비즈니스 로직으로부터 받은 결과를 프론트 또는 사용자에게 돌려주게 되는 것

실제 핵심 로직들 즉, 비즈니스 로직들이 존재하는 파일 => service.ts

// productCategory.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProductCategory } from './entities/productCategory.entity';

@Injectable()
export class ProductCategoryService {
  constructor(
    @InjectRepository(ProductCategory)
    private readonly productCategoryRepository: Repository<ProductCategory>,
  ) {}

  async create({ name }) {
    const result = await this.productCategoryRepository.save({ name });
    console.log(result); // { name: "전자제품" }

    return result;
  }
}
  • ProductCategoryService 클래스에 생성자에서 Repository를 의존성주입
  • @InjectRepository 를 사용해서 생성자를 주입
  • private으로 생성자를 선언하게 되면 인스턴스 생성이 불가능하게 됨 즉 외부에서 접근이 불가능하게 해줌으로써, 사용한 클래스 내부에서만 수정이 가능하고 외부에서는 수정이 불가능 하여 안전하게 사용가능
  • readonly 를 통해 생성자를 선언하면 선언 당시 또는 생성자에서 초기화된 후 값이 변경되지 X , 따라서 readonly까지 작성하게 되면 클래스 내부에서도 수정이 불가능
  • Repository<ProductCategory> 는 productCategory 테이블과 비즈니스 로직을 연동시켜 주는 역활
  • this.productCategoryRepository.save
    • .create : 데이터 한 줄 등록
    • .save : 데이터를 한 줄 등록하고, 입력한 결과를 찾아오기
    • 우리는 데이터를 한 줄 등록하고 무슨 데이터가 등록되었는지 알려주기 위해 .save 를 사용
  • async ~ await : 등록( 데이터가 저장 )이 잘 이루어졌다고 응답이 올 때 까지 기다려줘야하기에 사용
  • return : productCategory.resolver.tsthis.productCategoryService.save() 로 등록한 데이터 객체를 return 해주게 된 것
// productCategory.entity.ts 

import { Field, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
@ObjectType()
export class ProductCategory {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  id: string;

  @Column({ unique: true })
  @Field(() => String)
  name: string;
}
  • @ObjectType() , @Field(() => String) 추가, 이 데코레이터들은 graphql 에게 무슨 타입인지 알려주는 역활
    @Field(() => String) 부분을 주석처리하면 graphql 에 보이지 않게됨
    - 데코레이터 사용으로 스키마가 자동으로 생성( code-first 방식 )
    - import를 어디서 했는지 확인하면 좀 더 쉽게 이해
  • @Column({ unique: true }) 옵션 값을 추가에 유일한 값으로 지정
    • 유일한 값으로 지정하게 되면, 동일한 이름을 사용할 수 없게 X

각각의 모듈을 최종적으로 app.module.ts 파일로 조립

//app.module.ts

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardModule } from './apis/boards/board.module';
import { ProductCategoriesModule } from './apis/productsCategories/productCategories.module';

@Module({
  imports: [
    BoardModule,
    ProductCategoriesModule,// 모듈 추가 
    ConfigModule.forRoot(),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'src/commons/graphql/schema.gql',
    }),
    TypeOrmModule.forRoot({
      type: process.env.DATABASE_TYPE as 'mysql',
      host: process.env.DATABASE_HOST,
      port: Number(process.env.DATABASE_PORT),
      username: process.env.DATABASE_USERNAME,
      password: process.env.DATABASE_PASSWORD,
      database: process.env.DATABASE_DATABASE,
      entities: [__dirname + '/apis/**/*.entity.*'],
      synchronize: true,
      logging: true,
    }),
  ],
})
export class AppModule {}

Product Create

네트워크 간에 데이터를 어떤 식으로 보낼지를 정의한 객체를 정의하기 위해서 DTO 적용
=> createProduct.input.ts

// createProduct.input.ts

import { Field, InputType, Int } from '@nestjs/graphql';
import { Min } from 'class-validator';

@InputType()
export class CreateProductInput {
  @Field(() => String)
  name: string;

  @Field(() => String)
  description: string;

	@Min(0) //음수 값이 나올수 없기때문에 최소값 설정(Min(0))
  @Field(() => Int)
  price: number;
}
// product.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';

@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product) //프로덕트 entity
    private readonly productRepository: Repository<Product>,
  ) {}

  async create({ createProductInput }) {
			const result = await this.productRepository.save({
      ...createProductInput, // 스프레드연산자 사용하기
              // 코드 간결의 장점 존재 

			// 하나하나 직접 나열하는 방식
      // name: createProductInput.name,
      // description: createProductInput.description,
      // price: createProductInput.price,
    });
		return result
  }
}
  • async ~ await : SQL Query문으로 변환되어 DB로 들어가 저장될 때까지 기다려 줘야하기에 async ~ await를 사용
  • 등록된 상품 객체를 result 변수에 담아서 브라우저에 다시 전달해주기 위해서는 함수가 실행되는 product.resolver.ts 파일로 돌아가야하기에 return을 사용
// product.entity.ts
@Entity()
@ObjectType() //graphql에 보여줌 
export class Product {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)//graphql에 보여줌 
  id: string;

  @Column()
  @Field(() => String)
  name: string;

  @Column()
  @Field(() => String)
  description: string;

  @Column()
  @Field(() => Int)
  price: number;

  @Column({ default: false }) // 고정값 
  @Field(() => Boolean)
  isSoldout: boolean;

  @JoinColumn()
  @OneToOne(() => ProductSaleslocation)
  @Field(() => ProductSaleslocation)
  productSaleslocation: ProductSaleslocation;

  @ManyToOne(() => ProductCategory)
  @Field(() => ProductCategory)
  productCategory: ProductCategory;

  @ManyToOne(() => User)
  @Field(() => User)
  user: User;

  @JoinTable()
  @ManyToMany(() => ProductTag, (productTags) => productTags.products)
  @Field(() => [ProductTag]) //graphql 방식 
  productTags: ProductTag[]; //typescript 방식
}
  • GraphQL을 위한 @ObjectType(), @Field 추가
  • isSoldOut 컬럼은 판매여부를 기록하는 컬럼이기에, 기본 고정 값( default 값 )을 판매가 되지 않은 상태인 false로 지정하기 위해서 @Column({ default: false }) 추가
    • 데이터를 저장할 때 초기값을 false로 자동으로 등록
  • typescript에서는 배열 타입을 ProductTag[]로 작성해주었지만,
    grahql에서는 배열타입을 [ProductTag] 로 작성

fetchProduct (단일 상품조회)

// product.resolver.ts
 
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/createProduct.input';
import { Product } from './entities/product.entity';
import { ProductService } from './product.service';

@Resolver()
export class ProductResolver {
  constructor(private readonly productService: ProductService) {}

  @Query(() => Product) //entity 타입
  fetchProduct(
    @Args('productId') productId: string, //
  ) {
    return this.productService.findOne({ productId }); // 1개 조회
  }

  @Mutation(() => Product)
  createProduct(
    @Args('createProductInput') createProductInput: CreateProductInput,
  ) {
    return this.productService.create({ createProductInput });
  }
}
  • @Query 를 import 할 때에는 '@nestjs/graphql' 에서 import 되어야함
    ‘@nestjs/common’ 에서 import 가 된 것은 아닌지 꼭 확인!
  • 비즈니스 로직에서 한 개 가져오는 findOne 을 사용
// product.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';

@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}

  async findOne({ productId }) {
    return await this.productRepository.findOne({ where: { id: productId } });
  }

  async create({ createProductInput }) {
    const result = await this.productRepository.save({
      ...createProductInput,

      // 하나하나 직접 나열하는 방식
      // name: createProductInput.name,
      // description: createProductInput.description,
      // price: createProductInput.price,
    });
    return result;
  }
}
  • TypeOrm의 findOne 메서드를 사용해서 product 테이블에 매개변수로 받은 productId 에 해당하는 상품을 1개 조회하는 비즈니스 로직을 추가
  • findOne메소드는 조건에 where을 명시해야만 함
    • { where: { id: productId } } : where을 통해 조회하고자 하는 조건
  • async ~ await : findOne 이 이루어질 때까지 기다려 줘야함

fetchProducts (상품 리스트 조회)

// product.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/createProduct.input';
import { Product } from './entities/product.entity';
import { ProductService } from './product.service';

@Resolver()
export class ProductResolver {
 constructor(private readonly productService: ProductService) {}

 @Query(() => [Product]) // 배열 안에 객체의 타입으로 지정 
 fetchProducts() {
   return this.productService.findAll(); // 리스트 조회 
 }

 @Query(() => Product)
 fetchProduct(
   @Args('productId') productId: string, //
 ) {
   return this.productService.findOne({ productId });
 }

 @Mutation(() => Product)
 createProduct(
   @Args('createProductInput') createProductInput: CreateProductInput,
 ) {
   return this.productService.create({ createProductInput });
 }
}
// product.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
//Repository 사용을 위해 @InjectRepository() 추가
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';

@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}

  async findAll() {
    return await this.productRepository.find();
  } //find 메서드를 사용해서 product 테이블의 존재하는 모든 데이터를 조회

  async findOne({ productId }) {
    return await this.productRepository.findOne({ where: { id: productId } });
  }

  async create({ createProductInput }) {
    const result = await this.productRepository.save({
      ...createProductInput,

      // 하나하나 직접 나열하는 방식
      // name: createProductInput.name,
      // description: createProductInput.description,
      // price: createProductInput.price,
    });
    return result;
  }
}

Product Update (수정)

// updateProduct.input.ts

import { InputType, Field, Int } from '@nestjs/graphql';
import { Min } from 'class-validator';

@InputType()
export class UpdateProductInput {
  @Field(() => String, { nullable: true })
  name: string;

  @Field(() => String, { nullable: true })
  description: string;

  @Min(0)
  @Field(() => Int, { nullable: true })
  price: number;
} //아래 방식 추천
  • 모든 데이터의 수정이 일어날 수 있지만, 특정 데이터만 수정을 해야할 때도 존재 => { nullable: true } 를 사용하여 해당 값을 꼭 입력하지 않아도 API이 실행이 가능하게 만들어줌
// updateProduct.input.ts

import { InputType, PartialType, OmitType, PickType } from '@nestjs/graphql';
import { CreateProductInput } from './createProduct.input';

@InputType()
export class UpdateProductInput extends PartialType(CreateProductInput) {}

// PickType(CreateProductInput, ["name", "price"])
// OmitType(CreateProductInput, ["description"])
  • CreateProductInput => UpdateProductInput로 바꿔주는 방법
    번거롭게 다시 한번 나열해서 작성X
  • extends를 사용하여 CreateProductInput을 상속

GraphQL Type

  • PartialType : 모든 컬럼을 선택사항으로 바꿔주는 역할을 하므로, { nullable : true } 와 같은 역할
  • PickType : 원하는 컬럼만을 뽑아서 사용
    • PickType(CreateProductInput, ["name", "price"]) : CreateProductInput의 name, price 컬럼만 사용하겠다는 의미
  • OmitType : 원하는 컬럼만 제거하여 사용하고 싶을 때 사용
    • OmitType(CreateProductInput, ["description"]) : CreateProductInput의 description 컬럼을 제외한 나머지 컬럼들은 모두 사용
// product.resolver.ts
  @Mutation(() => Product)
  async updateProduct(
    @Args('productId') productId: string,
    @Args('updateProductInput') updateProductInput: UpdateProductInput,
  ) {
    return this.productService.update({ productId, updateProductInput });
  }
  • 어떤 상품이 수정이 되었는지 리턴 받아야하기에 Product 를 리턴 타입으로 지정
  • @Args를 사용해 productId 를 받아서 해당 productId 를 가진 product 를 수정 => productid는 수정 조건
  • @Args를 사용해 updateProductInput 를 받아서 수정하고 싶은 값 들을 수정 => updateProductInput은 수정 대상
// product.service.ts
  async update({ productId, updateProductInput }) {
    const myproduct = await this.productRepository.findOne({
      where: { id: productId },
    });

    const newProduct = {
      ...myproduct,
      id: productId,
      ...updateProductInput,
    };

    return await this.productRepository.save(newProduct);
  } // .save 사용 
  • 타입스크립트의 인터페이스를 사용해서 데이터 수정에 필요한 요청값의 타입의 유효성을 검사
  • this.productRepository.findOne : 데이터를 수정하기 위해 DB에서 수정할 데이터를 찾아와야함
    • 내가 수정한 컬럼에 대해서만 조회할때는 findOne을 해주지 않아도 되지만, 수정한 컬럼을 포함한 해당 상품의 다른 컬럼들을 함께 조회할 때는 findOne을 통해서 데이터를 먼저 찾아와 주어야 함
    • 즉, return 받는 값에 수정한 컬럼을 포함한 다른 컬럼들이 존재해야 한다면 findOne을 먼저 해줘야함
  • this.productRepository.save
    • .update : 데이터를 수정할 때 사용
    • .save : 데이터를 수정하고, 수정한 결과를 찾아오기 수정할 때 .save 를 사용하기 위해서는 먼저 데이터를 찾아와야지 사용 가능
  • 데이터를 수정하고 무슨 데이터가 수정 되었는지 프론트에게 알려주기 위해 .save 를 사용
    update({ productId, updateProductInput }){
	this.productRepository.update( //.update 사용 
	{ id: productId  }, // 무엇에 대해서 (조건)
	{ ...updateProductInput } // 무엇을 수정 할 것인지 (대상)
	)
	return "수정이 완료되었습니다!"
}

Product Update With Error

먼저 상품이 판매가 되었는지 확인하는 유효성 검사를 했을 때 판매가 완료되었다면 상품 데이터를 수정할 필요가 없으므로, 이때 에러를 반환하는 방법

// product.resolver.ts
@Resolver()
export class ProductResolver {
  constructor(private readonly productService: ProductService) {}
   @Mutation(() => Product)
  async updateProduct(
    @Args('productId') productId: string,
    @Args('updateProductInput') updateProductInput: UpdateProductInput,
  ) {
    // 판매 완료가 되었는지 확인해보기
    await this.productService.checkSoldout({ productId });

    // 수정하기
    return await this.productService.update({ productId, updateProductInput });
  }
}
// product.service.ts
@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}
  
 async checkSoldout({ productId }) {
    const product = await this.productRepository.findOne({
      where: { id: productId },
    });

    if (product.isSoldout)
      throw new UnprocessableEntityException('이미 판매 완료된 상품입니다.');

    // if (product.isSoldout) {
    //   throw new HttpException(
    //     '이미 판매 완료된 상품입니다.',
    //     HttpStatus.UNPROCESSABLE_ENTITY,
    //   );
    // }
  }
  • productRepository 에서 해당 product 데이터를 찾아온 뒤, 그 상품이 이미 판매가 된 상품이라면 throw new UnprocessableEntityException 을 통해 에러를 만들어서 프론트로 에러메세지를 전달
  • throw new HttpException(에러메세지, 에러상태코드) : 비즈니스 로직을 작성할 때 발생할 일들에 대해 예측할 수 있는 에러들, 기획에 맞지 않는 에러들이 발생하는 경우 처리할 수 없음을 나타내는 에러를 만들때 사용
  • 422 에러상태코드 대신에, 조금 더 보기 쉽게 사용할 수 있도록 NestJS에서 제공해주는 HttpStatus.UNPROCESSABLE_ENTITY 을 사용

❗️상태코드

  • 200 번대 : 성공 상태 코드
  • 400 번대 : 프론트에서 요청을 잘못했을 때 발생하는 에러 상태 코드
  • 500 번대 : 백엔드 서버에서 문제가 생겼을 때 발생하는 에러 상태코드

try ~ catch

예기치 못한 일에 대한 에러 방지

  • try 문 안에서 에러가 발생시, 에러 발생 시점부터 즉시 중단되어 아래 로직들은 실행이 되지않고 바로 catch 문으로 넘어감
  • catch 문 안에서는 console.log 를 통해 어떤 에러가 발생했는지 확인해 볼 수 있으며, throw error 를 통해 프론트로 에러를 반환
// product.service.ts

async create({ createProductInput }: ICreate) {
    try {
      await this.productRepository.save({
        ...createProductInput, // 스프레드연산자 사용하기

      });
      console.log('안녕하세요!!')
      console.log('안녕하세요!!')
      console.log('안녕하세요!!')
    } catch(error) { //에러반환 
      throw error
      console.log(error)
    }
  }

HttpExceptionFilter

try ~ catch 문을 하나하나 작성하지 않아도 모든 에러는 ExceptionFilter 로 자동 들어옴

// http-exception.filter.ts

import { Catch, ExceptionFilter, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException) {
    const status = exception.getStatus();
    const message = exception.message;

    console.log('===========================');
    console.log('예외가 발생했어요!!');
    console.log('예외내용:', message);
    console.log('예외코드:', status);
    console.log('===========================');
  }
}

// main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './commons/filter/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalFilters(new HttpExceptionFilter()); //삽입
  await app.listen(3000);
}
bootstrap();
  • @Catch(HttpException): 에러 관련된 내용이 들어가 있음을 NestJS 에게 알려주기 위한 데코레이터를 사용
  • catch : Exception 상황 발생시 비즈니스 로직에 try ~ catch 문이 없더라도 자동으로 에러가 catch 문으로 들어옴
    • exception: HttpException : HttpException type 으로 정의
  • implements ExceptionFilter : class 에서 타입을 정의 해주기 위해 implements 사용
    • ExceptionFilter : 인터페이스catch 함수가 반드시 존재해야하는 타입 => catch 문을 작성하지 않으면 에러가 발생
  • exception.getStatus() : 에러가 발생하면 나타나는 상태코드를 가져옴
  • exception.message : 에러가 발생시 나타나는 에러 메세지

3. HW

0개의 댓글