관리 메뉴

공부 기록장 💻

[NestJS/오세유] User, Recruitment Entity / Relations 생성 본문

# Develop/Project

[NestJS/오세유] User, Recruitment Entity / Relations 생성

dream_for 2022. 8. 30. 13:43


오세유 프로젝트의 User, Recruitment 테이블 Entity를 생성하고,
두 테이블 간 Relations 을 정의하는 부분까지 NestJS 프레임워크를 이용해 구현해보자.

ER Diagram


먼저 전체적인 ER Diagram을 살펴보자.
테이블은 User, Recruitment, Application, Bookmark, Payment 크게 5개이다.
현재까지는 완성된 ER 설계본은 아니며, 추후에 개발을 하며 hash태그 기능을 비롯한 기능들을 개발, 구현하며
필요에 따라 테이블은 추가될 수 있다.



User Table


먼저, 사용자 User 유저 엔티티에 포함되어야 할 칼럼들은 다음과 같다.
primary key로 사용될 id, 사용자 ID, 비밀번호, 휴대폰번호, 프로필 사진, created/updated 날짜를 포함한다.

id (int, PK)
user_id (char)
password (char)
phone (char)
image (blob)
user_created (datetime)
user_updated (datetime)




Recruitment Table

구인글 테이블 , Recruitment 엔티티는 다음과 같다.
사용자가 다수의 구인글을 올릴 수 있기 때문에, User Table 과 One-To-Many (1:N) 관계를 맺는 테이블이다.

post_id (int, PK)
user_id (FK - User tbl)
work_name (string)
image (file)
address (string)
detailed_address (string)
district (enum)
start_date (date)
end_date (date)
start_time (int)
end_time (int)
days_of_work (int)
num_of_people (int)
daily_wage (int)
lodging_offered (boolean)
recommended_lodging (string)
meals_offered (boolean)
trans_offered (boolean)
contents (string)
tags (string)
is_closed (boolean)




Nest JS 상에서 테이블을 정의하기 위해 앞으로 더 공부해보아야 할 내용은 다음과 같다.
(+ 회의 이후 피드백 수정 완료)

더보기

1. User Table의 password_validated 칼럼

- JWT 를 이용해 인증 기능을 구현할 것이지만, 비밀번호 확인을 하는 부분에 대해 더 고민해봐야 한다.

-> Front 에서 비밀번호 확인 절차 처리 가능

2. User, Recruitment Table의 image file

- NodeJS 는 multer라는 middleware을 이용해 이미지 파일을 관리한다. 공부가 더 필요한 부분이다.

3. Recruitment Table의 start_date, end_date 칼럼

- date 형식이지만, yyyy-mm-dd 형식으로 저장하기 위한 방법을 고려해야 한다.

- front 부분에서 어떻게 년-월-일을 저장하여 데이터를 전송하는지에 대해서도 상의해야 한다.

-> Front에서 개발 진행하며 상의 예정 (현재는 Date 형식으로 지정 완료 및 api 검증 완료 상태)

4. Recruitment Table의 start_time, end_time 칼럼

- 이 부분도 마찬가지로, 형식을 어떻게 지정할 것인지 정확히 지정해야 한다. (00~23 방식으로 할 것인가?)

-> int 형식으로 저장

5. Recruitment Table의 recommended_lodging 칼럼

- lodging_offered 칼럼이 true인 경우는 유저가 직접 recommended_lodging에 매칭해준 숙박시설을 입력하는 것이고, false인 경우에는 시스템 상에서 자동으로 인근 숙박 시설을 추천해줘야 한다. 이 기능에 대한 로직을 구현할 필요가 있다.

-> 1차 테스트 및 배포 완료 후 추가 예정

6. Recruitment Table의 tags 칼럼

- NestJS에서 해시태그를 어떻게 구현할 것인지, 타입은 어떻게 지정할 것인지 고민해 봐야 한다.

- 프론트에서 전체 string을 가져와서 '#' 를 기준으로 각 문자열을 분할하는 정규화 표현 방법도 있을 것이고, 프론트 단에서 이를 미리 해결하여 array 형식으로 데이터를 백에 전송하는 방법 등 다양한 방법이 있을 것이므로 상의가 필요하다.

-> string 데이터 주고 받는 것으로 결정 완료


User API 구현


User와 관련된 회원가입, 로그인, 로그아웃, 마이페이지에 대한 API endpoints는 다음과 같다.

mypage 의 front단 프로토타입이 아직 완성되지 않았고, 더 상의해야할 부분이 많아
api endpoints 완성본은 아니지만 현재까지 설계된 http method 와 url pattern, 전송할 데이터 body부분을 참고하여
설계를 해보도록 하자.



우선 현재 프로젝트 폴더는 다음과 같다.



User Entity


auth/user.entity.ts 파일을 생성하고 User Entity를 다음과 같이 설계하자.

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import * as bcrypt from 'bcrypt';


@Entity('user')
export class User{
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    user_id: string;

    @Column()
    password: string;

    @Column()
    name: string;

    @Column()
    phone: string;

    @Column('text', {nullable:true, default: true})
    image: string;

    @Column({type:"timestamp", nullable:true, default:null})
    user_created: Date;

    @Column({type:"timestamp", nullable:true, default:null})
    user_updated: Date;


    async validatePassword(password: string): Promise<boolean>{
        let isValid = await bcrypt.compare(password, this.password)
        return isValid;
    }
}


User DTO


auth/dto/user.dto.ts 파일을 생성하고 User의 DTO는 다음과 같이 설계하자.

export class UserDTO{
    user_id: string;
    password: string;
    name: string;
    phone: string;
    image: string;
    user_created: Date;
    user_updated: Date;
}



위와 같이 User Entity, DTO를 정의하였고, 나머지 부분들은 이전에 Auth 관련하여 공부했던 부분과 동일하게 작성을 하였다.


이전에 공부하며 작성했던 Nest JS Auth 글들
회원가입 기능 구현: https://dream-and-develop.tistory.com/197?category=1044686
로그인 기능 구현: https://dream-and-develop.tistory.com/199?category=1044686
JWT 토큰 인증 기능 추가: https://dream-and-develop.tistory.com/200?category=1044686




새로 설계한 User Entity에 따라, 변화된 부분들을 조금 살펴보고 넘어가자.


Payload Interface


JWT에서 payload에 들어갈 claim들은 다음과 같이 설정하였다.

// src/auth/security/payload.interface.ts

export interface Payload{
    id: number;
    user_id: string;
    name: string;
    phone: string;
}


Auth Service


AuthService 부분에서 살펴볼 부분은 registerUser 함수에서
새로운 User 객체 newUser의 user_created 칼럼에 현재 date 데이터를 추가하는 것이다.

로그인 함수인 validateSuer 함수의 payload 부분에도 claim 필드들을 변경해주었다.
유저의 토큰을 검증하는 tokenValidateUser 함수에서는 payload의 user_id를 검증하도록 변경하였다.

// src/auth/auth.service.ts

import { HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { UserDTO } from './dto/user.dto';
import { UserService } from './user.service';
import * as bcrypt from 'bcrypt';
import { Payload } from './security/payload.interface';
import { JwtService } from '@nestjs/jwt';
import { User } from './entity/user.entity';

@Injectable()
export class AuthService {
    constructor(
        private userService:UserService,
        private jwtService: JwtService
    ){}

    // 새로운 유저 등록
    async registerUser(newUser:UserDTO): Promise<UserDTO>{
        // 이미 등록되어 있는 이메일이 있는지 탐색
        let userFind: UserDTO = await await this.userService.findByFields({
            where: {user_id: newUser.user_id}
        });
        // 탐색되었다면, Bad Request 오류 처리 (HTTP상태 코드 400)
        if(userFind){
            throw new HttpException('이미 존재하는 유저 아이디입니다.', HttpStatus.BAD_REQUEST)
        }
        newUser.user_created = new Date();
        // 새로운 유저 정보 저장
        return await this.userService.save(newUser);
    }

    // 로그인
    async validateUser(userDTO: UserDTO): Promise<{accessToken: string} | undefined>{
        let userFind: User = await this.userService.findByFields({
            where: {user_id: userDTO.user_id}
        })
        const validatePassword = await bcrypt.compare(userDTO.password, userFind.password);

        // 사용자를 찾지 못한 경우와 비밀번호가 올바르지 않은 경우
        if(!userFind || !validatePassword ){
            throw new UnauthorizedException('로그인 실패');
        }

        const payload: Payload = { id: userFind.id, user_id: userFind.user_id, name: userFind.name, phone: userFind.phone};
        return {
            accessToken: this.jwtService.sign(payload)
        };
    }

    // 유저의 토큰 검증
    async tokenValidateUser(payload: Payload): Promise<User | undefined>{
        return await this.userService.findByFields({
            where: { user_id: payload.user_id }
        })
    }
}



Postman과 mysql Server 확인을 통한 API 검증

(위의 Entity, DTO 부분의 image 칼럼은 추후에 추가하였기 때문에 아래 API 검증에서는 나타나지 않는 칼럼이다.)



먼저 회원가입 부분을 살펴보자.
DTO에 사용되는 칼럼들인 user_id, password, name, phone 각각에 맞는 데이터를 Body에 json형태로 POST 요청을 보냈고,
올바르게 resonse 되는 것을 확인할 수 있다.
(이때, user_created 부분을 중간에 구현하였기 때문에 초반 몇 개 데이터에는 null 데이터가 들어가있다.)



mysql 에서 query문을 이용해 user 테이블을 확인해보니, 칼럼들이 잘 나타나있음을 확인할 수 있다.


Recruitment 모듈


이제 Recruitment 객체를 다루기 위해 위해 Recruitment 모듈과 컨트롤러, 서비스를 만들자.
모듈의 이름은 recruit 으로 설정하였다.

$ nest g module recruit
$ nest g controller recruit --no-spec
$ nest g service recruit --no-spec



디렉터리 구조는 다음과 같다.
dto 폴더, entity 폴더를 만들고 recruit.dto.ts, recruit.entity.ts 도 같이 추가하자.



Recruitment Entity


Recruitment Entity는 다음과 같다.
지역구 district 칼럼에 사용될 enum 객체 이름은 District로 설정하였다.

import { Column, DataSource, Entity, ManyToOne, PrimaryGeneratedColumn, Timestamp } from "typeorm";

export enum Distirct{
    SEOUL = '서울',
    BUSAN = '부산',
    DAEGU = '대구',
    INCHEON = '인천',
    GWANGJU = '광주',
    DAEJEON = '대전',
    ULSAN = '울산',
    SEJONG = '세종',
    GYEONGGI = '경기',
    GANGWON = '강원',
    CHUNGBUK = '충청북도',
    CHUNGNAM = '충청남도',
    JEONNAM = '전라남도',
    JEONBUK = '전라북도',
    GYEONGBUK = '경상북도',
    GYEONGNAM = '경상남도',
    JEJU = '제주'
}

@Entity('recruitment')
export class Recruitment{
    @PrimaryGeneratedColumn()
    post_id: number;

    @Column()
    work_name: string;

    @Column()
    image: string;

    @Column()
    address: string;

    @Column()
    detailed_address: string;

    @Column({
        type: 'enum',
        enum: Distirct,
        default: null
    })
    district: Distirct;
    
    @Column()
    start_date: string;

    @Column()
    end_date: string;

    @Column()
    start_time: number;

    @Column()
    end_time: number;

    @Column()
    days_of_work: number;

    @Column()
    num_of_people: number;

    @Column()
    daily_wage: number;

    @Column({default:false})
    lodging_offered: boolean;

    @Column({nullable:true})
    recommended_lodging: string;

    @Column({default:false})
    meals_offered: boolean;

    @Column({default:false})
    trans_offered: boolean;

    @Column("text")
    contents: string;

    @Column()
    tags: string;

    @Column({default: false})
    is_closed: boolean;
}


Recruit DTO


DTO는 다음과 같이 설계해주었다.

import { Distirct } from "../entity/recruit.entity";

export class RecruitDTO{
    work_name: string;
    image: string;
    address: string;
    detailed_address: string;
    district: Distirct;
    start_date: string;
    end_date: string;
    start_time: number;
    end_time: number;
    days_of_work: number;
    num_of_people: number;
    daily_wage: number;
    recommended_lodging: string;
    meals_offered: boolean;
    trans_offered: boolean;
    contents: string;
    tags: string;
}


Relations: Table 간 Relations 정의 (One-to-many, Many-to-one)



이제 User과 Recruitment 엔티티 간 Relations 관계 정의를 해주자.
사용자 한 명이 여러 개의 구인글을 등록할 수 있고, 각 구인글에 대해서는 작성자 유저가 한 명만 존재한다.
따라서 User:Recruit 의 관계는 1:N (One-to-Many이다.)

다음의 공식 문서를 참고하였다.
https://typeorm.io/many-to-one-one-to-many-relations



User Entity


먼저 User Entity 부분에 다음과 같이 @OneToMany 데코레이터를 사용하여 다음과 같이 정의해준다.
recruits가 Recruitment Entity 객체들을 담을 배열이 된다.

import { Recruitment } from "src/recruit/entity/recruit.entity";
import { Column, Entity, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";

@Entity('user')
export class User{
    @PrimaryGeneratedColumn()
    id: number;

	/*
    	*/

    @OneToMany(type => Recruitment, recruitment => recruitment.user)
    recruits: Recruitment[];
}


Recruitment Entity


@ManyToOne 데코레이터를 사용하여 Recruitment Entity는 다음과 같이 수정하자.

@Entity('recruitment')
export class Recruitment{
    @PrimaryGeneratedColumn()
    post_id: number;

	/*
    	*/

    @ManyToOne(() => User, (user) => user.recruits)
    user: User
}



MySQL DB 서버에서 테이블 간 관계 확인



이제 mysql DB에도 ORM이 적용되어 User, Recruitment Entity가 테이블로 잘 매핑되었는지 확인해보자.
recruitment 테이블과 각 칼럼들이 잘 생성되었음을 확인해보자.





MySQL Reverse Enginner을 이용한 ER Diagram 추출


이번에는 MysQL에서 제공하는 Reverse Engineer 기능을 통해
ER 다이어그램을 추출하여 테이블 간 Relations가 잘 정의되었는지 확인해보자.



아래와 같이 user 테이블과 recruitment 테이블의 1:M 관계가 ER 다이어그램으로도 잘 나타난다.
recruitment 테이블의 userID가 foreign key로 등록된 것 또한 확인할 수 있다.


class-validator 적용 및 일부 칼럼 수정

import { IsDate, IsString } from "@nestjs/class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Recruitment } from "src/domain/recruit.entity";
import * as bcrypt from 'bcrypt';


@Entity('user')
export class User{
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    @IsString()
    user_id: string;

    @Column()
    password: string;

    @Column()
    @IsString()
    name: string;

    @Column()
    phone: string;

    @Column('text', {nullable:true, default:null})
    @IsString()
    image: string;

    @Column({type:"timestamp", nullable:true, default:null})
    @IsDate()
    user_created: Date;

    @Column({type:"timestamp", nullable:true, default:null})
    @IsDate()
    user_updated: Date;

    @OneToMany(type => Recruitment, recruitment => recruitment.user, { eager: true})
    recruits: Recruitment[];


    async validatePassword(password: string): Promise<boolean>{
        let isValid = await bcrypt.compare(password, this.password)
        return isValid;
    }
}
import { User } from "src/auth/entity/user.entity";
import { Column, DataSource, Entity, ManyToOne, PrimaryGeneratedColumn, Timestamp } from "typeorm";
import { IsBoolean, IsDate, IsDateString, IsInt, IsString, validate } from '@nestjs/class-validator';

export enum Distirct{
    SEOUL = '서울',
    BUSAN = '부산',
    DAEGU = '대구',
    INCHEON = '인천',
    GWANGJU = '광주',
    DAEJEON = '대전',
    ULSAN = '울산',
    SEJONG = '세종',
    GYEONGGI = '경기',
    GANGWON = '강원',
    CHUNGBUK = '충청북도',
    CHUNGNAM = '충청남도',
    JEONNAM = '전라남도',
    JEONBUK = '전라북도',
    GYEONGBUK = '경상북도',
    GYEONGNAM = '경상남도',
    JEJU = '제주'
}

@Entity('recruitment')
export class Recruitment{
    @PrimaryGeneratedColumn()
    post_id: number;

    @Column()
    @IsString()
    work_name: string;

    @Column()
    @IsString()
    image: string;

    @Column()
    @IsString()
    address: string;

    @Column()
    @IsString()
    detailed_address: string;

    @Column({
        type: 'enum',
        enum: Distirct,
        default: null
    })
    district: Distirct;
    
    @Column({type:'timestamp', nullable:true})
    @IsDate()
    start_date: Date;

    @Column({type:'timestamp', nullable:true})
    @IsDate()
    end_date: Date;

    @Column('smallint')
    @IsInt()
    start_time: number;

    @Column('smallint')
    @IsInt()
    end_time: number;

    @Column('smallint')
    @IsInt()
    days_of_work: number;

    @Column('smallint')
    @IsInt()
    num_of_people: number;

    @Column('int')
    daily_wage: number;

    @Column('bool', {default:false})
    @IsBoolean()
    lodging_offered: boolean;

    @Column({nullable:true})
    @IsString()
    recommended_lodging: string;

    @Column('bool', {default:false})
    @IsBoolean()
    meals_offered: boolean;

    @Column('bool', {default:false})
    @IsBoolean()
    trans_offered: boolean;

    @Column('text')
    @IsString()
    contents: string;

    @Column('text')
    @IsString()
    tags: string;

    @Column('bool', {default: false})
    @IsBoolean()
    is_closed: boolean;

    @ManyToOne(() => User, (user) => user.recruits)
    user: User
}


File Upload MiddleWare : Multer

NodeJS에서는 Multuer라는 MiddleWare을 사용하여 파일 이미지를 업로드 할 수 있다.
따라서 중간에 User, Recruitment 모델에 image 칼럼을 string 타입으로 지정하여 추가해주었다.


https://docs.nestjs.com/techniques/file-upload
https://velog.io/@fj2008/NestJS-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC%EC%97%85%EB%A1%9C%EB%93%9C




[참고자료]

https://typeorm.io/many-to-one-one-to-many-relations
https://typeorm.io/decorator-reference#createdatecolumn
https://stackoverflow.com/questions/62696628/how-can-i-create-columns-with-type-date-and-type-datetime-in-nestjs-with-typeorm



728x90
반응형
Comments