├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.module.ts ├── auth │ ├── auth.module.ts │ ├── auth.service.ts │ ├── constants.ts │ ├── guards │ │ ├── jwt-auth.guard.ts │ │ └── local-auth.guard.ts │ └── strategies │ │ ├── auth.jwt.strategy.ts │ │ └── auth.local.strategy.ts ├── configs │ └── typeorm.config.ts ├── entity │ ├── board.entity.ts │ ├── category.entity.ts │ ├── company_infomation.entity.ts │ ├── profile.entity.ts │ └── user.entity.ts ├── main.ts ├── middleware │ └── auth.middleware.ts ├── repository │ └── user.repository.ts ├── user │ ├── dto │ │ ├── sign_in.dto.ts │ │ └── user.dto.ts │ ├── user.controller.ts │ ├── user.module.ts │ └── user.service.ts └── utils │ ├── http-exception.filter.ts │ ├── multer.options.ts │ └── swagger.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | #File 38 | uploads -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nest.js RESTFul API 서버 2 | 3 | ```sh 4 | 5 | - 프로젝트 설치 및 실행 6 | 7 | Node.js 필수 설치 8 | 9 | 1. npm i : 모듈 설치 10 | 2. npm run start:dev : 서버 실행 11 | 12 | ``` 13 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-nest", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.0.0", 25 | "@nestjs/core": "^8.0.0", 26 | "@nestjs/jwt": "^8.0.0", 27 | "@nestjs/mapped-types": "^1.0.0", 28 | "@nestjs/passport": "^8.0.1", 29 | "@nestjs/platform-express": "^8.0.0", 30 | "@nestjs/swagger": "^5.1.5", 31 | "@nestjs/typeorm": "^8.0.2", 32 | "class-transformer": "^0.4.0", 33 | "class-validator": "^0.13.1", 34 | "mysql2": "^2.3.3-rc.0", 35 | "passport": "^0.5.0", 36 | "passport-jwt": "^4.0.0", 37 | "passport-local": "^1.0.0", 38 | "reflect-metadata": "^0.1.13", 39 | "rimraf": "^3.0.2", 40 | "rxjs": "^7.2.0", 41 | "swagger-ui-express": "^4.3.0", 42 | "typeorm": "^0.2.40" 43 | }, 44 | "devDependencies": { 45 | "@nestjs/cli": "^8.0.0", 46 | "@nestjs/schematics": "^8.0.0", 47 | "@nestjs/testing": "^8.0.0", 48 | "@types/express": "^4.17.13", 49 | "@types/jest": "^27.0.1", 50 | "@types/node": "^16.0.0", 51 | "@types/passport-jwt": "^3.0.6", 52 | "@types/passport-local": "^1.0.34", 53 | "@types/supertest": "^2.0.11", 54 | "@typescript-eslint/eslint-plugin": "^5.0.0", 55 | "@typescript-eslint/parser": "^5.0.0", 56 | "eslint": "^8.0.1", 57 | "eslint-config-prettier": "^8.3.0", 58 | "eslint-plugin-prettier": "^4.0.0", 59 | "jest": "^27.2.5", 60 | "prettier": "^2.3.2", 61 | "source-map-support": "^0.5.20", 62 | "supertest": "^6.1.3", 63 | "ts-jest": "^27.0.3", 64 | "ts-loader": "^9.2.3", 65 | "ts-node": "^10.0.0", 66 | "tsconfig-paths": "^3.10.1", 67 | "typescript": "^4.3.5" 68 | }, 69 | "jest": { 70 | "moduleFileExtensions": [ 71 | "js", 72 | "json", 73 | "ts" 74 | ], 75 | "rootDir": "src", 76 | "testRegex": ".*\\.spec\\.ts$", 77 | "transform": { 78 | "^.+\\.(t|j)s$": "ts-jest" 79 | }, 80 | "collectCoverageFrom": [ 81 | "**/*.(t|j)s" 82 | ], 83 | "coverageDirectory": "../coverage", 84 | "testEnvironment": "node" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Module, 3 | NestModule, 4 | MiddlewareConsumer, 5 | RequestMethod, 6 | } from '@nestjs/common'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { typeORMConfig } from './configs/typeorm.config'; 9 | 10 | import { UserModule } from './user/user.module'; 11 | import { AuthMiddleware } from './middleware/auth.middleware'; 12 | import { UserController } from './user/user.controller'; 13 | import { AuthModule } from './auth/auth.module'; 14 | 15 | @Module({ 16 | imports: [ 17 | TypeOrmModule.forRoot(typeORMConfig), // TypeORM 설정 파일 연결 18 | UserModule, 19 | AuthModule, 20 | ], 21 | controllers: [], 22 | providers: [], 23 | }) 24 | export class AppModule implements NestModule { 25 | configure(consumer: MiddlewareConsumer) { 26 | consumer 27 | .apply(AuthMiddleware) 28 | //exclude 함수는 제외 하고싶은 라우터를 등록합니다. 29 | .exclude({ path: 'user/create_user', method: RequestMethod.POST }) // 유저 생성 30 | .exclude({ path: 'user/user_all', method: RequestMethod.GET }) // 유저 전체 조회 31 | .forRoutes(UserController); // 1.유저 컨트롤러 등록 32 | // .forRoutes('user'); // 2.유저 컨트롤러 경로 등록 -> 위 1번과 동일 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { LocalStrategy } from './strategies/auth.local.strategy'; 4 | import { AuthService } from './auth.service'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { UserRepository } from 'src/repository/user.repository'; 7 | import { JwtModule } from '@nestjs/jwt'; 8 | import { jwtConstants } from './constants'; 9 | import { JwtStrategy } from './strategies/auth.jwt.strategy'; 10 | 11 | @Module({ 12 | imports: [ 13 | PassportModule, 14 | JwtModule.register({ 15 | //토큰 서명 값 설정 16 | secret: jwtConstants.secret, 17 | //토큰 유효시간 (임의 1시간) 18 | signOptions: { expiresIn: '1h' }, 19 | }), 20 | TypeOrmModule.forFeature([UserRepository]), 21 | ], 22 | providers: [AuthService, LocalStrategy, JwtStrategy], 23 | exports: [AuthService], 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { UserRepository } from 'src/repository/user.repository'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | constructor( 9 | @InjectRepository(UserRepository) 10 | private readonly userRepository: UserRepository, 11 | private readonly jwtService: JwtService, 12 | ) {} 13 | 14 | /** 15 | * @author Ryan 16 | * @description 단일 유저 조회 17 | * 18 | * @param user_id 유저 아이디 19 | * @param password 유저 비밀번호 20 | * @returns User 21 | */ 22 | async validateUser(user_id: string, password: string): Promise { 23 | console.log('AuthService'); 24 | 25 | const user = await this.userRepository.findByLogin(user_id, password); 26 | 27 | //사용자가 요청한 비밀번호와 DB에서 조회한 비밀번호 일치여부 검사 28 | if (user && user.password === password) { 29 | const { password, ...result } = user; 30 | 31 | //유저 정보를 통해 토큰 값을 생성 32 | const accessToken = await this.jwtService.sign(result); 33 | 34 | //토큰 값을 추가한다. 35 | result['token'] = accessToken; 36 | 37 | //비밀번호를 제외하고 유저 정보를 반환 38 | return result; 39 | } 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'secretKey', 3 | }; 4 | -------------------------------------------------------------------------------- /src/auth/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/auth/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/auth/strategies/auth.jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { jwtConstants } from '../constants'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor() { 9 | super({ 10 | //Request에서 JWT 토큰을 추출하는 방법을 설정 -> Authorization에서 Bearer Token에 JWT 토큰을 담아 전송해야한다. 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | //true로 설정하면 Passport에 토큰 검증을 위임하지 않고 직접 검증, false는 Passport에 검증 위임 13 | ignoreExpiration: false, 14 | //검증 비밀 값(유출 주위) 15 | secretOrKey: jwtConstants.secret, 16 | }); 17 | } 18 | 19 | /** 20 | * @author Ryan 21 | * @description 클라이언트가 전송한 Jwt 토큰 정보 22 | * 23 | * @param payload 토큰 전송 내용 24 | */ 25 | async validate(payload: any) { 26 | return { userId: payload.sub, username: payload.username }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/auth/strategies/auth.local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { AuthService } from '../auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | // usernaem 키 이름 변경 user_id로 요청 10 | super({ 11 | usernameField: 'user_id', 12 | }); 13 | } 14 | 15 | async validate(user_id: string, password: string): Promise { 16 | console.log('LocalStrategy'); 17 | 18 | const user = await this.authService.validateUser(user_id, password); 19 | if (!user) { 20 | throw new UnauthorizedException(); 21 | } 22 | return user; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/configs/typeorm.config.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | 3 | export const typeORMConfig: TypeOrmModuleOptions = { 4 | type: 'mysql', //Database 설정 5 | host: 'localhost', 6 | port: 3306, 7 | username: 'root', 8 | password: '1234', 9 | database: 'Ryan', 10 | entities: ['dist/**/*.entity.{ts,js}'], // Entity 연결 11 | synchronize: true, //true 값을 설정하면 어플리케이션을 다시 실행할 때 엔티티안에서 수정된 컬럼의 길이 타입 변경값등을 해당 테이블을 Drop한 후 다시 생성해준다. 12 | }; 13 | -------------------------------------------------------------------------------- /src/entity/board.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | CreateDateColumn, 7 | UpdateDateColumn, 8 | DeleteDateColumn, 9 | ManyToMany, 10 | JoinTable, 11 | } from 'typeorm'; 12 | import { Category } from './category.entity'; 13 | 14 | @Entity({ name: 'board' }) 15 | export class Board extends BaseEntity { 16 | @PrimaryGeneratedColumn('uuid') 17 | id: string; 18 | 19 | //length 설정하지 않으면 기본 255 길이 설정 20 | @Column({ 21 | type: 'varchar', 22 | comment: '게시글 이름', 23 | }) 24 | name: string; 25 | 26 | @Column({ 27 | type: 'varchar', 28 | length: 4000, 29 | comment: '게시글 내용', 30 | }) 31 | content: string; 32 | 33 | @Column({ 34 | type: 'int', 35 | default: 0, 36 | comment: '게시글 조회수', 37 | }) 38 | view: number; 39 | 40 | @CreateDateColumn({ name: 'create_at', comment: '생성일' }) 41 | createdAt: Date; 42 | 43 | @UpdateDateColumn({ name: 'update_at', comment: '수정일' }) 44 | updatedAt: Date; 45 | 46 | @DeleteDateColumn({ name: 'delete_at', comment: '삭제일' }) 47 | deletedAt?: Date | null; 48 | 49 | @ManyToMany(() => Category) 50 | @JoinTable() 51 | category: Category; 52 | } 53 | -------------------------------------------------------------------------------- /src/entity/category.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | CreateDateColumn, 7 | UpdateDateColumn, 8 | DeleteDateColumn, 9 | } from 'typeorm'; 10 | 11 | @Entity({ name: 'category' }) 12 | export class Category extends BaseEntity { 13 | @PrimaryGeneratedColumn('uuid') 14 | id: string; 15 | 16 | //length 설정하지 않으면 기본 255 길이 설정 17 | @Column({ 18 | type: 'varchar', 19 | comment: '카테고리 이름', 20 | }) 21 | name: string; 22 | 23 | @CreateDateColumn({ name: 'create_at', comment: '생성일' }) 24 | createdAt: Date; 25 | 26 | @UpdateDateColumn({ name: 'update_at', comment: '수정일' }) 27 | updatedAt: Date; 28 | 29 | @DeleteDateColumn({ name: 'delete_at', comment: '삭제일' }) 30 | deletedAt?: Date | null; 31 | } 32 | -------------------------------------------------------------------------------- /src/entity/company_infomation.entity.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | import { 3 | BaseEntity, 4 | Column, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | CreateDateColumn, 8 | UpdateDateColumn, 9 | DeleteDateColumn, 10 | OneToMany, 11 | } from 'typeorm'; 12 | import { User } from './user.entity'; 13 | 14 | @Entity({ name: 'company_information' }) 15 | export class CompanyInformation extends BaseEntity { 16 | @PrimaryGeneratedColumn('uuid') 17 | id: string; 18 | 19 | //length 설정하지 않으면 기본 255 길이 설정 20 | @Column({ 21 | type: 'varchar', 22 | comment: '회사 이름', 23 | }) 24 | hospital_name: string; 25 | 26 | @IsEmail() 27 | @Column({ 28 | type: 'varchar', 29 | length: 50, 30 | comment: '회사 이메일', 31 | }) 32 | email: string; 33 | 34 | @Column({ type: 'varchar', length: 50, comment: '회사 관리자 이름' }) 35 | name: string; 36 | 37 | @Column({ 38 | type: 'varchar', 39 | length: 72, 40 | comment: '회사 전화번호', 41 | }) 42 | phone: string; 43 | 44 | @Column({ 45 | type: 'varchar', 46 | length: 72, 47 | comment: '팩스번호', 48 | }) 49 | fax: string; 50 | 51 | @Column({ 52 | type: 'varchar', 53 | length: 45, 54 | name: 'business_number', 55 | comment: '사업자등록 번호', 56 | }) 57 | business_number: string; 58 | 59 | @Column({ 60 | type: 'text', 61 | comment: '주소', 62 | }) 63 | address: string; 64 | 65 | @Column({ 66 | type: 'text', 67 | comment: '상세주소', 68 | }) 69 | detail_address: string; 70 | 71 | @CreateDateColumn({ name: 'create_at', comment: '생성일' }) 72 | createdAt: Date; 73 | 74 | @UpdateDateColumn({ name: 'update_at', comment: '수정일' }) 75 | updatedAt: Date; 76 | 77 | @DeleteDateColumn({ name: 'delete_at', comment: '삭제일' }) 78 | deletedAt?: Date | null; 79 | 80 | /** 81 | * 1 : M 관계 설정 82 | * @OneToMany -> 해당 엔티티(CompanyInformation) To 대상 엔티티 (User) 83 | */ 84 | @OneToMany(() => User, (user) => user.id) 85 | userId: User[]; 86 | } 87 | -------------------------------------------------------------------------------- /src/entity/profile.entity.ts: -------------------------------------------------------------------------------- 1 | //Enum 설정 2 | enum STATUS { 3 | PAUSE = 'PAUSE', 4 | ACTIVE = 'ACTIVE', 5 | } 6 | 7 | import { 8 | BaseEntity, 9 | Column, 10 | Entity, 11 | PrimaryGeneratedColumn, 12 | CreateDateColumn, 13 | UpdateDateColumn, 14 | DeleteDateColumn, 15 | } from 'typeorm'; 16 | 17 | @Entity({ name: 'profile' }) 18 | export class Profile extends BaseEntity { 19 | @PrimaryGeneratedColumn('uuid') 20 | id: string; 21 | 22 | @Column({ 23 | type: 'varchar', 24 | length: 50, 25 | comment: '유저 권한(Ex: 관리자, 일반 유저 등등)', 26 | }) 27 | permission: string; 28 | 29 | @Column({ 30 | type: 'enum', 31 | enum: STATUS, 32 | default: STATUS.PAUSE, 33 | comment: '계정상태(ACTIVE, PAUSE)', 34 | }) 35 | status: string; 36 | 37 | @Column({ type: 'tinyint', width: 1, default: 0, comment: '계정 블락 유무' }) 38 | block: string; 39 | 40 | @Column({ 41 | type: 'date', 42 | comment: '계정 유효기간(패키지 별 설정) Ex) 2021-12-14', 43 | }) 44 | account_expired: Date; 45 | 46 | @Column({ 47 | type: 'date', 48 | comment: '비밀번호 유효기간(주기적 업데이트) Ex) 2021-12-14', 49 | }) 50 | password_expired: Date; 51 | 52 | @CreateDateColumn({ name: 'create_at', comment: '생성일' }) 53 | createdAt: Date; 54 | 55 | @UpdateDateColumn({ name: 'update_at', comment: '수정일' }) 56 | updatedAt: Date; 57 | 58 | @DeleteDateColumn({ name: 'delete_at', comment: '삭제일' }) 59 | deletedAt?: Date | null; 60 | } 61 | -------------------------------------------------------------------------------- /src/entity/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | CreateDateColumn, 7 | UpdateDateColumn, 8 | DeleteDateColumn, 9 | Unique, 10 | OneToOne, 11 | JoinColumn, 12 | ManyToOne, 13 | } from 'typeorm'; 14 | import { CompanyInformation } from './company_infomation.entity'; 15 | import { Profile } from './profile.entity'; 16 | 17 | @Entity({ name: 'user' }) 18 | @Unique(['user_id']) 19 | export class User extends BaseEntity { 20 | @PrimaryGeneratedColumn('uuid') 21 | id: string; 22 | 23 | @Column({ type: 'varchar', length: 50, comment: '유저 아이디' }) 24 | user_id: string; 25 | 26 | @Column({ type: 'varchar', length: 255, comment: '유저 비밀번호' }) 27 | password: string; 28 | 29 | @Column({ type: 'varchar', length: 255, comment: 'salt' }) 30 | salt: string; 31 | 32 | @Column({ type: 'varchar', length: 30, comment: '유저 이름' }) 33 | name: string; 34 | 35 | @Column({ type: 'tinyint', comment: '유저 나이' }) 36 | age: number; 37 | 38 | @CreateDateColumn({ name: 'create_at', comment: '생성일' }) 39 | createdAt: Date; 40 | 41 | @UpdateDateColumn({ name: 'update_at', comment: '수정일' }) 42 | updatedAt: Date; 43 | 44 | @DeleteDateColumn({ name: 'delete_at', comment: '삭제일' }) 45 | deletedAt?: Date | null; 46 | 47 | /** 48 | * 1 : 1 관계 설정 49 | * @OneToOne -> 해당 엔티티(User) To 대상 엔티티(Profile) 50 | * 하나의 유저는 하나의 개인정보를 갖는다. 51 | */ 52 | @OneToOne(() => Profile) 53 | @JoinColumn({ name: 'profile_id' }) 54 | profile: Profile; 55 | 56 | /** 57 | * 1 : M 관계 설정 58 | * @ManyToOne -> 해당 엔티티(User) To 대상 엔티티(CompanyInformation) 59 | * 여러 유저는 하나의 회사에 소속 60 | */ 61 | @ManyToOne( 62 | () => CompanyInformation, 63 | (comapnyInformation) => comapnyInformation.userId, 64 | ) 65 | @JoinColumn({ name: 'company_id' }) 66 | companyInformation: CompanyInformation; 67 | } 68 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import { HttpExceptionFilter } from './utils/http-exception.filter'; 5 | import { existsSync, mkdirSync } from 'fs'; 6 | import { setupSwagger } from './utils/swagger'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | 11 | const uploadPath = 'uploads'; 12 | 13 | if (!existsSync(uploadPath)) { 14 | // uploads 폴더가 존재하지 않을시, 생성합니다. 15 | mkdirSync(uploadPath); 16 | } 17 | 18 | //예외 필터 연결 19 | app.useGlobalFilters(new HttpExceptionFilter()); 20 | 21 | //Global Middleware 설정 -> Cors 속성 활성화 22 | app.enableCors({ 23 | origin: '*', 24 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', 25 | optionsSuccessStatus: 200, 26 | }); 27 | 28 | app.useGlobalPipes( 29 | new ValidationPipe({ 30 | /** 31 | * whitelist: DTO에 없은 속성은 무조건 거른다. 32 | * forbidNonWhitelisted: 전달하는 요청 값 중에 정의 되지 않은 값이 있으면 Error를 발생합니다. 33 | * transform: 네트워크를 통해 들어오는 데이터는 일반 JavaScript 객체입니다. 34 | * 객체를 자동으로 DTO로 변환을 원하면 transform 값을 true로 설정한다. 35 | * disableErrorMessages: Error가 발생 했을 때 Error Message를 표시 여부 설정(true: 표시하지 않음, false: 표시함) 36 | * 배포 환경에서는 true로 설정하는 걸 추천합니다. 37 | */ 38 | whitelist: true, 39 | forbidNonWhitelisted: true, 40 | transform: true, 41 | disableErrorMessages: true, 42 | }), 43 | ); 44 | 45 | //Swagger 환경설정 연결 46 | setupSwagger(app); 47 | 48 | await app.listen(3000); 49 | } 50 | bootstrap(); 51 | -------------------------------------------------------------------------------- /src/middleware/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestMiddleware, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { Request, Response, NextFunction } from 'express'; 7 | 8 | @Injectable() 9 | export class AuthMiddleware implements NestMiddleware { 10 | use(req: Request, res: Response, next: NextFunction) { 11 | // // 논리합 연산자 -> 왼쪽 피연산자가 false라면 오른쪽 피연산자가 실행 12 | // const name: string = req.query.name || req.body.name; 13 | 14 | // if (name == 'Ryan') { 15 | // next(); 16 | // } else { 17 | // // Ryan 유저가 아니라면 허가 받지 않은 유저이기 때문에 401 Error를 반환 18 | // throw new UnauthorizedException(); 19 | // } 20 | 21 | next(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/repository/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpException, 3 | HttpStatus, 4 | ForbiddenException, 5 | NotFoundException, 6 | } from '@nestjs/common'; 7 | import { CreateUserDto, UpdateUserDto } from 'src/user/dto/user.dto'; 8 | import { EntityRepository, Repository } from 'typeorm'; 9 | import { User } from '../entity/user.entity'; 10 | 11 | @EntityRepository(User) 12 | export class UserRepository extends Repository { 13 | //유저 생성 14 | async onCreate(createUserDto: CreateUserDto): Promise { 15 | try { 16 | const { user_id, password, name, age } = createUserDto; 17 | 18 | const user = await this.save({ 19 | user_id, 20 | password, 21 | salt: '임시', 22 | name, 23 | age, 24 | }); 25 | 26 | return user ? true : false; 27 | } catch (error) { 28 | throw new HttpException( 29 | { 30 | message: 'SQL에러', 31 | error: error.sqlMessage, 32 | }, 33 | HttpStatus.FORBIDDEN, 34 | ); 35 | } 36 | } 37 | 38 | //모든 유저 조회 39 | async findAll(): Promise { 40 | return await this.find(); 41 | } 42 | 43 | //단일 유저 조회 44 | async findById(id: string): Promise { 45 | const user = await this.findOne(id); 46 | 47 | if (!user) { 48 | throw new HttpException( 49 | { 50 | message: 1, 51 | error: '유저를 찾을 수 없습니다.', 52 | }, 53 | HttpStatus.NOT_FOUND, 54 | ); 55 | } 56 | 57 | return user; 58 | } 59 | 60 | //단일 유저 수정 61 | async onChnageUser( 62 | id: string, 63 | updateUserDto: UpdateUserDto, 64 | ): Promise { 65 | const { name, age } = updateUserDto; 66 | 67 | const chnageUser = await this.update({ id }, { name, age }); 68 | 69 | if (chnageUser.affected !== 1) { 70 | throw new NotFoundException('유저가 존재하지 않습니다.'); 71 | } 72 | 73 | return true; 74 | } 75 | 76 | //전체 유저 수정 77 | async onChnageUsers(updateUserDto: UpdateUserDto[]): Promise { 78 | const user = updateUserDto.map((data) => { 79 | return this.update(data.id, { name: data.name, age: data.age }); 80 | }); 81 | 82 | await Promise.all(user); 83 | 84 | return true; 85 | } 86 | 87 | //유저 삭제 88 | async onDelete(id: string): Promise { 89 | /** 90 | * remove() & delete() 91 | * - remove: 존재하지 않는 아이템을 삭제하면 404 Error가 발생합니다. 92 | * - delete: 해당 아이템이 존재 유무를 파악하고 존재하면 삭제하고, 없다면 아무 에러도 발생하지 않는다. 93 | */ 94 | const deleteUser = await this.delete(id); 95 | 96 | if (deleteUser.affected === 0) { 97 | throw new NotFoundException('유저가 존재하지 않습니다.'); 98 | } 99 | 100 | return true; 101 | } 102 | 103 | //로그인 유저 조회 104 | async findByLogin(user_id: string, password: string): Promise { 105 | const user = await this.findOne({ where: { user_id, password } }); 106 | 107 | if (!user) { 108 | throw new ForbiddenException('아이디와 비밀번호를 다시 확인해주세요.'); 109 | } 110 | 111 | return user; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/user/dto/sign_in.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString, Matches } from 'class-validator'; 3 | 4 | export class SignInDto { 5 | @ApiProperty({ 6 | example: 'Ryan', 7 | description: '유저 아이디', 8 | required: true, 9 | }) 10 | @IsNotEmpty() 11 | @IsString() 12 | user_id: string; 13 | 14 | @ApiProperty({ 15 | example: '1234qweR!!', 16 | description: '유저 비밀번호', 17 | required: true, 18 | }) 19 | @IsNotEmpty() 20 | @IsString() 21 | // 최소 8자 및 최대 16자 하나의 소문자, 하나의 숫자 및 하나의 특수 문자 22 | @Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d~!@#$%^&*()+|=]{8,16}$/, { 23 | message: '비밀번호 양식에 맞게 작성하세요.', 24 | }) 25 | password: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/user/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsNotEmpty, 3 | IsNumber, 4 | IsString, 5 | Matches, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | import { PartialType } from '@nestjs/mapped-types'; 10 | import { ApiProperty } from '@nestjs/swagger'; 11 | 12 | /** 13 | * @description SRP를 위반하는 구조이지만 테스트용으로 한 파일에 두 클래스를 선언했다. 14 | * 15 | * SRP란: 한 클래스는 하나의 책임만 가져야한다. (단일 책임의 원칙) 16 | */ 17 | export class CreateUserDto { 18 | @ApiProperty({ 19 | example: 'Ryan', 20 | description: '유저 아이디', 21 | required: true, 22 | }) 23 | @IsString() 24 | @IsNotEmpty() 25 | user_id: string; // 유저 아이디 26 | 27 | @ApiProperty({ 28 | example: '1234qweR!!', 29 | description: '유저 비밀번호', 30 | required: true, 31 | }) 32 | @IsString() 33 | @IsNotEmpty() 34 | @MinLength(8) 35 | @MaxLength(16) 36 | // 최소 8자 및 최대 16자, 하나 이상의 대문자, 하나의 소문자, 하나의 숫자 및 하나의 특수 문자 37 | @Matches( 38 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}$/, 39 | { 40 | message: '비밀번호 양식에 맞게 작성하세요.', 41 | }, 42 | ) 43 | password: string; //유저 비밀번호 44 | 45 | @ApiProperty({ 46 | example: 'Ryan', 47 | description: '유저 이름', 48 | required: true, 49 | }) 50 | @IsString() 51 | @IsNotEmpty() 52 | name: string; // 유저 이름 53 | 54 | @ApiProperty({ 55 | example: '25', 56 | description: '유저 나이', 57 | required: true, 58 | }) 59 | @IsNumber() 60 | @IsNotEmpty() 61 | age: number; //유저 나이 62 | } 63 | 64 | export class UpdateUserDto extends PartialType(CreateUserDto) { 65 | @IsString() 66 | id: string; 67 | } 68 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Patch, 8 | Post, 9 | Put, 10 | Query, 11 | UsePipes, 12 | ValidationPipe, 13 | ParseUUIDPipe, 14 | UseGuards, 15 | Request, 16 | HttpStatus, 17 | Res, 18 | UseFilters, 19 | UseInterceptors, 20 | UploadedFiles, 21 | Bind, 22 | } from '@nestjs/common'; 23 | import { Response } from 'express'; 24 | import { UserService } from './user.service'; 25 | import { CreateUserDto, UpdateUserDto } from './dto/user.dto'; 26 | import { User } from 'src/entity/user.entity'; 27 | import { LocalAuthGuard } from 'src/auth/guards/local-auth.guard'; 28 | import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; 29 | import { HttpExceptionFilter } from 'src/utils/http-exception.filter'; 30 | import { FilesInterceptor } from '@nestjs/platform-express'; 31 | import { 32 | multerDiskOptions, 33 | multerDiskDestinationOutOptions, 34 | multerMemoryOptions, 35 | } from 'src/utils/multer.options'; 36 | import { 37 | ApiBearerAuth, 38 | ApiCreatedResponse, 39 | ApiOperation, 40 | ApiTags, 41 | } from '@nestjs/swagger'; 42 | import { SignInDto } from './dto/sign_in.dto'; 43 | 44 | let userId = ''; 45 | @Controller('user') 46 | @ApiTags('User') // Swagger Tage 설정 47 | export class UserController { 48 | constructor(private readonly userService: UserService) {} 49 | 50 | @Get() //경로를 설정하지 않으면 "user/" 경로로 설정이 된다. 51 | @UseFilters(new HttpExceptionFilter()) 52 | getHelloWorld(): string { 53 | return this.userService.getHelloWorld(); 54 | } 55 | 56 | /** 57 | * @author Ryan 58 | * @description @Body 방식 - @Body 어노테이션 여러개를 통해 요청 객체를 접근할 수 있습니다. 59 | * 60 | * CreateUserDto를 사용해서 @Body 전달 방식을 변경합니다. 61 | * 62 | * @param id 유저 고유 아이디 63 | * @param name 유저 이름 64 | */ 65 | @Post('/create_user') 66 | @UsePipes(ValidationPipe) 67 | @ApiOperation({ 68 | summary: '유저 생성', 69 | description: '유저 생성 API', 70 | }) 71 | @ApiCreatedResponse({ 72 | description: '성공여부', 73 | schema: { 74 | example: { success: true }, 75 | }, 76 | }) 77 | onCreateUser(@Res() res: Response, @Body() createUserDto: CreateUserDto) { 78 | return this.userService.onCreateUser(createUserDto).then((result) => { 79 | res.status(HttpStatus.OK).json({ success: result }); 80 | }); 81 | } 82 | 83 | /** 84 | * @author Ryan 85 | * @description 전체 유저 조회 86 | */ 87 | @Get('/user_all') 88 | @UseGuards(JwtAuthGuard) 89 | @ApiBearerAuth('access-token') //JWT 토큰 키 설정 90 | @ApiOperation({ 91 | summary: '전체 유저 조회', 92 | description: '전체 유저 조회 API', 93 | }) 94 | @ApiCreatedResponse({ 95 | description: '성공여부', 96 | schema: { 97 | example: { 98 | success: true, 99 | data: [ 100 | { 101 | id: 'cea1d926-6f1b-4a37-a46c-8ddf0b17a0bc', 102 | user_id: 'Ryan', 103 | password: '1234qweR!!', 104 | salt: '임시', 105 | name: 'Ryan', 106 | age: 25, 107 | createdAt: '2021-12-25T23:30:51.371Z', 108 | updatedAt: '2021-12-25T23:30:51.371Z', 109 | deletedAt: null, 110 | }, 111 | ], 112 | }, 113 | }, 114 | }) 115 | getUserAll(@Res() res: Response) { 116 | return this.userService.getUserAll().then((result) => { 117 | res.status(HttpStatus.OK).json({ success: true, data: result }); 118 | }); 119 | } 120 | 121 | /** 122 | * @author Ryan 123 | * @description @Query 방식 - 단일 유저 조회 124 | * 125 | * @param id 유저 고유 아이디 126 | */ 127 | @Get('/user') 128 | findByUserOne1(@Query('id', ParseUUIDPipe) id: string): Promise { 129 | return this.userService.findByUserOne(id); 130 | } 131 | 132 | /** 133 | * @author Ryan 134 | * @description @Param 방식 - 단일 유저 조회 135 | * 136 | * @param id 유저 고유 아이디 137 | */ 138 | @Get('/user/:id') 139 | findByUserOne2(@Param('id', ParseUUIDPipe) id: string): Promise { 140 | return this.userService.findByUserOne(id); 141 | } 142 | 143 | /** 144 | * @author Ryan 145 | * @description @Param & @Body 혼합 방식 - 단일 유저 수정 146 | * 147 | * @param id 유저 고유 아이디 148 | * @param name 유저 이름 149 | */ 150 | @Patch('/user/:id') 151 | @UsePipes(ValidationPipe) 152 | setUser( 153 | @Param('id', ParseUUIDPipe) id: string, 154 | @Body() updateUserDto: UpdateUserDto, 155 | ): Promise { 156 | return this.userService.setUser(id, updateUserDto); 157 | } 158 | 159 | /** 160 | * @author Ryan 161 | * @description @Body 방식 - 전체 유저 수정 162 | * 163 | * @param updateUserDto 유저 정보 164 | */ 165 | @Put('/user/update') 166 | @UsePipes(ValidationPipe) 167 | setAllUser(@Body() updateUserDto: UpdateUserDto[]): Promise { 168 | return this.userService.setAllUser(updateUserDto); 169 | } 170 | 171 | /** 172 | * @author Ryan 173 | * @description @Query 방식 - 단일 유저 삭제 174 | * 175 | * @param id 유저 고유 아이디 176 | */ 177 | @Delete('/user/delete') 178 | deleteUser(@Query('id', ParseUUIDPipe) id: string): Promise { 179 | return this.userService.deleteUser(id); 180 | } 181 | 182 | /** 183 | * @author Ryan 184 | * @description 로그인 185 | * 186 | * @param req Request 데코레이터 187 | * @returns User 188 | */ 189 | @Post('/auth/login') 190 | @UseGuards(LocalAuthGuard) 191 | @ApiOperation({ 192 | summary: '로그인 API', 193 | description: '아이디와 비밀번호를 통해 로그인을 진행', 194 | }) 195 | @ApiCreatedResponse({ 196 | description: '로그인 정보', 197 | schema: { 198 | example: { 199 | id: 'cea1d926-6f1b-4a37-a46c-8ddf0b17a0bc', 200 | user_id: 'Ryan', 201 | salt: '임시', 202 | name: 'Ryan', 203 | age: 25, 204 | createdAt: '2021-12-25T23:30:51.371Z', 205 | updatedAt: '2021-12-25T23:30:51.371Z', 206 | deletedAt: null, 207 | token: 208 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNlYTFkOTI2LTZmMWItNGEzNy1hNDZjLThkZGYwYjE3YTBiYyIsInVzZXJfaWQiOiJSeWFuIiwic2FsdCI6IuyehOyLnCIsIm5hbWUiOiJSeWFuIiwiYWdlIjoyNSwiY3JlYXRlZEF0IjoiMjAyMS0xMi0yNVQyMzozMDo1MS4zNzFaIiwidXBkYXRlZEF0IjoiMjAyMS0xMi0yNVQyMzozMDo1MS4zNzFaIiwiZGVsZXRlZEF0IjpudWxsLCJpYXQiOjE2NDA1MDc0NzMsImV4cCI6MTY0MDUwNzUzM30.gm-Yf_C8szEOvcy-bK-r-CP4Nz6aCr1AgqvH8KonxvU', 209 | }, 210 | }, 211 | }) 212 | // Swageer API를 사용하기 위해 DTO적용 213 | async login(@Request() req, @Body() sign_in_dto: SignInDto) { 214 | console.log('Login Route'); 215 | 216 | return req.user; 217 | } 218 | 219 | /** 220 | * @author Ryan 221 | * @description 디스크 방식 파일 업로드 (1)-> Destination 옵션 설정 222 | * 223 | * @param {File[]} files 다중 파일 224 | * @param res Response 객체 225 | */ 226 | @Post('/disk_upload1') 227 | @UseInterceptors(FilesInterceptor('files', null, multerDiskOptions)) 228 | @Bind(UploadedFiles()) 229 | uploadFileDisk(files: File[], @Res() res: Response) { 230 | res.status(HttpStatus.OK).json({ 231 | success: true, 232 | data: this.userService.uploadFileDisk(files), 233 | }); 234 | } 235 | 236 | /** 237 | * @author Ryan 238 | * @description 디스크 방식 파일 업로드 (2)-> Destination 옵션 미설정 239 | * 240 | * @param {File[]} files 다중 파일 241 | * @param user_id 유저 아이디 242 | * @param res Response 객체 243 | */ 244 | @Post('/disk_upload2') 245 | @UseInterceptors( 246 | FilesInterceptor('files', null, multerDiskDestinationOutOptions), 247 | ) 248 | @Bind(UploadedFiles()) 249 | uploadFileDiskDestination( 250 | files: File[], 251 | @Body('user_id') user_id: string, 252 | @Res() res: Response, 253 | ) { 254 | if (user_id != undefined) { 255 | userId = user_id; 256 | } 257 | 258 | res.status(HttpStatus.OK).json({ 259 | success: true, 260 | data: this.userService.uploadFileDiskDestination(userId, files), 261 | }); 262 | } 263 | 264 | /** 265 | * @author Ryan 266 | * @description 메모리 방식 파일 업로드 267 | * 268 | * @param {File[]} files 다중 파일 269 | * @param user_id 유저 아이디 270 | * @param res Response 객체 271 | */ 272 | @Post('/memory_upload') 273 | @UseInterceptors(FilesInterceptor('files', null, multerMemoryOptions)) 274 | @Bind(UploadedFiles()) 275 | uploadFileMemory( 276 | files: File[], 277 | @Body('user_id') user_id: string, 278 | @Res() res: Response, 279 | ) { 280 | if (user_id != undefined) { 281 | userId = user_id; 282 | } 283 | res.status(HttpStatus.OK).json({ 284 | success: true, 285 | data: this.userService.uploadFileMemory(userId, files), 286 | }); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserRepository } from 'src/repository/user.repository'; 4 | import { UserController } from './user.controller'; 5 | import { UserService } from './user.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([UserRepository])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | }) 12 | export class UserModule {} 13 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { UserRepository } from 'src/repository/user.repository'; 4 | import { CreateUserDto, UpdateUserDto } from './dto/user.dto'; 5 | import { User } from 'src/entity/user.entity'; 6 | import { uploadFileURL } from 'src/utils/multer.options'; 7 | import * as fs from 'fs'; 8 | import { extname } from 'path'; 9 | 10 | @Injectable() 11 | export class UserService { 12 | constructor( 13 | @InjectRepository(UserRepository) private userRepository: UserRepository, 14 | ) {} 15 | 16 | /** 17 | * @author Ryan 18 | * @description 유저 생성 19 | * 20 | * @param createUserDto 유저 데이터 21 | * 22 | * @returns {User[]} users 23 | */ 24 | onCreateUser(createUserDto: CreateUserDto): Promise { 25 | return this.userRepository.onCreate(createUserDto); 26 | } 27 | 28 | /** 29 | * @author Ryan 30 | * @description 모든 유저 조회 31 | * 32 | * @returns {User[]} users 33 | */ 34 | getUserAll(): Promise { 35 | return this.userRepository.findAll(); 36 | } 37 | 38 | /** 39 | * @author Ryan 40 | * @description 단일 유저 조회 41 | * 42 | * @param id 유저 고유 아이디 43 | * @returns {User} users 44 | */ 45 | findByUserOne(id: string): Promise { 46 | return this.userRepository.findById(id); 47 | } 48 | 49 | /** 50 | * @author Ryan 51 | * @description 단일 유저 수정 52 | * 53 | * @param id 유저 고유 아이디 54 | * @param updateUserDto 유저 정보 55 | * 56 | * @returns {Promise} true 57 | */ 58 | setUser(id: string, updateUserDto: UpdateUserDto): Promise { 59 | return this.userRepository.onChnageUser(id, updateUserDto); 60 | } 61 | 62 | /** 63 | * @author Ryan 64 | * @description 전체 유저 수정 65 | * 66 | * @param updateUserDto 유저 정보 67 | * 68 | * @returns {Promise} true 69 | */ 70 | setAllUser(updateUserDto: UpdateUserDto[]): Promise { 71 | return this.userRepository.onChnageUsers(updateUserDto); 72 | } 73 | 74 | /** 75 | * @author Ryan 76 | * @description 유저 삭제 77 | * 78 | * @param id 79 | * @returns {Promise} true 80 | */ 81 | deleteUser(id: string): Promise { 82 | return this.userRepository.onDelete(id); 83 | } 84 | 85 | getHelloWorld(): string { 86 | return 'Hello World!!'; 87 | } 88 | 89 | /** 90 | * @author Ryan 91 | * @description 디스크 방식 파일 업로드 (1) 92 | * 93 | * @param files 파일 데이터 94 | * @returns {String[]} 파일 경로 95 | */ 96 | uploadFileDisk(files: File[]): string[] { 97 | return files.map((file: any) => { 98 | //파일 이름 반환 99 | return uploadFileURL(file.filename); 100 | }); 101 | } 102 | 103 | /** 104 | * @author Ryan 105 | * @description 디스크 방식 파일 업로드 (2) 106 | * 107 | * @param user_id 유저 아이디 108 | * @param files 파일 데이터 109 | * @returns {String[]} 파일 경로 110 | */ 111 | uploadFileDiskDestination(user_id: string, files: File[]): string[] { 112 | //유저별 폴더 생성 113 | const uploadFilePath = `uploads/${user_id}`; 114 | 115 | if (!fs.existsSync(uploadFilePath)) { 116 | // uploads 폴더가 존재하지 않을시, 생성합니다. 117 | fs.mkdirSync(uploadFilePath); 118 | } 119 | return files.map((file: any) => { 120 | //파일 이름 121 | const fileName = Date.now() + extname(file.originalname); 122 | //파일 업로드 경로 123 | const uploadPath = 124 | __dirname + `/../../${uploadFilePath + '/' + fileName}`; 125 | 126 | //파일 생성 127 | fs.writeFileSync(uploadPath, file.path); // file.path 임시 파일 저장소 128 | 129 | return uploadFileURL(uploadFilePath + '/' + fileName); 130 | }); 131 | } 132 | 133 | /** 134 | * @author Ryan 135 | * @description 메모리 방식 파일 업로드 136 | * 137 | * @param user_id 유저 아이디 138 | * @param files 파일 데이터 139 | * @returns {String[]} 파일 경로 140 | */ 141 | uploadFileMemory(user_id: string, files: File[]): any { 142 | //유저별 폴더 생성 143 | const uploadFilePath = `uploads/${user_id}`; 144 | 145 | if (!fs.existsSync(uploadFilePath)) { 146 | // uploads 폴더가 존재하지 않을시, 생성합니다. 147 | fs.mkdirSync(uploadFilePath); 148 | } 149 | 150 | return files.map((file: any) => { 151 | //파일 이름 152 | const fileName = Date.now() + extname(file.originalname); 153 | //파일 업로드 경로 154 | const uploadPath = 155 | __dirname + `/../../${uploadFilePath + '/' + fileName}`; 156 | 157 | //파일 생성 158 | fs.writeFileSync(uploadPath, file.buffer); 159 | 160 | //업로드 경로 반환 161 | return uploadFileURL(uploadFilePath + '/' + fileName); 162 | }); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/utils/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | 9 | /** 10 | * @Catch(HttpException) 11 | * 해당 데코레이터는 필요한 메타 데이터를 ExceptionFilter에 바인딩하여, 12 | * 필터가 HttpException 타입의 예외만 찾고 있다는 것을 Nset.js에 알리기 위해 선언한다. 13 | */ 14 | @Catch(HttpException) 15 | export class HttpExceptionFilter implements ExceptionFilter { 16 | /** 17 | * @author Ryan 18 | * @description 예외 처리 함수 19 | * 20 | * @param exception 현재 처리 중인 예외 객체 21 | * @param host ArgumentsHost 객체 -> 핸들러에 전달되는 인수를 검색하는 메서드를 제공한다 (Express를 사용하는 경우 - Response & Request & Next 제공) 22 | */ 23 | catch(exception: HttpException, host: ArgumentsHost) { 24 | const ctx = host.switchToHttp(); 25 | const response = ctx.getResponse(); 26 | const request = ctx.getRequest(); 27 | 28 | const status = 29 | exception instanceof HttpException 30 | ? exception.getStatus() 31 | : HttpStatus.INTERNAL_SERVER_ERROR; 32 | 33 | /** 34 | * @author Ryan 35 | * @description HttpException에서 전송한 데이터를 추출할 때 사용 36 | */ 37 | const res: any = exception.getResponse(); 38 | 39 | //요청 url 및 에러 정보 40 | const url: string = request.url; 41 | const error: string = res.error; 42 | const timestamp: string = new Date().toISOString(); 43 | 44 | console.log('요청 url : ', url); 45 | console.log('error 정보 : ', error); 46 | console.log('발생 시간 : ', timestamp); 47 | 48 | /* 클라이언트에게 정보를 전달한다. */ 49 | response.status(status).json({ 50 | success: false, 51 | message: res.message, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/multer.options.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | import { existsSync, mkdirSync } from 'fs'; 3 | import { diskStorage, memoryStorage } from 'multer'; 4 | import { extname } from 'path'; 5 | 6 | export const multerDiskOptions = { 7 | /** 8 | * @author Ryan 9 | * @description 클라이언트로 부터 전송 받은 파일 정보를 필터링 한다 10 | * 11 | * @param request Request 객체 12 | * @param file 파일 정보 13 | * @param callback 성공 및 실패 콜백함수 14 | */ 15 | fileFilter: (request, file, callback) => { 16 | if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) { 17 | // 이미지 형식은 jpg, jpeg, png만 허용합니다. 18 | callback(null, true); 19 | } else { 20 | callback( 21 | new HttpException( 22 | { 23 | message: 1, 24 | error: '지원하지 않는 이미지 형식입니다.', 25 | }, 26 | HttpStatus.BAD_REQUEST, 27 | ), 28 | false, 29 | ); 30 | } 31 | }, 32 | /** 33 | * @description Disk 저장 방식 사용 34 | * 35 | * destination 옵션을 설정 하지 않으면 운영체제 시스템 임시 파일을 저정하는 기본 디렉토리를 사용합니다. 36 | * filename 옵션은 폴더안에 저장되는 파일 이름을 결정합니다. (디렉토리를 생성하지 않으면 에러가 발생!! ) 37 | */ 38 | storage: diskStorage({ 39 | destination: (request, file, callback) => { 40 | const uploadPath = 'uploads'; 41 | if (!existsSync(uploadPath)) { 42 | // uploads 폴더가 존재하지 않을시, 생성합니다. 43 | mkdirSync(uploadPath); 44 | } 45 | callback(null, uploadPath); 46 | }, 47 | filename: (request, file, callback) => { 48 | //파일 이름 설정 49 | callback(null, `${Date.now()}${extname(file.originalname)}`); 50 | }, 51 | }), 52 | limits: { 53 | fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes) 54 | filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB) 55 | fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한) 56 | fileSize: 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한) 57 | files: 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한) 58 | }, 59 | }; 60 | 61 | export const multerDiskDestinationOutOptions = { 62 | /** 63 | * @author Ryan 64 | * @description 클라이언트로 부터 전송 받은 파일 정보를 필터링 한다 65 | * 66 | * @param request Request 객체 67 | * @param file 파일 정보 68 | * @param callback 성공 및 실패 콜백함수 69 | */ 70 | fileFilter: (request, file, callback) => { 71 | if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) { 72 | // 이미지 형식은 jpg, jpeg, png만 허용합니다. 73 | callback(null, true); 74 | } else { 75 | callback( 76 | new HttpException( 77 | { 78 | message: 1, 79 | error: '지원하지 않는 이미지 형식입니다.', 80 | }, 81 | HttpStatus.BAD_REQUEST, 82 | ), 83 | false, 84 | ); 85 | } 86 | }, 87 | /** 88 | * @description Disk 저장 방식 사용 89 | * 90 | * destination 옵션을 설정 하지 않으면 운영체제 시스템 임시 파일을 저정하는 기본 디렉토리를 사용합니다. 91 | * filename 옵션은 폴더안에 저장되는 파일 이름을 결정합니다. (디렉토리를 생성하지 않으면 에러가 발생!! ) 92 | */ 93 | storage: diskStorage({ 94 | filename: (request, file, callback) => { 95 | //파일 이름 설정 96 | callback(null, `${Date.now()}${extname(file.originalname)}`); 97 | }, 98 | }), 99 | limits: { 100 | fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes) 101 | filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB) 102 | fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한) 103 | fileSize: 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한) 104 | files: 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한) 105 | }, 106 | }; 107 | 108 | export const multerMemoryOptions = { 109 | /** 110 | * @author Ryan 111 | * @description 클라이언트로 부터 전송 받은 파일 정보를 필터링 한다 112 | * 113 | * @param request Request 객체 114 | * @param file 파일 정보 115 | * @param callback 성공 및 실패 콜백함수 116 | */ 117 | fileFilter: (request, file, callback) => { 118 | console.log('multerMemoryOptions : fileFilter'); 119 | if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) { 120 | // 이미지 형식은 jpg, jpeg, png만 허용합니다. 121 | callback(null, true); 122 | } else { 123 | callback( 124 | new HttpException( 125 | { 126 | message: 1, 127 | error: '지원하지 않는 이미지 형식입니다.', 128 | }, 129 | HttpStatus.BAD_REQUEST, 130 | ), 131 | false, 132 | ); 133 | } 134 | }, 135 | /** 136 | * @description Memory 저장 방식 사용 137 | */ 138 | stroage: memoryStorage(), 139 | limits: { 140 | fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes) 141 | filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB) 142 | fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한) 143 | fileSize: 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한) 144 | files: 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한) 145 | }, 146 | }; 147 | 148 | /** 149 | * @author Ryan 150 | * @description 파일 업로드 경로 151 | * @param file 파일 정보 152 | * 153 | * @returns {String} 파일 업로드 경로 154 | */ 155 | export const uploadFileURL = (fileName): string => 156 | `http://localhost:3000/${fileName}`; 157 | -------------------------------------------------------------------------------- /src/utils/swagger.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { 3 | SwaggerModule, 4 | DocumentBuilder, 5 | SwaggerCustomOptions, 6 | } from '@nestjs/swagger'; 7 | 8 | //웹 페이지를 새로고침을 해도 Token 값 유지 9 | const swaggerCustomOptions: SwaggerCustomOptions = { 10 | swaggerOptions: { 11 | persistAuthorization: true, 12 | }, 13 | }; 14 | 15 | /** 16 | * @author Ryan 17 | * @description Swagger 세팅 18 | */ 19 | export function setupSwagger(app: INestApplication): void { 20 | const options = new DocumentBuilder() 21 | .setTitle('개발이 취미인 사람') 22 | .setDescription('개발이 취미인 사람 Swagger API 서버') 23 | .setVersion('1.0.0') 24 | //JWT 토큰 설정 25 | .addBearerAuth( 26 | { 27 | type: 'http', 28 | scheme: 'bearer', 29 | name: 'JWT', 30 | in: 'header', 31 | }, 32 | 'access-token', 33 | ) 34 | .build(); 35 | 36 | const document = SwaggerModule.createDocument(app, options); 37 | SwaggerModule.setup('api-docs', app, document, swaggerCustomOptions); 38 | } 39 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------