관리 메뉴

공부 기록장 💻

[NestJS/오세유] Recruitment Entity 생성 및 구인글 등록 기능 및 Auth 인증 절차 추가와 User 정보 포함 본문

# Develop/Project

[NestJS/오세유] Recruitment Entity 생성 및 구인글 등록 기능 및 Auth 인증 절차 추가와 User 정보 포함

dream_for 2022. 8. 31. 20:32

NestJS 프레임워크에서 구인글 등록하는 Recruitment Post 기능을 만들어보자.

이전에 작성했던 회원가입 기능 - 새로운 유저 등록 (https://dream-and-develop.tistory.com/197) 부분과 동일한 방식으로 구현을 하였다.

 

또한 지난 시간에 Recruitment 모델을 만든 것을 바탕으로 서비스와 컨트롤러를 작성해보자. (https://dream-and-develop.tistory.com/208)

 

 

 

우선 recruit의 전체 디렉터리의 구조는 다음과 같다.

 


 

 

Recruit Repository

 

우선 recruit.repository.ts 는 다음과 같이 작성해주자.

이전에 User Repository 작성했던 것과 동일하게, typeorm 최신 버전에서 사라진 EntityRepository 대신

새로운 typeorm-ex.decorator과 typeorm-ex.module에서 만든 CustomRepository를 데코레이터로 사용해주자.

 

import { CustomRepository } from "src/db/typeorm-ex.module";
import { Repository } from "typeorm";
import { Recruitment } from "./entity/recruit.entity";

@CustomRepository(Recruitment)
export class RecruitRepository extends Repository<Recruitment>{}

 

 

 

Recruit Service

 

recruit.service.ts 에서는 다음과 같은 함수들을 작성해주었다.

 

createPost() : 신규 구인글 등록
getAllPosts() : 전체 구인글 목록 가져오기
getPostById() : 구인글 객체 ID로 가져오기
deletePost() : 구인글 객체 삭제하기

 

서비스 부분에는 Recruit Repository를 사용하기 위해 생성자 부분에 주입시키고,

다음과 같이 구현을 해주었다.

 

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RecruitDTO } from './dto/recruit.dto';
import { Recruitment } from '../domain/recruit.entity';
import { RecruitRepository } from './recruit.repository';
import { User } from 'src/domain/user.entity';

@Injectable()
export class RecruitService {
    constructor(
        @InjectRepository(RecruitRepository)
        private recruitRepository:RecruitRepository,
    ){}

    // 신규 구인글 등록
    async createPost(recruitDTO: RecruitDTO): Promise<RecruitDTO | undefined>{
        return await this.recruitRepository.save(recruitDTO);
    }

    // 전체 구인글 목록 가져오기
    async getAllPosts():Promise<Recruitment[]>{
        return this.recruitRepository.find();
    }
 
    // ID로 구인글 객체 가져오기
    async getPostById(post_id): Promise<Recruitment>{
        const found = await this.recruitRepository.findOne({where: {post_id:post_id}});

        if(!found){
            throw new NotFoundException(`해당 게시글을 찾을 수 없습니다.`)
        }
        return found;
    }

    // 해당 ID 구인글 삭제
    async deletePost(post_id, user) : Promise<void> {
        const result = await this.recruitRepository.delete({post_id, user});

        if(result.affected === 0){
            throw new NotFoundException(`ID(${post_id})의 게시글을 찾을 수 없습니다.`);
        }
    }
}

 

 

Recruit Controller

 

API endpoints 를 바탕으로 controller 부분을 작성해보자.

 

우선 수정 부분은 제외하고,

구인글 전체 목록, 구인글 등록, 구인글 상세보기, 구인글 삭제 부분을 구현해보자.

각각 위의 Recruit Service 에서 작성한 createPost(), getAllPosts(), getPostById(), deletePost() 함수를 호출하여

Post, Get, Delete 요청을 하게 된다.

 

import { Controller, Param, Body, Post, Res, Req, Get, Delete, UseGuards  } from '@nestjs/common';
import { RecruitDTO } from './dto/recruit.dto';
import { RecruitService } from './recruit.service';
import { Request } from 'express';
import { AuthGuard } from '@nestjs/passport';
import { Recruitment } from '../domain/recruit.entity';
import { GetUser } from 'src/auth/get-user.decorator';
import { Logger } from '@nestjs/common/services';
import { User } from 'src/domain/user.entity';
import { Response } from 'express';

@Controller('recruit')
@UseGuards(AuthGuard())
export class RecruitController {
    private logger = new Logger('Recruitment');
    constructor(private recruitService:RecruitService){}

    // 새로운 구인글 등록
    @Post('/new')
    async newPost(
        @Req() req:Request, @Body() recruitDTO: RecruitDTO): Promise<RecruitDTO>{
            return await this.recruitService.createPost(recruitDTO);
        }
    
    // 모든 구인글 목록 가져오기
    @Get('/recruitment-lists')
    async getAllPosts(): Promise<Recruitment[]>{
        return this.recruitService.getAllPosts();
       
    }

    // ID로 구인글 가져오기
    @Get('/:post_id')
    getPostById(@Param('post_id') post_id:number) : Promise<Recruitment>{
        return this.recruitService.getPostById(post_id);
    }

    // ID로 구인글 삭제하기
    @Delete('/:post_id')
    deleteBoard(@Param('post_id') post_id:number,
    @GetUser() user: User
    ): Promise<void>{
        this.logger.verbose(`유저 ${user.name}이 ID(${post_id})를 삭제합니다.`);
        return this.recruitService.deletePost(post_id, user);
    }
}

 

 


인증 권한 부여

 

이제는 인증된 유저만 게시물을 등록하고 조회할 수 있도록 만들어보자.

 

 

Recruit Module

 

우선 Recruit Module 에서 Auth Module의 Auth Guard를 사용할 수 있도록 

imports 부분에 AuthModule을 추가해주자.

 

// recruit/recruit.module.ts

@Module({
  imports: [
  	/* */
    AuthModule
  ],
  
  /* */
})

 

 

Recruit Controller

 

다음은 recruit.controller.ts에 AuthGuard()를 controller-level로 추가해주자.

이전에 만들어둔 AuthGuard() 를 UseGuards 데코레이터에 주입함으로써

게시물 생성 및 조회, 삭제 시 인증된 유저만 가능하도록 설정하도록 한다.

 

import { UseGuards } from '@nestjs/common';

@Controller('recruit')
@UseGuards(AuthGuard())
export class RecruitController {
	/* */
    
 }

 

 


 

게시물 생성 시 유저 정보 포함하기

 

이전에 User과 Recruit 의 One-To-Many Relations 관계를 엔티티 내부에 포함시켰으므로,

이제 게시물 생성 시 유저 정보를 게시물에 포함시키는 기능을 만들어보자.

 

게시물 생성 요청을 하게 되면, 헤더 안에 있는 토큰으로 유저 정보를 얻고,

유저 정보와 게시물 관계를 형성하여 게시물을 생성하게 되는 구조이다.

 

 

 

Get-User Decorator

 

우선 auth/get-user.decorator.ts 파일을 생성하고 다음과 같이 작성하자.

User의 정보를 Parameter로 전달할 수 있도록 새로운 Decorator을 만드는 과정이다.

 

import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { User } from "./entity/user.entity";

export const GetUser = createParamDecorator((data, ctx: ExecutionContext): User => {
    const req = ctx.switchToHttp().getRequest();
    return req.user;
})

 

 

 

Recruit Repository

 

이제 새로운 구인글 등록을 할 때, User 의 정보를 추가하기 위해

RecruitRepository에 createPost 함수를 새로 만들어 구조를 이전과 다르게 변경해보자.

 

createPost 함수의 인수에는 RecruitDTO와 더불어, User 객체를 포함시킨다.

 

기존 recruitDTO에는 Recruitment 에 필요한 칼럼들을 모두 포함시키고,

post 객체를 만들어, 마지막에 user을 포함하여 이를 저장하고 리턴하도록 하자.

 

import { User } from "src/auth/entity/user.entity";
import { CustomRepository } from "src/db/typeorm-ex.module";
import { Repository } from "typeorm";
import { RecruitDTO } from "./dto/recruit.dto";
import { Recruitment } from "./entity/recruit.entity";

@CustomRepository(Recruitment)
export class RecruitRepository extends Repository<Recruitment>{
    async createPost(recruitDTO:RecruitDTO, user:User) : Promise<Recruitment>{
        const {
            work_name,
            image,
            address,
            detailed_address,
            district,
            start_date,
            end_date,
            start_time,
            end_time,
            days_of_work,
            num_of_people,
            daily_wage,
            recommended_lodging,
            meals_offered,
            trans_offered,
            contents,
            tags,
        } = recruitDTO;

        const post = this.create({
            work_name,
            image,
            address,
            detailed_address,
            district,
            start_date,
            end_date,
            start_time,
            end_time,
            days_of_work,
            num_of_people,
            daily_wage,
            recommended_lodging,
            meals_offered,
            trans_offered,
            contents,
            tags,
            user
        })

    await this.save(post);
    return post;
    }
}

 

 

Recruit Service

 

Recruit Service에서는 생성자 constructor 부분에 RecruitRepository를 주입시켜준 후,

createPost 함수에 인수와 리턴 값에 user을 추가해주자.

 

import { User } from 'src/auth/entity/user.entity';
import { UserRepository } from 'src/auth/user.repository';

/* */


export class RecruitService {
    constructor(
        @InjectRepository(RecruitRepository)
        private recruitRepository:RecruitRepository
    ){}export class RecruitService {

	/* */
    
    // 신규 구인글 등록
    async createPost(recruitDTO: RecruitDTO, user:User): Promise<RecruitDTO | undefined>{
        return await this.recruitRepository.createPost(recruitDTO, user);
    }


		/* */
        
 }

 

 

 

Recruit Controller

 

recruit.controller.ts 파일의 새로운 구인글을 등록하는 newPost 함수의 인수에

위에서 만든 @GetUser() 데코레이터를 포함시켜 유저 객체를 가져오도록 하고,

객체를 리턴할 때, recruitDTO와 함께 user을 함께 반환하도록 하자.

 

/* */

import { User } from 'src/auth/entity/user.entity';
import { GetUser } from 'src/auth/get-user.decorator';

export class RecruitController {
		/* */
        

    // 새로운 구인글 등록
    @Post('/new')
    async newPost(
        @Req() req:Request, @Body() recruitDTO: RecruitDTO, @GetUser() user:User): Promise<RecruitDTO>{
            return await this.recruitService.createPost(recruitDTO, user);
        }
        
       /* */
 }

 

 

 


 

Postman을 이용한 API 검증

 

postman을 이용해 각각의 컨트롤러에서 작성한 API 검증을 해보도록 하자.

 

 

 

signup

 

Body 에 user_id, password, name, phone, image 칼럼에 각각의 데이터를 추가하여 json 형태로 POST 요청 전송을 한다.

 

 

 

pk인 id 값이 새로 부여되었고, user_created 에 날짜 데이터가 추가되어 Response 값으로 유저 객체가 전달됨을 확인할 수 있다. 

 

 

 

login

 

user_id와 password를 이용해 로그인을 해보자.

알맞지 않은 password인 경우 로그인 실패를 하게 되고,

올바르게 로그인한 경우 accessToken이 발급된다.

 

 

 

mypage

 

발급받은 accessToken으로 마이페이지에 GET 요청을 보내보자.

 

 

recruits 배열을 칼럼에 포함한, 현재 로그인된 user 객체가 Response로 반환됨을 확인할 수 있다.

 

 

 

logout

 

accessToken으로 로그아웃 POST 요청을 하면, OK 메세지와 함께 로그아웃 완료가 된다.

 

 

 

Decoding issued JWT Token

 

발급받은 accessToken을 https://jwt.io/ 을 이용해 디코딩해보자.

header, payload, signature 부분에 토큰을 디코딩한 결과가 각각 나타나는 것을 확인할 수 있다.

 

 

 

 


 

Recruitment Test

이제 Recruit 모듈에 대한 API를 검증해보도록 하자.

 

 

구인글 등록 new

 

먼저 새로운 구인글을 등록하는 new API를 검증해보자.

첫번째 케이스는, district가 enum에 포함되지 않은 상수를 사용하는 경우이다.

"충청남도" , "충청북도"는 포함되어 있지만, 다음과 같이 "충청도"를 입력하는 경우 문제가 발생한다.

 

Data truncated for column 'district' at row 1 이라는 error message가 뜬다.

mysql db에 올바르지 못한 데이터 타입으로 인해 query가 failed 한 것이다.

 

 

두번째 케이스는, boolean type 을 잘못 지정하는 경우이다.

이번에는 district value로 "충청남도"를 사용하여 올바르게 타입을 매칭시키고,

trans_offered에 0, 1 이 아닌 string 값 "False" value를 지정했을때 발생하는 문제를 확인해보자.

 

 

다음과 같이 Incorrect integer value: 'False' for column 'trans_offrered' at row 1 라는 메세지가 전달되며

친절하게 데이터 타입이 잘못되었음을 알려주는 QueryFailedError 가 발생한다.

typeORM에서 boolean 타입의 값이 nestJS에서 mysql로 전달될 때, tiny integer value로 매핑되므로 0, 1의 값이 지정되어야 한다.

 

 

 

이번에는 trans_offered에 0이라는 올바른 integer value를 지정하여 전송해보자.

별다른 에러 발생 없이 올바르게 POST 요청이 전송되어, response로 recruitment 객체를 전달한 것을 확인할 수 있다.

 

 

 

mysql 서버에서도 객체가 잘 저장되었는지 확인해보자.

마지막 칼럼에 userId 가 추가되어, 유저 정보가 같이 저장되었음을 확인할 수 있다.

 

 

 

recruitment-lists

 

GET 요청을 보내 등록된 전체 구인글을 확인해보자.

위에서 저장한 post_id 1번의 객체가 나타남을 확인할 수 있다.

(recruitment-lists부분에서 user 정보까지 추가되어야 하므로, 수정이 필요하다.)

 

 

 

 

Get by ID - ID값으로 recruitment 객체 조회

 

recruit/1 에 GET 요청을 보내보자.

post_id 값이 1인 구인글 객체가 다음과 같이 나타남을 확인할 수 있다.

 

 

구인글 삭제 - Delete

 

recruit/1 로 DELETE 요청을 보내보자.

local 서버 터미널 창에 다음과 같이 로그가 나타난다.

어떤 user가 어떤 post_id 값의 구인글을 삭제했는지 확인할 수 있다.

 

 

 

 

 

MyPage, Profile

 

마이페이지와 프로필에 GET 요청을 보내보자.

mypage는 user 객체 전체를 전달하고, 새로 추가한 profile에서는 user의 user_id, name, image, phone 칼럼 부분만 전달한다.

mypage에서 해당 user가 생성한 recruit 객체들을 담은 recruits 배열을 포함하여 DTO에 포함된 전체 데이터를 반환함을 확인할 수 있다.

 

 

 

 

 


 

[참고자료]

https://www.youtube.com/watch?v=3JminDpCJNE 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형
Comments