├── .env.example
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── data-source.ts
├── docs
├── 1. MySQL_installation_for_Windows.md
├── 2. nestjs_version_7_to_8_migration.md
└── 3. Policy.md
├── ecosystem.config.js
├── index.html
├── nest-cli.json
├── nestia.config.ts
├── ormconfig.ts
├── package-lock.json
├── package.json
├── src
├── app.module.ts
├── auth
│ ├── auth.controller.ts
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── guards
│ │ ├── jwt.guard.ts
│ │ ├── kakao.guard.ts
│ │ ├── local.guard.ts
│ │ └── throttler-behind-proxy.guard.ts
│ └── strategies
│ │ ├── jwt.strategy.ts
│ │ ├── kakao.strategy.ts
│ │ └── local.strategy.ts
├── common
│ ├── decorators
│ │ ├── user-id.decorator.ts
│ │ └── user.decorator.ts
│ ├── filters
│ │ └── http-exception.fiter.ts
│ └── interceptors
│ │ └── timout.interceptor.ts
├── config
│ ├── constant
│ │ └── index.ts
│ ├── swagger
│ │ └── index.ts
│ └── typeorm
│ │ └── index.ts
├── controllers
│ ├── categories.controller.ts
│ ├── products.controller.ts
│ ├── sellers.controller.ts
│ └── users.controller.ts
├── decorators
│ ├── is-not-empty-boolean.decorator.ts
│ ├── is-not-empty-number.decorator.ts
│ ├── is-not-empty-string.decorator.ts
│ ├── is-optional-boolean.decorator.ts
│ ├── is-optional-number.decorator.ts
│ └── is-optional-string.decorator.ts
├── interceptors
│ ├── logging.interceptor.ts
│ └── transform.interceptor.ts
├── main.ts
├── models
│ ├── common
│ │ └── time-columns.ts
│ ├── dtos
│ │ ├── create-user.dto.ts
│ │ └── login-user.dto.ts
│ ├── repositories
│ │ └── products.repository.ts
│ └── tables
│ │ ├── bodyImage.ts
│ │ ├── category.ts
│ │ ├── headerImage.ts
│ │ ├── option.ts
│ │ ├── optionGroup.ts
│ │ ├── product.ts
│ │ ├── productHasCategory.ts
│ │ ├── seller.ts
│ │ ├── user.ts
│ │ └── userLikeProduct.ts
├── modules
│ ├── categories.module.ts
│ ├── products.module.ts
│ ├── sellers.module.ts
│ └── users.module.ts
├── providers
│ ├── categories.service.ts
│ ├── products.service.ts
│ ├── sellers.service.ts
│ └── users.service.ts
├── test
│ ├── auth.spec.ts
│ ├── categories.spec.ts
│ ├── products.spec.ts
│ └── users.spec.ts
├── types
│ └── index.d.ts
└── utils
│ └── getOffset.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.api.json
├── tsconfig.build.json
├── tsconfig.json
└── tsconfig.models.json
/.env.example:
--------------------------------------------------------------------------------
1 | DB_TYPE=mysql
2 |
3 | DEVELOPMENT_DB_HOST=localhost
4 | DEVELOPMENT_DB_PORT=3306
5 | DEVELOPMENT_DB_USERNAME=root
6 | DEVELOPMENT_DB_DATABASE=e-commerce-example
7 | DEVELOPMENT_DB_PASSWORD=
8 |
9 | PRODUCTION_DB_TYPE=
10 | PRODUCTION_DB_HOST=
11 | PRODUCTION_DB_PORT=
12 | PRODUCTION_DB_USERNAME=
13 | PRODUCTION_DB_DATABASE=
14 | PRODUCTION_DB_PASSWORD=
15 |
16 | # JWT_TOKEN_KEY_FOR_HASH
17 | ACCESS_KEY=test
18 |
19 | # https://developers.kakao.com/
20 | KAKAO_REST_API_KEY=
--------------------------------------------------------------------------------
/.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 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
35 |
36 | # Diagnostic reports (https://nodejs.org/api/report.html)
37 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
38 |
39 | # Runtime data
40 | pids
41 | *.pid
42 | *.seed
43 | *.pid.lock
44 |
45 | # Directory for instrumented libs generated by jscoverage/JSCover
46 | lib-cov
47 |
48 | # Coverage directory used by tools like istanbul
49 | coverage
50 | *.lcov
51 |
52 | # nyc test coverage
53 | .nyc_output
54 |
55 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
56 | .grunt
57 |
58 | # Bower dependency directory (https://bower.io/)
59 | bower_components
60 |
61 | # node-waf configuration
62 | .lock-wscript
63 |
64 | # Compiled binary addons (https://nodejs.org/api/addons.html)
65 | build/Release
66 |
67 | # Dependency directories
68 | node_modules/
69 | jspm_packages/
70 |
71 | # TypeScript v1 declaration files
72 | typings/
73 |
74 | # TypeScript cache
75 | *.tsbuildinfo
76 |
77 | # Optional npm cache directory
78 | .npm
79 |
80 | # Optional eslint cache
81 | .eslintcache
82 |
83 | # Microbundle cache
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 | .node_repl_history
91 |
92 | # Output of 'npm pack'
93 | *.tgz
94 |
95 | # Yarn Integrity file
96 | .yarn-integrity
97 |
98 | # dotenv environment variables file
99 | .env
100 | .env.test
101 |
102 | # parcel-bundler cache (https://parceljs.org/)
103 | .cache
104 |
105 | # Next.js build output
106 | .next
107 |
108 | # Nuxt.js build / generate output
109 | .nuxt
110 | dist
111 |
112 | # Gatsby files
113 | .cache/
114 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
115 | # https://nextjs.org/blog/next-9-1#public-directory-support
116 | # public
117 |
118 | # vuepress build output
119 | .vuepress/dist
120 |
121 | # Serverless directories
122 | .serverless/
123 |
124 | # FuseBox cache
125 | .fusebox/
126 |
127 | # DynamoDB Local files
128 | .dynamodb/
129 |
130 | # TernJS port file
131 | .tern-port
132 |
133 | packages
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Kyungsu Kang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 만약 커머스가 아니라 NestJS에 대한 예제가 필요하다면 아래를 참고하세요.
2 | https://github.com/picktogram/server
3 |
4 | # nestjs-e-commerce
5 |
6 | e-commerce 프로젝트를 진행하기 위한 B.E. 설정 및 기본 코드를 구현합니다.
7 | B.E.나 F.E. 구분할 것 없이 양자에게 도움이 되기를 바랍니다.
8 | 더 나은 방법, 기능 추가 등은 아래로 문의주시면 감사하겠습니다.
9 |
10 | email : kscodebase@gmail.com
11 |
12 | # How to use?
13 |
14 | 1. MySQL 설치
15 | 2. .env에 각종 값들을 모두 기입 ( 기입 후에는 .env.example 파일 이름을 .env로 고쳐주세요. )
16 | 3. 아래 명령어로 tables를 MySQL 상에 synchronize.
17 |
18 | ```bash
19 | $ npm install # 필요한 node package module을 install 합니다.
20 | $ npm run schema:sync # 정의된 entity들을 즉시 생성합니다.
21 | ```
22 |
23 | - 만약 잘못된 데이터가 있다면 일단 workbench로 삭제해주세요.
24 | - 또는 schema:drop 후 schema:sync로 DB를 삭제, 재생성해주세요.
25 |
26 | 4. 서버 실행 후 F.E. 에 자유롭게 연동
27 |
28 | ```bash
29 | $ npm run start:dev # 또는 npm run start
30 | ```
31 |
32 | 5. localhost:3000/api 경로에서 swagger 문서 확인 가능
33 | - 해당 문서를 통해 경로, 기능, Request와 Response를 확인합니다.
34 |
35 | 추후 pm2를 이용한 클러스터링을 설명하겠습니다.
36 |
--------------------------------------------------------------------------------
/data-source.ts:
--------------------------------------------------------------------------------
1 | // ./data-source.ts
2 | import { DataSource } from 'typeorm';
3 | import * as dotenv from 'dotenv';
4 | import * as path from 'path';
5 |
6 | dotenv.config();
7 |
8 | const NODE_ENV = process.env.NODE_ENV;
9 |
10 | export default new DataSource({
11 | type: 'mysql',
12 | host: process.env[`${NODE_ENV}_DB_HOST`] as string,
13 | port: Number(process.env[`${NODE_ENV}_DB_PORT`]) as number,
14 | username: process.env[`${NODE_ENV}_DB_USERNAME`] as string,
15 | database: process.env[`${NODE_ENV}_DB_DATABASE`] as string,
16 | password: process.env[`${NODE_ENV}_DB_PASSWORD`] as string,
17 | entities: [
18 | path.join(__dirname, './src/models/tables/*.ts'),
19 | path.join(__dirname, './src/models/tables/*.js'),
20 | ],
21 | synchronize: false,
22 | logging: true,
23 | });
24 |
--------------------------------------------------------------------------------
/docs/1. MySQL_installation_for_Windows.md:
--------------------------------------------------------------------------------
1 | # Windows User들을 위한 MySQL 설치 방법
2 |
3 | ## 1. MySQL 설치
4 |
5 | Windows 10 부터는 내부적으로 Linux를 사용 가능하다.
6 | Windows Ubuntu를 설치하여, bash 환경에서 mysql 을 설치한다.
7 | 사실, Windows 환경에서든 Linux 환경에서든 설치는 크게 어렵지 않다.
8 |
9 | ```bash
10 | $ apt-get update
11 | $ apt-get install mysql-server
12 | ```
13 |
14 | apt-get 명령어만 가지고도 손 쉽게 MySQL을 설치할 수 있다.
15 |
16 | ## 2. 기존에 Windows에 Workbench를 설치한 경우
17 |
18 | Windows 사용자들 중 아래 같은 문제점을 발견한 사람들이 있을 수 있다.
19 |
20 | ```text
21 | ERROR 2002 (HY000): Can't connect to local MySQL server through socket
22 | '/var/run/mysqld/mysqld.sock' (2)
23 | ```
24 |
25 | 대충 해당 폴더로 갔을 때 sock 파일이 없다는 의미가 된다.
26 | 나의 경우에는 기존에 문제가 없었으나 workbench를 설치 후 문제가 생겼다.
27 | Error message를 먼저 확인해보자.
28 | MySQL은 error.log를 따로 저장해서 가지기 때문에 이를 확인하면 정확한 원인을 파악할 수 있겠다.
29 |
30 | ```bash
31 | $ cat /var/log/mysql/error.log
32 | ```
33 |
34 | 나의 경우에는 3306 port가 충돌한 게 문제였다.
35 | 아무래도 workbench와 mysql이 동일한 port를 사용한 탓이다.
36 | Windows에서 mysql workbench가 설치되어, Linux의 mysql과 동일한 Port를 차지한 게 문제이다.
37 | workbench는 아마 사용할 것이므로,
38 | mysql 설정 파일에서 port 번호를 수정한다.
39 |
40 | Windows에서의 MySQL 설정 파일 경로는 `/etc/mysql/mysql.conf.d/mysqld.cnf` 이다.
41 | 여기서 주석처리 되어 있을 port 번호 3306을, 3307으로 수정하고 저장한다.
42 |
43 | ```bash
44 | $ /etc/init.d/mysql restart
45 | ```
46 |
47 | 다시 MySQL을 재시작한다.
48 |
--------------------------------------------------------------------------------
/docs/2. nestjs_version_7_to_8_migration.md:
--------------------------------------------------------------------------------
1 | # nest version이 달라졌을 경우
2 |
3 | ```bash
4 | $ npm uninstall @nestjs/common
5 | $ npm uninstall @nestjs/core
6 |
7 | $ npm install @nestjs/common@latest --force
8 | $ npm install @nestjs/core@latest --force
9 | ```
10 |
11 | 일일히 수정하기에는 양이 많기 때문에, `--force` option을 사용한다.
12 |
--------------------------------------------------------------------------------
/docs/3. Policy.md:
--------------------------------------------------------------------------------
1 | # 생각해봐야 할 주제
2 |
3 | ## Seller
4 |
5 | 1. 판매자는 승인을 받아야 합니다.
6 | - commerce 라면 사업자등록번호를 포함해, 외부 API로 검증해야 합니다.
7 | - 등록되지 않은 사람이 물건을 팔아서는 안 됩니다.
8 |
9 | ## Product
10 |
11 | 1. 상품의 정보제공고시, 정책 안내는 반드시 있어야 합니다.
12 | 2. 상품에는 승인 여부를 나타내는 Boolean 또는 Date 값이 필요합니다.
13 |
14 | - 만약 관리자만이 상품을 올릴 수 있다면 불필요합니다.
15 | - 다만 여기서는 승인된 판매자만이 올릴 수 있다고 가정합니다.
16 |
17 | 3. 상품의 상태는 아래와 같이 나타냅니다.
18 | - 심사 : 승인되지도 삭제되지도 않은 상태
19 | - 판매 : 승인되었고 삭제되지 않은 상태
20 | - 품절 : 승인되었으나 삭제된 상태
21 | - 중단 : 승인되지 않았으나 삭제된 상태 ( 관리자에 의한 판매 중단 )
22 | - 심사 이후 판매자는 판매, 품절을 결정할 수 있습니다.
23 | - 중단은 관리자만이 결정할 수 있습니다.
24 |
25 | ## Category
26 |
27 | 1. 카테고리에 상품이 없는 경우, getAll에서 제외해야 할 수도 있습니다.
28 | - 상품이 없어도 카테고리는 존재할 수 있으나,
29 | - Client 개발자가 이에 대한 처리를 안해줄 수도 있기 때문입니다.
30 | - 여기서는 별도의 체크를 하지 않았습니다.
31 |
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [
3 | {
4 | name: 'app',
5 | script: './dist/main.js',
6 | instances: 0,
7 | exec_mode: 'cluster',
8 | wait_ready: true,
9 | listen_timeout: 50000,
10 | kill_timeout: 5000,
11 | },
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/nestia.config.ts:
--------------------------------------------------------------------------------
1 | export = {
2 | input: 'src/controllers',
3 | output: 'src/api',
4 | };
5 |
--------------------------------------------------------------------------------
/ormconfig.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from 'dotenv';
2 | import { join } from 'path';
3 |
4 | dotenv.config();
5 |
6 | const NODE_ENV = process.env.NODE_ENV;
7 |
8 | export const ormconfig = {
9 | type: 'mysql',
10 | host: process.env[`${NODE_ENV}_DB_HOST`],
11 | port: Number(process.env[`${NODE_ENV}_DB_PORT`]),
12 | username: process.env[`${NODE_ENV}_DB_USERNAME`],
13 | password: process.env[`${NODE_ENV}_DB_PASSWORD`],
14 | database: process.env[`${NODE_ENV}_DB_DATABASE`],
15 | entities: [join(__dirname, '/src/models/tables/*.ts')],
16 | synchronize: false,
17 | charset: 'utf8mb4',
18 | logging: true,
19 | };
20 |
21 | export default ormconfig;
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-e-commerce",
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 | "build:tsc": "tsc -p tsconfig.build.json",
12 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
13 | "start": "cross-env NODE_ENV=PRODUCTION nest start",
14 | "start:dev": "cross-env NODE_ENV=DEVELOPMENT nest start --watch",
15 | "start:debug": "cross-env NODE_ENV=DEVELOPMENT nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "start:pm2": "cross-env NODE_ENV=PRODUCTION pm2 start",
18 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
19 | "test": "jest",
20 | "test:watch": "cross-env NODE_ENV=TEST jest --watch --detectOpenHandles",
21 | "test:cov": "jest --coverage",
22 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
23 | "test:e2e": "jest --config ./test/jest-e2e.json",
24 | "nestia": "npx nestia sdk",
25 | "schema:drop": "cross-env NODE_ENV=DEVELOPMENT ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js schema:drop -d ./data-source.ts",
26 | "schema:sync": "cross-env NODE_ENV=DEVELOPMENT ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js schema:sync -d ./data-source.ts",
27 | "build:api": "rimraf packages/api/lib && npx nestia sdk && tsc -p tsconfig.api.json",
28 | "build:models": "rimraf packages/models/lib && tsc -p tsconfig.models.json",
29 | "package:api": "npm run build:api && cd packages/api && npm publish",
30 | "package:models": "npm run build:models && cd packages/models && npm publish"
31 | },
32 | "dependencies": {
33 | "@nestjs/common": "^9.0.3",
34 | "@nestjs/config": "^2.2.0",
35 | "@nestjs/core": "^9.0.3",
36 | "@nestjs/jwt": "^9.0.0",
37 | "@nestjs/passport": "^9.0.0",
38 | "@nestjs/platform-express": "^9.0.3",
39 | "@nestjs/swagger": "^6.0.3",
40 | "@nestjs/throttler": "^3.0.0",
41 | "@nestjs/typeorm": "^9.0.0",
42 | "bcrypt": "^5.0.1",
43 | "class-transformer": "^0.5.1",
44 | "class-validator": "^0.13.2",
45 | "cross-env": "^7.0.3",
46 | "mysql2": "^2.3.3",
47 | "nestia-fetcher": "^2.0.1",
48 | "passport": "^0.6.0",
49 | "passport-jwt": "^4.0.0",
50 | "passport-kakao": "^1.0.1",
51 | "passport-local": "^1.0.0",
52 | "reflect-metadata": "^0.1.13",
53 | "rimraf": "^3.0.2",
54 | "rxjs": "^7.5.6",
55 | "swagger-ui-express": "^4.4.0",
56 | "typeorm": "^0.3.7"
57 | },
58 | "devDependencies": {
59 | "@nestjs/cli": "^9.0.0",
60 | "@nestjs/schematics": "^9.0.1",
61 | "@nestjs/testing": "^9.0.3",
62 | "@types/bcrypt": "^5.0.0",
63 | "@types/express": "^4.17.13",
64 | "@types/jest": "^28.1.4",
65 | "@types/node": "^18.0.3",
66 | "@types/passport-jwt": "^3.0.6",
67 | "@types/passport-kakao": "^1.0.0",
68 | "@types/passport-local": "^1.0.34",
69 | "@types/supertest": "^2.0.12",
70 | "@typescript-eslint/eslint-plugin": "^5.30.6",
71 | "@typescript-eslint/parser": "^5.30.6",
72 | "eslint": "^8.19.0",
73 | "eslint-config-prettier": "^8.5.0",
74 | "eslint-plugin-prettier": "^4.2.1",
75 | "jest": "^28.1.2",
76 | "prettier": "^2.7.1",
77 | "supertest": "^6.2.4",
78 | "ts-jest": "^28.0.5",
79 | "ts-loader": "^9.3.1",
80 | "ts-node": "^10.8.2",
81 | "tsconfig-paths": "^4.0.0",
82 | "typescript": "^4.7.4"
83 | },
84 | "types": "src/types/index.d.ts",
85 | "jest": {
86 | "moduleFileExtensions": [
87 | "js",
88 | "json",
89 | "ts"
90 | ],
91 | "rootDir": "src",
92 | "moduleNameMapper": {
93 | "^@root/(.*)$": "/$1"
94 | },
95 | "testRegex": ".*\\.spec\\.ts$",
96 | "transform": {
97 | "^.+\\.(t|j)s$": "ts-jest"
98 | },
99 | "collectCoverageFrom": [
100 | "**/*.(t|j)s"
101 | ],
102 | "coverageDirectory": "../coverage",
103 | "testEnvironment": "node"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
3 | import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
4 | import { HttpExceptionFilter } from './common/filters/http-exception.fiter';
5 | import { TimeoutInterceptor } from './common/interceptors/timout.interceptor';
6 | import { ConfigModule } from '@nestjs/config';
7 | import { TypeOrmModule } from '@nestjs/typeorm';
8 | import { TypeOrmModuleOptions } from './config/typeorm';
9 | import { AuthModule } from './auth/auth.module';
10 | import { LoggingInterceptor } from './interceptors/logging.interceptor';
11 | import { TransformInterceptor } from './interceptors/transform.interceptor';
12 |
13 | @Module({
14 | imports: [
15 | TypeOrmModule.forRootAsync(TypeOrmModuleOptions),
16 | ConfigModule.forRoot({ isGlobal: true }),
17 | ThrottlerModule.forRoot({ ttl: 60, limit: 10 }),
18 | AuthModule,
19 | ],
20 | controllers: [],
21 | providers: [
22 | {
23 | provide: APP_GUARD,
24 | useClass: ThrottlerGuard,
25 | },
26 | {
27 | provide: APP_INTERCEPTOR,
28 | useClass: TimeoutInterceptor,
29 | },
30 | {
31 | provide: APP_FILTER,
32 | useClass: HttpExceptionFilter,
33 | },
34 | {
35 | provide: APP_INTERCEPTOR,
36 | useClass: LoggingInterceptor,
37 | },
38 | {
39 | provide: APP_INTERCEPTOR,
40 | useClass: TransformInterceptor,
41 | },
42 | ],
43 | })
44 | export class AppModule {}
45 |
--------------------------------------------------------------------------------
/src/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Request, Post, UseGuards } from '@nestjs/common';
2 | import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
3 | import { LoginUserDto } from '@root/models/dtos/login-user.dto';
4 | import { LocalGuard } from './guards/local.guard';
5 |
6 | @ApiTags('권한 / Auth')
7 | @Controller('api/auth')
8 | export class AuthController {
9 | @ApiOperation({ summary: '이메일과 패스워드를 이용한 로그인' })
10 | @ApiBody({ type: LoginUserDto })
11 | @UseGuards(LocalGuard)
12 | @Post('login')
13 | async login(@Request() req) {
14 | return req.user;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule, ConfigService } from '@nestjs/config';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { User } from '../models/tables/user';
5 | import { PassportModule } from '@nestjs/passport';
6 | import { JwtModule } from '@nestjs/jwt';
7 | import { AuthService } from './auth.service';
8 | import { JwtStrategy } from './strategies/jwt.strategy';
9 | import { CategoriesModule } from '@root/modules/categories.module';
10 | import { ProductsModule } from '@root/modules/products.module';
11 | import { SellersModule } from '@root/modules/sellers.module';
12 | import { UsersModule } from '@root/modules/users.module';
13 | import { AuthController } from './auth.controller';
14 | import { LocalStrategy } from './strategies/local.strategy';
15 |
16 | @Module({
17 | imports: [
18 | PassportModule.register({ session: false }),
19 | JwtModule.registerAsync({
20 | imports: [ConfigModule],
21 | inject: [ConfigService],
22 | useFactory: (configService: ConfigService) => {
23 | return {
24 | secret: configService.get('ACCESS_KEY'),
25 | signOptions: { algorithm: 'HS256', expiresIn: '1y' },
26 | };
27 | },
28 | }),
29 | TypeOrmModule.forFeature([User]),
30 | UsersModule,
31 | CategoriesModule,
32 | SellersModule,
33 | ProductsModule,
34 | ],
35 | controllers: [AuthController],
36 | providers: [AuthService, JwtStrategy, LocalStrategy],
37 | exports: [AuthService],
38 | })
39 | export class AuthModule {}
40 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { User } from '../models/tables/user';
4 | import * as bcrypt from 'bcrypt';
5 | import { UsersService } from '@root/providers/users.service';
6 |
7 | @Injectable()
8 | export class AuthService {
9 | constructor(
10 | private readonly jwtService: JwtService,
11 | private readonly usersService: UsersService,
12 | ) {}
13 |
14 | async validateUser(email: string, password: string): Promise {
15 | const user = await this.usersService.findOneByEmail(email);
16 | if (user) {
17 | const isRightPassword = await bcrypt.compare(password, user.password);
18 | if (isRightPassword) {
19 | delete user.password;
20 | return user;
21 | }
22 | }
23 | return null;
24 | }
25 |
26 | userLogin(user: User) {
27 | const token = this.jwtService.sign({ ...user });
28 | return { token };
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/auth/guards/jwt.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class JwtGuard extends AuthGuard('jwt') {}
6 |
--------------------------------------------------------------------------------
/src/auth/guards/kakao.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class KaKaoGuard extends AuthGuard('kakao') {}
6 |
--------------------------------------------------------------------------------
/src/auth/guards/local.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class LocalGuard extends AuthGuard('local') {}
6 |
--------------------------------------------------------------------------------
/src/auth/guards/throttler-behind-proxy.guard.ts:
--------------------------------------------------------------------------------
1 | import { ThrottlerGuard } from '@nestjs/throttler';
2 | import { Injectable } from '@nestjs/common';
3 |
4 | @Injectable()
5 | export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
6 | protected getTracker(req: Record): string {
7 | return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/auth/strategies/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { ConfigService } from '@nestjs/config';
5 |
6 | @Injectable()
7 | export class JwtStrategy extends PassportStrategy(Strategy) {
8 | constructor(private readonly configService: ConfigService) {
9 | super({
10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
11 | ignoreExpiration: true,
12 | secretOrKey: configService.get('ACCESS_KEY'),
13 | });
14 | }
15 |
16 | async validate(payload: any) {
17 | const { iat, exp, ...user } = payload;
18 | return user;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/auth/strategies/kakao.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Strategy, Profile } from 'passport-kakao';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { config } from 'dotenv';
5 |
6 | config();
7 |
8 | @Injectable()
9 | export class KakaoStrategy extends PassportStrategy(Strategy) {
10 | constructor() {
11 | super({
12 | clientID: process.env.KAKAO_REST_API_KEY,
13 | clientSecret: '',
14 | callbackURL: 'http://127.0.0.1:3000/api/users/kakao/callback',
15 | });
16 | }
17 |
18 | async validate(
19 | accessToken: string,
20 | refreshToken: string,
21 | profile: Profile,
22 | done,
23 | ) {
24 | done(null, profile);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/auth/strategies/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 | import { JwtService } from '@nestjs/jwt';
6 |
7 | @Injectable()
8 | export class LocalStrategy extends PassportStrategy(Strategy) {
9 | constructor(
10 | private authService: AuthService,
11 | private readonly jwtService: JwtService,
12 | ) {
13 | super({
14 | usernameField: 'email',
15 | passwordField: 'password',
16 | });
17 | }
18 |
19 | async validate(email: string, password: string): Promise {
20 | const user = await this.authService.validateUser(email, password);
21 | if (!user) {
22 | throw new UnauthorizedException();
23 | }
24 |
25 | return this.jwtService.sign({ ...user });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/common/decorators/user-id.decorator.ts:
--------------------------------------------------------------------------------
1 | import { User } from './user.decorator';
2 |
3 | export const UserId = () => User('id');
4 |
--------------------------------------------------------------------------------
/src/common/decorators/user.decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 |
3 | export const User = createParamDecorator(
4 | (data: string, ctx: ExecutionContext) => {
5 | const request = ctx.switchToHttp().getRequest();
6 | const user = request.user;
7 |
8 | return data ? user?.[data] : user;
9 | },
10 | );
11 |
--------------------------------------------------------------------------------
/src/common/filters/http-exception.fiter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ExceptionFilter,
3 | Catch,
4 | ArgumentsHost,
5 | HttpException,
6 | } from '@nestjs/common';
7 | import { Request, Response } from 'express';
8 |
9 | @Catch(HttpException)
10 | export class HttpExceptionFilter implements ExceptionFilter {
11 | catch(exception: HttpException, host: ArgumentsHost) {
12 | const ctx = host.switchToHttp();
13 | const response = ctx.getResponse();
14 | const request = ctx.getRequest();
15 | const status = exception.getStatus();
16 |
17 | response.status(status).json({
18 | statusCode: status,
19 | timestamp: new Date().toISOString(),
20 | path: request.url,
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/common/interceptors/timout.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | RequestTimeoutException,
7 | } from '@nestjs/common';
8 | import { Observable, throwError, TimeoutError } from 'rxjs';
9 | import { catchError, timeout } from 'rxjs/operators';
10 |
11 | @Injectable()
12 | export class TimeoutInterceptor implements NestInterceptor {
13 | intercept(context: ExecutionContext, next: CallHandler): Observable {
14 | return next.handle().pipe(
15 | timeout(5000),
16 | catchError((err) => {
17 | if (err instanceof TimeoutError) {
18 | return throwError(new RequestTimeoutException());
19 | }
20 | return throwError(err);
21 | }),
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/config/constant/index.ts:
--------------------------------------------------------------------------------
1 | export const NON_PAGINATION = 'NON_PAGENATION';
2 |
--------------------------------------------------------------------------------
/src/config/swagger/index.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
3 |
4 | export const SwaggerSetting = (app: INestApplication) => {
5 | const config = new DocumentBuilder()
6 | .setTitle('nestjs-e-commerce-example')
7 | .setDescription('The API description')
8 | .setVersion('1.0')
9 | .addTag('kakasoo')
10 | .addBearerAuth({ type: 'http', scheme: 'bearer', in: 'header' }, 'Bearer')
11 | .build();
12 |
13 | const document = SwaggerModule.createDocument(app, config);
14 | SwaggerModule.setup('api', app, document);
15 | };
16 |
--------------------------------------------------------------------------------
/src/config/typeorm/index.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule, ConfigService } from '@nestjs/config';
2 | import * as path from 'path';
3 |
4 | export const TypeOrmModuleOptions = {
5 | imports: [ConfigModule],
6 | inject: [ConfigService],
7 | useFactory: async (configService: ConfigService) => {
8 | const NODE_ENV = configService.get('NODE_ENV');
9 |
10 | const option = {
11 | type: configService.get('DB_TYPE'),
12 | host: configService.get(`${NODE_ENV}_DB_HOST`),
13 | port: Number(configService.get(`${NODE_ENV}_DB_PORT`)),
14 | username: configService.get(`${NODE_ENV}_DB_USERNAME`),
15 | database: configService.get(`${NODE_ENV}_DB_DATABASE`),
16 | password: configService.get(`${NODE_ENV}_DB_PASSWORD`),
17 | entities: [
18 | path.join(__dirname, '../../models/tables/*.ts'),
19 | path.join(__dirname, '../../models/tables/*.js'),
20 | ],
21 | synchronize: true,
22 | socketPath: '/tmp/mysql.sock',
23 | ...(NODE_ENV === 'DEVELOPMENT'
24 | ? { retryAttempts: 10, logging: true }
25 | : { logging: false }),
26 | };
27 |
28 | console.log(option);
29 | return option;
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/controllers/categories.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | DefaultValuePipe,
4 | Get,
5 | Param,
6 | ParseIntPipe,
7 | Query,
8 | } from '@nestjs/common';
9 | import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
10 | import { CategoriesService } from '../providers/categories.service';
11 | import { Category } from '../models/tables/category';
12 |
13 | @ApiTags('카테고리 / Categories')
14 | @Controller('api/categories')
15 | export class CategoriesController {
16 | constructor(private readonly categoriesService: CategoriesService) {}
17 |
18 | @ApiOperation({ summary: 'MVP : 카테고리 별 상품 조회' })
19 | @ApiParam({ name: 'id', description: 'categoryId' })
20 | @Get(':id/products')
21 | async getProductsBy(
22 | @Param('id', ParseIntPipe) categoryId: number,
23 | @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
24 | ) {
25 | return await this.categoriesService.getProductsBy(categoryId, page);
26 | }
27 |
28 | @ApiOperation({ summary: "MVP : 생성된 '모든' 카테고리 조회" })
29 | @Get()
30 | async getAll(): Promise {
31 | return this.categoriesService.getAll();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/controllers/products.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
2 | import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
3 | import { ProductsService } from '../providers/products.service';
4 |
5 | @ApiTags('상품 / Products')
6 | @Controller('api/products')
7 | export class ProductsController {
8 | constructor(private readonly productsService: ProductsService) {}
9 |
10 | @ApiOperation({ summary: 'MVP : 상품의 상세 내역을 조회' })
11 | @ApiParam({ name: 'id', description: 'productId' })
12 | @Get('id')
13 | async getDetail(@Param('id', ParseIntPipe) productId: number) {
14 | return await this.productsService.getDetail(productId);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/controllers/sellers.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
2 | import { ApiTags } from '@nestjs/swagger';
3 | import { SellersService } from '../providers/sellers.service';
4 |
5 | @ApiTags('판매자 / Sellers')
6 | @Controller('api/sellers')
7 | export class SellersController {
8 | constructor(private readonly sellersService: SellersService) {}
9 |
10 | @Get('id')
11 | async getSellerInfo(@Param('id', ParseIntPipe) id: number) {}
12 | }
13 |
--------------------------------------------------------------------------------
/src/controllers/users.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
2 | import { User as UserEntity } from '../models/tables/user';
3 | import { CreateUserDto } from '../models/dtos/create-user.dto';
4 | import { UsersService } from '../providers/users.service';
5 | import { User } from '../common/decorators/user.decorator';
6 | import { JwtGuard } from '../auth/guards/jwt.guard';
7 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
8 |
9 | @ApiTags('유저 / User')
10 | @Controller('api/users')
11 | export class UsersController {
12 | constructor(private readonly usersService: UsersService) {}
13 |
14 | @ApiOperation({ summary: 'MVP : Local 로그인을 위한 User 생성' })
15 | @Post('sign-up')
16 | async signUp(@Body() createUserDto: CreateUserDto) {
17 | return await this.usersService.create(createUserDto);
18 | }
19 |
20 | @ApiOperation({ summary: 'MVP : 유저 프로필 조회 & 토큰에 담긴 값 Parsing.' })
21 | @ApiBearerAuth('Bearer')
22 | @UseGuards(JwtGuard)
23 | @Get('profile')
24 | async getProfile(@User() user: UserEntity) {
25 | return user;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/decorators/is-not-empty-boolean.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { IsBoolean, IsNotEmpty } from 'class-validator';
3 | import { ParseOptionalBoolean } from './is-optional-boolean.decorator';
4 |
5 | export function IsNotEmptyBoolean() {
6 | return applyDecorators(IsNotEmpty(), ParseOptionalBoolean(), IsBoolean());
7 | }
8 |
--------------------------------------------------------------------------------
/src/decorators/is-not-empty-number.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { Type } from 'class-transformer';
3 | import { IsInt, IsNotEmpty } from 'class-validator';
4 |
5 | export function IsNotEmptyNumber() {
6 | return applyDecorators(
7 | IsInt(),
8 | IsNotEmpty(),
9 | Type(() => Number),
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/decorators/is-not-empty-string.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { IsNotEmpty, IsString, Length } from 'class-validator';
3 |
4 | export function IsNotEmptyString(min: number, max: number) {
5 | return applyDecorators(IsNotEmpty(), IsString(), Length(min, max));
6 | }
7 |
--------------------------------------------------------------------------------
/src/decorators/is-optional-boolean.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { Transform } from 'class-transformer';
3 | import { IsBoolean, IsOptional } from 'class-validator';
4 |
5 | const optionalBooleanMapper = new Map([
6 | ['undefined', undefined],
7 | ['true', true],
8 | ['false', false],
9 | ]);
10 |
11 | export const ParseOptionalBoolean = () =>
12 | Transform((params) => {
13 | return optionalBooleanMapper.get(String(params.value));
14 | });
15 |
16 | export function IsOptionalBoolean() {
17 | return applyDecorators(IsOptional(), ParseOptionalBoolean(), IsBoolean());
18 | }
19 |
--------------------------------------------------------------------------------
/src/decorators/is-optional-number.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { Type } from 'class-transformer';
3 | import { IsInt, IsOptional } from 'class-validator';
4 |
5 | export function IsOptionalNumber() {
6 | return applyDecorators(
7 | IsOptional(),
8 | IsInt(),
9 | Type(() => Number),
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/decorators/is-optional-string.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators } from '@nestjs/common';
2 | import { IsOptional, IsString, Length } from 'class-validator';
3 |
4 | export function IsOptionalString(min: number, max: number) {
5 | return applyDecorators(IsOptional(), IsString(), Length(min, max));
6 | }
7 |
--------------------------------------------------------------------------------
/src/interceptors/logging.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | } from '@nestjs/common';
7 | import { Observable } from 'rxjs';
8 | import { tap } from 'rxjs/operators';
9 |
10 | @Injectable()
11 | export class LoggingInterceptor implements NestInterceptor {
12 | intercept(context: ExecutionContext, next: CallHandler): Observable {
13 | const request = context.switchToHttp().getRequest();
14 | const { path, user, body, query } = request;
15 | const now = Date.now();
16 |
17 | return next
18 | .handle()
19 | .pipe(
20 | tap((response) =>
21 | console.log(
22 | `logging\n${request.method} ${path} time : ${
23 | Date.now() - request.now
24 | }ms\nuser : ${JSON.stringify(user)}\nbody : ${JSON.stringify(
25 | body,
26 | )}\nquery : ${JSON.stringify(query)}`,
27 | ),
28 | ),
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/interceptors/transform.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | } from '@nestjs/common';
7 | import { NON_PAGINATION } from '@root/config/constant';
8 | import { Request } from 'express';
9 | import { Observable } from 'rxjs';
10 | import { map } from 'rxjs/operators';
11 |
12 | export const calcListTotalCount = (
13 | totalCount = 0,
14 | limit = 0,
15 | ): { totalResult: number; totalPage: number } => {
16 | const totalResult = totalCount;
17 | const totalPage =
18 | totalResult % limit === 0
19 | ? totalResult / limit
20 | : Math.floor(totalResult / limit) + 1;
21 | return { totalResult, totalPage };
22 | };
23 |
24 | @Injectable()
25 | export class TransformInterceptor
26 | implements NestInterceptor>
27 | {
28 | intercept(
29 | context: ExecutionContext,
30 | next: CallHandler,
31 | ): Observable> {
32 | const request: Request = context.switchToHttp().getRequest();
33 |
34 | return next.handle().pipe(
35 | map((value) => {
36 | if (value instanceof Object && 'count' in value && 'list' in value) {
37 | const { list, count, ...restData } = value;
38 |
39 | const limit = request.query['limit']
40 | ? request.query['limit']
41 | : NON_PAGINATION;
42 | const page = request.query['page'];
43 | const search = request.query['search'];
44 |
45 | return {
46 | result: true,
47 | code: 1000,
48 | data: {
49 | ...restData,
50 | list,
51 | ...(limit === NON_PAGINATION
52 | ? { totalResult: count, totalPage: 1 }
53 | : calcListTotalCount(count, Number(limit))),
54 | ...(search ? { search } : { search: null }),
55 | ...(page && { page }),
56 | } as ListOutputValue,
57 | };
58 | }
59 |
60 | return { result: true, code: 1000, data: value };
61 | }),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { SwaggerSetting } from './config/swagger';
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(AppModule);
7 |
8 | app.enableCors();
9 |
10 | SwaggerSetting(app);
11 | await app.listen(3000, () => {
12 | if (process.env.NODE_ENV === 'production') {
13 | process.send('ready');
14 | }
15 | });
16 | }
17 | bootstrap();
18 |
--------------------------------------------------------------------------------
/src/models/common/time-columns.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateDateColumn,
3 | UpdateDateColumn,
4 | DeleteDateColumn,
5 | BaseEntity,
6 | } from 'typeorm';
7 |
8 | export abstract class TimeColumns extends BaseEntity {
9 | @CreateDateColumn()
10 | public readonly createdAt!: Date;
11 |
12 | @UpdateDateColumn()
13 | public readonly updatedAt!: Date;
14 |
15 | @DeleteDateColumn()
16 | public readonly deletedAt!: Date;
17 | }
18 |
--------------------------------------------------------------------------------
/src/models/dtos/create-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 | import { User } from '../tables/user';
3 |
4 | export class CreateUserDto extends PickType(User, [
5 | 'name',
6 | 'nickname',
7 | 'email',
8 | 'password',
9 | 'phoneNumber',
10 | 'birth',
11 | 'gender',
12 | 'smsAdsConsent',
13 | 'emailAdsConsent',
14 | ] as const) {}
15 |
--------------------------------------------------------------------------------
/src/models/dtos/login-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 | import { User } from '../tables/user';
3 |
4 | export class LoginUserDto extends PickType(User, [
5 | 'email',
6 | 'password',
7 | ] as const) {}
8 |
--------------------------------------------------------------------------------
/src/models/repositories/products.repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import { Product } from '../tables/product';
3 |
4 | @EntityRepository(Product)
5 | export class ProductsRepository extends Repository {
6 | async getProduct(productId: number) {
7 | return await this.createQueryBuilder('product')
8 | .withDeleted()
9 | .leftJoinAndMapMany('prouct.headers', 'product.headers', 'header')
10 | .leftJoinAndMapMany('product.bodies', 'product.bodies', 'body')
11 | .leftJoinAndMapMany(
12 | 'product.categories',
13 | 'product.categories',
14 | 'category',
15 | 'category.deletedAt IS NULL',
16 | )
17 | .leftJoinAndMapMany(
18 | 'product.options',
19 | 'product.options',
20 | 'option',
21 | 'option.deletedAt IS NULL AND option.isSale = :isSale',
22 | { isSale: true },
23 | )
24 | .where('product.id = :productId', { productId })
25 | .andWhere('product.isSale = :isSale', { isSale: true })
26 | .getOne();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/models/tables/bodyImage.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | JoinColumn,
7 | Index,
8 | } from 'typeorm';
9 | import { Product } from './product';
10 |
11 | @Index('FK__BodyImage__Product', ['productId'], {})
12 | @Entity()
13 | export class BodyImage {
14 | @PrimaryGeneratedColumn()
15 | id: number;
16 |
17 | @Column('int', { nullable: false, select: false })
18 | productId: number;
19 |
20 | @Column('varchar', { nullable: false })
21 | url: string;
22 |
23 | @Column('decimal', { name: 'position', precision: 6, scale: 5, default: 0 })
24 | position: number;
25 |
26 | @ManyToOne(() => Product, (product) => product.bodies)
27 | @JoinColumn({ name: 'productId', referencedColumnName: 'id' })
28 | product: Product;
29 | }
30 |
--------------------------------------------------------------------------------
/src/models/tables/category.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
2 | import { TimeColumns } from '../common/time-columns';
3 | import { Product } from './product';
4 |
5 | @Entity()
6 | export class Category extends TimeColumns {
7 | @PrimaryGeneratedColumn()
8 | id: number;
9 |
10 | @Column('varchar', { nullable: false, unique: true })
11 | name: string;
12 |
13 | @ManyToMany(() => Product, (product) => product.categories)
14 | products: Product[];
15 | }
16 |
--------------------------------------------------------------------------------
/src/models/tables/headerImage.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | JoinColumn,
7 | Index,
8 | } from 'typeorm';
9 | import { Product } from './product';
10 |
11 | @Index('FK__HeaderImage__Product', ['productId'], {})
12 | @Entity()
13 | export class HeaderImage {
14 | @PrimaryGeneratedColumn()
15 | id: number;
16 |
17 | @Column('int', { nullable: false, select: false })
18 | productId: number;
19 |
20 | @Column('varchar', { nullable: false })
21 | url: string;
22 |
23 | @Column('decimal', { name: 'position', precision: 6, scale: 5, default: 0 })
24 | position: number;
25 |
26 | @ManyToOne(() => Product, (product) => product.headers)
27 | @JoinColumn({ name: 'productId', referencedColumnName: 'id' })
28 | product: Product;
29 | }
30 |
--------------------------------------------------------------------------------
/src/models/tables/option.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | Index,
7 | JoinColumn,
8 | } from 'typeorm';
9 | import { TimeColumns } from '../common/time-columns';
10 | import { OptionGroup } from './optionGroup';
11 |
12 | @Index('FK__Option__OptionGroup', ['groupId'], {})
13 | @Entity()
14 | export class Option extends TimeColumns {
15 | @PrimaryGeneratedColumn()
16 | public id!: number;
17 |
18 | @Column('int', { nullable: false, select: false })
19 | public groupId!: number;
20 |
21 | @Column('varchar', { nullable: false, comment: '옵션 이름' })
22 | public title!: string;
23 |
24 | @Column('tinyint', { width: 1, nullable: false, default: false })
25 | public isSale!: boolean;
26 |
27 | @ManyToOne(() => OptionGroup, (group) => group.options)
28 | @JoinColumn({ name: 'groupId', referencedColumnName: 'id' })
29 | group: OptionGroup;
30 | }
31 |
--------------------------------------------------------------------------------
/src/models/tables/optionGroup.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | JoinColumn,
7 | OneToMany,
8 | Index,
9 | } from 'typeorm';
10 | import { TimeColumns } from '../common/time-columns';
11 | import { Option } from './option';
12 | import { Product } from './product';
13 |
14 | @Index('FK__OptionGroup__Product', ['productId'], {})
15 | @Entity()
16 | export class OptionGroup extends TimeColumns {
17 | @PrimaryGeneratedColumn()
18 | id: number;
19 |
20 | @Column('int', { nullable: false, select: false })
21 | public productId!: number;
22 |
23 | @Column('tinyint', {
24 | width: 1,
25 | nullable: false,
26 | default: false,
27 | comment: '선택 옵션 유무',
28 | })
29 | public isOptional!: boolean;
30 |
31 | @Column('varchar', { nullable: false, comment: '옵션 그룹 이름' })
32 | public title!: string;
33 |
34 | @ManyToOne(() => Product, (product) => product.optionGroups)
35 | @JoinColumn({ name: 'productId', referencedColumnName: 'id' })
36 | product: Product;
37 |
38 | @OneToMany(() => Option, (option) => option.group)
39 | options: Option[];
40 | }
41 |
--------------------------------------------------------------------------------
/src/models/tables/product.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | Index,
7 | OneToMany,
8 | ManyToMany,
9 | JoinTable,
10 | } from 'typeorm';
11 | import { TimeColumns } from '../common/time-columns';
12 | import { BodyImage } from './bodyImage';
13 | import { Category } from './category';
14 | import { HeaderImage } from './headerImage';
15 | import { OptionGroup } from './optionGroup';
16 | import { Seller } from './seller';
17 | import { User } from './user';
18 |
19 | @Index('FK__Product__Seller', ['sellerId'], {})
20 | @Entity()
21 | export class Product extends TimeColumns {
22 | @PrimaryGeneratedColumn()
23 | public readonly id: number;
24 |
25 | @Column('int', { nullable: false, comment: '판매자 ID' })
26 | public sellerId: number;
27 |
28 | @Column('varchar', { nullable: false, comment: '상품 이름' })
29 | public title: string;
30 |
31 | @Column('varchar', { nullable: true, comment: '상품 설명' })
32 | public description: string;
33 |
34 | @Column('varchar', { nullable: true, comment: '안내 문구' })
35 | public guide: string;
36 |
37 | @Column('int', { nullable: false, comment: '원가' })
38 | public originalPrice: number;
39 |
40 | @Column('int', { nullable: false, comment: '판매가' })
41 | public salesPrice: number;
42 |
43 | @Column('int', { nullable: false, comment: '출고 소요 일자' })
44 | public releaseCount: number;
45 |
46 | @Column('text', { nullable: false, select: false, comment: '정보 제공 고시' })
47 | public announcement: string;
48 |
49 | @Column('text', { nullable: false, select: false, comment: '정책 안내' })
50 | public policy: string;
51 |
52 | @ManyToOne((Type) => Seller, (seller) => seller.products)
53 | seller: Seller;
54 |
55 | @OneToMany(() => HeaderImage, (image) => image.product)
56 | headers: HeaderImage[];
57 |
58 | @OneToMany(() => BodyImage, (image) => image.product)
59 | bodies: BodyImage[];
60 |
61 | @Column('tinyint', { width: 1, nullable: false, default: false })
62 | public isSale!: boolean;
63 |
64 | @ManyToMany(() => Category, (category) => category.products)
65 | @JoinTable({ name: 'product_has_categories' })
66 | categories: Category[];
67 |
68 | @OneToMany(() => OptionGroup, (group) => group.product)
69 | optionGroups: OptionGroup[];
70 |
71 | @ManyToMany(() => User, (user) => user.products, { nullable: false })
72 | @JoinTable({ name: 'user_like_product' })
73 | users: User[];
74 | }
75 |
--------------------------------------------------------------------------------
/src/models/tables/productHasCategory.ts:
--------------------------------------------------------------------------------
1 | import { Entity, ManyToOne, PrimaryColumn } from 'typeorm';
2 | import { Product } from './product';
3 |
4 | @Entity({ name: 'product_has_categories' })
5 | export class ProductHasCategory {
6 | @PrimaryColumn()
7 | public readonly categoryId!: number;
8 |
9 | @PrimaryColumn()
10 | public readonly productId!: number;
11 |
12 | @ManyToOne(() => Product, (product) => product.id)
13 | product: Product;
14 | }
15 |
--------------------------------------------------------------------------------
/src/models/tables/seller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | OneToMany,
6 | JoinColumn,
7 | } from 'typeorm';
8 | import { TimeColumns } from '../common/time-columns';
9 | import { Product } from './product';
10 |
11 | @Entity()
12 | export class Seller extends TimeColumns {
13 | @PrimaryGeneratedColumn()
14 | public readonly id!: number;
15 |
16 | @Column('varchar', { unique: true, select: false })
17 | public address!: string;
18 |
19 | @Column('varchar', { select: false })
20 | public password!: string;
21 |
22 | @Column('varchar')
23 | public name!: string;
24 |
25 | @Column('varchar', { nullable: true, select: false })
26 | public nickname!: string;
27 |
28 | @Column('varchar', { nullable: true, select: false })
29 | public profileImage!: string;
30 |
31 | @Column('varchar', { nullable: true, unique: true, select: false })
32 | public phoneNumber!: string;
33 |
34 | @Column('varchar', { nullable: true, unique: true, select: false })
35 | public email!: string;
36 |
37 | @Column('varchar', { comment: '사업장 명' })
38 | public shopName!: string;
39 |
40 | @Column('int', { default: 0, comment: '기본 배송비' })
41 | public basicFee!: number;
42 |
43 | @Column('int', { default: 0, comment: '도서 산간 지방 배송비' })
44 | public exceptionFee: number;
45 |
46 | @Column('int', { default: 0, comment: '배송비 무료 기준 금액' })
47 | public baseFee: number;
48 |
49 | @Column('varchar', { select: false, comment: '사업장 명' })
50 | public companyName!: string;
51 |
52 | @Column('varchar', { select: false, comment: '영업용 메일' })
53 | public companyEmail!: string;
54 |
55 | @Column('varchar', { select: false, comment: '영업용 번호' })
56 | public companyPhonNumber!: string;
57 |
58 | @Column('varchar', { select: false, comment: '사업자번호' })
59 | public businessNumber!: string;
60 |
61 | @OneToMany(() => Product, (product) => product.seller)
62 | @JoinColumn({ name: 'sellerId', referencedColumnName: 'id' })
63 | products: Product[];
64 | }
65 |
--------------------------------------------------------------------------------
/src/models/tables/user.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsNotEmptyString } from '@root/decorators/is-not-empty-string.decorator';
3 | import { IsOptionalBoolean } from '@root/decorators/is-optional-boolean.decorator';
4 | import { Type } from 'class-transformer';
5 | import { IsDate, IsEmail, IsOptional } from 'class-validator';
6 | import {
7 | Entity,
8 | PrimaryGeneratedColumn,
9 | Column,
10 | ManyToMany,
11 | JoinTable,
12 | } from 'typeorm';
13 | import { TimeColumns } from '../common/time-columns';
14 | import { Product } from './product';
15 |
16 | @Entity()
17 | export class User extends TimeColumns {
18 | @PrimaryGeneratedColumn()
19 | public readonly id!: number;
20 |
21 | @ApiProperty({ description: '이름 칼럼으로 사용자의 이름을 의미' })
22 | @IsNotEmptyString(1, 50)
23 | @Column('varchar', { length: 50, nullable: false, select: false })
24 | public name!: string;
25 |
26 | @ApiProperty({ description: '사용자의 별칭, 설정하지 않는 경우도 있다.' })
27 | @IsNotEmptyString(1, 50)
28 | @Column('varchar', { length: 50 })
29 | public nickname!: string;
30 |
31 | @ApiProperty({ description: '사용자의 프로필 이미지' })
32 | @Column('varchar', { nullable: true, select: false })
33 | public profileImage!: string;
34 |
35 | @ApiProperty({ description: '사용자의 전화번호로 동일한 값은 없다.' })
36 | @IsNotEmptyString(1, 50)
37 | @Column('varchar', { nullable: true, unique: true, select: false })
38 | public phoneNumber!: string;
39 |
40 | @ApiProperty({ description: '사용자의 이메일 주소로 로그인 시 필요' })
41 | @IsEmail()
42 | @IsNotEmptyString(1, 50)
43 | @Column('varchar', { nullable: true, unique: true, select: false })
44 | public email!: string;
45 |
46 | @ApiProperty({ description: '사용자의 비밀번호로 로그인 시 필요' })
47 | @Column({ select: false })
48 | password: string;
49 |
50 | @ApiProperty({ description: '생일 이벤트 및 고객 분석을 위해 수집' })
51 | @IsOptional()
52 | @IsDate()
53 | @Type(() => Date)
54 | @Column('datetime', { nullable: true, select: false })
55 | public birth!: Date;
56 |
57 | @ApiProperty({ description: '사용자의 성별로 true면 남자라고 가정한다.' })
58 | @IsOptionalBoolean()
59 | @Column({ nullable: true, width: 1, select: false })
60 | public gender!: boolean;
61 |
62 | // @ApiProperty({ description: '사용자가 현재 가지고 있는 마일리지' })
63 | // @Column('int', {
64 | // nullable: false,
65 | // select: false,
66 | // comment: '마일리지 잔여금',
67 | // default: 0,
68 | // })
69 | // public mileage!: number;
70 |
71 | @ApiProperty({ description: '회원 가입 시 받는 값으로 수신 거부 가능' })
72 | @IsOptionalBoolean()
73 | @Column({
74 | width: 1,
75 | nullable: false,
76 | select: false,
77 | default: false,
78 | comment: 'sms 광고 수신 동의',
79 | })
80 | public smsAdsConsent!: boolean;
81 |
82 | @ApiProperty({ description: '회원 가입 시 받는 값으로 수신 거부 가능' })
83 | @IsOptionalBoolean()
84 | @Column({
85 | width: 1,
86 | nullable: false,
87 | select: false,
88 | default: false,
89 | comment: 'email 광고 수신 동의',
90 | })
91 | public emailAdsConsent!: boolean;
92 |
93 | @ManyToMany(() => Product, (product) => product.users, { nullable: false })
94 | @JoinTable({ name: 'user_like_product' })
95 | products: Product[];
96 | }
97 |
--------------------------------------------------------------------------------
/src/models/tables/userLikeProduct.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm';
2 |
3 | @Entity({ name: 'user_like_product' })
4 | export class UserLikeProduct {
5 | @PrimaryColumn()
6 | public readonly userId!: number;
7 |
8 | @PrimaryColumn()
9 | public readonly productId!: number;
10 |
11 | @CreateDateColumn()
12 | public readonly createdAt: Date;
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/categories.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { CategoriesService } from '../providers/categories.service';
3 | import { CategoriesController } from '../controllers/categories.controller';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { Category } from '../models/tables/category';
6 | import { ProductHasCategory } from '../models/tables/productHasCategory';
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([Category, ProductHasCategory])],
10 | controllers: [CategoriesController],
11 | providers: [CategoriesService],
12 | })
13 | export class CategoriesModule {}
14 |
--------------------------------------------------------------------------------
/src/modules/products.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ProductsService } from '../providers/products.service';
3 | import { ProductsController } from '../controllers/products.controller';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { ProductsRepository } from '@root/models/repositories/products.repository';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([ProductsRepository])],
9 | controllers: [ProductsController],
10 | providers: [ProductsService],
11 | })
12 | export class ProductsModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/sellers.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { SellersService } from '../providers/sellers.service';
3 | import { SellersController } from '../controllers/sellers.controller';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { Seller } from '../models/tables/seller';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([Seller])],
9 | controllers: [SellersController],
10 | providers: [SellersService],
11 | })
12 | export class SellersModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { User } from '../models/tables/user';
4 | import { UsersController } from '../controllers/users.controller';
5 | import { UsersService } from '../providers/users.service';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([User])],
9 | controllers: [UsersController],
10 | providers: [UsersService],
11 | exports: [UsersService],
12 | })
13 | export class UsersModule {}
14 |
--------------------------------------------------------------------------------
/src/providers/categories.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Category } from '../models/tables/category';
4 | import { Product } from '../models/tables/product';
5 | import { Repository } from 'typeorm';
6 | import { ProductHasCategory } from '@root/models/tables/productHasCategory';
7 | import { getOffset } from '@root/utils/getOffset';
8 |
9 | @Injectable()
10 | export class CategoriesService {
11 | constructor(
12 | @InjectRepository(Category)
13 | private readonly categoriesRepository: Repository,
14 | @InjectRepository(ProductHasCategory)
15 | private readonly productHasCategoriesRepository: Repository,
16 | ) {}
17 |
18 | async getAll(): Promise {
19 | return await this.categoriesRepository.find();
20 | }
21 |
22 | async getProductsBy(
23 | categoryId: number,
24 | page: number,
25 | ): Promise<{ count: number; products: Product[] }> {
26 | const [count, relations] = await Promise.all([
27 | this.productHasCategoriesRepository.count({ where: { categoryId } }),
28 | this.productHasCategoriesRepository.find({
29 | relations: { product: true },
30 | where: { categoryId },
31 | ...getOffset(page),
32 | }),
33 | ]);
34 |
35 | return { count, products: relations.map((el) => el.product) };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/providers/products.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { ProductsRepository } from '@root/models/repositories/products.repository';
4 |
5 | @Injectable()
6 | export class ProductsService {
7 | constructor(
8 | @InjectRepository(ProductsRepository)
9 | private readonly productsRepository: ProductsRepository,
10 | ) {}
11 |
12 | async getDetail(productId: number) {
13 | const product = await this.productsRepository.getProduct(productId);
14 | if (!product) {
15 | throw new Error('there is not product!');
16 | }
17 | return product;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/providers/sellers.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Seller } from '../models/tables/seller';
4 | import { Repository } from 'typeorm';
5 |
6 | @Injectable()
7 | export class SellersService {
8 | constructor(
9 | @InjectRepository(Seller)
10 | private readonly sellersRepository: Repository,
11 | ) {}
12 | }
13 |
--------------------------------------------------------------------------------
/src/providers/users.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { User } from '../models/tables/user';
4 | import { Repository } from 'typeorm';
5 | import { CreateUserDto } from '../models/dtos/create-user.dto';
6 | import * as bcrypt from 'bcrypt';
7 |
8 | @Injectable()
9 | export class UsersService {
10 | constructor(
11 | @InjectRepository(User) private readonly usersRepository: Repository,
12 | ) {}
13 |
14 | async create(createUserDto: CreateUserDto) {
15 | const users = await this.usersRepository.find({
16 | where: [
17 | { email: createUserDto.email },
18 | { phoneNumber: createUserDto.phoneNumber },
19 | ],
20 | });
21 |
22 | if (users.length) {
23 | // throw new BadRequestException(
24 | return '이미 해당 전화번호나 이메일로 만들어진 유저가 있습니다.';
25 | // );
26 | }
27 |
28 | const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
29 |
30 | return await this.usersRepository.save({
31 | ...createUserDto,
32 | password: hashedPassword,
33 | });
34 | }
35 |
36 | async findOneByEmail(email: string) {
37 | return await this.usersRepository.findOne({
38 | select: {
39 | id: true,
40 | name: true,
41 | nickname: true,
42 | phoneNumber: true,
43 | email: true,
44 | password: true,
45 | birth: true,
46 | gender: true,
47 | },
48 | where: { email },
49 | });
50 | }
51 |
52 | async findOne(userId: number): Promise {
53 | return await this.usersRepository.findOne({ where: { id: userId } });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/auth.spec.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '@nestjs/config';
2 | import { Test } from '@nestjs/testing';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { User } from '../models/tables/user';
5 | import { TypeOrmModuleOptions } from '../config/typeorm';
6 | import { AuthModule } from '../auth/auth.module';
7 | import { AuthController } from '@root/auth/auth.controller';
8 |
9 | describe('Auth Entity', () => {
10 | let controller: AuthController;
11 |
12 | beforeAll(async () => {
13 | const module = await Test.createTestingModule({
14 | imports: [
15 | TypeOrmModule.forRootAsync(TypeOrmModuleOptions),
16 | TypeOrmModule.forFeature([User]),
17 | ConfigModule.forRoot({ isGlobal: true }),
18 | AuthModule,
19 | ],
20 | controllers: [],
21 | providers: [],
22 | }).compile();
23 |
24 | controller = module.get(AuthController);
25 | });
26 |
27 | describe('0. 테스트 환경을 확인합니다.', () => {
28 | it.only('0-1. controller 가 정의되어야 합니다.', async () => {
29 | expect(controller).toBeDefined();
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/test/categories.spec.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '@nestjs/config';
2 | import { Test } from '@nestjs/testing';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { Category } from '../models/tables/category';
5 | import { TypeOrmModuleOptions } from '../config/typeorm';
6 | import { CategoriesController } from '../controllers/categories.controller';
7 | import { CategoriesService } from '../providers/categories.service';
8 | import { CategoriesModule } from '../modules/categories.module';
9 |
10 | describe('Category Entity', () => {
11 | let controller: CategoriesController;
12 | let service: CategoriesService;
13 | let category;
14 |
15 | beforeAll(async () => {
16 | const module = await Test.createTestingModule({
17 | imports: [
18 | TypeOrmModule.forRootAsync(TypeOrmModuleOptions),
19 | TypeOrmModule.forFeature([Category]),
20 | ConfigModule.forRoot({ isGlobal: true }),
21 | CategoriesModule,
22 | ],
23 | controllers: [],
24 | providers: [],
25 | }).compile();
26 |
27 | service = module.get(CategoriesService);
28 | controller = module.get(CategoriesController);
29 | });
30 |
31 | describe('0. 테스트 환경을 확인합니다.', () => {
32 | it.only('0-1. Service와 Controller 가 정의되어야 합니다.', async () => {
33 | expect(controller).toBeDefined();
34 | expect(service).toBeDefined();
35 | });
36 | });
37 |
38 | describe('1. 카테고리들을 조회합니다.', () => {
39 | it.only('1-1. Response는 Category의 배열입니다.', async () => {
40 | const categories = await controller.getAll();
41 | expect(categories).toBeInstanceOf(Array);
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/test/products.spec.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '@nestjs/config';
2 | import { Test } from '@nestjs/testing';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { Product } from '../models/tables/product';
5 | import { TypeOrmModuleOptions } from '../config/typeorm';
6 | import { ProductsController } from '../controllers/products.controller';
7 | import { ProductsService } from '../providers/products.service';
8 | import { ProductsModule } from '../modules/products.module';
9 |
10 | describe('Product Entity', () => {
11 | let controller: ProductsController;
12 | let service: ProductsService;
13 | let product;
14 |
15 | beforeAll(async () => {
16 | const module = await Test.createTestingModule({
17 | imports: [
18 | TypeOrmModule.forRootAsync(TypeOrmModuleOptions),
19 | TypeOrmModule.forFeature([Product]),
20 | ConfigModule.forRoot({ isGlobal: true }),
21 | ProductsModule,
22 | ],
23 | controllers: [],
24 | providers: [],
25 | }).compile();
26 |
27 | service = module.get(ProductsService);
28 | controller = module.get(ProductsController);
29 | });
30 |
31 | describe('0. 테스트 환경을 확인합니다.', () => {
32 | it.only('0-1. Service와 Controller 가 정의되어야 합니다.', async () => {
33 | expect(controller).toBeDefined();
34 | expect(service).toBeDefined();
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/test/users.spec.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '@nestjs/config';
2 | import { Test } from '@nestjs/testing';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { User } from '../models/tables/user';
5 | import { TypeOrmModuleOptions } from '../config/typeorm';
6 | import { UsersController } from '../controllers/users.controller';
7 | import { UsersService } from '../providers/users.service';
8 | import { AuthModule } from '../auth/auth.module';
9 | import { UsersModule } from '../modules/users.module';
10 | import { AuthService } from '../auth/auth.service';
11 |
12 | describe('User Entity', () => {
13 | let controller: UsersController;
14 | let service: UsersService;
15 | let authService: AuthService;
16 | let user;
17 |
18 | beforeAll(async () => {
19 | const module = await Test.createTestingModule({
20 | imports: [
21 | TypeOrmModule.forRootAsync(TypeOrmModuleOptions),
22 | TypeOrmModule.forFeature([User]),
23 | ConfigModule.forRoot({ isGlobal: true }),
24 | UsersModule,
25 | AuthModule,
26 | ],
27 | controllers: [],
28 | providers: [],
29 | }).compile();
30 |
31 | service = module.get(UsersService);
32 | controller = module.get(UsersController);
33 |
34 | authService = module.get(AuthService);
35 | });
36 |
37 | describe('0. 테스트 환경을 확인합니다.', () => {
38 | it('0-1. Service와 Controller 가 정의되어야 합니다.', async () => {
39 | expect(controller).toBeDefined();
40 | expect(service).toBeDefined();
41 | });
42 | });
43 |
44 | describe('1. 유저의 생성 로직을 검증합니다.', () => {
45 | let user: User;
46 |
47 | afterEach(async () => {
48 | const searched = await User.findOne({ where: { id: user.id } });
49 | if (searched) {
50 | await User.remove(searched);
51 | }
52 | });
53 |
54 | it('1-1. 유저를 생성하거나 조회합니다.', async () => {
55 | user = await controller.signUp({
56 | name: 'test',
57 | nickname: 'test',
58 | email: 'test',
59 | password: 'test',
60 | phoneNumber: 'test',
61 | birth: new Date(1997, 10, 6),
62 | gender: true,
63 | smsAdsConsent: true,
64 | emailAdsConsent: true,
65 | });
66 | console.log(user);
67 |
68 | expect(user).toBeDefined();
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | type ListType = {
2 | data: any[];
3 | count: number;
4 | };
5 |
6 | type ListOutputValue = {
7 | totalPage: number;
8 | totalResult: number;
9 | list: any[];
10 | };
11 |
12 | type ExtendedResponse = {
13 | result: boolean;
14 | code: number;
15 | // data: T;
16 | data: T extends ListType ? ListOutputValue : T;
17 | };
18 |
--------------------------------------------------------------------------------
/src/utils/getOffset.ts:
--------------------------------------------------------------------------------
1 | export const NUM_OF_ENTITIES = 10;
2 |
3 | export const getOffset = (page: number) => {
4 | const skip = page > 1 ? NUM_OF_ENTITIES * (page - 1) : 0;
5 | const take = NUM_OF_ENTITIES;
6 | return { skip, take };
7 | };
8 |
--------------------------------------------------------------------------------
/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.api.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | "lib": [
10 | "DOM",
11 | "ES2015"
12 | ] /* Specify library files to be included in the compilation. */,
13 | // "allowJs": true, /* Allow javascript files to be compiled. */
14 | // "checkJs": true, /* Report errors in .js files. */
15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
16 | "declaration": true /* Generates corresponding '.d.ts' file. */,
17 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
18 | // "sourceMap": true, /* Generates corresponding '.map' file. */
19 | // "outFile": "./", /* Concatenate and emit output to single file. */
20 | "outDir": "./packages/api/lib" /* Redirect output structure to the directory. */,
21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
22 | // "composite": true, /* Enable project compilation */
23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
24 | // "removeComments": true, /* Do not emit comments to output. */
25 | // "noEmit": true, /* Do not emit outputs. */
26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
27 | "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */,
28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
29 |
30 | /* Strict Type-Checking Options */
31 | "strict": true /* Enable all strict type-checking options. */,
32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
33 | // "strictNullChecks": true, /* Enable strict null checks. */
34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
39 |
40 | /* Additional Checks */
41 | "noUnusedLocals": true /* Report errors on unused locals. */,
42 | "noUnusedParameters": true /* Report errors on unused parameters. */,
43 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
44 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
45 |
46 | /* Module Resolution Options */
47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
51 | // "typeRoots": [], /* List of folders to include type definitions from. */
52 | // "types": [], /* Type declaration files to be included in compilation. */
53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
57 |
58 | /* Source Map Options */
59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
63 |
64 | /* Experimental Options */
65 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
66 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
67 | "stripInternal": true,
68 |
69 | /* Advanced Options */
70 | "skipLibCheck": true /* Skip type checking of declaration files. */,
71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72 | },
73 | "include": ["src/api"]
74 | }
75 |
--------------------------------------------------------------------------------
/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": "es2020",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "paths": {
15 | "@root/*": ["./src/*"]
16 | }
17 | },
18 | "include": ["./src", "types"],
19 | "exclude": ["node_modules", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.models.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | "lib": [
10 | "DOM",
11 | "ES2015"
12 | ] /* Specify library files to be included in the compilation. */,
13 | // "allowJs": true, /* Allow javascript files to be compiled. */
14 | // "checkJs": true, /* Report errors in .js files. */
15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
16 | "declaration": true /* Generates corresponding '.d.ts' file. */,
17 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
18 | // "sourceMap": true, /* Generates corresponding '.map' file. */
19 | // "outFile": "./", /* Concatenate and emit output to single file. */
20 | "outDir": "./packages/models/lib" /* Redirect output structure to the directory. */,
21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
22 | // "composite": true, /* Enable project compilation */
23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
24 | // "removeComments": true, /* Do not emit comments to output. */
25 | // "noEmit": true, /* Do not emit outputs. */
26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
27 | "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */,
28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
29 |
30 | /* Strict Type-Checking Options */
31 | "strict": true /* Enable all strict type-checking options. */,
32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
33 | // "strictNullChecks": true, /* Enable strict null checks. */
34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
39 |
40 | /* Additional Checks */
41 | "noUnusedLocals": true /* Report errors on unused locals. */,
42 | "noUnusedParameters": true /* Report errors on unused parameters. */,
43 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
44 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
45 |
46 | /* Module Resolution Options */
47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
51 | // "typeRoots": [], /* List of folders to include type definitions from. */
52 | // "types": [], /* Type declaration files to be included in compilation. */
53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
57 |
58 | /* Source Map Options */
59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
63 |
64 | /* Experimental Options */
65 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
66 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
67 | "stripInternal": true,
68 |
69 | /* Advanced Options */
70 | "skipLibCheck": true /* Skip type checking of declaration files. */,
71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72 | },
73 | "include": ["src/models"]
74 | }
75 |
--------------------------------------------------------------------------------