Nest.js - 상품 등록, 조회 API

Temporary·2024년 8월 7일
0

Nods.js

목록 보기
26/39

CRUD API 구현

이번에는 TypeormGraphql 사용해 CRU API 구현해보자

이전에는 실제 데이터와 연결만 진행했지만 이번에는 API를 이용해서 데이터베이스에 저장하고 저장한 데이터를 읽어와서 수정까지 진행해보자

Product Create

이번에는 카테고리에 해당하는 상품을 등록해보자

section10 폴더 안에 10-01-mysql-relation 폴더를 복붙하여

사본 폴더의 이름을 10-03-typeorm-crud 폴더로 수정해준다.

10-03-typeorm-crudsrcapisproducts 폴더에

products.module.ts, products.resolver.ts, products.service.ts 파일 총 3개를 먼저 만들어 준다.

products.module.ts 파일 먼저 확인해보자

// products.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entities/product.entity';
import { ProductsResolver } from './products.resolver';
import { ProductsService } from './products.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([ // 엔티티를 전달하는 부분 
      Product, //
    ]),
  ],
  providers: [
    ProductsResolver, // 
    ProductsService,
  ],
})
export class ProductsModule {}
  • import 옵션에 TypeOrmModule.forFeature([Product])을 추가한다. TypeOrmModule.forFeature메서드는 엔티티를 전달한다.
    • 만약 import 하지 않으면 typeorm을 이용해 데이터베이스와 연동이 되지않는다.
  • providers 옵션은 ProductsResolver, ProductsService 을 주입해준다.

Board 만들 때 데이터 전송 객체. 즉, 네트워크 간에 데이터를 어떤 식으로 보낼지를 정의한 객체를 정의하기 위해서 DTO 를 적용했었다.

이번에는 미리 DTO 를 정의해서 Resolver와 Service에 적용하겠다.

10-03-typeorm-crudsrcapisproducts 폴더 안에 dto 폴더를 생성하고

dto 폴더 안에 create-product.input.ts 파일을 만들어준다.

// create-product.input.ts

import { Field, InputType, Int } from '@nestjs/graphql';

@InputType() // 입력받기 때문에 ObjectType이 아님 
export class CreateProductInput {
  @Field(() => String)
  name: string;

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

  @Field(() => Int)
  price: number;
}
  • 입력을 할 때 필요하기에 InputType 을 사용한다.
    • 리턴 받는 ObjectType 을 사용하면 에러가 발생한다.
  • isSoldout의 dafault 값을 false로 설정하는것은 입력값을 설정하는 InputType에서 정의할 수는 있지만,
    판매가 완료되지 않은 기본값 설정을 하는것이므로 entity에서 설정을 해주면 굳이 입력값을 주고 받지 않아도 되므로 더 효율적인 방법이 된다.

이제 다시 돌아와서 products.resolver.ts 을 만들어 보자

// products.resolver.ts

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

@Resolver()
export class ProductsResolver {
  constructor(
    private readonly productsService: ProductsService, // 생성자로 의존성 받아주기
  ) {}

  @Mutation(() => Product)
  createProduct(
    @Args('createProductInput') createProductInput: CreateProductInput, // Args를 통해 전달
  ): Promise<Product> {
    // << 브라우저에 결과 보내는 2가지 방법 >>

    // 1. 저장된 객체 그대로 돌려보내주기 => 프론트엔드 개발자분이 브라우저에 임시저장(캐시) 해놓을 수 있음
    return this.productsService.create({ createProductInput });

    // 2. 결과메시지만 보내주기
    // return '정상적으로 카테고리가 등록되었습니다.';
  }
}
  • 라우트를 핸들링하는 리졸버에도 @Args로 전달받은 데이터를 객체 요소 하나하나의 타입을 createProductInput 을 통해서 유효성 검사를 한다.
  • @Mutation(() => Product) : Mutation의 반환 결과를 Product 타입으로 지정해 주었다.
    • 즉, 작성한 내용들( name, description, price )이 들어간 데이터를 그대로 프론트로 보내주기 위해서 Product 타입을 사용한 것이다.
  • productsService의존성 주입받아 create 함수를 사용할 수 있게 되었다.
  • products.service.ts 파일로부터 전달 받은 상품 객체를 return을 통해 브라우저로 전달된다.

이제 products.service.ts 파일을 만들어 보자.

// products.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import {
  IProductsServiceCreate,
} from './interfaces/products-service.interface';

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

  create({ createProductInput }: IProductsServiceCreate): Promise<Product> {
    const result = this.productsRepository.save({
      ...createProductInput,

      // 하나 하나 직접 나열하는 방식
      //   name: '마우스',
      //   description: '좋은 마우스',
      //   price: 3000,
    });
    return result; 
			// {id: akljdfq-1283aad, 
			//	name: "마우스", 
      //  description: "좋은 마우스", 
      //  price: 3000}
  }
}
  • constructor를 사용하여 Product 엔티티를 비즈니스 로직에 의존성 주입을 했다.
  • 데이터를 저장하는 비즈니스로직의 방법은 두가지가 있다.
      1. 하나씩 나열해서 데이터를 저장하는 방법
      1. 스프레드 문법을 사용해서 객체를 펼쳐서 데이터를 저장하는 방법 ( ✔️ 코드 간결의 장점 )
  • async ~ await : SQL Query문으로 변환되어 DB로 들어가 저장될 때까지 기다려 줘야하기에 async ~ await를 사용해야 한다.
  • 등록된 상품 객체를 result 변수에 담아서 브라우저에 다시 전달해주기 위해서는 함수가 실행되는 products.resolver.ts 파일로 돌아가야하기에 return을 사용했다.

10-03-typeorm-crudsrcapisproductsinterfaces 폴더를 생성 후 products-service.interface.ts 파일을 생성해준다.

// products-service.interface.ts

import { CreateProductInput } from '../dto/create-product.input';

export interface IProductsServiceCreate {
  createProductInput: CreateProductInput;
}

interface를 이용해 createProductInput의 타입을 지정해 준다.

app.module.ts 파일에서 지금까지 만든 ProductModule 을 조립하자

// app.module.ts

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardsModule } from './apis/boards/boards.module';
import { ConfigModule } from '@nestjs/config';
import { ProductsCategoriesModule } from './apis/productsCategories/productsCategories.module';
import { ProductsModule } from './apis/products/products.module';

@Module({
  imports: [
    BoardsModule,
    ProductsModule, // 
    ProductsCategoriesModule,
    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 {}

GraphQL API를 만들고 있기 때문에 product.entity.ts 에 graphql 타입을 만들어 줘야한다.

아래와 같이 graphql 타입을 만들어준다.

// product.entity.ts

import { Field, Int, ObjectType } from '@nestjs/graphql';
import { ProductCategory } from 'src/apis/productsCategories/entities/productCategory.entity';
import { ProductSaleslocation } from 'src/apis/productsSaleslocations/entities/productSaleslocation.entity';
import { ProductTag } from 'src/apis/productsTags/entities/productTag.entity';
import { User } from 'src/apis/users/entities/user.entity';
import {
  Column,
  Entity,
  JoinColumn,
  JoinTable,
  ManyToMany,
  ManyToOne,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
@ObjectType()
export class Product {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  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])
  productTags: ProductTag[];
}
  • GraphQL을 위한 @ObjectType(), @Field 추가한다.
  • isSoldOut 컬럼은 판매여부를 기록하는 컬럼이기 때문에, 기본 고정 값( default 값 )을 판매가 되지 않은 상태인 false로 지정하기 위해서 @Column({ default: false }) 추가한다.
    • 데이터를 저장할 때 초기값을 false로 자동으로 등록하게 해준다.
  • typescript에서는 배열 타입을 ProductTag[]로 작성해주었지만, graphql에서는 배열타입을 [ProductTag] 로 작성한다.


createProduct API 를 요청하게 되면 데이터가 등록이 되고,
등록된 데이터가 Product type 으로 리턴되어서 나타나게 된다는 것을 API를 만들면서 알게 되었다.

여기서 Product 에 대한 graphql type 을 아직 만들어주지 않았기 때문에 code-first 기능을 활용하여 entity 를 만들어 준 것이다.

즉, entity를 따로 만들어 주었기에 API를 실행하면 graphql type 이 Product 로 나오게 되는 것 이다.

하지만, Product entity를 보면 ProductSaleslocation, ProductCategory, User, ProductTag 컬럼이 존재하는데 현재 4가지 entity 모두 graphql type이 없다.

이런 상태로 실행하면 API 를 실행하게 되면 에러가 발생 한다.

따라서, ProductSaleslocation, ProductCategory, User, ProductTag entity에 각각 graphql type인
@ObjectType(), @Field 를 추가해야 정상적으로 createProduct API 를 사용할 수 있다.

모든 entity에 다음과 같이 수정해준다.

productSaleslocation.entity.ts 파일 먼저 graphql type 을 추가해보자

// productSaleslocation.entity.ts

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

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

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

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

  @Column({ type: 'decimal', precision: 9, scale: 6 })
  @Field(() => Float)
  lat: number;

  @Column({ type: 'decimal', precision: 9, scale: 6 })
  @Field(() => Float)
  lng: number;

  @Column()
  @Field(() => Date)
  meetingTime: Date;
}
  • Float type : 실수 타입으로 지정
  • Date type : 날짜 타입을 지정

productTag.entity.ts 파일도 아래와 같이 추가해준다.

// productTag.entity.ts

import { Field, ObjectType } from '@nestjs/graphql';
import { Product } from 'src/apis/products/entities/product.entity';
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';

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

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

  @ManyToMany(() => Product, (products) => products.productTags)
  @Field(() => [Product])
  products: Product[];
}

user.entity.ts 파일도 마찬가지로 추가해준다.

// user.entity.ts

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

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

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

  @Column()
  @Field(() => String)
  password: string;
}

이렇게 entity 까지 모두 작성이 끝났다.

이제 만든 createProduct API 를 확인해 보자

yarn start:dev 를 입력해 서버를 실행한다.

🚨 Cannot determine a GraphQL output type 에러가 발생한다면,
Entity 를 제대로 작성하지 않아서 나타나는 에러다. Entity 타입을 제대로 작성해야 한다.

http://localhost:3000/graphql 에 접속해서 플레이그라운드에서 api 요청해보면

🚨 플레이그라운드에서 만든 API 가 나타나지 않는다면,
@Mutation@Query 데코레이터가 빠지지 않았는지 확인해 주자

playground에서 요청 데이터에 isSoldout을 받지 않았지만 false로 저장된 것을 확인할 수 있다.

이전에 entity에서 @Column({ default: false })으로 지정했기 때문이다.

query가 어떻게 날라갔는지 터미널을 통해 확인해 보면

DBeaver도 확인했을 때 데이터가 잘 저장된 것을 확인할 수 있다.



Product Read (Fetch)

이번에는 데이터를 조회하는 로직을 만들어보자

데이터를 조회하는 fetch 에는 데이터 목록을 가져오는 로직과 데이터 하나씩 가져오는 로직으로 총 2개가 존재한다.

fetchProduct

일단 데이터 하나 하나 가져와서 데이터의 상세보기를 하는 로직을 먼저 만들어 보자

products.resolver.ts 파일을 아래와 같이 작성한다.

// products.resolver.ts

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

@Resolver()
export class ProductsResolver {
  constructor(
    private readonly productsService: ProductsService, //
  ) {}

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

  @Mutation(() => Product)
  createProduct(
    @Args('createProductInput') createProductInput: CreateProductInput,
  ): Promise<Product> {
    return this.productsService.create({ createProductInput });
  }
}

데이터를 조회할때는 query를 사용했었다.

  • @Query를 사용해서 반환값을 product를 지정해서 product 엔티티의 객체가 반환된다.

  • @Query 를 import 할 때에는 '@nestjs/graphql' 에서 import 되어야 한다!!

    ‘@nestjs/common’ 에서 import 가 된 것은 아닌지 꼭 확인해주자

  • 비즈니스 로직에서 한 개 가져오는 findOne 을 사용했다.

products.service.ts 파일을 아래와 같이 작성해준다.

findOne 메서드는 TypeORM을 함께 사용할 때, 데이터베이스에서 특정 조건을 만족하는 단일 엔티티(Entity)를 조회할 때 사용된다.
주로 Repository나 EntityManager를 통해 사용되며, 주어진 조건에 따라 첫 번째로 일치하는 레코드를 반환한다.

// products.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import {
  IProductsServiceCreate,
  IProductsServiceFindOne,
} from './interfaces/products-service.interface';

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

  // 1개씩 fetch 
  findOne({ productId }: IProductsServiceFindOne): Promise<Product> {
    return this.productsRepository.findOne({ where: { id: productId } });
  }

  create({ createProductInput }: IProductsServiceCreate): Promise<Product> {
    const result = this.productsRepository.save({
      ...createProductInput,

      // 하나하나 직접 나열하는 방식
      //   name: '마우스',
      //   description: '좋은 마우스',
      //   price: 3000,
    });
    return result; // {id: akljdfq-1283aad, name: "마우스", description: "좋은 마우스", price: 3000}
  }
}
  • TypeOrm의 findOne 메서드를 사용해서 product 테이블에 매개변수로 받은 productId 에 해당하는 상품을 1개 조회하는 비즈니스 로직을 추가한다.
    • { where: { id: productId } } : where을 통해 조회하고자 하는 조건을 적어주었다.
  • async ~ await : findOne 이 이루어질 때까지 기다려 줘야한다.

products-service.interface.ts 파일을 아래와 같이 작성해준다.

// products-service.interface.ts

import { CreateProductInput } from '../dto/create-product.input';
import { UpdateProductInput } from '../dto/update-product.input';
import { Product } from '../entities/product.entity';

export interface IProductsServiceCreate {
  createProductInput: CreateProductInput;
} 

// 추가
export interface IProductsServiceFindOne {
  productId: string;
} 

$yarn start:dev 를 입력해 서버를 실행시켜 준다

http://localhost:3000/graphql 에 접속해서 플레이그라운들 실행해 api가 잘 작동하는지 확인해준다.

product id 값을 DBeaver에서 복사해서 productId 값에 넣어서 데이터를 조회한다.

해당하는 데이터가 조회된다면 성공이다.



fetchProducts

이번에는 데이터 목록을 조회하는 로직을 만들어 보자

products.resolver.ts 파일을 아래와 같이 작성한다.

// products.resolver.ts

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

@Resolver()
export class ProductsResolver {
  constructor(
    private readonly productsService: ProductsService, //
  ) {}

  @Query(() => [Product])
  fetchProducts(): Promise<Product[]> {
    return this.productsService.findAll();
  }

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

  @Mutation(() => Product)
  createProduct(
    @Args('createProductInput') createProductInput: CreateProductInput,
  ): Promise<Product> {
    return this.productsService.create({ createProductInput });
  }
}
  • @Query를 사용해서 반환값을 [product] 를 적용해서 배열 안에 객체의 타입으로 지정했다.
  • 비즈니스 로직에서 모두 가져오는 findAll 을 사용했다.

products.service.ts 파일을 아래와 같이 작성한다.

// products.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import {
  IProductsServiceCreate,
  IProductsServiceFindOne,
} from './interfaces/products-service.interface';

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

  findAll(): Promise<Product[]> {
    return this.productsRepository.find();
  }

  findOne({ productId }: IProductsServiceFindOne): Promise<Product> {
    return this.productsRepository.findOne({ where: { id: productId } });
  }

  create({ createProductInput }: IProductsServiceCreate): Promise<Product> {
    const result = this.productsRepository.save({
      ...createProductInput,

      // 하나하나 직접 나열하는 방식
      //   name: '마우스',
      //   description: '좋은 마우스',
      //   price: 3000,
    });
    return result; // {id: akljdfq-1283aad, name: "마우스", description: "좋은 마우스", price: 3000}
  }
}
  • TypeOrm의 find 메서드를 사용해서 product 테이블의 존재하는 모든 데이터를 조회하는 비즈니스 로직을 추가했다.
  • async ~ await : find 가 이루어질 때까지 기다려 줘야한다.

http://localhost:3000/graphql 에 접속해서 플레이그라운들 실행시켜주고

다음과 같이 생성된 데이터 모두가 배열 안에 객체 형태로 데이터 목록이 조회되는 것을 확인 할 수 있다.

profile
Temporary Acoount

0개의 댓글