├── .github └── ISSUE_TEMPLATE │ └── 책관련-질문.md ├── .gitignore ├── appendix-typescript ├── 2.10-function-type.ts ├── 2.5-any-void-never.ts ├── 2.6-uniontype-narrowing.ts ├── 2.7-type-alias.ts ├── 3.2-interface-example.ts ├── 3.3-interface-extends.ts ├── 3.4-interface-merge.ts ├── 3.5-class-method-property.ts ├── 3.6-implement-interface-in-class.ts ├── 3.7-abstract-class.ts ├── 3.8-access-control-class.ts ├── 4.1-generic.ts ├── 4.2-generic-interface.ts ├── 4.3-generic-constriants.ts ├── 4.4-decorator.ts ├── 4.5-mapped-type.ts ├── array-and-tuple.ts ├── hello-typescript.ts ├── intersection-type.ts ├── literal-type.ts ├── package-lock.json ├── package.json ├── primitive-types.ts ├── tsconfig.json └── type-annotation.ts ├── assential_keywords.txt ├── backend-roadmap ├── 1.png ├── api.md ├── batch.md ├── database.md ├── deploy.md ├── frameworks.md ├── git-github.md ├── images │ ├── 2023-02-20-19-12-33.png │ └── 2023-02-20-19-31-16.png ├── introduce.md └── what-is-internet.md ├── bookmark-app ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── bookmark.db ├── nest-cli.json ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── bookmark │ │ ├── bookmark.controller.ts │ │ ├── bookmark.entity.ts │ │ ├── bookmark.module.ts │ │ └── bookmark.service.ts │ └── main.ts ├── tsconfig.build.json └── tsconfig.json ├── bun-test ├── .gitignore ├── README.md ├── bun.lockb ├── index.ts ├── package.json └── tsconfig.json ├── chapter0 └── hello-node.js ├── chapter10 └── nest-auth-test │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-auth-test.sqlite │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.guard.ts │ │ ├── auth.http │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── local.strategy.ts │ │ └── session.serializer.ts │ ├── main.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.dto.ts │ │ ├── user.entity.ts │ │ ├── user.http │ │ ├── user.module.ts │ │ └── user.service.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── chapter11 └── nest-auth-test │ ├── .prettierrc │ ├── README.md │ ├── auth-test.sqlite │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.guard.ts │ │ ├── auth.http │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── google.strategy.ts │ │ ├── local.strategy.ts │ │ └── session.serializer.ts │ ├── main.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.dto.ts │ │ ├── user.entity.ts │ │ ├── user.http │ │ ├── user.module.ts │ │ └── user.service.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── chapter12 └── nest-file-upload │ ├── .prettierrc │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── cat.jpg │ ├── file-upload.http │ ├── main.ts │ ├── multer.options.ts │ └── test.txt │ ├── static │ └── form.html │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── uploads │ ├── 123fbb67-42fe-4e6c-bf94-c6674e102fda.jpg │ └── 3f29bb4b-2a21-452d-9060-c40f007e789d.txt ├── chapter13 ├── echo-websocket │ ├── client.html │ ├── package-lock.json │ ├── package.json │ └── server.js ├── nest-chat │ ├── .eslintrc.js │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.controller.ts │ │ ├── app.gateway.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ └── main.ts │ ├── static │ │ ├── index.html │ │ └── script.js │ ├── tsconfig.build.json │ └── tsconfig.json ├── package-lock.json ├── package.json └── simple-nest-chat │ ├── .eslintrc.js │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.ts │ ├── app.gateway.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts │ ├── static │ └── index.html │ ├── tsconfig.build.json │ └── tsconfig.json ├── chapter2 ├── callstack.js ├── callstackWithEventloop.js ├── doWorkWithCallback.js ├── hello.js └── test_hello.js ├── chapter3 ├── code3-1-ok-server.js ├── code3-2-implement-router.js ├── code3-3-implement-router.js ├── code3-4-implement-router2.js ├── code3-5-add-server-error.js ├── code3-5-refactoring-router.js ├── express-server │ ├── board.js │ ├── hello-express.js │ ├── package-lock.json │ ├── package.json │ └── refactoring-to-express.js └── simpleServer.js ├── chapter4 ├── npm-install-test │ ├── package-lock.json │ └── package.json ├── npm-test │ ├── package-lock.json │ └── package.json ├── sample-package │ ├── index.js │ └── package.json ├── sample-test │ ├── index.js │ ├── package-lock.json │ └── package.json ├── test-npx │ ├── index.js │ ├── package-lock.json │ └── package.json ├── test-package-lock │ ├── index.js │ ├── package-lock.json │ └── package.json ├── test-scripts │ └── package.json └── test-yarn │ ├── .editorconfig │ ├── .gitignore │ ├── .pnp.cjs │ ├── .pnp.loader.mjs │ ├── .yarn │ ├── cache │ │ ├── ansi-styles-npm-6.1.0-4f6a594d04-7a7f8528c0.zip │ │ ├── chalk-https-da1a746d42-5804e5429a.zip │ │ └── supports-color-npm-9.2.1-1ef7bf7d73-8a2bfeb64c.zip │ └── releases │ │ └── yarn-3.1.1.cjs │ ├── .yarnrc.yml │ ├── README.md │ ├── index.js │ ├── main.js │ ├── package.json │ └── yarn.lock ├── chapter5 └── callback-promise-async-await │ ├── async-await.js │ ├── callback-test.js │ ├── ideal-promise-code.js │ ├── package-lock.json │ ├── package.json │ ├── promise-anti-pattern1.js │ ├── promise-anti-pattern2.js │ ├── promise-test.js │ ├── promise-test2.js │ ├── top20-movie-async-await.js │ └── top20-movie-promise-code.js ├── chapter6 ├── test-mongoose │ ├── http-client.env.json │ ├── mongoose-crud.js │ ├── package-lock.json │ ├── package.json │ ├── person-model.js │ └── person.http └── try-mongo │ ├── mongo-crud.js │ ├── package-lock.json │ ├── package.json │ ├── test-connection.js │ └── test-crud.js ├── chapter7 ├── board-tailwind │ ├── app.js │ ├── configs │ │ ├── handlebars-helpers.js │ │ └── mongodb-connection.js │ ├── package-lock.json │ ├── package.json │ ├── services │ │ └── post-service.js │ ├── src │ │ ├── base.css │ │ └── index.html │ ├── statics │ │ └── base.css │ ├── utils │ │ └── paginator.js │ └── views │ │ ├── detail.handlebars │ │ ├── home.handlebars │ │ ├── layouts │ │ └── main.handlebars │ │ └── write.handlebars └── board │ ├── app.js │ ├── configs │ ├── handlebars-helpers.js │ ├── mongodb-connection.js │ └── mongoose-connection.js │ ├── package-lock.json │ ├── package.json │ ├── services │ └── post-service.js │ ├── utils │ └── paginator.js │ └── views │ ├── detail.handlebars │ ├── home.handlebars │ ├── layouts │ └── main.handlebars │ └── write.handlebars ├── chapter8 ├── blog-file │ ├── .eslintrc.js │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.module.ts │ │ ├── blog.controller.ts │ │ ├── blog.data.json │ │ ├── blog.http │ │ ├── blog.model.ts │ │ ├── blog.repository.ts │ │ ├── blog.service.ts │ │ └── main.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── blog-memory │ ├── .eslintrc.js │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.module.ts │ │ ├── blog.controller.ts │ │ ├── blog.http │ │ ├── blog.model │ │ ├── blog.model.ts │ │ ├── blog.service.ts │ │ └── main.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── blog-mongodb │ ├── .eslintrc.js │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.module.ts │ │ ├── blog.controller.ts │ │ ├── blog.data.json │ │ ├── blog.http │ │ ├── blog.model.ts │ │ ├── blog.repository.ts │ │ ├── blog.schema.ts │ │ ├── blog.service.ts │ │ └── main.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── hello-nestjs-javascript │ ├── .babelrc │ ├── index.js │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── main.js └── hello-nestjs │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── hello.controller.ts │ ├── hello.module.ts │ └── main.ts │ └── tsconfig.json ├── chapter9 ├── config-test │ ├── .eslintrc.js │ ├── .prettierrc │ ├── README.md │ ├── envs │ │ ├── config.yaml │ │ ├── dev.env │ │ ├── local.env │ │ └── prod.env │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── configs │ │ │ ├── common.ts │ │ │ ├── config.ts │ │ │ ├── dev.ts │ │ │ ├── local.ts │ │ │ └── prod.ts │ │ ├── main.ts │ │ └── weather │ │ │ ├── weather.controller.ts │ │ │ └── weather.module.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json └── weather_api_test │ ├── .env │ ├── .eslintrc.js │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── main.ts │ └── weather │ │ ├── weather.controller.ts │ │ └── weather.module.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── movieinfo.json └── nodejs20_major_features ├── env-test ├── .env └── env-test.js ├── esm-loader-hook ├── andy.mjs ├── main.js ├── package.json ├── register.js └── url-loader.mjs ├── permission-model ├── file-permission.js └── test.txt ├── recursive-read-dirs ├── dir1 │ ├── dir2 │ │ ├── dir3 │ │ │ ├── test3.txt │ │ │ └── test4.txt │ │ └── test1.txt │ └── test2.txt ├── package.json └── read-directories.js ├── sea ├── hello ├── hello.js ├── sea-config.json └── sea-prep.blob ├── test-runner ├── calculator.js ├── calculator.test.js ├── http-request.js ├── http-request.test.js ├── logger.js └── package.json └── v8_upgrade.js /.github/ISSUE_TEMPLATE/책관련-질문.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 책관련 질문 3 | about: 'Node.js 백엔드 개발자 되기 책 관련 질문 ' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: 책 관련 질문 12 | title: "[챕터] 질문 " 13 | labels: book-question 14 | --- 15 | ** 페이지 ** 16 | 17 | 18 | ** 질문 ** 19 | 20 | 21 | ** 스크릿 샷 (옵션) ** 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | node_modules 3 | dist/* 4 | dist 5 | # compiled output 6 | /dist 7 | /node_modules 8 | 9 | .env 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | pnpm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | lerna-debug.log* 19 | 20 | # OS 21 | .DS_Store 22 | 23 | # Tests 24 | /coverage 25 | /.nyc_output 26 | 27 | # IDEs and editors 28 | /.idea 29 | .project 30 | .classpath 31 | .c9/ 32 | *.launch 33 | .settings/ 34 | *.sublime-workspace 35 | 36 | # IDE - VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | -------------------------------------------------------------------------------- /appendix-typescript/2.10-function-type.ts: -------------------------------------------------------------------------------- 1 | function echo(message: string): string { 2 | console.log(message); 3 | return message; 4 | } 5 | 6 | const funcEcho: (message: string) => string = echo; 7 | funcEcho("test"); 8 | 9 | type FuncEcho = (message: string) => string; 10 | const funcEcho2: FuncEcho = echo; 11 | funcEcho2("test2"); 12 | 13 | type FuncEcho3 = { 14 | (message: string): string; 15 | }; 16 | const funcEcho3: FuncEcho3 = echo; 17 | funcEcho3("test3"); 18 | // funcEcho3(123); // 타입 에러 19 | -------------------------------------------------------------------------------- /appendix-typescript/2.5-any-void-never.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | let anyValue: any = 10; 4 | print(anyValue); 5 | anyValue = "hello"; 6 | print(anyValue); 7 | anyValue = true; 8 | print(anyValue); 9 | 10 | function print(value: any): void { 11 | console.log(value); 12 | } 13 | 14 | function throwError(message: string): never { 15 | throw new Error(message); 16 | } 17 | 18 | function infiniteLoop(): never { 19 | while (true) {} 20 | } 21 | -------------------------------------------------------------------------------- /appendix-typescript/2.6-uniontype-narrowing.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | let anyValue: number | string | boolean = 10; 4 | printAny(anyValue); 5 | anyValue = "hello"; 6 | printAny(anyValue); 7 | anyValue = true; 8 | printAny(anyValue); 9 | 10 | function printAny(value: number | string | boolean): void { 11 | if (typeof value === "number") { 12 | console.log(value.toExponential(3)); 13 | } else if (typeof value === "string") { 14 | console.log(value.toUpperCase()); 15 | } else if (typeof value === "boolean") { 16 | console.log(value ? "참" : "거짓"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /appendix-typescript/2.7-type-alias.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | // 타입 별칭 4 | type nsb = number | string | boolean; 5 | 6 | let anyValue: nsb = 10; 7 | anyValue = "hello"; 8 | anyValue = true; 9 | anyValue = null; 10 | 11 | // 타입 별칭에 null, undefined 추가 12 | type nullableNsb = nsb | null; 13 | 14 | let nullableValue: nullableNsb = null; 15 | nullableValue = 20; 16 | nullableValue = "nullable"; 17 | nullableValue = false; 18 | nullableValue = undefined; 19 | -------------------------------------------------------------------------------- /appendix-typescript/3.2-interface-example.ts: -------------------------------------------------------------------------------- 1 | type BookType = { 2 | title: string; 3 | price: number; 4 | author: string; 5 | }; 6 | 7 | interface Book { 8 | title: string; 9 | price: number; 10 | author: string; 11 | } 12 | 13 | let bookType: BookType = { 14 | title: "백엔드 개발자 되기", 15 | price: 10000, 16 | author: "박승규", 17 | }; 18 | 19 | let book: Book = { 20 | title: "백엔드 개발자 되기", 21 | price: 10000, 22 | author: "박승규", 23 | }; 24 | 25 | let wrongBookType: BookType = { 26 | title: "백엔드 개발자 되기", 27 | price: 10000, 28 | author: 1234, 29 | }; 30 | 31 | let wrongBook: Book = { 32 | title: "백엔드 개발자 되기", 33 | price: 10000, 34 | author: 1234, 35 | }; 36 | 37 | interface Car { 38 | name: string; 39 | price: number; 40 | brand: string; 41 | options?: string[]; 42 | } 43 | 44 | let avante: Car = { 45 | name: "아반떼", 46 | price: 1500, 47 | brand: "현대", 48 | options: ["에어컨", "네비게이션"], 49 | }; 50 | 51 | let morning: Car = { 52 | name: "모닝", 53 | price: 650, 54 | brand: "기아", 55 | }; 56 | 57 | interface Citizen { 58 | id: string; 59 | name: string; 60 | region: string; 61 | readonly age: number; 62 | } 63 | 64 | let seungkyoo: Citizen = { 65 | id: "123456", 66 | name: "박승규", 67 | region: "경기", 68 | age: 40, 69 | }; 70 | 71 | seungkyoo.age = 39; // Error 72 | -------------------------------------------------------------------------------- /appendix-typescript/3.3-interface-extends.ts: -------------------------------------------------------------------------------- 1 | interface WebtoonCommon { 2 | title: string; 3 | createdDate: Date; 4 | updatedDate: Date; 5 | } 6 | 7 | interface Episode extends WebtoonCommon { 8 | episodeNumber: number; 9 | seriesNumber: number; 10 | } 11 | 12 | interface Series extends WebtoonCommon { 13 | seriesNumber: number; 14 | author: string; 15 | } 16 | 17 | const episode: Episode = { 18 | title: "나 혼자도 레벨업 1화", 19 | createdDate: new Date(), 20 | updatedDate: new Date(), 21 | episodeNumber: 1, 22 | seriesNumber: 123, 23 | }; 24 | 25 | const series: Series = { 26 | title: "나 혼자도 레벨업", 27 | createdDate: new Date(), 28 | updatedDate: new Date(), 29 | seriesNumber: 123, 30 | author: "천재작가", 31 | }; 32 | -------------------------------------------------------------------------------- /appendix-typescript/3.4-interface-merge.ts: -------------------------------------------------------------------------------- 1 | interface Clock { 2 | time: Date; 3 | } 4 | 5 | interface Clock { 6 | brand: string; 7 | } 8 | 9 | interface Clock { 10 | price: number; 11 | } 12 | 13 | /** 14 | * 내부적으로는 다음과 같이 정의가 병합된다. 15 | * 16 | * interface Clock { 17 | * time: Date; 18 | * brand: string; 19 | * price: number; 20 | * } 21 | * 22 | */ 23 | 24 | const wrongClock: Clock = { 25 | time: new Date(), 26 | }; 27 | 28 | const clock: Clock = { 29 | time: new Date(), 30 | brand: "놀렉스", 31 | price: 10000, 32 | }; 33 | -------------------------------------------------------------------------------- /appendix-typescript/3.5-class-method-property.ts: -------------------------------------------------------------------------------- 1 | // 3.5 클래스의 메서드와 속성 2 | 3 | class Hello { 4 | // 생성자 5 | constructor() { 6 | this.sayHello("created"); 7 | } 8 | 9 | // 메서드 10 | sayHello(message: string) { 11 | console.log(message); 12 | } 13 | } 14 | 15 | const hello = new Hello(); 16 | hello.sayHello("안녕하세요~"); 17 | 18 | // 사각형 클래스 19 | class Rectangle { 20 | width: number; 21 | height: number; 22 | 23 | constructor(width: number, height: number) { 24 | this.width = width; 25 | this.height = height; 26 | } 27 | 28 | getArea() { 29 | return this.width * this.height; 30 | } 31 | } 32 | 33 | const rectangle = new Rectangle(10, 5); 34 | rectangle.getArea(); 35 | -------------------------------------------------------------------------------- /appendix-typescript/3.6-implement-interface-in-class.ts: -------------------------------------------------------------------------------- 1 | interface IClicker { 2 | count: number; 3 | click():number; 4 | } 5 | 6 | class Clicker implements IClicker { 7 | // count의 기본값(0)을 넣어준다. 8 | count: number = 0; 9 | 10 | click(): number { 11 | this.count += 1 12 | console.log(`Click! [count] : ${this.count}`); 13 | return this.count; 14 | } 15 | } 16 | 17 | const clicker = new Clicker(); 18 | clicker.click(); 19 | clicker.click(); 20 | clicker.click(); 21 | -------------------------------------------------------------------------------- /appendix-typescript/3.7-abstract-class.ts: -------------------------------------------------------------------------------- 1 | abstract class Logger { 2 | 3 | prepare() { 4 | console.log("=======================") 5 | console.log("로그를 남기기 위한 준비") 6 | } 7 | 8 | log(message: string) { 9 | this.prepare(); 10 | this.execute(message); 11 | this.complete(); 12 | }; 13 | 14 | // 추상 메서드는 함수명 앞에 abstract 키워드를 붙이며 구현부가 없다. 15 | abstract execute(message: string): void; 16 | 17 | complete() { 18 | console.log("작업 완료") 19 | console.log("") 20 | } 21 | } 22 | 23 | 24 | class FileLogger extends Logger { 25 | filename: string; 26 | 27 | constructor(filename:string) { 28 | super(); // 상속을 받은 경우 생성자에서 super()를 먼저 실행해야한다. 29 | this.filename = filename; 30 | } 31 | 32 | execute(message: string): void { 33 | // 파일에 직접 쓰지는 않지만 쓴다고 가정 34 | console.log(`[${this.filename}] > `, message); 35 | } 36 | } 37 | 38 | class ConsoleLogger extends Logger { 39 | execute(message: string): void { 40 | console.log(message); 41 | } 42 | } 43 | 44 | const fileLogger = new FileLogger("test.log"); 45 | fileLogger.log("파일에 로그 남기기 테스트") 46 | 47 | 48 | const consoleLogger = new ConsoleLogger(); 49 | consoleLogger.log("로그 남기기") 50 | -------------------------------------------------------------------------------- /appendix-typescript/3.8-access-control-class.ts: -------------------------------------------------------------------------------- 1 | class Parent { 2 | openInfo = "공개 정보" 3 | protected lagacy = "유산"; 4 | private parentSecret = "부모의 비밀 정보"; 5 | 6 | checkMySecret() { 7 | console.log(this.parentSecret); 8 | } 9 | } 10 | 11 | class Child extends Parent{ 12 | private secret = "자녀의 비밀 정보"; 13 | 14 | // protected 확인 가능 15 | checkLagacy() { 16 | console.log(super.lagacy); 17 | } 18 | 19 | checkParentSecret() { 20 | // console.log(super.parentSecret); 21 | } 22 | } 23 | 24 | class Someone { 25 | checkPublicInfo() { 26 | const p = new Parent(); 27 | console.log(p.openInfo); 28 | // console.log(p.lagacy) 29 | // console.log(p.parentSecret) 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /appendix-typescript/4.1-generic.ts: -------------------------------------------------------------------------------- 1 | function echo(message: any) : any { 2 | console.log(message); 3 | return message; 4 | } 5 | 6 | type phone = { 7 | name: string, 8 | price : number, 9 | brand: string, 10 | } 11 | 12 | const myPhone = {name: "iPhone", price: 1000, brand: "Apple"} 13 | 14 | echo(1); 15 | echo("안녕"); 16 | echo(myPhone); 17 | 18 | function genericEcho(message: T) : T { 19 | console.log(message); 20 | return message; 21 | } 22 | 23 | genericEcho(1) // 없는 경우 컴파일러가 타입 추론 24 | genericEcho("안녕") // 타입을 명시적으로 지정 25 | genericEcho(myPhone); // any를 타입으로 넣으면 제네릭을 쓸 이유가 없다. 26 | // genericEcho(myPhone); // 타입이 달라서 에러 발생 -------------------------------------------------------------------------------- /appendix-typescript/4.2-generic-interface.ts: -------------------------------------------------------------------------------- 1 | interface ILabel { 2 | label:Type; 3 | } 4 | 5 | const stringLabel:ILabel = { 6 | label: "Hello" 7 | } 8 | 9 | const numberLabel:ILabel = { 10 | label: 100 11 | } 12 | 13 | // const booleanLabel:ILabel = { 14 | // label: 3.14 15 | // } 16 | 17 | -------------------------------------------------------------------------------- /appendix-typescript/4.3-generic-constriants.ts: -------------------------------------------------------------------------------- 1 | interface ICheckLength { 2 | length: number; 3 | } 4 | 5 | function echoWithLength(message: T){ 6 | console.log(message); 7 | } 8 | 9 | echoWithLength("Hello"); 10 | echoWithLength([1,2,3]); 11 | echoWithLength({length: 10}); 12 | // echoWithLength(10); // number는 length가 없기 때문에 에러 발생 13 | 14 | 15 | // 문자열과 숫자만 지원하는 echoWithLength2 함수 16 | function echoWithLength2(message: T){ 17 | console.log(message); 18 | } 19 | 20 | echoWithLength2("Hello"); 21 | echoWithLength2(10); 22 | // echoWithLength2([1,2,3]); // 배열은 문자열과 숫자가 아니기 때문에 에러 발생 -------------------------------------------------------------------------------- /appendix-typescript/4.4-decorator.ts: -------------------------------------------------------------------------------- 1 | type Constructor = new (...args: any[]) => {}; 2 | function HelloDecorator(constructor: T) { 3 | console.log(constructor); 4 | return class extends constructor { 5 | constructor(...args: any[]) { 6 | super(...args); 7 | console.log(`Hello Decorator`); 8 | } 9 | }; 10 | } 11 | 12 | @HelloDecorator 13 | class DecoratorTest { 14 | constructor() { 15 | console.log(`인스턴스 생성됨`); 16 | } 17 | } 18 | 19 | // const decoTest = new DecoratorTest(); 20 | 21 | // console.time("실행 시간"); 22 | // execute(); 23 | function execute() { 24 | setTimeout(() => { 25 | console.log(`실행`); 26 | console.timeEnd("실행 시간"); 27 | }, 500); 28 | } 29 | 30 | function Timer() { 31 | return function (target: any, key: string, descriptor: PropertyDescriptor) { 32 | const originalMethod = descriptor.value; 33 | descriptor.value = function (...args: any[]) { 34 | console.time(`Elapsed time`); 35 | const result = originalMethod.apply(this, args); // 이 때의 this는 클래스의 인스턴스 36 | console.timeEnd(`Elapsed time`); 37 | return result; 38 | }; 39 | }; 40 | } 41 | 42 | class ElapsedTime { 43 | someVar = "test"; 44 | 45 | @Timer() 46 | hello() { 47 | console.log(`Hello`); 48 | } 49 | } 50 | 51 | new ElapsedTime().hello(); 52 | 53 | function NamedTimer(label: string) { 54 | return function (target: any, key: string, descriptor: PropertyDescriptor) { 55 | const originalMethod = descriptor.value; 56 | descriptor.value = function (...args: any[]) { 57 | console.time(label); 58 | const result = originalMethod.apply(this, args); 59 | console.timeEnd(label); 60 | return result; 61 | }; 62 | }; 63 | } 64 | 65 | class NamedElapsedTime { 66 | @NamedTimer(`헬로 시간 측정`) 67 | hello() { 68 | console.log(`Hello`); 69 | } 70 | } 71 | 72 | new NamedElapsedTime().hello(); -------------------------------------------------------------------------------- /appendix-typescript/4.5-mapped-type.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | type Feature = { 4 | event: string; 5 | coupon: string; 6 | } 7 | type FeatureKeys = keyof Feature; // 'event' | 'coupon' 과 동일 8 | 9 | const aEvent:FeatureKeys = 'event' ; 10 | const aCoupon: FeatureKeys = 'coupon' ; 11 | // const aSale:FeatureKeys = 'sale'; // 컴파일 에러 12 | 13 | type FeaturePermission = { [key in keyof Feature]?: boolean }; 14 | 15 | // utility type 16 | type PartialFeature = Partial; 17 | 18 | // readonly 19 | type ReadonlyFeature = Readonly; -------------------------------------------------------------------------------- /appendix-typescript/array-and-tuple.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | const numbers: number[] = [1, 2, 3, 4, 5]; 4 | const numbers2: number[] = [6, 7, 8, 9, 10]; 5 | const stringArray: Array = ["a", "b", "c", "d", "e"]; 6 | 7 | // 스프레드 연산자로 합치기 가능 8 | const oneToTen = [...numbers, ...numbers2]; 9 | console.log(...oneToTen); 10 | 11 | const idols: { name: string; birth: number }[] = [ 12 | { name: "minji", birth: 2004 }, 13 | { name: "hani", birth: 2004 }, 14 | { name: "danielle", birth: 2005 }, 15 | { name: "haerin", birth: 2006 }, 16 | { name: "hyein", birth: 2008 }, 17 | ]; 18 | 19 | const gameConsoleArray: Array<{ name: string; launch: number }> = [ 20 | { name: "플레이스테이션5", launch: 2020 }, 21 | { name: "엑스박스 시리즈 X/S", launch: 2020 }, 22 | { name: "닌텐도 스위치", launch: 2017 }, 23 | { name: "스팀덱", launch: 2021 }, 24 | ]; 25 | 26 | const myTuple: [string, number] = ["seungkyoo", 179]; 27 | 28 | // 튜플은 함수의 파라메터가 여러개 일 때 유용 29 | function printMyInfo(label: string, info: [string, number]): void { 30 | console.log(`[${label}]`, ...info); 31 | } 32 | 33 | printMyInfo("튜플 테스트", myTuple); 34 | 35 | // 튜플을 리턴하는 함수 36 | function fetchUser(): [string, number] { 37 | return ["seungkyoo", 179]; 38 | } 39 | 40 | // 결괏값을 분해해서 받을 수 있음 41 | const [name24, height24] = fetchUser(); 42 | console.log(name24, height24); 43 | -------------------------------------------------------------------------------- /appendix-typescript/hello-typescript.ts: -------------------------------------------------------------------------------- 1 | const message: string = "Hello World"; 2 | console.log(message); 3 | -------------------------------------------------------------------------------- /appendix-typescript/intersection-type.ts: -------------------------------------------------------------------------------- 1 | type cup = { 2 | size: string; 3 | }; 4 | 5 | type brand = { 6 | brandName: string; 7 | }; 8 | 9 | type brandedCup = cup & brand; 10 | 11 | let starbucksGrandeSizeCup: brandedCup = { 12 | brandName: "스타벅스", 13 | size: "grande", 14 | }; 15 | 16 | type impossible = number & string; 17 | let testImpossible: impossible = 10; // Error 18 | -------------------------------------------------------------------------------- /appendix-typescript/literal-type.ts: -------------------------------------------------------------------------------- 1 | type CoffeeSize = "small" | "medium" | "large"; 2 | 3 | let myCoffeeSize: CoffeeSize = "small"; 4 | let starbucksCoffeeSize: CoffeeSize = "tall"; // 타입에러 5 | 6 | type OneToFive = 1 | 2 | 3 | 4 | 5; 7 | const rightNumber: OneToFive = 1; 8 | const wrongNumber: OneToFive = 6; // 타입에러 9 | -------------------------------------------------------------------------------- /appendix-typescript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appendix-typescript", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "typescript": "^4.9.5" 9 | } 10 | }, 11 | "node_modules/typescript": { 12 | "version": "4.9.5", 13 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 14 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 15 | "bin": { 16 | "tsc": "bin/tsc", 17 | "tsserver": "bin/tsserver" 18 | }, 19 | "engines": { 20 | "node": ">=4.2.0" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "typescript": { 26 | "version": "4.9.5", 27 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 28 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /appendix-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "typescript": "^4.9.5" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /appendix-typescript/primitive-types.ts: -------------------------------------------------------------------------------- 1 | const one: number = 1; 2 | const myName: string = "seungkyoo"; 3 | const trueOrFalse: boolean = true; 4 | const unIntended: undefined = undefined; 5 | const nullable: null = null; 6 | const bigNumber: bigint = 1234567890123456789012345678901234567890n; 7 | const symbolValue: symbol = Symbol("symbol"); 8 | 9 | console.log(one + 1); 10 | console.log(myName + " is my name"); 11 | console.log(trueOrFalse ? "true" : "false"); 12 | console.log(bigNumber / 10000000000000000n); 13 | console.log(symbolValue === Symbol("symbol")); 14 | -------------------------------------------------------------------------------- /appendix-typescript/type-annotation.ts: -------------------------------------------------------------------------------- 1 | // 변수에 타입을 지정하는 방법 2 | let username: string = "seungkyoo"; 3 | let height: number = 179; 4 | let isConditionGood: boolean = true; 5 | 6 | // 함수의 파라미터에 타입을 지정 7 | function printMessage(message: string): void { 8 | console.log(message); 9 | } 10 | 11 | // 객체의 타입을 지정하는 방법 12 | let myInfo: { name: string; height: number; isConditionGood: boolean } = { 13 | name: "seungkyoo", 14 | height: 179, 15 | isConditionGood: true, 16 | }; 17 | 18 | let myInfoWithGender: { 19 | name: string; 20 | height: number; 21 | isConditionGood: boolean; 22 | gender?: string; 23 | } = { 24 | name: "seungkyoo", 25 | height: 179, 26 | isConditionGood: true, 27 | }; 28 | 29 | // isCritical 값은 옵션 30 | function printMessageWithAlert(message: string, isCritical?: boolean): void { 31 | console.log(message); 32 | 33 | if (isCritical) { 34 | alert(message); 35 | } 36 | } 37 | 38 | console.log(typeof myInfo); 39 | -------------------------------------------------------------------------------- /assential_keywords.txt: -------------------------------------------------------------------------------- 1 | 1장 2 | 백엔드 개발, 개발, 배포, QA, 유지보수 3 | 아키텍처, 계층형 아키텍처, 이벤트 기반 아키텍처, 마이크로서비스 아키텍처 4 | 데이터베이스, 트랜잭션, 무결성, ACID, 캐시, NoSQL 5 | 인프라, 클라우드, 빌드, 네트워크 6 | 자바스크립트, npm, 리액트, NestJS 7 | 8 | 2장 9 | Node.js, 자바스크립트 런타임, 싱글 스레드, 이벤트 기반 아키텍처 10 | V8엔진, 컴파일러, 인터프리터, 이그니션, 터보팬, JIT컴파일러 11 | libuv, 이벤트 루프, 스레드 풀 12 | 13 | 3장 14 | Express, 웹 프레임워크, 라우팅, 미들웨어 15 | REST API, HTTP 메서드, HTTP 헤더 16 | 17 | 4장 18 | 패키지 매니저, npm, npx, 패키지 스코프, 시맨틱 버전 19 | 의존성 트리, 패키지잠금, 유령 의존성 20 | yarn, PnP, 제로 인스톨 21 | 22 | 5장 23 | 비동기 처리, 콜백 24 | 프로미스, 이행, 거절, 연기 25 | 어싱크 어웨이트, 가독성 26 | 27 | 6장 28 | 데이터베이스, RDB, NoSQL, 키-밸류, 컬럼, 그래프 29 | 몽고디비, 도큐먼트, 컬렉션, 클러스터, 샤드, BSON, 아틀라스, 콤파스, 몽구스 30 | 31 | 7장 32 | 게시판, 글쓰기, 글수정, 글삭제 33 | 리스트, 페이지네이션, 필터 34 | 상세 페이지, 프로젝션, 조회수 35 | 템플릿 엔진, 핸들바, 렌더링, 커스텀 헬퍼 함수 36 | 서버 기동, 재시작, nodemon 37 | 38 | 8장 39 | NestJS, 프레임워크, 의존성 주입, 데코레이터 40 | 웹 API, 모듈, 컨트롤러, 프로바이더, Rest 클라이언트 41 | 타입스크립트, 인터페이스 42 | 몽고디비, 리포지터리, 스키마, DTO 43 | 44 | 9장 45 | 환경 설정, dotenv, YAML, 확장 변수, 커스텀 환경 설정, NODE_ENV 46 | 서버 기동, bootstrap, 초기화 및 환경 변수 병합, ConfigModule, ConfigSerivce 47 | 48 | 10장 49 | 회원 가입, bcrypt 50 | 인증, 인가, 쿠키, 토큰, 세션, JWT 51 | 유효성 검증, 파이프, ValidationPipe, class-validator, 가드 52 | 패스포트, 스트래티지, 세션 시리얼라이저, 믹스인 53 | 데이터베이스, SQLite, TypeORM, 엔티티, 리포지터리, 서비스 54 | 55 | 11장 56 | OAuth, 액세스 토큰, 리프레시 토큰, providerId 57 | 패스포트, GoogleStrategy, GoogleAuthGuard, 리다이렉트 58 | 59 | 12장 60 | 파일 업로드, 멀티파트, 컨텐트 디스포지션 61 | 멀터, 스토리지, 인터셉터, 파일 인터셉터 62 | 정적 파일 서비스, 서버 스태틱 모듈, NestExpressApplication 63 | 64 | 13장 65 | 실시간 웹 애플리케이션, 폴링, 롱폴링, 웹소켓 66 | socket.io, 채팅방, 브로드캐스팅, 네임스페이스 67 | 게이트웨이, @WebSocketGateway -------------------------------------------------------------------------------- /backend-roadmap/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/backend-roadmap/1.png -------------------------------------------------------------------------------- /backend-roadmap/api.md: -------------------------------------------------------------------------------- 1 | API를 작성할 때 많은 경우 REST 방법으로 API를 작성합니다. 2 | 3 | { 4 | user(id: "1000") { 5 | name 6 | createDt 7 | } 8 | } -------------------------------------------------------------------------------- /backend-roadmap/batch.md: -------------------------------------------------------------------------------- 1 | 배치 처리에 대해서 알아보겠습니다. 2 | 배치 처리는 일정기간동안 데이터를 모아두었다가 대량의 데이터작업을 한번에 하는 것을 의미합니다. 또한 일정시간마다 주기적으로 실행 해야하는 작업을 말하기도합니다. 개발적으로 얘기한다면 작업을 스케줄링 하는 것을 의미합니다. 3 | 4 | 배치처리를 하는 가장 간단한 방법은 OS의 스케줄링 기능을 사용하는 것입니다. 윈도우에는 작업스케줄러라는 기능이 있고, 리눅스에는 크론탭이 있습니다. 5 | 리눅스의 크론탭만 간단하게 살펴보겠습니다. 6 | 7 | crontab -l 을 실행하면 현재 설정되어 있는 스케줄 잡이 보입니다. 8 | 수정은 crontab -e 로 들어가서 수정할 수 있습니다. 9 | 10 | 간단하게 우분투에서 한번 만들어보겠습니다. 11 | 12 | * * * * * echo "hello crontab $(date '+\%Y-\%m-\%d \%H:\%M:\%S')" >> /home/ubuntu/tmp/hello.txt -------------------------------------------------------------------------------- /backend-roadmap/deploy.md: -------------------------------------------------------------------------------- 1 | 배포라는 단어를 사용했지만, 실제로는 배포에 관련된 모든 업무를 뜻합니다. 2 | 아마도 괜찮은 회사라면 배포 도구를 사용해서 배포를 진행할 것입니다. 3 | 도구가 없다면, ftp를 사용해서 수작업으로 압축파일을 업로드 하고 있을 수도 있을 것입니다. 4 | 5 | 배포는 수작업으로 하기 보다는 도구를 사용하여 배포하는 것이 좋습니다. 반복적인 작업이지만, 매우 중요한 작업이기 때문에 실수하면 안되기 때문입니다. 6 | 7 | 손으로 하는 배포의 다음에는 스크립트를 사용해서 배포하는 방법이 있습니다. 다음으로는 스크립트를 실행해주는 도구를 사용하는 것입니다. 대표적으로 젠킨스, 서클CI등이 있습니다. 최근에는 깃헙액션이나, argoCD같은 프로그램들이 있을 수 있겠습니다. 8 | 9 | CI와 CD는 둘다 continuous integration, continuous deploy로 지속적으로 통합하고 지속적으로 배포한다는 한국말로는 약간이해하기 어려운 단어로 되어 있습니다. 10 | 11 | 쉽게 얘기하면 CI는 배포하기 전의 모든 단계를 자동화 하는 것을 의미합니다. 12 | 그러면 CD는 배포에 대한 것들을 자동화 하는 것을 말합니다. 13 | 배포도 그냥 하는 것이 아니라 서버여러대에 배포하는 경우에 몇대씩 나눠서 배포하는 롤링배포, 똑같은 수의 서버를 미리 띄워두고 게이트웨이만 변경하는 블루/그린배포, 특정 퍼센트의 서버에만 배포한다음 문제없으면 점차 배포를 확대하는 카나리 배포등 꽤나 복잡한 작업이 필요합니다. -------------------------------------------------------------------------------- /backend-roadmap/frameworks.md: -------------------------------------------------------------------------------- 1 | 프레임워크는 개발시에 필요한 것들을 미리 만들어줘서, 작업을 편하게 해줍니다. 2 | 개발시의 설정을 어떻게 하고, 코드는 어떻게 짜야하는지까지 제한을 하는 프레임워크가 있는반면, 3 | 최소한의 설정과 제약만을 가진 프레임워크도 있습니다. 4 | 제가 설명드리는 프레임워크는 모두 서버측에서 API를 만드는데 필요한 프레임워크입니다. 5 | 6 | Node.js에는 express가 대표적이지만, NestJS를 적어두었습니다. 7 | express는 최소한의 제약만을 주고 서드파티 라이브러리로 확장해나가는 프레임워크입니다. 8 | 반면 NestJS는 아키텍처구조를 어느정도 잡아줍니다. 9 | 나름 최신기능인 데코레이터를 사용하여 별도의 설정없이 라우터나 미들웨어들을 설정할 수 있게 해줍니다. 10 | 내부적으로 express와 fastify를 사용하고 있기 때문에, 기존의 express의 미들웨어를 사용할 수 있는 장점이 있습니다. 11 | 하지만, 아직까지는 상용 프로그램을 위한 프레임워크로 보자면 자바의 스프링에 비해서는 보안이나 인증, 인가, 배치 같은 부분에서 아쉬운 부분이 있습니다. 12 | 최근에 express가 발전이 미미하기 때문에 개인적으로는 NestJS가 조금더 발전 가능성이 있다고 생각하여 NestJS를 소개드렸습니다. 13 | 14 | 다음으로 자바진영의 스프링입니다. 스프링은 기업용 애플리케이션을 만드는데에 필요한 것들을 모두 갖추고 있는 프레임워크입니다. Spring webflux라고 불리는 리액티브 프로그래밍에서는 로그 추적이 조금 귀찮아진다던지, 멀티코어 활용이 어렵다던지 하는 아쉬운 부분이 있습니다만, 제가 사용해본 프레임워크중에는 기업용 애플리케이션을 작성하는데에는 가장 지원이 잘 되어 있는 프레임워크입니다. 다만 스프링은 태생자체가 기업용 애플리케이션을 작성을 돕는 프레임워크였기 때문에, 다른 프레임워크에 비해서 학습을 해야하는 것들이 상당히 많이 있습니다. 또한 같이 사용하는 jpa나 스프링 배치, 시큐리티등의 퀄리티도 굉장히 높은 반면 각각 책이 몇권씩 있을 정도로 학습량이 많습니다. 스프링은 참 좋지만, 자바가 발전이 더딘편이며 행사코드가 참 많습니다. 다만 이 부분은 최근에 코틀린이 나와서 어느정도 해소되었다고 생각합니다. 국내에서는 취업에 유리한 부분도 분명히 있습니다. 큰회사에서는 대부분 스프링을 사용하니까요. 많은 사람들이 사용하고 있기 때문에 커뮤니티도 잘 되어 있어서 모르는게 나오거나 문제 발생시에 물어볼 사람이 많이 있다는 것도 장점입니다. 15 | 16 | FastAPI는 파이썬 커뮤니티에서 최근에 인기가 있는 프레임워크입니다. 파이썬에는 장고와 플라스크라는 매우 유명한 프레임워크가 있습니다만, API만 작성하는 용도라면 앞으로는 FastAPI를 배워두는 것이 좋을 것 같다고 생각하여 소개드립니다. 최소한의 설정과 제약을 주는 프레임워크로, 플라스크와 비슷한 컨셉의 프레임워크입니다. 스프링과 비슷하게 의존 성주입을 사용합니다. Pydantic기반의 유효성 검증이 좋습니다. 스웨거 기반의 문서 자동화도 굉장히 잘되어 있습니다. async/await기반의 동시성을 도입하여 성능이 좋고, 빠르고, 배우기 쉽습니다. 기업용 애플리케이션을 작성하기 위해서는 스프링에 비해서는 직접 해야하하는 일이 많습니다. 17 | 18 | 여기까지 프레임워크에 대해서 설명드렸습니다. 제가 설명 드린 것 이외에도 많은 프레임워크가 있습니다. 19 | 백엔드 개발을 하기 위해서는 프레임워크 하나 정도는 깊게 공부를 해두는 것이 좋습니다. 20 | 21 | -------------------------------------------------------------------------------- /backend-roadmap/git-github.md: -------------------------------------------------------------------------------- 1 | JSON은 JavaScript Object Notation의 약자로 데이터를 표현하는 방법을 말합니다. 자바스크립트의 객체 표현법과 유사하게 생겼고, 직관적이고 단순한 구조입니다. 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend-roadmap/images/2023-02-20-19-12-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/backend-roadmap/images/2023-02-20-19-12-33.png -------------------------------------------------------------------------------- /backend-roadmap/images/2023-02-20-19-31-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/backend-roadmap/images/2023-02-20-19-31-16.png -------------------------------------------------------------------------------- /backend-roadmap/introduce.md: -------------------------------------------------------------------------------- 1 | 2 | ``` 3 | 안녕하세요. 4 | 저는 2008년부터 개발을 시작해서 만으로 15년 넘게 개발을 하고 있는 백엔드 개발자입니다. 5 | 저의 경험을 살려서 백엔드 개발자를 희망하시는분들에게 조금이나마 도움이 되고자 6 | 백엔드 개발자 로드맵 영상을 찍게 되었습니다. 모쪼록 도움이 되셨으면 좋겠습니다. 7 | 8 | 우선 백엔드 개발자가 되기 위해 알아야 하는 것들을 살펴보겠습니다. 9 | ``` 10 | 11 | 12 | [https://roadmap.sh/backend](https://roadmap.sh/backend) 에 13 | 가면 다른 사람들이 백엔드 개발자 로드맵으로 만들어 놓은 것을 볼 수 있습니다. 14 | 15 | 인터넷, 깃, 데이터베이스, 캐시, CI/CD, 디자인 & 개발 원칙, 웹소켓, 보안 관련등등 굉장히 다양하게 나와있습니다. 16 | 저 표와는 조금 다르지만, 저는 초보 개발자가 알아야 할 로드맵으로 다시 구성해보았습니다. 17 | 초보 개발자는 도움을 조금 받으면 자기에게 맡겨진 일을 해결 할 수 있는 개발자라고 생각했습니다. 18 | 19 | 우선 제가 정리해서 보여드리는 것들을 먼저 보시고 20 | 부족한 부분들은 해당 웹사이트 혹은 협업에서 필요한 부분들을 골라서 학습을 하시는 것을 추천드립니다. 21 | 22 | 인터넷, 버전 컨트롤, 개발 언어, 데이터 표현법, 리눅스 명령어, 웹서버, 인증과 인가, 프레임워크, 데이터베이스, API, 배치처리, 배포에 대하여 각각의 영상으로 다시 찾아 봽겠습니다. 23 | 감사합니다. -------------------------------------------------------------------------------- /backend-roadmap/what-is-internet.md: -------------------------------------------------------------------------------- 1 | ## 인터넷은 무엇일까요? 2 | 인터넷은 전세계의 컴퓨터들이 서로 정보를 주고 받을 수 있도록 한 거대한 네트워크입니다. 네트워크란 Net와 Work의 합성어로 여러대의 컴퓨터가 서로 통신을 할 수 있게 하는 것입니다. 네트워크는 어디에나 있습니다. 유선으로 연결된 네트워크, 무선으로 연결된 네트워크, 공개가 되어 있는 네트워크, 사설 네트워크, 학교, 병원, 회사, 기관, 정부등 온 세상이 네트워크 천지 입니다. 이런 네트워크들의 집합체가 바로 인터넷입니다. 그래서 네트워크들의 네트워크를 인터넷이라 부르기도 합니다. 3 | 4 | ## 인터넷은 어떻게 동작할까요? 5 | 컴퓨터 두 대를 서로 연결해서 통신을 주고 받을 수 있게 한다고 가정해봅시다. 6 | 7 | 컴퓨터가 두 대가 있고 서로 통신을 하고 싶다고 가정합시다. 인터넷은 TCP/IP라는 프로토콜로 통신을 한다고 했습니다. TCP/IP프로토콜에서 두 대의 컴퓨터는 각각 고유한 주소인 IP(Internet Protocol)을 가집니다. IP는 ping 명령어로 쉽게 확인할 수 있습니다. 터미널에서 ping google.com 을 하면 구글의 IP주소 중 하나를 확인 할 수 있습니다. 142.251.42.142을 주소창에 치면 구글에 접속할 수 있습니다. 그런데 IP주소는 외우기가 어렵기 때문에 사람이 기억하기 쉽게 만든 것이 바로 DNS입니다. 일종의 전화번호부라고 할 수 있습니다. 전화번호부에서 사람이름을 찾으면 전화번호를 외우지 않아도 되듯이, 도메인을 알고 있으면 IP를 외우지 않아도 됩니다. 8 | 9 | ![](images/2023-02-20-19-31-16.png) 10 | 11 | https://www.youtube.com/watch?v=7_LPdttKXPc 12 | 13 | https://developer.mozilla.org/ko/docs/Learn/Common_questions/Web_mechanics/How_does_the_Internet_work 14 | 15 | http://web.stanford.edu/class/msande91si/www-spr04/readings/week1/InternetWhitepaper.htm 16 | 17 | 18 | https://roadmap.sh/guides/what-is-internet 19 | 20 | https://velog.io/@jiseung/01-how-does-the-internet-work 21 | 22 | 23 | https://www.youtube.com/watch?v=Dxcc6ycZ73M 24 | 25 | http://www.tcpschool.com/webbasic/intro#:~:text=%EC%9D%B8%ED%84%B0%EB%84%B7(Internet)%EC%9D%98%20%EA%B0%9C%EB%85%90,%EC%BB%B4%ED%93%A8%ED%84%B0%20%ED%86%B5%EC%8B%A0%EB%A7%9D%EC%9D%84%20%EC%9D%98%EB%AF%B8%ED%95%A9%EB%8B%88%EB%8B%A4. 26 | 27 | 종립님의 인정을 받다니... 뿌듯합니다. -------------------------------------------------------------------------------- /bookmark-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /bookmark-app/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /bookmark-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /bookmark-app/README.md: -------------------------------------------------------------------------------- 1 | # NestJS 북마크 앱 2 | 3 | - 북마크 추가 기능: 4 | - 사용자는 북마크의 제목(title)과 URL(url)을 입력하여 새로운 북마크를 추가할 수 있어야 합니다. 5 | - 북마크 추가 시 짧은 URL(shortUrl)이 자동으로 생성되어야 합니다. 6 | - 생성된 짧은 URL은 중복되지 않는 고유한 값이어야 합니다. 7 | - 북마크 리스트 조회 기능: 8 | - 사용자는 추가된 모든 북마크의 리스트를 조회할 수 있어야 합니다. 9 | - 각 북마크 항목에는 제목, URL, 짧은 URL이 표시되어야 합니다. 10 | - 제목과 URL은 클릭 가능한 링크로 표시되어야 하며, 해당 링크를 클릭하면 새 탭에서 해당 URL로 이동해야 합니다. 11 | - 북마크 상세 조회 기능: 12 | - 사용자는 짧은 URL을 통해 해당 북마크의 상세 정보를 조회할 수 있어야 합니다. 13 | - 상세 정보에는 제목, URL, 짧은 URL이 표시되어야 합니다. 14 | - 북마크 수정 기능: 15 | - 사용자는 기존 북마크의 제목과 URL을 수정할 수 있어야 합니다. 16 | - 수정된 내용은 즉시 반영되어야 하며, 리스트에서도 업데이트되어 표시되어야 합니다. 17 | - 북마크 삭제 기능: 18 | - 사용자는 불필요한 북마크를 삭제할 수 있어야 합니다. 19 | - 삭제 시 해당 북마크가 리스트에서 즉시 제거되어야 합니다. 20 | - 짧은 URL 리다이렉션 기능: 21 | - 사용자가 짧은 URL을 브라우저에 입력하면 해당 북마크의 원래 URL로 리다이렉션되어야 합니다. 22 | - 리다이렉션 시 HTTP 301 (Permanent Redirect) 상태 코드를 사용하여 영구적인 리다이렉션을 나타내야 합니다. 23 | - 사용자 인터페이스: 24 | - 사용자 인터페이스는 간단하게 만들면 됩니다. 25 | - 북마크 추가, 수정, 삭제 버튼이 표시되어야 합니다. 26 | - 북마크 리스트는 페이징이 필요없으며, 각 북마크 항목은 구분되어 보여야 합니다. 27 | - 데이터 저장소: 28 | - 북마크 데이터는 SQLite 데이터베이스를 사용합니다. 29 | - 데이터베이스 스키마는 북마크의 제목, URL, 짧은 URL 등의 필드를 포함해야 합니다. 30 | - 백엔드 API: 31 | - 백엔드는 NestJS를 사용합니다. 32 | - RESTful API 형식으로 북마크 추가, 조회, 수정, 삭제 기능을 제공해야 합니다. 33 | - 짧은 URL에 대한 리다이렉션 기능도 백엔드에서 처리되어야 합니다. 34 | - 프론트엔드: 35 | - 프론트엔드는 HTML, CSS, JavaScript를 사용하면되고, 리액트등의 프론트엔드 기술은 자유롭게 사용가능합니다. 36 | - 사용자 인터페이스는 반응형으로 디자인되어 다양한 화면 크기에 적합하게 표시되어야 합니다. 37 | - 북마크 추가, 수정, 삭제 등의 기능은 AJAX를 사용하여 백엔드 API와 통신합니다. 38 | 39 | ## Installation 40 | 41 | ```bash 42 | $ npm install 43 | ``` 44 | 45 | ## Running the app 46 | 47 | ```bash 48 | # development 49 | $ npm run start 50 | 51 | # watch mode 52 | $ npm run start:dev 53 | 54 | # production mode 55 | $ npm run start:prod 56 | ``` 57 | 58 | ## Test 59 | 60 | ```bash 61 | # unit tests 62 | $ npm run test 63 | 64 | # e2e tests 65 | $ npm run test:e2e 66 | 67 | # test coverage 68 | $ npm run test:cov 69 | ``` 70 | 71 | ## Support 72 | 73 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 74 | 75 | -------------------------------------------------------------------------------- /bookmark-app/bookmark.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/bookmark-app/bookmark.db -------------------------------------------------------------------------------- /bookmark-app/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /bookmark-app/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Res } from "@nestjs/common"; 2 | import { BookmarkService } from "./bookmark/bookmark.service"; 3 | import { Response } from 'express'; 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private readonly bookmarkService: BookmarkService) {} 8 | 9 | @Get(':shortUrl') 10 | async redirectToUrl(@Param('shortUrl') shortUrl: string, @Res() res:Response) { 11 | const bookmark = await this.bookmarkService.findByShortUrl(shortUrl); 12 | console.log(bookmark); 13 | if (bookmark) { 14 | return res.redirect(bookmark.url); 15 | } else { 16 | return res.status(404).send('알 수 없는 북마크입니다.'); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /bookmark-app/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BookmarkModule } from './bookmark/bookmark.module'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { ServeStaticModule } from '@nestjs/serve-static'; 5 | import { join } from 'path'; 6 | import { AppController } from './app.controller'; 7 | 8 | @Module({ 9 | imports: [ 10 | TypeOrmModule.forRoot({ 11 | type: 'sqlite', 12 | database: 'bookmark.db', 13 | entities: [__dirname + "/**/*.entity{.ts,.js}"], 14 | synchronize: true 15 | }), 16 | ServeStaticModule.forRoot({ 17 | rootPath: join(__dirname, '..', 'public'), // <-- path to the static files 18 | }), 19 | BookmarkModule, 20 | ], 21 | controllers: [AppController] 22 | }) 23 | export class AppModule {} 24 | -------------------------------------------------------------------------------- /bookmark-app/src/bookmark/bookmark.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Put, Delete, Post } from '@nestjs/common'; 2 | import { BookmarkService } from './bookmark.service'; 3 | import { Bookmark } from './bookmark.entity'; 4 | 5 | @Controller('bookmarks') 6 | export class BookmarkController { 7 | constructor(private readonly bookmarkService: BookmarkService) {} 8 | 9 | 10 | @Put(':id') 11 | async update(@Param('id') id: number, @Body() bookmark: Partial): Promise { 12 | return this.bookmarkService.update(id, bookmark); 13 | } 14 | 15 | @Delete(':id') 16 | async delete(@Param('id') id: number): Promise { 17 | return this.bookmarkService.delete(id); 18 | } 19 | 20 | @Post() 21 | async create(@Body() bookmark: Bookmark): Promise { 22 | return this.bookmarkService.create(bookmark); 23 | } 24 | 25 | @Get("/all") 26 | async findAll(): Promise { 27 | console.log("findAll") 28 | return this.bookmarkService.findAll(); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /bookmark-app/src/bookmark/bookmark.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Bookmark { 5 | 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | title: string; 11 | 12 | @Column() 13 | url: string; 14 | 15 | @Column() 16 | shortUrl: string; 17 | } -------------------------------------------------------------------------------- /bookmark-app/src/bookmark/bookmark.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BookmarkController } from './bookmark.controller'; 3 | import { BookmarkService } from './bookmark.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Bookmark } from './bookmark.entity'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forFeature([Bookmark]), 10 | ], 11 | controllers: [BookmarkController], 12 | providers: [BookmarkService], 13 | exports: [BookmarkService] // AppModule내의 AppController에서 사용예정이므로 내보낸다. 14 | }) 15 | export class BookmarkModule {} 16 | -------------------------------------------------------------------------------- /bookmark-app/src/bookmark/bookmark.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Bookmark } from './bookmark.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { nanoid } from 'nanoid'; 6 | 7 | @Injectable() 8 | export class BookmarkService { 9 | constructor( 10 | @InjectRepository(Bookmark) 11 | private readonly bookmarkRepository: Repository, 12 | ) {} 13 | 14 | async create(bookmark:Bookmark): Promise { 15 | const shortUrl = nanoid(8); 16 | const createdBookmark = this.bookmarkRepository.create({ 17 | ...bookmark, 18 | shortUrl, 19 | }); 20 | return this.bookmarkRepository.save(createdBookmark); 21 | } 22 | 23 | async findAll(): Promise { 24 | return this.bookmarkRepository.find(); 25 | } 26 | 27 | async findByShortUrl(shortUrl: string): Promise { 28 | return this.bookmarkRepository.findOne({ where: { shortUrl } }); 29 | } 30 | 31 | async update(id: number, bookmark: Partial): Promise { 32 | await this.bookmarkRepository.update(id, bookmark); 33 | return await this.bookmarkRepository.findOne({ where: { id } }); 34 | } 35 | 36 | async delete(id: number): Promise { 37 | await this.bookmarkRepository.delete(id); 38 | } 39 | } -------------------------------------------------------------------------------- /bookmark-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /bookmark-app/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /bookmark-app/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": "ESNext", 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 | -------------------------------------------------------------------------------- /bun-test/README.md: -------------------------------------------------------------------------------- 1 | # bun-test 2 | 3 | ## bun이란? 4 | 5 | Bun은 번들러, 테스트 실행기 및 Node.js 호환 패키지 관리자를 갖춘 속도를 위해 설계된 올인원 JavaScript 런타임 및 툴킷. 6 | 7 | 즉 Node.js에서 고민했던 부분을 많이 해소해준 자바스크립트/타입스크립트 런타임 8 | 9 | 10 | 1.1에서 window를 지원함 11 | https://bun.sh/docs/installation#windows 12 | 13 | 14 | ## bun 설치 15 | 16 | ``` 17 | curl -fsSL https://bun.sh/install | bash 18 | ``` 19 | 20 | 21 | ## 프로젝트 초기화 22 | 23 | ``` 24 | bun init 25 | ``` 26 | 27 | 28 | ## 의존성 설치 29 | 30 | ```bash 31 | bun install 32 | ``` 33 | 34 | To run: 35 | 36 | ```bash 37 | bun run index.ts 38 | ``` 39 | 40 | ## figlet과 bun.serve 를 활용하여 간단한 웹서버 만들기 41 | 42 | ```bash 43 | bun add figlet 44 | bun add @types/figlet 45 | ``` 46 | 47 | ```javascript 48 | import figlet from "figlet"; 49 | 50 | const server = Bun.serve({ 51 | port:3000, 52 | fetch(req) { 53 | var url = req.url; 54 | const { searchParams } = new URL(req.url) 55 | console.log(searchParams); 56 | const text = searchParams.get("text", "Hello Bun"); 57 | const body = figlet.textSync(text); 58 | return Response(body); 59 | }, 60 | }); 61 | 62 | console.log(`서버 기동중 localhost:${server.port}`); 63 | ``` 64 | -------------------------------------------------------------------------------- /bun-test/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/bun-test/bun.lockb -------------------------------------------------------------------------------- /bun-test/index.ts: -------------------------------------------------------------------------------- 1 | import figlet from "figlet"; 2 | 3 | const server = Bun.serve({ 4 | port:3000, 5 | fetch(req) { 6 | var url = req.url; 7 | const { searchParams } = new URL(req.url) 8 | console.log(searchParams); 9 | const text = searchParams.get("text") || "Hello, Bun!"; 10 | const body = figlet.textSync(text); 11 | return new Response(body); 12 | }, 13 | }); 14 | 15 | console.log(`서버 기동중 http://localhost:${server.port}`); 16 | console.log("\nhttp://localhost:3000/?text=Become%20Node.js%20Developer") -------------------------------------------------------------------------------- /bun-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-test", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "peerDependencies": { 9 | "typescript": "^5.0.0" 10 | }, 11 | "dependencies": { 12 | "@types/figlet": "^1.5.8", 13 | "figlet": "^1.7.0" 14 | } 15 | } -------------------------------------------------------------------------------- /bun-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chapter0/hello-node.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); // ❶ http 객체 생성 2 | 3 | let count = 0; 4 | 5 | // 노드 서버 객체 생성 6 | const server = http.createServer((req, res) => { 7 | console.log((count += 1)); // ❷ 8 | res.statusCode = 200; // ❸ 9 | res.setHeader("Content-Type", "text/plain"); // ➍ 10 | res.write("hello\n"); // ➎ 11 | // prettier-ignore 12 | setTimeout(() => { // ➏ 13 | res.end("Node.js"); 14 | }, 2000); 15 | }); 16 | 17 | server.listen(8000, () => console.log("Hello Node.js")); // ➐ 접속 대기 18 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/.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 -------------------------------------------------------------------------------- /chapter10/nest-auth-test/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter10/nest-auth-test/nest-auth-test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter10/nest-auth-test/nest-auth-test.sqlite -------------------------------------------------------------------------------- /chapter10/nest-auth-test/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { UserModule } from './user/user.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { User } from 'src/user/user.entity'; 7 | import { AuthModule } from './auth/auth.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forRoot({ 12 | // sqlite 설정 메서드 13 | type: 'sqlite', // ❶ 데이터베이스의 타입 14 | database: 'nest-auth-test.sqlite', // ❷ 데이터베이스 파일명 15 | entities: [User], // ❸ 엔티티 리스트 16 | synchronize: true, // ❹ 데이터베이스에 스키마를 동기화 17 | logging: true, // ❺ SQL 실행 로그 확인 18 | }), 19 | UserModule, 20 | AuthModule, 21 | ], 22 | controllers: [AppController], 23 | providers: [AppService], 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() // ❶ Injectable이 있으니 프로바이더 6 | export class LoginGuard implements CanActivate { 7 | // ❷ CanActivate 인터페이스 구현 8 | constructor(private authService: AuthService) {} // ❸ authService를 주입받음 9 | 10 | async canActivate(context: any): Promise { 11 | // ❹ CanActivate 인터페이스의 메서드 12 | // ❺ 컨텍스트에서 리퀘스트 정보를 가져옴 13 | const request = context.switchToHttp().getRequest(); 14 | 15 | // ❻ 쿠키가 있으면 인증된 것 16 | if (request.cookies['login']) { 17 | return true; 18 | } 19 | 20 | // ❼ 쿠키가 없으면 request의 body 정보 확인 21 | if (!request.body.email || !request.body.password) { 22 | return false; 23 | } 24 | 25 | // ❽ 인증 로직은 기존의 authService.validateUser를 사용한다. 26 | const user = await this.authService.validateUser( 27 | request.body.email, 28 | request.body.password, 29 | ); 30 | 31 | // 유저 정보가 없으면 false를 반환 32 | if (!user) { 33 | return false; 34 | } 35 | // ❿ 있으면 request에 user 정보를 추가하고 true를 반환 36 | request.user = user; 37 | return true; 38 | } 39 | } 40 | 41 | @Injectable() 42 | // ❷ AuthGuard 상속 43 | export class LocalAuthGuard extends AuthGuard('local') { 44 | async canActivate(context: any): Promise { 45 | const result = (await super.canActivate(context)) as boolean; // ❸ 로컬 스트래티지 실행 46 | const request = context.switchToHttp().getRequest(); 47 | await super.logIn(request); // ❹ 세션 저장 48 | return result; 49 | } 50 | } 51 | 52 | @Injectable() 53 | export class AuthenticatedGuard implements CanActivate { 54 | canActivate(context: ExecutionContext): boolean { 55 | const request = context.switchToHttp().getRequest(); 56 | return request.isAuthenticated(); // ❺ 세션에서 정보를 읽어서 인증 확인 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/auth/auth.http: -------------------------------------------------------------------------------- 1 | ### 회원 가입 2 | 3 | POST http://localhost:3000/auth/register 4 | content-type: application/json 5 | 6 | { 7 | "email" : "andy8@podo.com", 8 | "password" : "1234", 9 | "username" : "andy" 10 | } 11 | 12 | ### 로그인 13 | POST http://localhost:3000/auth/login 14 | content-type: application/json 15 | 16 | { 17 | "email" : "andy1@podo.com", 18 | "password" : "1234" 19 | } 20 | 21 | ### 로그인 2 : LoginGuard 22 | POST http://localhost:3000/auth/login2 23 | content-type: application/json 24 | 25 | { 26 | "email" : "andy1@podo.com", 27 | "password" : "1234" 28 | } 29 | 30 | ### Guard 테스트 31 | GET http://localhost:3000/auth/test-guard 32 | 33 | ### 로그인 3 : ❶ 세션을 사용하는 테스트 34 | POST http://localhost:3000/auth/login3 35 | content-type: application/json 36 | 37 | { 38 | "email" : "andy8@podo.com", 39 | "password" : "1234" 40 | } 41 | 42 | ### 로그인 3 : ❷ 틀린 패스워드로 테스트 43 | POST http://localhost:3000/auth/login3 44 | content-type: application/json 45 | 46 | { 47 | "email" : "andy8@podo.com", 48 | "password" : "12345" 49 | } 50 | 51 | ### ❸ 인증이 성공 하는지 테스트 52 | GET http://localhost:3000/auth/test-guard2 53 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { UserModule } from 'src/user/user.module'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { SessionSerializer } from './session.serializer'; 7 | import { LocalStrategy } from './local.strategy'; 8 | 9 | @Module({ 10 | imports: [UserModule, PassportModule.register({ session: true })], 11 | providers: [AuthService, LocalStrategy, SessionSerializer], 12 | controllers: [AuthController], 13 | }) 14 | export class AuthModule {} 15 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { CreateUserDto } from 'src/user/user.dto'; 3 | import { UserService } from 'src/user/user.service'; 4 | import * as bcrypt from 'bcrypt'; 5 | 6 | @Injectable() // ❶ 프로바이더로 사용 7 | export class AuthService { 8 | constructor(private userService: UserService) {} // ❷ 생성자에서 UserService를 주입받음 9 | 10 | async register(userDto: CreateUserDto) { 11 | // ❸ 메서드 내부에 await 구문이 있으므로 async 필요 12 | // ❹ 이미 가입된 유저가 있는지 체크 13 | const user = await this.userService.getUser(userDto.email); 14 | if (user) { 15 | // ❺ 이미 가입된 유저가 있다면 에러 발생 16 | throw new HttpException( 17 | '해당 유저가 이미 있습니다.', 18 | HttpStatus.BAD_REQUEST, 19 | ); 20 | } 21 | 22 | // ❻ 패드워드 암호화 23 | const encryptedPassword = bcrypt.hashSync(userDto.password, 10); 24 | 25 | // 데이터베이스에 저장. 저장 중 에러가 나면 서버 에러 발생 26 | try { 27 | const user = await this.userService.createUser({ 28 | ...userDto, 29 | password: encryptedPassword, 30 | }); 31 | // ❼ 회원 가입 후 반환하는 값에는 password를 주지 않음 32 | user.password = undefined; 33 | return user; 34 | } catch (error) { 35 | throw new HttpException('서버 에러', 500); 36 | } 37 | } 38 | 39 | async validateUser(email: string, password: string) { 40 | const user = await this.userService.getUser(email); // ❶ 이메일로 유저 정보를 받아옴 41 | 42 | if (!user) { 43 | // ❷ 유저가 없으면 검증 실패 44 | return null; 45 | } 46 | const { password: hashedPassword, ...userInfo } = user; // ❸ 패스워드를 따로 뽑아냄 47 | if (bcrypt.compareSync(password, hashedPassword)) { 48 | // ❹ 패스워드가 일치하면 성공 49 | return userInfo; 50 | } 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy } from 'passport-local'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | // ❶ PassportStrategy 믹스인 9 | constructor(private authService: AuthService) { 10 | super({ usernameField: 'email' }); // ❷ 기본값이 username이므로 email로 변경해줌 11 | } 12 | 13 | // ❸ 유저 정보의 유효성 검증 14 | async validate(email: string, password: string): Promise { 15 | const user = await this.authService.validateUser(email, password); 16 | if (!user) { 17 | return null; // ❹ null이면 401 에러 발생 18 | } 19 | return user; // ❺ null이 아니면 user 정보 반환 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/auth/session.serializer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportSerializer } from '@nestjs/passport'; 3 | import { UserService } from 'src/user/user.service'; 4 | 5 | @Injectable() 6 | export class SessionSerializer extends PassportSerializer { 7 | // ❶ PassportSerializer 상속받음 8 | constructor(private userSerivice: UserService) { 9 | // ❷ userService를 주입받음 10 | super(); 11 | } 12 | 13 | // ❸ 세션에 정보를 저장할 때 사용 14 | serializeUser(user: any, done: (err: Error, user: any) => void): any { 15 | done(null, user.email); // 세션에 저장할 정보 16 | } 17 | 18 | // ❹ 세션에서 정보를 꺼내 올 때 사용 19 | async deserializeUser( 20 | payload: any, 21 | done: (err: Error, payload: any) => void, 22 | ): Promise { 23 | const user = await this.userSerivice.getUser(payload); 24 | // ❺ 유저 정보가 없는 경우 done() 함수에 에러 전달 25 | if (!user) { 26 | done(new Error('No User'), null); 27 | return; 28 | } 29 | const { password, ...userInfo } = user; 30 | 31 | // ❻ 유저 정보가 있다면 유저 정보 반환 32 | done(null, userInfo); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import * as cookieParser from 'cookie-parser'; 5 | import * as session from 'express-session'; 6 | import * as passport from 'passport'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | app.useGlobalPipes(new ValidationPipe()); 11 | app.use(cookieParser()); 12 | app.use( 13 | session({ 14 | secret: 'very-important-secret', // ❶ 세션 암호화에 사용되는 키 15 | resave: false, // ❷ 세션을 항상 저장할지 여부 16 | saveUninitialized: false, // ❸ 세션이 저장되기 전에는 초기화지 않은상태로 세션을 미리 만들어 저장 17 | cookie: { maxAge: 3600000 }, // ❹ 쿠키 유효기간 1시간 18 | }), 19 | ); 20 | // ❺ passport 초기화 및 세션 저장소 초기화 21 | app.use(passport.initialize()); 22 | app.use(passport.session()); 23 | 24 | await app.listen(3000); 25 | } 26 | bootstrap(); 27 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Post, 6 | Param, 7 | Put, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { User } from './user.entity'; 11 | import { UserService } from './user.service'; 12 | import { CreateUserDto, UpdateUserDto } from './user.dto'; 13 | 14 | @Controller('user') // ❶ 컨트롤러 설정 데코레이터 15 | export class UserController { 16 | constructor(private userService: UserService) {} // ❷ 유저 서비스를주입 17 | 18 | @Post('/create') 19 | createUser(@Body() user: CreateUserDto) { 20 | // ❸ 유저 생성 21 | return this.userService.createUser(user); 22 | } 23 | 24 | @Get('/getUser/:email') 25 | async getUser(@Param('email') email: string) { 26 | // ❹ 한 명의 유저 찾기 27 | const user = await this.userService.getUser(email); 28 | console.log(user); 29 | return user; 30 | } 31 | 32 | @Put('/update/:email') 33 | updateUser(@Param('email') email: string, @Body() user: UpdateUserDto) { 34 | // ❺ 유저 정보 업데이트 35 | console.log(user); 36 | return this.userService.updateUser(email, user); 37 | } 38 | 39 | @Delete('/delete/:email') 40 | deleteUser(@Param('email') email: string) { 41 | // ❻ 유저 삭제 42 | return this.userService.deleteUser(email); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; // ❶ IsEmail, IsString 임포트 2 | 3 | // ❷ email, password, username 필드를 만들고 데코레이터를 붙이기 4 | export class CreateUserDto { 5 | @IsEmail() 6 | email: string; 7 | 8 | @IsString() 9 | password: string; 10 | 11 | @IsString() 12 | username: string; 13 | } 14 | 15 | // ❸ 업데이트의 유효성 검증 시 사용할 DTO 16 | export class UpdateUserDto { 17 | @IsString() 18 | username: string; 19 | 20 | @IsString() 21 | password: string; 22 | } 23 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | // ❶ 데코레이터 임포트 2 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity() // ❷ 엔티티 객체임을 알려주기 위한 데코레이터 5 | export class User { 6 | @PrimaryGeneratedColumn() 7 | id?: number; // ❸ id는 pk이며 자동 증가하는 값 8 | 9 | @Column({ unique: true }) 10 | email: string; // ❹ email은 유니크한 값 11 | 12 | @Column() 13 | password: string; 14 | 15 | @Column() 16 | username: string; 17 | 18 | @Column({ type: "datetime", default: () => "CURRENT_TIMESTAMP"}) // ❺ 기본값을 넣어줌 19 | createdDt: Date = new Date(); 20 | } 21 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/user/user.http: -------------------------------------------------------------------------------- 1 | ### Create ❶ 유저 생성 2 | POST http://localhost:3000/user/create 3 | content-type: application/json 4 | 5 | { 6 | "username": "andy", 7 | "password": "test1234", 8 | "email": "andy@podo.com" 9 | } 10 | 11 | ### GetUser ❷ 유저 정보 찾기 12 | GET http://localhost:3000/user/getUser/andy@podo.com 13 | 14 | 15 | ### Update User ❸ 유저 정보 업데이트 16 | PUT http://localhost:3000/user/update/andy@podo.com 17 | content-type: application/json 18 | 19 | { 20 | "email": "andy@podo.com", 21 | "username": "andy2", 22 | "password": "test12345" 23 | } 24 | 25 | ### Delete User ❹ 유저 삭제 26 | DELETE http://localhost:3000/user/delete/andy@podo.com 27 | 28 | ### 잘못된 이메일을 입력한 경우 29 | POST http://localhost:3000/user/create 30 | content-type: application/json 31 | 32 | { 33 | "username": "andy", 34 | "password": "test1234", 35 | "email": "andy-podo2" 36 | } 37 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './user.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [UserService], // UserService를 외부 모듈에서 사용하도록 설정 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from './user.entity'; 5 | 6 | @Injectable() // ❶ DI를 위한 데코레이터 7 | export class UserService { 8 | // ❷ 리포지터리 주입 9 | constructor( 10 | @InjectRepository(User) private userRepository: Repository, 11 | ) {} 12 | 13 | // ❸ 유저 생성 14 | createUser(user): Promise { 15 | console.log(user); 16 | return this.userRepository.save(user); 17 | } 18 | 19 | // ❹ 한 명의 유저 정보 찾기 20 | async getUser(email: string) { 21 | const result = await this.userRepository.findOne({ 22 | where: { email }, 23 | }); 24 | return result; 25 | } 26 | 27 | // ❺ 유저 정보 업데이트. username과 password만 변경 28 | async updateUser(email, _user) { 29 | const user: User = await this.getUser(email); 30 | console.log(_user); 31 | user.username = _user.username; 32 | user.password = _user.password; 33 | console.log(user); 34 | this.userRepository.save(user); 35 | } 36 | 37 | // ❻ 유저 정보 삭제 38 | deleteUser(email: any) { 39 | return this.userRepository.delete({ email }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/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 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/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 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter10/nest-auth-test/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 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter11/nest-auth-test/auth-test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter11/nest-auth-test/auth-test.sqlite -------------------------------------------------------------------------------- /chapter11/nest-auth-test/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { UserModule } from './user/user.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { AuthModule } from './auth/auth.module'; 7 | import { ConfigModule } from '@nestjs/config'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forRoot({ 12 | type: 'sqlite', 13 | database: 'auth-test.sqlite', 14 | autoLoadEntities: true, // for data source 15 | synchronize: true, 16 | logging: true, 17 | }), 18 | UserModule, 19 | AuthModule, 20 | ConfigModule.forRoot(), 21 | ], 22 | controllers: [AppController], 23 | providers: [AppService], 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/auth/auth.http: -------------------------------------------------------------------------------- 1 | ### 회원가입 2 | 3 | POST http://localhost:3000/auth/register 4 | content-type: application/json 5 | 6 | { 7 | "email" : "andy8@podo.com", 8 | "password" : "1234", 9 | "username" : "andy" 10 | } 11 | 12 | ### 로그인 13 | POST http://localhost:3000/auth/login 14 | content-type: application/json 15 | 16 | { 17 | "email" : "andy8@podo.com", 18 | "password" : "1234" 19 | } 20 | 21 | ### 로그인2 : LoginGuard 22 | POST http://localhost:3000/auth/login2 23 | content-type: application/json 24 | 25 | { 26 | "email" : "andy8@podo.com", 27 | "password" : "1234" 28 | } 29 | 30 | ### Guard Test 31 | GET http://localhost:3000/auth/test-guard 32 | 33 | ### 로그인3 : With Session 34 | POST http://localhost:3000/auth/login3 35 | content-type: application/json 36 | 37 | { 38 | "email" : "andy8@podo.com", 39 | "password" : "1234" 40 | } 41 | 42 | 43 | ### 로그인3 : 틀린 패스워드 44 | POST http://localhost:3000/auth/login3 45 | content-type: application/json 46 | 47 | { 48 | "email" : "andy8@podo.com", 49 | "password" : "12345" 50 | } 51 | 52 | 53 | ### 인증됐는지 테스트 54 | GET http://localhost:3000/auth/test-guard2 55 | 56 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { UserModule } from 'src/user/user.module'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { SessionSerializer } from './session.serializer'; 7 | import { LocalStrategy } from './local.strategy'; 8 | import { GoogleStrategy } from './google.strategy'; 9 | 10 | @Module({ 11 | imports: [UserModule, PassportModule.register({ session: true })], 12 | providers: [AuthService, LocalStrategy, SessionSerializer, GoogleStrategy], 13 | controllers: [AuthController], 14 | }) 15 | export class AuthModule {} 16 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { CreateUserDto } from 'src/user/user.dto'; 3 | import { UserService } from 'src/user/user.service'; 4 | import * as bcrypt from 'bcrypt'; 5 | 6 | @Injectable() // ❶ 프로바이더로 사용 7 | export class AuthService { 8 | constructor(private userSerivice: UserService) {} // ❷ 생성자에서 UserService를 주입 받음. 9 | 10 | // ❸ 함수내부에 await 구문이 있으므로 async가 필요 11 | async register(userDto: CreateUserDto) { 12 | // ❹ 이미가입된 유저가 있는지 체크 13 | const user = await this.userSerivice.getUser(userDto.email); 14 | if (user) { 15 | // ❺ 기가입된 유저가 있다면 에러발생 16 | throw new HttpException( 17 | '해당 유저가 이미 있습니다.', 18 | HttpStatus.BAD_REQUEST, 19 | ); 20 | } 21 | 22 | // ❻ 패드워드 암호화 23 | const encryptedPassword = bcrypt.hashSync(userDto.password, 10); 24 | 25 | // ❼ 디비에 저장. 저장중 에러가 나면 서버에러 발생 26 | try { 27 | const user = await this.userSerivice.createUser({ 28 | ...userDto, 29 | password: encryptedPassword, 30 | }); 31 | // ❽회원가입 후 반환하는 값에는 password를 주지 않음 32 | user.password = undefined; 33 | return user; 34 | } catch (error) { 35 | throw new HttpException('서버에러', 500); 36 | } 37 | } 38 | 39 | async validateUser(email: string, password: string) { 40 | const user = await this.userSerivice.getUser(email); 41 | 42 | if (!user) { 43 | return null; 44 | } 45 | 46 | const { password: hashedPassword, ...userInfo } = user; 47 | 48 | if (bcrypt.compareSync(password, hashedPassword)) { 49 | return userInfo; 50 | } 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/auth/google.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Profile, Strategy } from 'passport-google-oauth20'; 4 | import { User } from 'src/user/user.entity'; 5 | import { UserService } from 'src/user/user.service'; 6 | 7 | @Injectable() 8 | export class GoogleStrategy extends PassportStrategy(Strategy) { 9 | constructor(private userService: UserService) { 10 | console.log(process.env); 11 | super({ 12 | clientID: process.env.GOOGLE_CLIENT_ID, 13 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 14 | callbackURL: 'http://localhost:3000/auth/google', 15 | scope: ['email', 'profile'], 16 | }); 17 | } 18 | async validate(accessToken: string, refreshToken: string, profile: Profile) { 19 | const { id, name, emails } = profile; 20 | console.log(accessToken); 21 | console.log(refreshToken); 22 | 23 | const providerId = id; 24 | const email = emails[0].value; 25 | 26 | const user: User = await this.userService.findByEmailOrSave( 27 | email, 28 | name.familyName + name.givenName, 29 | providerId, 30 | ); 31 | 32 | return user; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy } from 'passport-local'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | super({ usernameField: 'email' }); 10 | } 11 | 12 | async validate(email: string, password: string): Promise { 13 | console.log('2'); 14 | console.log(email); 15 | 16 | const user = await this.authService.validateUser(email, password); 17 | if (!user) { 18 | return null; 19 | } 20 | return user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/auth/session.serializer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportSerializer } from '@nestjs/passport'; 3 | import { UserService } from 'src/user/user.service'; 4 | 5 | @Injectable() 6 | export class SessionSerializer extends PassportSerializer { 7 | constructor(private userSerivice: UserService) { 8 | super(); 9 | } 10 | 11 | serializeUser(user: any, done: (err: Error, user: any) => void): any { 12 | console.log('serialise'); 13 | console.log(user.email); 14 | done(null, user.email); // 세션에 저장할 정보 15 | } 16 | 17 | async deserializeUser( 18 | payload: any, 19 | done: (err: Error, payload: any) => void, 20 | ): Promise { 21 | console.log('deserialise'); 22 | console.log(payload); 23 | 24 | const user = await this.userSerivice.getUser(payload); 25 | if (!user) { 26 | done(new Error('No User'), null); 27 | return; 28 | } 29 | const { password, ...userInfo } = user; 30 | done(null, userInfo); // 세션에서 가져온 정보로 유저정보를 반환 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import * as cookieParser from 'cookie-parser'; 5 | import * as session from 'express-session'; 6 | import * as passport from 'passport'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | app.useGlobalPipes(new ValidationPipe()); 11 | app.use(cookieParser()); 12 | app.use( 13 | session({ 14 | secret: 'very-important-secret', // 세션 암호화에 사용되는 키 15 | resave: false, // 세션을 항상 저장할 지 여부 16 | saveUninitialized: false, // 세션이 저장되기 전에 uninitialized 상태로 미리 만들어서 저장 17 | cookie: { maxAge: 3600000 }, // 쿠키 유효기간 1시간 18 | }), 19 | ); 20 | app.use(passport.initialize()); 21 | app.use(passport.session()); 22 | await app.listen(3000); 23 | } 24 | bootstrap(); 25 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Post, 6 | Param, 7 | Put, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { CreateUserDto, UpdateUserDto } from './user.dto'; 11 | import { UserService } from './user.service'; 12 | 13 | @Controller('user') 14 | export class UserController { 15 | constructor(private userService: UserService) {} 16 | 17 | @Post('/create') 18 | createUser(@Body() user: CreateUserDto) { 19 | return this.userService.createUser(user); 20 | } 21 | 22 | @Get('/getUser/:email') 23 | async getUser(@Param('email') email: string) { 24 | const user = await this.userService.getUser(email); 25 | console.log(user); 26 | return user; 27 | } 28 | 29 | @Put('/update/:email') 30 | updateUser(@Param('email') email: string, @Body() user: CreateUserDto) { 31 | console.log(email); 32 | console.log(user); 33 | return this.userService.updateUser(email, user); 34 | } 35 | 36 | @Delete('/delete/:email') 37 | deleteUser(@Param('email') email: string) { 38 | return this.userService.deleteUser(email); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class CreateUserDto { 4 | @IsEmail() 5 | email?: string; 6 | 7 | @IsString() 8 | password: string; 9 | 10 | @IsString() 11 | username: string; 12 | } 13 | 14 | export class UpdateUserDto { 15 | @IsString() 16 | username: string; 17 | 18 | @IsString() 19 | password: string; 20 | } 21 | 22 | export class LoginUserDto { 23 | @IsEmail() 24 | email: string; 25 | 26 | @IsString() 27 | password: string; 28 | } 29 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class User { 5 | @PrimaryGeneratedColumn() 6 | id?: number; 7 | 8 | @Column({ unique: true }) 9 | email: string; 10 | 11 | @Column({ nullable: true }) 12 | password: string; 13 | 14 | @Column() 15 | username: string; 16 | 17 | @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) 18 | createdDt: Date; 19 | 20 | @Column({ nullable: true }) 21 | providerId: string; 22 | } 23 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/user/user.http: -------------------------------------------------------------------------------- 1 | ### Create 2 | POST http://localhost:3000/user/create 3 | content-type: application/json 4 | 5 | { 6 | "username": "andy", 7 | "password": "test1234", 8 | "email": "andy3@podo.com" 9 | } 10 | 11 | ### GetUser 12 | GET http://localhost:3000/user/getUser/andy3@podo.com 13 | 14 | 15 | ### Update User 16 | PUT http://localhost:3000/user/update/andy3@podo.com 17 | content-type: application/json 18 | 19 | { 20 | "email": "andy3@podo.com", 21 | "username": "andy2", 22 | "password": "test12345" 23 | } 24 | 25 | ### Delete User 26 | DELETE http://localhost:3000/user/delete/andy3@podo.com -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserController } from './user.controller'; 4 | import { User } from './user.entity'; 5 | import { UserService } from './user.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from './user.entity'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | constructor( 9 | @InjectRepository(User) private userRepository: Repository, 10 | ) {} 11 | 12 | createUser(user): Promise { 13 | return this.userRepository.save(user); 14 | } 15 | 16 | async getUser(email: string) { 17 | const result = await this.userRepository.findOne({ 18 | where: { email }, 19 | }); 20 | console.log(result); 21 | return result; 22 | } 23 | 24 | // username과 password만 변경 가능 25 | async updateUser(email, _user) { 26 | const user: User = await this.getUser(email); 27 | console.log(_user); 28 | user.username = _user.username; 29 | user.password = _user.password; 30 | console.log(user); 31 | this.userRepository.save(user); 32 | } 33 | 34 | deleteUser(email: any) { 35 | return this.userRepository.delete({ email }); 36 | } 37 | 38 | async findByEmailOrSave(email, username, providerId): Promise { 39 | const foundUser = await this.getUser(email); 40 | if (foundUser) { 41 | return foundUser; 42 | } 43 | 44 | const newUser = await this.userRepository.save({ 45 | email, 46 | username, 47 | providerId, 48 | }); 49 | return newUser; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/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 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/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 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter11/nest-auth-test/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 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter12/nest-file-upload/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | UploadedFile, 6 | UseInterceptors, 7 | } from '@nestjs/common'; 8 | import { FileInterceptor } from '@nestjs/platform-express'; 9 | import { AppService } from './app.service'; 10 | import { multerOption } from './multer.options'; 11 | 12 | @Controller() 13 | export class AppController { 14 | constructor(private readonly appService: AppService) {} 15 | 16 | @Get() 17 | getHello(): string { 18 | return this.appService.getHello(); 19 | } 20 | 21 | @Post('file-upload') 22 | @UseInterceptors(FileInterceptor('file', multerOption)) 23 | fileUpload(@UploadedFile() file: Express.Multer.File) { 24 | console.log(file); 25 | // console.log(file.buffer.toString('utf-8')); 26 | return `${file.originalname} File Uploaded check http://localhost:3000/uploads/${file.filename}`; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ServeStaticModule } from '@nestjs/serve-static'; 3 | import { join } from 'path'; 4 | import { AppController } from './app.controller'; 5 | import { AppService } from './app.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | ServeStaticModule.forRoot({ 10 | rootPath: join(__dirname, '..', 'uploads'), 11 | serveRoot: '/uploads', 12 | }), 13 | ], 14 | controllers: [AppController], 15 | providers: [AppService], 16 | }) 17 | export class AppModule {} 18 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter12/nest-file-upload/src/cat.jpg -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/file-upload.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/file-upload 2 | Content-Type: multipart/form-data; boundary=test-file-upload 3 | 4 | --test-file-upload 5 | Content-Disposition: form-data; name="file"; filename="test.txt" 6 | 7 | 여기에 텍스트 파일의 내용을 넣을 수 있습니다. 8 | --test-file-upload-- 9 | 10 | 11 | ### 실제 파일로 업로드 12 | POST http://localhost:3000/file-upload 13 | Content-Type: multipart/form-data; boundary=test-file-upload 14 | 15 | --test-file-upload 16 | Content-Disposition: form-data; name="file"; filename="test.txt" 17 | 18 | < test.txt 19 | --test-file-upload-- 20 | 21 | ### 사진 업로드 22 | POST http://localhost:3000/file-upload 23 | Content-Type: multipart/form-data; boundary=image-file-upload 24 | 25 | --image-file-upload 26 | Content-Disposition: form-data; name="file"; filename="cat.jpg" 27 | Content-Type: image/jpeg 28 | 29 | < cat.jpg 30 | --image-file-upload-- 31 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { join } from 'path'; 3 | import { NestExpressApplication } from '@nestjs/platform-express'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useStaticAssets(join(__dirname, '..', 'static')); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/multer.options.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import { diskStorage } from 'multer'; 3 | import { extname, join } from 'path'; 4 | 5 | export const multerOption = { 6 | storage: diskStorage({ 7 | destination: join(__dirname, '..', 'uploads'), 8 | filename: (req, file, cb) => { 9 | cb(null, randomUUID() + extname(file.originalname)); 10 | }, 11 | }), 12 | }; 13 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/src/test.txt: -------------------------------------------------------------------------------- 1 | 실제 파일의 테스트입니다. -------------------------------------------------------------------------------- /chapter12/nest-file-upload/static/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/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 | -------------------------------------------------------------------------------- /chapter12/nest-file-upload/uploads/123fbb67-42fe-4e6c-bf94-c6674e102fda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter12/nest-file-upload/uploads/123fbb67-42fe-4e6c-bf94-c6674e102fda.jpg -------------------------------------------------------------------------------- /chapter12/nest-file-upload/uploads/3f29bb4b-2a21-452d-9060-c40f007e789d.txt: -------------------------------------------------------------------------------- 1 | 실제 파일의 테스트입니다. -------------------------------------------------------------------------------- /chapter13/echo-websocket/client.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 |

13 | 14 | 15 |
16 | 17 | 18 | 47 | -------------------------------------------------------------------------------- /chapter13/echo-websocket/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo-websocket", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "ws": "^8.11.0" 9 | } 10 | }, 11 | "node_modules/ws": { 12 | "version": "8.11.0", 13 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 14 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 15 | "engines": { 16 | "node": ">=10.0.0" 17 | }, 18 | "peerDependencies": { 19 | "bufferutil": "^4.0.1", 20 | "utf-8-validate": "^5.0.2" 21 | }, 22 | "peerDependenciesMeta": { 23 | "bufferutil": { 24 | "optional": true 25 | }, 26 | "utf-8-validate": { 27 | "optional": true 28 | } 29 | } 30 | } 31 | }, 32 | "dependencies": { 33 | "ws": { 34 | "version": "8.11.0", 35 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 36 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 37 | "requires": {} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapter13/echo-websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ws": "^8.11.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /chapter13/echo-websocket/server.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const server = new WebSocket.Server({ port: 3000 }); 3 | 4 | server.on('connection', ws => { 5 | ws.send('[서버 접속 완료!]'); 6 | 7 | ws.on('message', message => { 8 | ws.send(`서버로부터 응답: ${message}`); 9 | }); 10 | 11 | ws.on('close', () => { 12 | console.log('클라이언트 접속 해제'); 13 | }); 14 | }); -------------------------------------------------------------------------------- /chapter13/nest-chat/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter13/nest-chat/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter13/nest-chat/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chapter13/nest-chat/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chapter13/nest-chat/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ChatGateway, RoomGateway } from './app.gateway'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [AppController], 9 | providers: [AppService, ChatGateway, RoomGateway], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /chapter13/nest-chat/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter13/nest-chat/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { NestExpressApplication } from '@nestjs/platform-express'; 3 | import { join } from 'path'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useStaticAssets(join(__dirname, '..', 'static')); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /chapter13/nest-chat/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nest Chat 6 | 7 | 8 |
9 |

채팅방 목록

10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 |
    18 |

    공지

    19 |
    20 |
    21 | 22 |
    23 |

    채팅

    24 |
    25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /chapter13/nest-chat/static/script.js: -------------------------------------------------------------------------------- 1 | const socket = io('http://localhost:3000/chat'); 2 | const roomSocket = io('http://localhost:3000/room'); 3 | const nickname = prompt('닉네임을 입력해 주세요.'); 4 | let currentRoom = ''; 5 | 6 | function sendMessage() { 7 | if (currentRoom === "") { 8 | alert('방을 선택해주세요.'); 9 | return; 10 | } 11 | const message = $('#message').val(); 12 | const data = { message, nickname, room: currentRoom }; 13 | $('#chat').append(`
    나 : ${message}
    `); 14 | roomSocket.emit('message', data); 15 | return false; 16 | } 17 | 18 | socket.on('connect', (socket) =>{ 19 | console.log('Connected to server'); 20 | console.log(socket); 21 | roomSocket.emit('getRooms'); 22 | }); 23 | 24 | socket.on('message', (data) => { 25 | console.log(data); 26 | $('#chat').append(`
    ${data.message}
    `); 27 | }); 28 | 29 | socket.on('notice', (data) => { 30 | console.log(data); 31 | $('#notice').append(`
    ${data.message}
    `); 32 | }) 33 | 34 | roomSocket.on('message', (data) => { 35 | console.log(data); 36 | $('#chat').append(`
    ${data.message}
    `); 37 | }); 38 | 39 | roomSocket.on("rooms", (data) => { 40 | console.log(data); 41 | $('#rooms').empty(); 42 | data.forEach((room) => { 43 | $('#rooms').append(`
  • ${room}
  • `); 44 | }); 45 | }); 46 | 47 | function createRoom() { 48 | const room = prompt('생성하실 방의 이름을 입력해주세요.'); 49 | roomSocket.emit('createRoom', { room, nickname }); 50 | return false; 51 | } 52 | 53 | // 방에 들어갈 때 기존에 있던 방에서는 나간다. 54 | function joinRoom(room) { 55 | roomSocket.emit('joinRoom', { room, nickname, toLeaveRoom: currentRoom }); 56 | currentRoom = room; 57 | return false; 58 | } 59 | -------------------------------------------------------------------------------- /chapter13/nest-chat/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter13/nest-chat/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 | -------------------------------------------------------------------------------- /chapter13/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@nestjs/platform-socket.io": "^9.2.0", 4 | "@nestjs/websockets": "^9.2.0" 5 | }, 6 | "devDependencies": { 7 | "@types/socket.io": "^3.0.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/src/app.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | SubscribeMessage, 5 | } from '@nestjs/websockets'; 6 | 7 | import { Server, Socket } from 'socket.io'; 8 | 9 | @WebSocketGateway() 10 | export class ChatGateway { 11 | @WebSocketServer() server: Server; 12 | 13 | @SubscribeMessage('message') 14 | handleMessage(socket: Socket, data: any): void { 15 | console.log(socket.id.length); 16 | this.server.emit( 17 | 'message', 18 | `client-${socket.id.substring(0, 4)} : ${data}`, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { ChatGateway } from './app.gateway'; 4 | import { AppService } from './app.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [AppController], 9 | providers: [AppService, ChatGateway], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { NestExpressApplication } from '@nestjs/platform-express'; 3 | import { join } from 'path'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useStaticAssets(join(__dirname, '..', 'static')); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Nest Chat 6 | 7 | 8 |

    Simple Nest Chat

    9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 33 | 34 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter13/simple-nest-chat/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 | -------------------------------------------------------------------------------- /chapter2/callstack.js: -------------------------------------------------------------------------------- 1 | function func1() { 2 | console.log("1"); 3 | func2(); 4 | return; 5 | } 6 | 7 | function func2() { 8 | console.log("2"); 9 | return; 10 | } 11 | 12 | func1(); 13 | -------------------------------------------------------------------------------- /chapter2/callstackWithEventloop.js: -------------------------------------------------------------------------------- 1 | console.log("1"); 2 | setTimeout(() => console.log(2), 1000); 3 | console.log("3"); 4 | -------------------------------------------------------------------------------- /chapter2/doWorkWithCallback.js: -------------------------------------------------------------------------------- 1 | function doWorkWithCallback(callback) { 2 | setTimeout(callback, 10000); 3 | } 4 | 5 | doWorkWithCallback(() => { 6 | console.log("오래걸림"); 7 | }); 8 | -------------------------------------------------------------------------------- /chapter2/hello.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); // ❶ http 객체 생성 2 | 3 | let count = 0; 4 | 5 | // 노드 서버 객체 생성 6 | const server = http.createServer((req, res) => { 7 | log(count); 8 | res.statusCode = 200; // ❸ 9 | res.setHeader("Content-Type", "text/plain"); // ➍ 10 | res.write("hello\n"); // ➎ 11 | // prettier-ignore 12 | setTimeout(() => { // ➏ 13 | res.end("Node.js"); 14 | }, 2000); 15 | }); 16 | 17 | function log(count) { 18 | console.log((count += 1)); // ❷ 19 | } 20 | 21 | server.listen(8000, () => console.log("Hello Node.js")); // ➐ 접속 대기 22 | -------------------------------------------------------------------------------- /chapter2/test_hello.js: -------------------------------------------------------------------------------- 1 | import http from "k6/http"; 2 | 3 | export const options = { 4 | vus: 100, 5 | duration: "10s", 6 | }; 7 | 8 | export default function () { 9 | http.get("http://localhost:8000"); 10 | } 11 | -------------------------------------------------------------------------------- /chapter3/code3-1-ok-server.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const server = http.createServer((req, res) => { 3 | res.setHeader("Content-Type", "text/html"); 4 | res.end("OK"); 5 | }); 6 | 7 | server.listen("3000", () => console.log("OK서버 시작!")); 8 | -------------------------------------------------------------------------------- /chapter3/code3-2-implement-router.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const url = require("url"); // ❶ 3 | http 4 | .createServer((req, res) => { 5 | const path = url.parse(req.url, true).pathname; // ❷ 6 | res.setHeader("Content-Type", "text/html; charset=utf-8"); 7 | 8 | if (path === "/user") { 9 | res.end("[user] name : andy, age: 30"); // ❸ 10 | } else if (path === "/feed") { 11 | res.end(`
      12 |
    • picture1
    • 13 |
    • picture2
    • 14 |
    • picture3
    • 15 |
    16 | `); // ➍ 17 | } else { 18 | res.statusCode = 404; 19 | res.end("404 page not found"); // ➎ 20 | } 21 | }) 22 | .listen("3000", () => console.log("라우터를 만들어보자!")); 23 | -------------------------------------------------------------------------------- /chapter3/code3-3-implement-router.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const url = require("url"); 3 | http 4 | .createServer((req, res) => { 5 | const path = url.parse(req.url, true).pathname; 6 | res.setHeader("Content-Type", "text/html"); 7 | if (path === "/user") { 8 | user(req, res); // 1 9 | } else if (path === "/feed") { 10 | feed(req, res); // 2 11 | } else { 12 | notFound(req, res); // 3 13 | } 14 | }) 15 | .listen("3000", () => console.log("라우터를 만들어보자!")); 16 | 17 | const user = (req, res) => { 18 | res.end(`[user] name : andy, age: 30`); 19 | }; 20 | 21 | const feed = (req, res) => { 22 | res.end(`
      23 |
    • picture1
    • 24 |
    • picture2
    • 25 |
    • picture3
    • 26 |
    27 | `); 28 | }; 29 | 30 | const notFound = (req, res) => { 31 | res.statusCode = 404; 32 | res.end("404 page not found"); 33 | }; 34 | -------------------------------------------------------------------------------- /chapter3/code3-4-implement-router2.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const url = require("url"); 3 | http 4 | .createServer((req, res) => { 5 | const path = url.parse(req.url, true).pathname; 6 | res.setHeader("Content-Type", "text/html"); 7 | if (path === "/user") { 8 | user(req, res); // 1 9 | } else if (path === "/feed") { 10 | feed(req, res); // 2 11 | } else { 12 | notFound(req, res); // 3 13 | } 14 | }) 15 | .listen("3000", () => console.log("라우터를 만들어보자!")); 16 | 17 | const user = (req, res) => { 18 | const userInfo = url.parse(req.url, true).query; 19 | res.end(`[user] name : ${userInfo.name}, age: ${userInfo.age}`); 20 | }; 21 | 22 | const feed = (req, res) => { 23 | res.end(`
      24 |
    • picture1
    • 25 |
    • picture2
    • 26 |
    • picture3
    • 27 |
    28 | `); 29 | }; 30 | 31 | const notFound = (req, res) => { 32 | res.statusCode = 404; 33 | res.end("404 page not found"); 34 | }; 35 | -------------------------------------------------------------------------------- /chapter3/code3-5-add-server-error.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const url = require("url"); 3 | 4 | http 5 | .createServer((req, res) => { 6 | const path = url.parse(req.url, true).pathname; 7 | res.setHeader("Content-Type", "text/html"); 8 | if (path in urlMap) { 9 | // 1 10 | try { 11 | urlMap[path](req, res); 12 | } catch (err) { 13 | console.log(err); 14 | serverError(req, res); 15 | } 16 | // 2 17 | } else { 18 | notFound(req, res); 19 | } 20 | }) 21 | .listen("3000", () => console.log("라우터를 리팩토링해보자!")); 22 | 23 | const user = (req, res) => { 24 | throw Error("!!!"); 25 | const user = url.parse(req.url, true).query; 26 | res.end(`[user] name : ${user.name}, age: ${user.age}`); 27 | }; 28 | 29 | const feed = (req, res) => { 30 | res.end(`
      31 |
    • picture1
    • 32 |
    • picture2
    • 33 |
    • picture3
    • 34 |
    35 | `); 36 | }; 37 | 38 | const notFound = (req, res) => { 39 | res.statusCode = 404; 40 | res.end("404 page not found"); 41 | }; 42 | 43 | const serverError = (req, res) => { 44 | res.statusCode = 500; 45 | res.end("500 server error"); 46 | }; 47 | 48 | // 3 49 | const urlMap = { 50 | "/": (req, res) => res.end("HOME"), 51 | "/user": user, 52 | "/feed": feed, 53 | }; 54 | -------------------------------------------------------------------------------- /chapter3/code3-5-refactoring-router.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const url = require("url"); 3 | 4 | http 5 | .createServer((req, res) => { 6 | const path = url.parse(req.url, true).pathname; 7 | res.setHeader("Content-Type", "text/html"); 8 | if (path in urlMap) { 9 | // 1 10 | urlMap[path](req, res); // 2 11 | } else { 12 | notFound(req, res); 13 | } 14 | }) 15 | .listen("3000", () => console.log("라우터를 리팩토링해보자!")); 16 | 17 | const user = (req, res) => { 18 | const user = url.parse(req.url, true).query; 19 | res.end(`[user] name : ${user.name}, age: ${user.age}`); 20 | }; 21 | 22 | const feed = (req, res) => { 23 | res.end(`
      24 |
    • picture1
    • 25 |
    • picture2
    • 26 |
    • picture3
    • 27 |
    28 | `); 29 | }; 30 | 31 | const notFound = (req, res) => { 32 | res.statusCode = 404; 33 | res.end("404 page not found"); 34 | }; 35 | 36 | // 3 37 | const urlMap = { 38 | "/": (req, res) => res.end("HOME"), 39 | "/user": user, 40 | "/feed": feed, 41 | }; 42 | -------------------------------------------------------------------------------- /chapter3/express-server/board.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | let posts = []; 4 | 5 | // req.body를 사용하려면 json 미들웨어를 사용해야한다. 6 | // 사용하지 않으면 undefined로 나옴. 7 | app.use(express.json()); 8 | 9 | // post요청이 application/x-www-form-urlencoded 인 경우 파싱을 위해 사용. 10 | app.use(express.urlencoded({ extended: true })); 11 | 12 | app.get("/", (req, res) => { 13 | res.json(posts); 14 | }); 15 | 16 | app.post("/posts", (req, res) => { 17 | console.log(typeof req.body); 18 | const { title, name, text } = req.body; 19 | posts.push({ id: posts.length + 1, title, name, text, createdDt: Date() }); 20 | res.json({ title, name, text }); 21 | }); 22 | 23 | app.delete("/posts/:id", (req, res) => { 24 | const id = req.params.id; 25 | const filteredPosts = posts.filter((post) => post.id !== +id); 26 | const isLengthChanged = posts.length !== filteredPosts.length; 27 | posts = filteredPosts; 28 | if (isLengthChanged) { 29 | res.json("OK"); 30 | return; 31 | } 32 | res.json("NOT CHANGED"); 33 | }); 34 | 35 | app.listen(3000, () => { 36 | console.log("welcome board START!"); 37 | }); 38 | -------------------------------------------------------------------------------- /chapter3/express-server/hello-express.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); // ❶ 2 | const app = express(); // ❷ 3 | const port = 3000; 4 | 5 | // prettier-ignore 6 | app.get("/", (req, res) => { // ❸ 7 | res.set({ "Content-Type": "text/html; charset=utf-8" }); // ➍ 8 | res.end("헬로 Express"); 9 | }); 10 | 11 | // prettier-ignore 12 | app.listen(port, () => { // ➎ 13 | console.log(`START SERVER : use ${port}`); 14 | }); 15 | -------------------------------------------------------------------------------- /chapter3/express-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "express": "^4.17.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /chapter3/express-server/refactoring-to-express.js: -------------------------------------------------------------------------------- 1 | const url = require("url"); 2 | const express = require("express"); 3 | const app = express(); 4 | const port = 3000; 5 | 6 | app.listen(port, () => { 7 | console.log("익스프레스로 라우터 리팩토링하기"); 8 | }); 9 | 10 | app.get("/", (_, res) => res.end("HOME")); 11 | app.get("/user", user); 12 | app.get("/feed", feed); 13 | 14 | function user(req, res) { 15 | const user = url.parse(req.url, true).query; 16 | res.json(`[user] name : ${user.name}, age: ${user.age}`); 17 | } 18 | 19 | function feed(_, res) { 20 | res.json(`
      21 |
    • picture1
    • 22 |
    • picture2
    • 23 |
    • picture3
    • 24 |
    25 | `); 26 | } 27 | -------------------------------------------------------------------------------- /chapter3/simpleServer.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const { homedir } = require("os"); 3 | const url = require("url"); 4 | 5 | const test = (req, res) => { 6 | res.end("TEST"); 7 | }; 8 | 9 | const home = (req, res) => { 10 | res.end("HOME"); 11 | }; 12 | 13 | const user = (req, res) => { 14 | console.log(req); 15 | const query = url.parse(req.url, true).query; 16 | console.log(query); 17 | res.end(userTemplate(query)); 18 | }; 19 | 20 | const feed = (req, res) => { 21 | res.end(`
      22 |
    • picture1
    • 23 |
    • picture2
    • 24 |
    • picture3
    • 25 |
    26 | `); 27 | }; 28 | 29 | const urlMap = { 30 | "/": home, 31 | "/test": test, 32 | "/user": user, 33 | "/feed": feed, 34 | }; 35 | 36 | http 37 | .createServer((req, res) => { 38 | const path = url.parse(req.url, true).pathname; 39 | res.setHeader("Content-Type", "text/html"); 40 | 41 | if (req.method !== "GET") { 42 | notAllowdMethod(req, res); 43 | return; 44 | } 45 | console.log(path); 46 | console.log(Object.keys(urlMap)); 47 | if (path in urlMap) { 48 | urlMap[path](req, res); 49 | } else { 50 | notFound(req, res); 51 | } 52 | }) 53 | .listen(3000, () => { 54 | console.log("심플 서버 시작"); 55 | }); 56 | 57 | const userTemplate = (user) => { 58 | return ` 59 | 60 | 61 |

    User info

    62 | name : ${user.name}
    63 | age : ${user.age} 64 | 65 | 66 | `; 67 | }; 68 | 69 | const loginTemplate = ` 70 |
    71 |
    72 | 73 | `; 74 | const login = (req, res) => { 75 | res.end(loginTemplate); 76 | }; 77 | 78 | const notFoundTemplate = ` 79 |

    404 page not found

    80 | `; 81 | 82 | const notFound = (req, res) => { 83 | res.end(notFoundTemplate); 84 | }; 85 | 86 | const notAllowdMethod = (req, res) => { 87 | res.end(`${req.method} is not allowed http method`); 88 | }; 89 | -------------------------------------------------------------------------------- /chapter4/npm-install-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lodash": "github:lodash/lodash#4.17.21" 4 | }, 5 | "devDependencies": { 6 | "jest": "^29.3.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter4/npm-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lodash": "github:lodash/lodash#4.17.21" 4 | }, 5 | "devDependencies": { 6 | "jest": "^27.5.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter4/sample-package/index.js: -------------------------------------------------------------------------------- 1 | console.log("require로 부르면 실행됩니다."); 2 | 3 | module.exports = { 4 | add: (a, b) => a + b, 5 | sub: (a, b) => a - b, 6 | multi: (a, b) => a * b, 7 | div: (a, b) => a / b, 8 | }; 9 | -------------------------------------------------------------------------------- /chapter4/sample-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-package", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /chapter4/sample-test/index.js: -------------------------------------------------------------------------------- 1 | const calc = require("sample-package"); 2 | 3 | const a = 17; 4 | const b = 3; 5 | 6 | console.log("a + b = ", calc.add(a, b)); 7 | console.log("a - b = ", calc.sub(a, b)); 8 | console.log("a * b = ", calc.multi(a, b)); 9 | console.log("a / b = ", calc.div(a, b)); 10 | -------------------------------------------------------------------------------- /chapter4/sample-test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-test", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "sample-package": "file:../sample-package" 9 | } 10 | }, 11 | "../sample-package": { 12 | "version": "1.0.0", 13 | "license": "ISC" 14 | }, 15 | "../sample-project": { 16 | "extraneous": true 17 | }, 18 | "node_modules/sample-package": { 19 | "resolved": "../sample-package", 20 | "link": true 21 | } 22 | }, 23 | "dependencies": { 24 | "sample-package": { 25 | "version": "file:../sample-package" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter4/sample-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "sample-package": "file:../sample-package" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /chapter4/test-npx/index.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | /* 주석도 포매팅 해줍니다. */ 3 | return Math.floor(Math.random() * (max - min)) + min; 4 | } 5 | 6 | console.log(getRandomInt(10, 20)); 7 | -------------------------------------------------------------------------------- /chapter4/test-npx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cowsay": "^1.5.0", 4 | "jest": "^27.5.1", 5 | "prettier": "^2.8.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter4/test-package-lock/index.js: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | 3 | const spinner = ora('Loading unicorns').start(); 4 | 5 | setTimeout(() => { 6 | spinner.color = 'yellow'; 7 | spinner.text = 'Loading rainbows'; 8 | }, 1000); 9 | -------------------------------------------------------------------------------- /chapter4/test-package-lock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ora": "^6.0.1" 4 | }, 5 | "type": "module" 6 | } 7 | -------------------------------------------------------------------------------- /chapter4/test-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-scripts", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "prehello": "echo 'PRE HELLO'", 6 | "hello": "echo 'hello Node.js'", 7 | "posthello": "echo 'POST HELLO'", 8 | "test": "echo 'test Node.js'", 9 | "stop": "echo 'stop Node.js'", 10 | "start": "echo 'start Node.js'", 11 | "restart": "echo 'restart Node.js'" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter4/test-yarn/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /chapter4/test-yarn/.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/patches 3 | !/.yarn/plugins 4 | !/.yarn/releases 5 | !/.yarn/sdks 6 | 7 | # Swap the comments on the following lines if you don't wish to use zero-installs 8 | # Documentation here: https://yarnpkg.com/features/zero-installs 9 | !/.yarn/cache 10 | #/.pnp.* 11 | -------------------------------------------------------------------------------- /chapter4/test-yarn/.yarn/cache/ansi-styles-npm-6.1.0-4f6a594d04-7a7f8528c0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter4/test-yarn/.yarn/cache/ansi-styles-npm-6.1.0-4f6a594d04-7a7f8528c0.zip -------------------------------------------------------------------------------- /chapter4/test-yarn/.yarn/cache/chalk-https-da1a746d42-5804e5429a.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter4/test-yarn/.yarn/cache/chalk-https-da1a746d42-5804e5429a.zip -------------------------------------------------------------------------------- /chapter4/test-yarn/.yarn/cache/supports-color-npm-9.2.1-1ef7bf7d73-8a2bfeb64c.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter4/test-yarn/.yarn/cache/supports-color-npm-9.2.1-1ef7bf7d73-8a2bfeb64c.zip -------------------------------------------------------------------------------- /chapter4/test-yarn/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 2 | -------------------------------------------------------------------------------- /chapter4/test-yarn/README.md: -------------------------------------------------------------------------------- 1 | # test-yarn 2 | -------------------------------------------------------------------------------- /chapter4/test-yarn/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | app.get("/", (req, res) => { 6 | res.set({ "Content-Type": "text/html; charset=utf-8" }); 7 | res.end("헬로 express"); 8 | }); 9 | 10 | app.listen(port, () => { 11 | console.log(`START SERVER : use ${port}`); 12 | }); 13 | -------------------------------------------------------------------------------- /chapter4/test-yarn/main.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | console.log(chalk.blue("안녕하세요!")); 4 | -------------------------------------------------------------------------------- /chapter4/test-yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-yarn", 3 | "packageManager": "yarn@3.1.1", 4 | "dependencies": { 5 | "chalk": "https://github.com/wapj/chalk" 6 | }, 7 | "type": "module" 8 | } 9 | -------------------------------------------------------------------------------- /chapter4/test-yarn/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 5 6 | cacheKey: 8 7 | 8 | "ansi-styles@npm:^6.1.0": 9 | version: 6.1.0 10 | resolution: "ansi-styles@npm:6.1.0" 11 | checksum: 7a7f8528c07a9d20c3a92bccd2b6bc3bb4d26e5cb775c02826921477377bd495d615d61f710d56216344b6238d1d11ef2b0348e146c5b128715578bfb3217229 12 | languageName: node 13 | linkType: hard 14 | 15 | "chalk@https://github.com/wapj/chalk": 16 | version: 5.0.0 17 | resolution: "chalk@https://github.com/wapj/chalk.git#commit=1eae5be2b9a0db207301bbba3591bd810755f15d" 18 | dependencies: 19 | ansi-styles: ^6.1.0 20 | supports-color: ^9.2.1 21 | checksum: 5804e5429afca448fcb13fae0f953b92a8c505b31c82c4d92a740f9d36fd7d83bbbf5633b7619b6fa1e8537015fadbdbc1df6885428b5733372881537ef792fd 22 | languageName: node 23 | linkType: hard 24 | 25 | "supports-color@npm:^9.2.1": 26 | version: 9.2.1 27 | resolution: "supports-color@npm:9.2.1" 28 | checksum: 8a2bfeb64c1512d21a1a998c1f64acdaa85cf1f6a101627286548f19785524b329d7b28d567a28fc2d708fc7aba32f4c82a9b224f76b30a337a39d3e53418ff7 29 | languageName: node 30 | linkType: hard 31 | 32 | "test-yarn@workspace:.": 33 | version: 0.0.0-use.local 34 | resolution: "test-yarn@workspace:." 35 | dependencies: 36 | chalk: "https://github.com/wapj/chalk" 37 | languageName: unknown 38 | linkType: soft 39 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/async-await.js: -------------------------------------------------------------------------------- 1 | async function myName() { 2 | return "Andy"; 3 | } 4 | 5 | async function showName() { 6 | const name = await myName(); 7 | console.log(name); 8 | } 9 | 10 | console.log(myName()); 11 | // console.log(showName()); 12 | 13 | function waitOneSecond(msg) { 14 | return new Promise((resolve, _) => { 15 | setTimeout(() => resolve(`${msg}`), 1000); 16 | }); 17 | } 18 | 19 | async function countOneToTen() { 20 | for (let x of [...Array(10).keys()]) { 21 | let result = await waitOneSecond(`${x + 1}초 대기중...`); 22 | console.log(result); 23 | } 24 | console.log("완료"); 25 | } 26 | 27 | countOneToTen(); 28 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/callback-test.js: -------------------------------------------------------------------------------- 1 | const DB = []; 2 | 3 | function register(user) { 4 | return saveDB(user, function (user) { 5 | return sendEmail(user, function (user) { 6 | return getResult(user); 7 | }); 8 | }); 9 | } 10 | 11 | function saveDB(user, callback) { 12 | DB.push(user); 13 | console.log(`save ${user.name} to DB`); 14 | return callback(user); 15 | } 16 | 17 | function sendEmail(user, callback) { 18 | console.log(`email to ${user.email}`); 19 | return callback(user); 20 | } 21 | 22 | function getResult(user) { 23 | return `success register ${user.name}`; 24 | } 25 | 26 | const result = register({ email: "andy@test.com", password: "1234", name: "andy" }); 27 | console.log(result); 28 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/ideal-promise-code.js: -------------------------------------------------------------------------------- 1 | function goodPromise(val) { 2 | return new Promise((resolve, reject) => { 3 | resolve(val); 4 | }); 5 | } 6 | 7 | goodPromise("세상에") 8 | .then((val) => { 9 | return val + " 이런"; 10 | }) 11 | .then((val) => { 12 | return val + " 코드는"; 13 | }) 14 | .then((val) => { 15 | return val + " 없습니다. "; 16 | }) 17 | .then((val) => { 18 | console.log(val); 19 | }) 20 | .catch((err) => { 21 | console.log(err); 22 | }); 23 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "axios": "^0.27.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/promise-anti-pattern1.js: -------------------------------------------------------------------------------- 1 | function myWork(work) { 2 | return new Promise((resolve, reject) => { 3 | if (work === 'done') { 4 | resolve('게임 가능'); 5 | } else { 6 | reject(new Error("게임 불가능")); 7 | } 8 | }) 9 | } 10 | 11 | myWork('done').then(function (value) { console.log(value) }, function (err) { console.error(err) }); 12 | 13 | myWork('doing') 14 | .then(function (value) { console.log(value) }) 15 | .catch(function (err) { console.error(err) }); -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/promise-anti-pattern2.js: -------------------------------------------------------------------------------- 1 | 2 | function myWork(work) { 3 | return new Promise((resolve, reject) => { 4 | resolve(work.toUpperCase()) 5 | }) 6 | } 7 | 8 | function playGame(work) { 9 | return new Promise((resolve, reject) => { 10 | if (work === 'DONE') { 11 | resolve('GO PLAY GAME'); 12 | } else { 13 | reject(new Error("DON'T")); 14 | } 15 | }) 16 | } 17 | 18 | myWork('done') 19 | .then(function (result) { 20 | playGame(result).then(function (val) { 21 | console.log(val); 22 | }); 23 | }) 24 | 25 | myWork('done') 26 | .then(playGame) 27 | .then(console.log) -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/promise-test.js: -------------------------------------------------------------------------------- 1 | const DB = []; 2 | 3 | function saveDB(user) { 4 | // 실패 테스트시 다음의 주석 해제 5 | const oldDBSize = DB.length + 1; 6 | // const oldDBSize = DB.length; 7 | 8 | DB.push(user); 9 | console.log(`save ${user.name} to DB`); 10 | return new Promise((resolve, reject) => { 11 | if (DB.length > oldDBSize) { 12 | resolve(user); 13 | } else { 14 | reject(new Error("Save DB Error!")); 15 | } 16 | }); 17 | } 18 | 19 | function sendEmail(user) { 20 | console.log(`email to ${user.email}`); 21 | return new Promise((resolve) => { 22 | resolve(user); 23 | }); 24 | } 25 | 26 | function getResult(user) { 27 | return new Promise((resolve, reject) => { 28 | resolve(`success register ${user.name}`); 29 | }); 30 | } 31 | 32 | function registerByPromise(user) { 33 | const result = saveDB(user) 34 | .then(sendEmail) 35 | .then(getResult) 36 | .catch(error => new Error(error)) 37 | .finally(() => console.log("완료!")); 38 | // 아직 완료되지 않았으므로 pending 상태로 나옴 39 | console.log(result); 40 | return result; 41 | } 42 | 43 | const myUser = { email: "andy@test.com", password: "1234", name: "andy" }; 44 | 45 | const result = registerByPromise(myUser); 46 | result.then(console.log); 47 | 48 | // allResult = Promise.all([saveDB(myUser), sendEmail(myUser), getResult(myUser)]); 49 | // allResult.then(console.log); 50 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/promise-test2.js: -------------------------------------------------------------------------------- 1 | const DB = []; 2 | 3 | function saveDB(user) { 4 | const oldDBSize = DB.length; 5 | 6 | DB.push(user); 7 | console.log(`save ${user.name} to DB`); 8 | return new Promise((resolve, reject) => { 9 | if (DB.length > oldDBSize) { 10 | resolve(user); 11 | } else { 12 | reject(new Error("Save DB Error!")); 13 | } 14 | }); 15 | } 16 | 17 | function sendEmail(user) { 18 | console.log(`email to ${user.email}`); 19 | return new Promise((resolve) => { 20 | resolve(user); 21 | }); 22 | } 23 | 24 | function getResult(user) { 25 | return new Promise((resolve, reject) => { 26 | resolve(`success register ${user.name}`); 27 | }); 28 | } 29 | 30 | function registerByPromise(user) { 31 | const result = saveDB(user) 32 | .then(sendEmail) 33 | .then(getResult) 34 | .catch(error => new Error(error)); 35 | // 아직 완료되지 않았으므로 pending 상태로 나옴 36 | console.log(result); 37 | return result; 38 | } 39 | 40 | const myUser = { email: "andy@test.com", password: "1234", name: "andy" }; 41 | 42 | 43 | allResult = Promise.all([saveDB(myUser), sendEmail(myUser), getResult(myUser)]); 44 | allResult.then(console.log); 45 | 46 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/top20-movie-async-await.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | async function getTop20Movies() { 4 | const url = "https://raw.githubusercontent.com/wapj/musthavenodejs/main/movieinfo.json"; 5 | try { 6 | const result = await axios.get(url); 7 | const { data } = result; 8 | if (!data.articleList || data.articleList.length == 0) { 9 | throw new Error("데이터가 없습니다."); 10 | } 11 | 12 | const movieInfos = data.articleList.map((article, idx) => { 13 | return { title: article.title, rank: idx + 1 }; 14 | }); 15 | 16 | for (let movieInfo of movieInfos) { 17 | console.log(`[${movieInfo.rank}위] ${movieInfo.title}`); 18 | } 19 | } catch (err) { 20 | throw new Error(err); 21 | } 22 | } 23 | 24 | getTop20Movies(); 25 | -------------------------------------------------------------------------------- /chapter5/callback-promise-async-await/top20-movie-promise-code.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const url = "https://raw.githubusercontent.com/wapj/musthavenodejs/main/movieinfo.json"; 3 | 4 | axios 5 | .get(url) 6 | .then((result) => { 7 | if (result.status != 200) { 8 | throw new Error("요청에 실패했습니다!"); 9 | } 10 | 11 | if (result.data) { 12 | return result.data; 13 | } 14 | 15 | throw new Error("데이터 없습니다."); 16 | }) 17 | .then((data) => { 18 | if (!data.articleList || data.articleList.length == 0) { 19 | throw new Error("데이터가 없습니다."); 20 | } 21 | return data.articleList; 22 | }) 23 | .then((articles) => { 24 | return articles.map((article, idx) => { 25 | return { title: article.title, rank: idx + 1 }; 26 | }); 27 | }) 28 | .then((results) => { 29 | for (let movieInfo of results) { 30 | console.log(`[${movieInfo.rank}위] ${movieInfo.title}`); 31 | } 32 | }) 33 | .catch((err) => { 34 | console.log("<<에러발생>>"); 35 | console.log(err); 36 | }); 37 | -------------------------------------------------------------------------------- /chapter6/test-mongoose/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "server": "http://localhost:3000" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /chapter6/test-mongoose/mongoose-crud.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const bodyParser = require("body-parser"); 3 | const mongoose = require("mongoose"); 4 | const Person = require("./person-model"); 5 | 6 | mongoose.set("strictQuery", false); // Mongoose 7이상에서는 설정해줘야 경고가 뜨지 않음. 7 | 8 | const app = express(); 9 | app.use(bodyParser.json()); 10 | app.listen(3000, () => { 11 | console.log("Server started"); 12 | const mongodbUri = 13 | "mongodb+srv://mymongo:test1234@cluster0.c4xru.mongodb.net/test?retryWrites=true&w=majority"; 14 | mongoose 15 | .connect(mongodbUri, { useNewUrlParser: true }) 16 | .then(console.log("Connected to MongoDB")); 17 | }); 18 | 19 | app.get("/person", async (req, res) => { 20 | const person = await Person.find({}); 21 | res.send(person); 22 | }); 23 | 24 | app.get("/person/:email", async (req, res) => { 25 | const person = await Person.findOne({ email: req.params.email }); 26 | res.send(person); 27 | }); 28 | 29 | app.post("/person", async (req, res) => { 30 | const person = new Person(req.body); 31 | await person.save(); 32 | res.send(person); 33 | }); 34 | 35 | app.put("/person/:email", async (req, res) => { 36 | const person = await Person.findOneAndUpdate( 37 | { email: req.params.email }, 38 | { $set: req.body }, 39 | { new: true } 40 | ); 41 | console.log(person); 42 | res.send(person); 43 | }); 44 | 45 | app.delete("/person/:email", async (req, res) => { 46 | await Person.deleteMany({ email: req.params.email }); 47 | res.send({ success: true }); 48 | }); 49 | -------------------------------------------------------------------------------- /chapter6/test-mongoose/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "express": "^4.18.2", 4 | "mongodb": "^4.12.1", 5 | "mongoose": "^6.8.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /chapter6/test-mongoose/person-model.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | const personSchema = new Schema({ 5 | name: String, 6 | age: Number, 7 | email: { type: String, required: true }, 8 | }); 9 | 10 | module.exports = mongoose.model('Person', personSchema); -------------------------------------------------------------------------------- /chapter6/test-mongoose/person.http: -------------------------------------------------------------------------------- 1 | # ❶ server 변수 설정 2 | @server = http://localhost:3000 3 | 4 | ### ❷ GET 요청 보내기 5 | GET {{server}}/person 6 | 7 | ### ❸ POST 요청 보내기 8 | POST {{server}}/person 9 | Content-Type: application/json 10 | 11 | { 12 | "name": "Andy Park", 13 | "age": 30, 14 | "email": "andy@backend.com" 15 | } 16 | 17 | ### ❹ 생성한 문서 확인 18 | GET {{server}}/person/andy@backend.com 19 | 20 | ### ❺ PUT 요청 보내기, 문서 수정하기 21 | PUT {{server}}/person/andy@backend.com 22 | Content-Type: application/json 23 | 24 | { 25 | "age": 32 26 | } 27 | 28 | ### ❻ 문서 삭제하기 29 | DELETE {{server}}/person/andy@backend.com 30 | -------------------------------------------------------------------------------- /chapter6/try-mongo/mongo-crud.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require('mongodb').MongoClient; 2 | const uri = "mongodb+srv://mymongo:test1234@cluster0.c4xru.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"; 3 | const client = new MongoClient(uri, { useNewUrlParser: true }); 4 | 5 | async function run() { 6 | try { 7 | await client.connect(); 8 | const collection = client.db("board").collection("users"); 9 | // Create 10 | const result = await collection.insertOne({ name: 'John', email: 'john@example.com' }); 11 | console.log(result.insertedId); 12 | // Read 13 | const documents = await collection.find().toArray(); 14 | console.log(documents); 15 | // Update 16 | const updateResult = await collection.updateOne({ name: 'John' }, { $set: { name: 'Mike' } }); 17 | console.log(updateResult.modifiedCount); 18 | // Delete 19 | const deleteResult = await collection.deleteOne({ name: 'Mike' }); 20 | console.log(deleteResult.deletedCount); 21 | } catch (error) { 22 | console.error(error); 23 | } finally { 24 | await client.close(); 25 | } 26 | } 27 | 28 | run(); -------------------------------------------------------------------------------- /chapter6/try-mongo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mongodb": "^4.12.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /chapter6/try-mongo/test-connection.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | const uri = "mongodb+srv://mymongo:test1234@cluster0.c4xru.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"; 3 | const client = new MongoClient(uri); 4 | 5 | async function run() { 6 | await client.connect(); 7 | const adminDB = client.db('test').admin(); 8 | const listDatabases = await adminDB.listDatabases(); 9 | console.log(listDatabases); 10 | return "OK"; 11 | } 12 | 13 | run() 14 | .then(console.log) 15 | .catch(console.error) 16 | .finally(() => client.close()); -------------------------------------------------------------------------------- /chapter6/try-mongo/test-crud.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require('mongodb').MongoClient; 2 | 3 | // Replace with your MongoDB Atlas password, and with the information about your cluster 4 | const url = 'mongodb+srv://mymongo:test1234@cluster0.c4xru.mongodb.net/test?retryWrites=true&w=majority'; 5 | 6 | // Create a new MongoClient 7 | const client = new MongoClient(url, { useNewUrlParser: true }); 8 | 9 | async function main() { 10 | try { 11 | // 커넥션을 생성하고 연결을 시도 12 | await client.connect(); 13 | 14 | console.log('MongoDB 접속 성공'); 15 | 16 | // test 데이터베이스의 person 컬렉션을 가져옴 17 | const collection = client.db('test').collection('person'); 18 | 19 | 20 | // Document 하나 추가 21 | await collection.insertOne({ name: 'Andy', age: 30 }); 22 | console.log('document 추가 완료'); 23 | 24 | // Document 찾기 25 | const documents = await collection.find({ name: 'Andy' }).toArray(); 26 | console.log('찾은 document:', documents); 27 | 28 | // Document 갱신하기 29 | await collection.updateOne({ name: 'Andy' }, { $set: { age: 31 } }); 30 | console.log('document 업데이트'); 31 | 32 | // 갱신된 Document 확인하기 33 | const updatedDocuments = await collection.find({ name: 'Andy' }).toArray(); 34 | console.log('갱신된 document :', updatedDocuments); 35 | 36 | // Document 삭제하기 37 | // await collection.deleteOne({ name: 'Andy' }); 38 | console.log('document 삭제'); 39 | 40 | // 연결 끊기 41 | await client.close(); 42 | } catch (err) { 43 | console.error(err); 44 | } 45 | } 46 | 47 | main(); 48 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/configs/handlebars-helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lengthOfList: (list = []) => list.length, 3 | eq: (val1, val2) => val1 === val2, 4 | dateString: (isoString) => new Date(isoString).toLocaleDateString(), 5 | }; 6 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/configs/mongodb-connection.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | const uri = "mongodb+srv://:@/board"; 3 | // const uri = "mongodb://localhost:27017"; 4 | 5 | module.exports = function (callback) { 6 | return MongoClient.connect(uri, callback); 7 | }; 8 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "board", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "npx nodemon app.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.17.3", 15 | "express-handlebars": "^6.0.3", 16 | "lodash": "4.17.21", 17 | "mongodb": "^4.13.0", 18 | "mongoose": "^6.8.1", 19 | "mustache-express": "1.3.2", 20 | "nodemon": "2.0.15" 21 | }, 22 | "devDependencies": { 23 | "http-server": "^14.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/services/post-service.js: -------------------------------------------------------------------------------- 1 | const { ObjectId } = require("mongodb"); 2 | const paginator = require("../utils/paginator"); 3 | 4 | // 글작성 5 | async function writePost(collection, post) { 6 | // 생성일시와 조회수를 넣어준다. 7 | post.hits = 0; 8 | post.createdDt = new Date().toISOString(); 9 | return await collection.insertOne(post); 10 | } 11 | 12 | // 글목록 13 | async function list(collection, page, search) { 14 | const perPage = 10; 15 | const query = { title: new RegExp(search, "i") }; 16 | const cursor = collection.find(query, { limit: perPage, skip: (page - 1) * perPage }).sort({ 17 | createdDt: -1, 18 | }); 19 | const totalCount = await collection.count(query); 20 | const posts = await cursor.toArray(); 21 | const paginatorObj = paginator({ totalCount, page, perPage: perPage }); 22 | return [posts, paginatorObj]; 23 | } 24 | 25 | // 패스워드는 노출 할 필요가 없으므로 결과값으로 가져오지않음. 26 | const projectionOption = { 27 | projection: { 28 | // 프로젝션(투영) 결과값에서 일부만 가져올 때 사용함. 29 | password: 0, 30 | "comments.password": 0, 31 | }, 32 | }; 33 | 34 | async function getDetailPost(collection, id) { 35 | return await collection.findOneAndUpdate({ _id: ObjectId(id) }, { $inc: { hits: 1 } }, projectionOption); 36 | } 37 | 38 | async function getPostByIdAndPassword(collection, { id, password }) { 39 | return await collection.findOne({ _id: ObjectId(id), password: password }, projectionOption); 40 | } 41 | 42 | async function getPostById(collection, id) { 43 | return await collection.findOne({ _id: ObjectId(id) }, projectionOption); 44 | } 45 | 46 | async function updatePost(collection, id, post) { 47 | const toUpdatePost = { 48 | $set: { 49 | ...post, 50 | }, 51 | }; 52 | 53 | return await collection.updateOne({ _id: ObjectId(id) }, toUpdatePost); 54 | } 55 | 56 | module.exports = { 57 | list, 58 | writePost, 59 | getDetailPost, 60 | getPostById, 61 | getPostByIdAndPassword, 62 | updatePost, 63 | projectionOption, 64 | }; 65 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/src/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
    12 | express로 게시판 만들기 13 |
    14 | 15 | 16 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/utils/paginator.js: -------------------------------------------------------------------------------- 1 | const lodash = require("lodash"); 2 | const PAGE_LIST_SIZE = 10; 3 | 4 | module.exports = ({ totalCount, page, perPage = 10 }) => { 5 | const PER_PAGE = perPage; 6 | const totalPage = Math.ceil(totalCount / PER_PAGE); 7 | 8 | // 시작페이지 9 | let quotient = parseInt(page / PAGE_LIST_SIZE); 10 | if (page % PAGE_LIST_SIZE === 0) { 11 | quotient -= 1; 12 | } 13 | const startPage = quotient * PAGE_LIST_SIZE + 1; 14 | 15 | // 끝페이지 : startPage + PAGE_LIST_SIZE - 1 16 | const endPage = startPage + PAGE_LIST_SIZE - 1 < totalPage ? startPage + PAGE_LIST_SIZE - 1 : totalPage; 17 | const isFirstPage = page === 1; 18 | const isLastPage = page === totalPage; 19 | const hasPrev = page > 1; 20 | const hasNext = page < totalPage; 21 | 22 | const paginator = { 23 | pageList: lodash.range(startPage, endPage + 1), 24 | page, 25 | prevPage: page - 1, 26 | nextPage: page + 1, 27 | startPage, 28 | lastPage: totalPage, 29 | hasPrev, 30 | hasNext, 31 | isFirstPage, 32 | isLastPage, 33 | }; 34 | return paginator; 35 | }; 36 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 게시판 프로젝트 5 | 6 | 7 | 8 | 9 | 10 | {{{body}}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /chapter7/board-tailwind/views/write.handlebars: -------------------------------------------------------------------------------- 1 |
    2 |
    [{{title}}] 글 {{#if (eq mode "create")}}작성{{else}}수정{{/if}}
    3 | 4 |
    5 | 6 | {{#if (eq mode "modify")}} 7 | 8 | {{/if}} 9 |
    10 |
    11 | 12 | 13 |
    14 | 15 |
    16 | 17 | 19 |
    20 | 21 |
    22 | 23 | 29 |
    30 | 31 |
    32 | 33 | 34 |
    35 |
    36 | 37 |
    38 | 39 | 40 |
    41 |
    42 | 43 |
    -------------------------------------------------------------------------------- /chapter7/board/configs/handlebars-helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // ❶ 리스트 길이를 반환 3 | lengthOfList: (list = []) => list.length, 4 | // ❷ 두 값을 비교해 같은지 유무를 반환 5 | eq: (val1, val2) => val1 === val2, 6 | // ❸ ISO 날짜 문자열에서 날짜만 반환 7 | dateString: (isoString) => new Date(isoString).toLocaleDateString(), 8 | }; 9 | -------------------------------------------------------------------------------- /chapter7/board/configs/mongodb-connection.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | const uri = "mongodb+srv://:@/board"; 3 | 4 | module.exports = function (callback) { 5 | return MongoClient.connect(uri, callback); 6 | }; 7 | -------------------------------------------------------------------------------- /chapter7/board/configs/mongoose-connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | mongoose.set("strictQuery", false); 3 | const uri = 4 | "mongodb+srv://:@/board?retryWrites=true&w=majority"; 5 | 6 | module.exports = function () { 7 | return mongoose.connect(uri, { useNewUrlParser: true }); 8 | }; 9 | -------------------------------------------------------------------------------- /chapter7/board/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "board", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npx nodemon app.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.18.2", 15 | "express-handlebars": "^6.0.6", 16 | "lodash": "^4.17.21", 17 | "mongodb": "^4.13.0", 18 | "mongoose": "^6.8.1", 19 | "nodemon": "^2.0.20" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chapter7/board/services/post-service.js: -------------------------------------------------------------------------------- 1 | const paginator = require("../utils/paginator"); 2 | const { ObjectId } = require("mongodb"); 3 | 4 | async function writePost(collection, post) { 5 | post.hits = 0; 6 | post.createdDt = new Date().toISOString(); 7 | return await collection.insertOne(post); 8 | } 9 | 10 | async function list(collection, page, search) { 11 | const perPage = 10; 12 | const query = { title: new RegExp(search, "i") }; 13 | const cursor = collection 14 | .find(query, { limit: perPage, skip: (page - 1) * perPage }) 15 | .sort({ 16 | createdDt: -1, 17 | }); 18 | const totalCount = await collection.count(query); 19 | const posts = await cursor.toArray(); 20 | const paginatorObj = paginator({ totalCount, page, perPage: perPage }); 21 | return [posts, paginatorObj]; 22 | } 23 | 24 | const projectionOption = { 25 | projection: { 26 | // 프로젝션(투영) 결괏값에서 일부만 가져올 때 사용 27 | password: 0, 28 | "comments.password": 0, 29 | }, 30 | }; 31 | 32 | async function getDetailPost(collection, id) { 33 | return await collection.findOneAndUpdate( 34 | { _id: ObjectId(id) }, 35 | { $inc: { hits: 1 } }, 36 | projectionOption 37 | ); 38 | } 39 | 40 | async function getPostByIdAndPassword(collection, { id, password }) { 41 | // ❶ findOne() 함수 사용 42 | return await collection.findOne( 43 | { _id: ObjectId(id), password: password }, 44 | projectionOption 45 | ); 46 | } 47 | 48 | // ❷ id로 데이터 불러오기 49 | async function getPostById(collection, id) { 50 | return await collection.findOne({ _id: ObjectId(id) }, projectionOption); 51 | } 52 | 53 | // ❸ 게시글 수정 54 | async function updatePost(collection, id, post) { 55 | const toUpdatePost = { 56 | $set: { 57 | ...post, 58 | }, 59 | }; 60 | return await collection.updateOne({ _id: ObjectId(id) }, toUpdatePost); 61 | } 62 | 63 | module.exports = { 64 | list, 65 | writePost, 66 | getDetailPost, 67 | getPostById, 68 | getPostByIdAndPassword, 69 | updatePost, 70 | }; 71 | -------------------------------------------------------------------------------- /chapter7/board/utils/paginator.js: -------------------------------------------------------------------------------- 1 | const lodash = require("lodash"); // ❶ 2 | const PAGE_LIST_SIZE = 10; // ❷ 3 | 4 | module.exports = ({ totalCount, page, perPage = 10 }) => { 5 | // ❸ 6 | const PER_PAGE = perPage; 7 | const totalPage = Math.ceil(totalCount / PER_PAGE); // ❹ 8 | 9 | // 시작 페이지 : 몫 * PAGE_LIST_SIZE + 1 10 | let quotient = parseInt(page / PAGE_LIST_SIZE); 11 | if (page % PAGE_LIST_SIZE === 0) { 12 | quotient -= 1; 13 | } 14 | const startPage = quotient * PAGE_LIST_SIZE + 1; // ❺ 15 | 16 | // 끝 페이지 : startPage + PAGE_LIST_SIZE - 1 17 | const endPage = 18 | startPage + PAGE_LIST_SIZE - 1 < totalPage 19 | ? startPage + PAGE_LIST_SIZE - 1 20 | : totalPage; // ❻ 21 | const isFirstPage = page === 1; 22 | const isLastPage = page === totalPage; 23 | const hasPrev = page > 1; 24 | const hasNext = page < totalPage; 25 | 26 | const paginator = { 27 | pageList: lodash.range(startPage, endPage + 1), // ❼ 28 | page, 29 | prevPage: page - 1, 30 | nextPage: page + 1, 31 | startPage, 32 | lastPage: totalPage, 33 | hasPrev, 34 | hasNext, 35 | isFirstPage, 36 | isLastPage, 37 | }; 38 | return paginator; 39 | }; 40 | -------------------------------------------------------------------------------- /chapter7/board/views/home.handlebars: -------------------------------------------------------------------------------- 1 |

    {{title}}

    2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 글쓰기 10 |
    11 | 12 | 13 |
    14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{#each posts}} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{/each}} 32 | 33 |
    제목작성자조회수등록일
    {{title}}{{writer}}{{hits}}{{dateString createdDt}}
    34 |
    35 | 36 | 37 |
    38 | {{#with paginator}} 39 | << 40 | {{#if hasPrev}} 41 | < 42 | {{else}} 43 | < 44 | {{/if}} 45 | {{#each pageList}} 46 | {{#if (eq this @root.paginator.page)}} 47 | {{.}} 48 | {{else}} 49 | {{.}} 50 | {{/if}} 51 | {{/each}} 52 | {{#if hasNext}} 53 | > 54 | {{else}} 55 | > 56 | {{/if}} 57 | >> 58 | {{/with}} 59 |
    -------------------------------------------------------------------------------- /chapter7/board/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 게시판 프로젝트 6 | 7 | 8 | 9 | {{{body}}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /chapter7/board/views/write.handlebars: -------------------------------------------------------------------------------- 1 | 2 |

    [{{title}}] 글 {{#if (eq mode "create")}}작성{{else}}수정{{/if}}

    3 |
    4 | 5 |
    6 | 7 | {{#if (eq mode "modify")}} 8 | 9 | {{/if}} 10 | 11 | 12 |
    13 | 14 | 15 |
    16 | 17 |
    18 | 19 | 20 |
    21 | 22 |
    23 | 24 | 25 |
    26 | 27 | 28 |
    29 |
    30 | 31 |
    32 | 33 |
    34 | 35 | 36 |
    37 | 38 |
    -------------------------------------------------------------------------------- /chapter8/blog-file/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter8/blog-file/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter8/blog-file/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BlogController } from './blog.controller'; 3 | import { BlogService } from './blog.service'; 4 | @Module({ 5 | imports: [], 6 | controllers: [BlogController], 7 | providers: [BlogService], 8 | }) 9 | export class AppModule {} -------------------------------------------------------------------------------- /chapter8/blog-file/src/blog.controller.ts: -------------------------------------------------------------------------------- 1 | // 1 데코레이터 함수 임포트 2 | import { 3 | Controller, 4 | Param, 5 | Body, 6 | Delete, 7 | Get, 8 | Post, 9 | Put, 10 | } from "@nestjs/common"; 11 | import { BlogService } from "./blog.service"; 12 | 13 | @Controller("blog") 14 | export class BlogController { 15 | constructor(private blogService: BlogService) {} 16 | 17 | @Get() 18 | getAllPosts() { 19 | return this.blogService.getAllPosts(); 20 | } 21 | 22 | @Post() 23 | createPost(@Body() postDto) { 24 | console.log("게시글 작성"); 25 | this.blogService.createPost(postDto); 26 | return "success"; 27 | } 28 | 29 | @Get("/:id") 30 | async getPost(@Param("id") id: string) { 31 | console.log("게시글 하나 가져오기"); 32 | const post = await this.blogService.getPost(id); 33 | console.log(post); 34 | return post; 35 | } 36 | 37 | @Delete("/:id") 38 | deletePost(@Param("id") id: string) { 39 | console.log("게시글 삭제"); 40 | this.blogService.delete(id); 41 | } 42 | 43 | @Put("/:id") 44 | updatePost(@Param("id") id, @Body() postDto) { 45 | console.log(`[${id}] 게시글 업데이트`, id, postDto); 46 | return this.blogService.updatePost(id, postDto); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter8/blog-file/src/blog.data.json: -------------------------------------------------------------------------------- 1 | [{"id":"1","title":"안녕하세요","content":"처음 인사드립니다.","name":"이름","createdDt":"2023-04-04T20:08:32.937Z"},{"id":"3","title":"타이틀 수정3","content":"본문수정3","name":"jerome.kim","updatedDt":"2023-04-04T20:08:45.423Z"}] -------------------------------------------------------------------------------- /chapter8/blog-file/src/blog.http: -------------------------------------------------------------------------------- 1 | @server = http://localhost:3000 2 | 3 | # 게시글 조회 4 | GET {{server}}/blog 5 | 6 | ### 게시글 생성 7 | POST {{server}}/blog 8 | Content-Type: application/json 9 | 10 | { 11 | "title": "안녕하세요", 12 | "content": "처음 인사드립니다.", 13 | "name": "이름" 14 | } 15 | 16 | ### 특정 게시글 조회 17 | GET {{server}}/blog/1 18 | 19 | ### 게시글 삭제 20 | DELETE {{server}}/blog/2 21 | 22 | ### 게시글 수정 23 | PUT {{server}}/blog/3 24 | Content-Type: application/json 25 | 26 | { 27 | "title": "타이틀 수정3", 28 | "content": "본문수정3", 29 | "name": "jerome.kim" 30 | } -------------------------------------------------------------------------------- /chapter8/blog-file/src/blog.model.ts: -------------------------------------------------------------------------------- 1 | export interface PostDto { 2 | id: string; 3 | title: string; 4 | content: string; 5 | name: string; 6 | createdDt: Date; 7 | updatedDt?: Date; 8 | } 9 | -------------------------------------------------------------------------------- /chapter8/blog-file/src/blog.repository.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "fs/promises"; 2 | import { PostDto } from "./blog.model"; 3 | 4 | // 블로그 인터페이스 5 | export interface BlogRepository { 6 | getAllPost(): Promise; 7 | createPost(postDto: PostDto); 8 | getPost(id: String): Promise; 9 | deletePost(id: String); 10 | updatePost(id: String, postDto: PostDto); 11 | } 12 | 13 | // 블로그 인터페이스를 구현하여 파일에 데이터를 저장하는 클래스 14 | export class BlogFileRepository implements BlogRepository { 15 | FILE_NAME = "./src/blog.data.json"; 16 | 17 | async getAllPost(): Promise { 18 | const datas = await readFile(this.FILE_NAME, "utf8"); 19 | const posts = JSON.parse(datas); 20 | return posts; 21 | } 22 | 23 | async createPost(postDto: PostDto) { 24 | const posts = await this.getAllPost(); 25 | const id = posts.length + 1; 26 | const createPost = { id: id.toString(), ...postDto, createdDt: new Date() }; 27 | posts.push(createPost); 28 | await writeFile(this.FILE_NAME, JSON.stringify(posts)); 29 | } 30 | 31 | async getPost(id: string): Promise { 32 | const posts = await this.getAllPost(); 33 | const result = posts.find((post) => post.id === id); 34 | return result; 35 | } 36 | 37 | async deletePost(id: string) { 38 | const posts = await this.getAllPost(); 39 | const filteredPosts = posts.filter((post) => post.id !== id); 40 | await writeFile(this.FILE_NAME, JSON.stringify(filteredPosts)); 41 | } 42 | 43 | async updatePost(id: string, postDto: PostDto) { 44 | const posts = await this.getAllPost(); 45 | const index = posts.findIndex((post) => post.id === id); 46 | const updatePost = { id, ...postDto, updatedDt: new Date() }; 47 | posts[index] = updatePost; 48 | await writeFile(this.FILE_NAME, JSON.stringify(posts)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /chapter8/blog-file/src/blog.service.ts: -------------------------------------------------------------------------------- 1 | import { PostDto } from "./blog.model"; 2 | import { BlogFileRepository, BlogRepository } from "./blog.repository"; 3 | 4 | export class BlogService { 5 | blogRepository: BlogRepository; 6 | constructor() { 7 | this.blogRepository = new BlogFileRepository(); 8 | } 9 | 10 | posts = []; 11 | 12 | async getAllPosts() { 13 | return await this.blogRepository.getAllPost(); 14 | } 15 | 16 | createPost(postDto: PostDto) { 17 | this.blogRepository.createPost(postDto); 18 | } 19 | 20 | async getPost(id): Promise { 21 | return await this.blogRepository.getPost(id); 22 | } 23 | 24 | delete(id) { 25 | this.blogRepository.deletePost(id); 26 | } 27 | 28 | updatePost(id, postDto: PostDto) { 29 | this.blogRepository.updatePost(id, postDto); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chapter8/blog-file/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /chapter8/blog-file/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter8/blog-file/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 | -------------------------------------------------------------------------------- /chapter8/blog-memory/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter8/blog-memory/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter8/blog-memory/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BlogController } from './blog.controller'; 3 | import { BlogService } from './blog.service'; 4 | @Module({ 5 | imports: [], 6 | controllers: [BlogController], 7 | providers: [BlogService], 8 | }) 9 | export class AppModule {} -------------------------------------------------------------------------------- /chapter8/blog-memory/src/blog.controller.ts: -------------------------------------------------------------------------------- 1 | // 1 데코레이터 함수 임포트 2 | import { 3 | Controller, 4 | Param, 5 | Body, 6 | Delete, 7 | Get, 8 | Post, 9 | Put, 10 | } from "@nestjs/common"; 11 | import { BlogService } from "./blog.service"; 12 | 13 | @Controller("blog") // 2 클래스에 붙이는 Controller 데코레이터 14 | export class BlogController { 15 | constructor(private blogService: BlogService) {} 16 | 17 | @Get() // 3 GET 요청 처리 18 | getAllPosts() { 19 | return this.blogService.getAllPosts(); 20 | } 21 | 22 | @Post() // 4 POST 요청 처리 23 | createPost(@Body() postDto) { 24 | // 5 HTTP 요청의 body 내용을 post에 할당 25 | console.log("게시글 작성"); 26 | this.blogService.createPost(postDto); 27 | return "success"; 28 | } 29 | 30 | @Get("/:id") // 6 GET에 URL 매개변수에 id가 있는 요청 처리 31 | getPost(@Param("id") id: string) { 32 | console.log(`[id: ${id}]게시글 하나 가져오기`); 33 | return this.blogService.getPost(id); 34 | } 35 | 36 | @Delete("/:id") // 7 DELETE 방식에 URL 매개변수로 id가 있는 요청 처리 37 | deletePost(@Param("id") id: string) { 38 | console.log("게시글 삭제"); 39 | this.blogService.delete(id); 40 | } 41 | 42 | @Put("/:id") // 8 PUT 방식에 URL 매개변수로 전달된 id가 있는 요청 처리 43 | updatePost(@Param("id") id, @Body() postDto) { 44 | console.log(`[${id}] 게시글 업데이트`, id, postDto); 45 | return this.blogService.updatePost(id, postDto); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /chapter8/blog-memory/src/blog.http: -------------------------------------------------------------------------------- 1 | @server = http://localhost:3000 2 | 3 | # 게시글 조회 4 | GET {{server}}/blog 5 | 6 | ### 게시글 생성 7 | POST {{server}}/blog 8 | Content-Type: application/json 9 | 10 | { 11 | "title": "안녕하세요", 12 | "content": "처음 인사드립니다.", 13 | "name": "이름" 14 | } 15 | 16 | ### 특정 게시글 조회 17 | GET {{server}}/blog/1 18 | 19 | ### 게시글 삭제 20 | DELETE {{server}}/blog/2 21 | 22 | ### 게시글 수정 23 | PUT {{server}}/blog/3 24 | Content-Type: application/json 25 | 26 | { 27 | "title": "타이틀 수정3", 28 | "content": "본문수정3", 29 | "name": "jerome.kim" 30 | } -------------------------------------------------------------------------------- /chapter8/blog-memory/src/blog.model: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/chapter8/blog-memory/src/blog.model -------------------------------------------------------------------------------- /chapter8/blog-memory/src/blog.model.ts: -------------------------------------------------------------------------------- 1 | export interface PostDto { 2 | id: string; 3 | title: string; 4 | content: string; 5 | name: string; 6 | createdDt: Date; 7 | updatedDt?: Date; 8 | } 9 | -------------------------------------------------------------------------------- /chapter8/blog-memory/src/blog.service.ts: -------------------------------------------------------------------------------- 1 | import { PostDto } from "./blog.model"; 2 | 3 | export class BlogService { 4 | posts = []; 5 | 6 | getAllPosts() { 7 | return this.posts; 8 | } 9 | 10 | createPost(postDto: PostDto) { 11 | const id = this.posts.length + 1; 12 | this.posts.push({ id: id.toString(), ...postDto, createdDt: new Date() }); 13 | } 14 | 15 | getPost(id) { 16 | const post = this.posts.find((post) => { 17 | return post.id === id; 18 | }); 19 | console.log(post); 20 | return post; 21 | } 22 | 23 | delete(id) { 24 | const filteredPosts = this.posts.filter((post) => post.id !== id); 25 | this.posts = [...filteredPosts]; 26 | } 27 | 28 | updatePost(id, postDto: PostDto) { 29 | let updateIndex = this.posts.findIndex((post) => post.id === id); 30 | const updatePost = { id, ...postDto, updatedDt: new Date() }; 31 | this.posts[updateIndex] = updatePost; 32 | return updatePost; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter8/blog-memory/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /chapter8/blog-memory/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter8/blog-memory/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 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { BlogController } from './blog.controller'; 4 | import { BlogFileRepository, BlogMongoRepository } from './blog.repository'; 5 | import { Blog, BlogSchema } from './blog.schema'; 6 | import { BlogService } from './blog.service'; 7 | 8 | @Module({ 9 | imports: [ 10 | MongooseModule.forRoot( 11 | 'mongodb+srv://{유저ID}:{password}@{클러스터주소}/blog', 12 | ), 13 | MongooseModule.forFeature([{ name: Blog.name, schema: BlogSchema }]), 14 | ], 15 | controllers: [BlogController], 16 | providers: [BlogService, BlogFileRepository, BlogMongoRepository], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/blog.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | } from '@nestjs/common'; 10 | import { BlogService } from './blog.service'; 11 | 12 | @Controller('blog') 13 | export class BlogController { 14 | constructor(private blogService: BlogService) {} 15 | 16 | @Get() 17 | getAllPosts() { 18 | return this.blogService.getAllPosts(); 19 | } 20 | 21 | @Post() 22 | createPost(@Body() postDto) { 23 | console.log('게시글 작성'); 24 | this.blogService.createPost(postDto); 25 | return 'success'; 26 | } 27 | 28 | @Get('/:id') 29 | async getPost(@Param('id') id: string) { 30 | console.log('하나의 게시글 가져오기'); 31 | const post = await this.blogService.getPost(id); 32 | console.log(post); 33 | return post; 34 | } 35 | 36 | @Delete('/:id') 37 | deletePost(@Param('id') id: string) { 38 | console.log('게시글 삭제'); 39 | this.blogService.delete(id); 40 | return 'success'; 41 | } 42 | 43 | @Put('/:id') 44 | updatePost(@Param('id') id: string, @Body() postDto) { 45 | console.log('게시글 업데이트', id, postDto); 46 | return this.blogService.updatePost(id, postDto); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/blog.data.json: -------------------------------------------------------------------------------- 1 | [{"id":"1","title":"안녕하세요","content":"처음 인사드립니다.","name":"이름","createdDt":"2023-04-04T20:08:32.937Z"},{"id":"3","title":"타이틀 수정3","content":"본문수정3","name":"jerome.kim","updatedDt":"2023-04-04T20:08:45.423Z"}] -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/blog.http: -------------------------------------------------------------------------------- 1 | @server = http://localhost:3000 2 | 3 | # 게시글 조회 4 | GET {{server}}/blog 5 | 6 | ### 게시글 생성 7 | POST {{server}}/blog 8 | Content-Type: application/json 9 | 10 | { 11 | "title": "안녕하세요", 12 | "content": "처음 인사드립니다.", 13 | "name": "이름" 14 | } 15 | 16 | ### 특정 게시글 조회 17 | GET {{server}}/blog/642c9bd6cfd8d8c93117e603 18 | 19 | ### 게시글 삭제 20 | DELETE {{server}}/blog/642c9bd7cfd8d8c93117e605 21 | 22 | ### 게시글 수정 23 | PUT {{server}}/blog/642c9bd6cfd8d8c93117e603 24 | Content-Type: application/json 25 | 26 | { 27 | "title": "타이틀 수정3", 28 | "content": "본문수정3", 29 | "name": "jerome.kim" 30 | } -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/blog.model.ts: -------------------------------------------------------------------------------- 1 | export interface PostDto { 2 | id: string; 3 | title: string; 4 | content: string; 5 | name: string; 6 | createdDt: Date; 7 | updatedDt?: Date; 8 | } 9 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/blog.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document } from 'mongoose'; 3 | 4 | export type BlogDocument = Blog & Document; 5 | 6 | @Schema() 7 | export class Blog { 8 | @Prop() 9 | id: string; 10 | 11 | @Prop() 12 | title: string; 13 | 14 | @Prop() 15 | content: string; 16 | 17 | @Prop() 18 | name: string; 19 | 20 | @Prop() 21 | createdDt: Date; 22 | 23 | @Prop() 24 | updatedDt: Date; 25 | } 26 | 27 | export const BlogSchema = SchemaFactory.createForClass(Blog); 28 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/blog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PostDto } from "./blog.model"; 3 | import { BlogMongoRepository } from "./blog.repository"; 4 | 5 | @Injectable() 6 | export class BlogService { 7 | 8 | constructor(private blogRepository: BlogMongoRepository) { } 9 | 10 | posts = []; 11 | 12 | async getAllPosts() { 13 | console.log(this.blogRepository) 14 | return await this.blogRepository.getAllPost(); 15 | } 16 | 17 | createPost(postDto: PostDto) { 18 | this.blogRepository.createPost(postDto); 19 | } 20 | 21 | async getPost(id): Promise { 22 | return await this.blogRepository.getPost(id); 23 | } 24 | 25 | delete(id) { 26 | this.blogRepository.deletePost(id); 27 | } 28 | 29 | updatePost(id, postDto: PostDto) { 30 | this.blogRepository.updatePost(id, postDto); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter8/blog-mongodb/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 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs-javascript/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 5 | [ 6 | "@babel/plugin-transform-runtime", 7 | { 8 | "regenerator": true 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs-javascript/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | require('./src/main'); 3 | 4 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs-javascript/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "experimentalDecorators": true 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "dist" 9 | ] 10 | } 11 | 12 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs-javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@nestjs/common": "^8.4.7", 4 | "@nestjs/core": "^8.4.7", 5 | "@nestjs/platform-express": "^8.4.7", 6 | "reflect-metadata": "^0.1.13", 7 | "rxjs": "^7.5.5" 8 | }, 9 | "scripts": { 10 | "start": "babel-node index.js" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.18.5", 14 | "@babel/node": "^7.18.5", 15 | "@babel/plugin-proposal-decorators": "^7.18.2", 16 | "@babel/plugin-transform-runtime": "^7.18.5", 17 | "@babel/preset-env": "^7.18.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs-javascript/src/main.js: -------------------------------------------------------------------------------- 1 | // 모듈과 컨트롤러 2 | 3 | import { Controller, Module, Get } from "@nestjs/common"; 4 | import { NestFactory } from "@nestjs/core/nest-factory"; 5 | 6 | @Controller() 7 | class AppController { 8 | @Get() 9 | getStart() { 10 | return "hello nestjs"; 11 | } 12 | } 13 | 14 | @Module({ 15 | controllers: [AppController], 16 | }) 17 | class AppModule {} 18 | 19 | async function bootstrap() { 20 | const app = await NestFactory.create(AppModule); 21 | await app.listen(3000); 22 | } 23 | 24 | bootstrap(); 25 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@nestjs/common": "^9.0.3", 4 | "@nestjs/core": "^9.0.3", 5 | "@nestjs/platform-express": "^9.0.3", 6 | "reflect-metadata": "^0.1.13", 7 | "typescript": "^4.7.4" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs/src/hello.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from "@nestjs/common"; 2 | 3 | @Controller() 4 | export class HelloController { 5 | @Get() 6 | hello() { 7 | return "안녕하세요! NestJS로 만든 첫 애플리케이션입니다."; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs/src/hello.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { HelloController } from "./hello.controller"; 3 | 4 | @Module({ 5 | controllers: [HelloController], 6 | }) 7 | export class HelloModule {} 8 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { HelloModule } from "./hello.module"; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(HelloModule); 6 | await app.listen(3000, () => { 7 | console.log("서버시작"); 8 | }); 9 | } 10 | 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /chapter8/hello-nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ESNext", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true 7 | } 8 | } -------------------------------------------------------------------------------- /chapter9/config-test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter9/config-test/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter9/config-test/envs/config.yaml: -------------------------------------------------------------------------------- 1 | http: 2 | port: 3000 3 | 4 | redis: 5 | host: 'localhost' 6 | port: 6379 7 | -------------------------------------------------------------------------------- /chapter9/config-test/envs/dev.env: -------------------------------------------------------------------------------- 1 | SERVICE_URL=http://dev.config-test.com -------------------------------------------------------------------------------- /chapter9/config-test/envs/local.env: -------------------------------------------------------------------------------- 1 | SERVER_DOMAIN=localhost 2 | SERVER_PORT=3000 3 | SERVER_URL=http://${SERVER_DOMAIN}:${SERVER_PORT} 4 | -------------------------------------------------------------------------------- /chapter9/config-test/envs/prod.env: -------------------------------------------------------------------------------- 1 | SERVICE_URL=http://config-test.com -------------------------------------------------------------------------------- /chapter9/config-test/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chapter9/config-test/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private configService: ConfigService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | const message = this.configService.get('MESSAGE'); 11 | return message; 12 | } 13 | 14 | @Get('service-url') 15 | getServiceUrl(): string { 16 | return this.configService.get('SERVICE_URL'); 17 | } 18 | 19 | @Get('db-info') 20 | getTest(): string { 21 | console.log(this.configService.get('logLevel')); 22 | console.log(this.configService.get('apiVersion')); 23 | return this.configService.get('dbInfo'); 24 | } 25 | 26 | @Get('redis-info') 27 | getRedisInfo(): string { 28 | return `${this.configService.get('redis.host')}:${this.configService.get('redis.port')}`; 29 | } 30 | 31 | @Get('server-url') 32 | getServerUrl(): string { 33 | return this.configService.get('SERVER_URL'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chapter9/config-test/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigObject } from '@nestjs/config'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { WeatherModule } from './weather/weather.module'; 6 | 7 | console.log('env : ' + process.env.NODE_ENV); 8 | console.log('current working directory : ' + process.cwd()); 9 | 10 | import config from './configs/config'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ 15 | isGlobal: true, 16 | envFilePath: `${process.cwd()}/envs/${process.env.NODE_ENV}.env`, 17 | load: [config], 18 | cache: true, 19 | expandVariables: true, 20 | 21 | }), 22 | WeatherModule, 23 | ], 24 | controllers: [AppController], 25 | providers: [AppService], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /chapter9/config-test/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter9/config-test/src/configs/common.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | logLevel: 'info', 3 | apiVersion: '1.0.0', 4 | MESSAGE: 'hello', 5 | SERVER_PORT: 3000, 6 | }; 7 | -------------------------------------------------------------------------------- /chapter9/config-test/src/configs/config.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import * as yaml from 'js-yaml'; 3 | import common from './common'; 4 | import local from './local'; 5 | import dev from './dev'; 6 | import prod from './prod'; 7 | 8 | const phase = process.env.NODE_ENV; 9 | 10 | let conf = {}; 11 | if (phase === 'local') { 12 | conf = local; 13 | } else if (phase === 'dev') { 14 | conf = dev; 15 | } else if (phase === 'prod') { 16 | conf = prod; 17 | } 18 | 19 | const yamlConfig: Record = yaml.load( 20 | readFileSync(`${process.cwd()}/envs/config.yaml`, 'utf8'), 21 | ); 22 | 23 | export default () => ({ 24 | ...common, 25 | ...conf, 26 | ...yamlConfig, 27 | }); 28 | -------------------------------------------------------------------------------- /chapter9/config-test/src/configs/dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | logLevel: 'debug', 3 | dbInfo: 'http://dev-mysql:3306', 4 | }; 5 | -------------------------------------------------------------------------------- /chapter9/config-test/src/configs/local.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | dbInfo: 'http://localhost:3306', 3 | }; 4 | -------------------------------------------------------------------------------- /chapter9/config-test/src/configs/prod.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | logLevel: 'error', 3 | dbInfo: 'http://prod-mysql:3306', 4 | }; 5 | -------------------------------------------------------------------------------- /chapter9/config-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | const configService = app.get(ConfigService); 8 | console.log("SERVER LISTEN : ",configService.get("SERVER_PORT")); 9 | await app.listen(configService.get("SERVER_PORT")); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /chapter9/config-test/src/weather/weather.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Controller('weather') 5 | export class WeatherController { 6 | constructor(private configService: ConfigService) {} 7 | 8 | @Get() 9 | public getWeather(): string { 10 | const apiUrl = this.configService.get('WEATHER_API_URL'); 11 | const apiKey = this.configService.get('WEATHER_API_KEY'); 12 | 13 | // 날씨 API 호출 14 | return this.callWeatherApi(apiUrl, apiKey); 15 | } 16 | 17 | private callWeatherApi(apiUrl: string, apiKey: string): string { 18 | console.log('날씨 정보 가져오는 중...'); 19 | console.log(apiUrl); 20 | console.log(apiKey); 21 | return '내일은 맑음'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chapter9/config-test/src/weather/weather.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WeatherController } from './weather.controller'; 3 | 4 | @Module({ 5 | controllers: [WeatherController], 6 | }) 7 | export class WeatherModule {} 8 | -------------------------------------------------------------------------------- /chapter9/config-test/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 | -------------------------------------------------------------------------------- /chapter9/config-test/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 | -------------------------------------------------------------------------------- /chapter9/config-test/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter9/config-test/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 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/.env: -------------------------------------------------------------------------------- 1 | WEATHER_API_URL=https://api.openweathermap.org/data/2.5/weather?lat=37.40&lon=127.109&lang=kr&appid= 2 | WEATHER_API_KEY=openweathermap.orgd에서 발급받은 API KEY를 넣으세요. 3 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chapter9/weather_api_test/README.md: -------------------------------------------------------------------------------- 1 | ### 확인해주세요! 2 | 3 | .env의 WEATHER_API_KEY에 openweathermap.org 에서 발급 받은 API키를 넣으셔야합니다. 4 | 5 | ### 실행방법 6 | `npm run start:dev` 실행후 브라우저의 다음 경로에서 확인 가능합니다. http://localhost:3000/weather 7 | 8 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ConfigModule } from '@nestjs/config'; 5 | import { WeatherModule } from './weather/weather.module'; 6 | 7 | @Module({ 8 | imports: [ 9 | ConfigModule.forRoot({isGlobal: true}), 10 | WeatherModule, 11 | ], 12 | controllers: [AppController], 13 | providers: [AppService], 14 | }) 15 | export class AppModule {} 16 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/src/weather/weather.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import axios from 'axios'; 4 | 5 | @Controller('weather') 6 | export class WeatherController { 7 | constructor(private configService: ConfigService) {} 8 | 9 | @Get() 10 | async getWeather() { 11 | const apiUrl = this.configService.get('WEATHER_API_URL'); 12 | const apiKey = this.configService.get('WEATHER_API_KEY'); 13 | 14 | // 날씨 API 호출 15 | return await this.callWeatherApi(apiUrl, apiKey); 16 | } 17 | 18 | async callWeatherApi(apiUrl: string, apiKey: string): Promise { 19 | console.log('날씨 정보 가져오는 중...'); 20 | console.log(apiUrl); 21 | const url = `${apiUrl}${apiKey}`; 22 | const result = await axios.get(url) 23 | const weather = result.data 24 | const mains = weather.weather.map((el) => el.main) 25 | return mains.join(" and "); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/src/weather/weather.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WeatherController } from './weather.controller'; 3 | 4 | @Module({ 5 | controllers: [WeatherController] 6 | }) 7 | export class WeatherModule {} 8 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chapter9/weather_api_test/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 | -------------------------------------------------------------------------------- /nodejs20_major_features/env-test/.env: -------------------------------------------------------------------------------- 1 | phase=dev 2 | service=jsbackend 3 | title=Node.js 20에서 변경된 점들 4 | -------------------------------------------------------------------------------- /nodejs20_major_features/env-test/env-test.js: -------------------------------------------------------------------------------- 1 | // node --env-file=.env env-test.js 2 | console.log(process.env.phase); 3 | console.log(process.env.service); 4 | console.log(process.env.title); 5 | -------------------------------------------------------------------------------- /nodejs20_major_features/esm-loader-hook/andy.mjs: -------------------------------------------------------------------------------- 1 | // andy.mjs 2 | import { pathToFileURL } from 'url'; 3 | import { resolve as resolvePath } from 'path'; 4 | 5 | export async function resolve(specifier, context, defaultResolve) { 6 | const { parentURL = null } = context; 7 | // 'andy:andy' 프로토콜을 확인 8 | if (specifier.startsWith('andy:')) { 9 | // 실제 파일 경로로 변환 10 | const filePath = specifier.replace('andy:', ''); 11 | const resolved = resolvePath(process.cwd(), `${filePath}.mjs`); 12 | return { 13 | url: pathToFileURL(resolved).href, 14 | shortCircuit: true 15 | }; 16 | } 17 | return defaultResolve(specifier, context, defaultResolve); 18 | } 19 | 20 | export async function load(url, context, defaultLoad) { 21 | if (url.includes('andy')) { 22 | return { 23 | format: 'module', 24 | source: ` 25 | // 사용자 정의 모듈 소스 26 | export function test() { 27 | console.log('Hello Node.JS! I am Andy'); 28 | } 29 | `, 30 | shortCircuit: true 31 | }; 32 | } 33 | return defaultLoad(url, context); 34 | } -------------------------------------------------------------------------------- /nodejs20_major_features/esm-loader-hook/main.js: -------------------------------------------------------------------------------- 1 | import rightPad from 'https://esm.sh/right-pad@1.0.1' 2 | import { test } from 'andy:andy' 3 | 4 | console.log(rightPad('foo', 20, '.')) 5 | test() 6 | 7 | // node --loader=./andy.mjs --experimental-loader=./url-loader.mjs main.js -------------------------------------------------------------------------------- /nodejs20_major_features/esm-loader-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs20_major_features", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "type":"module", 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /nodejs20_major_features/esm-loader-hook/register.js: -------------------------------------------------------------------------------- 1 | import { register } from "node:module"; 2 | import { pathToFileURL } from "node:url"; 3 | 4 | register("./andy.mjs", pathToFileURL("./")); 5 | register("./url-loader.mjs", pathToFileURL("./")); -------------------------------------------------------------------------------- /nodejs20_major_features/esm-loader-hook/url-loader.mjs: -------------------------------------------------------------------------------- 1 | // url-loader.mjs 2 | export async function load(url, context, nextLoad) { 3 | if (url.startsWith("https://")) { 4 | const response = await fetch(url, { redirect: "follow" }); 5 | const source = await response.text(); 6 | 7 | return {source, format: "module", shortCircuit: true} 8 | } else { 9 | return await nextLoad(url, context) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /nodejs20_major_features/permission-model/file-permission.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | try { 4 | const data = fs.readFileSync('/tmp/test.txt', 'utf8'); 5 | console.log(data); 6 | } catch(err) { 7 | console.error(err); 8 | } -------------------------------------------------------------------------------- /nodejs20_major_features/permission-model/test.txt: -------------------------------------------------------------------------------- 1 | abcdefg 2 | 가나다라마바사 3 | 4 | 5 | -------------------------------------------------------------------------------- /nodejs20_major_features/recursive-read-dirs/dir1/dir2/dir3/test3.txt: -------------------------------------------------------------------------------- 1 | 333 -------------------------------------------------------------------------------- /nodejs20_major_features/recursive-read-dirs/dir1/dir2/dir3/test4.txt: -------------------------------------------------------------------------------- 1 | 444 -------------------------------------------------------------------------------- /nodejs20_major_features/recursive-read-dirs/dir1/dir2/test1.txt: -------------------------------------------------------------------------------- 1 | 111 -------------------------------------------------------------------------------- /nodejs20_major_features/recursive-read-dirs/dir1/test2.txt: -------------------------------------------------------------------------------- 1 | 222 -------------------------------------------------------------------------------- /nodejs20_major_features/recursive-read-dirs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs20_major_features", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "type":"module", 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /nodejs20_major_features/recursive-read-dirs/read-directories.js: -------------------------------------------------------------------------------- 1 | // list_directories.js 2 | import { readdir } from "node:fs/promises"; 3 | 4 | async function readFiles(dirname) { 5 | const entries = await readdir(dirname, { recursive: true }); 6 | console.log(entries); 7 | } 8 | readFiles("dir1"); -------------------------------------------------------------------------------- /nodejs20_major_features/sea/hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/nodejs20_major_features/sea/hello -------------------------------------------------------------------------------- /nodejs20_major_features/sea/hello.js: -------------------------------------------------------------------------------- 1 | console.log(`Hello, ${process.argv[2]}!`); -------------------------------------------------------------------------------- /nodejs20_major_features/sea/sea-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "hello.js", 3 | "output": "sea-prep.blob" 4 | } -------------------------------------------------------------------------------- /nodejs20_major_features/sea/sea-prep.blob: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapj/jsbackend/f2b425ec80e047136f9cbf6debffd2715f378f40/nodejs20_major_features/sea/sea-prep.blob -------------------------------------------------------------------------------- /nodejs20_major_features/test-runner/calculator.js: -------------------------------------------------------------------------------- 1 | export default class Calculator { 2 | add(a, b) { 3 | return a + b; 4 | } 5 | 6 | subtract(a, b) { 7 | return a - b; 8 | } 9 | 10 | multiply(a, b) { 11 | return a * b; 12 | } 13 | 14 | divide(a, b) { 15 | if (b === 0) { 16 | throw new Error("Cannot divide by zero"); 17 | } 18 | return a / b; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /nodejs20_major_features/test-runner/calculator.test.js: -------------------------------------------------------------------------------- 1 | // calculator.test.js 2 | 3 | import { test, describe, beforeEach } from "node:test"; 4 | import assert from "node:assert"; 5 | import Calculator from "./calculator.js"; 6 | 7 | // calculator 테스트 블록 8 | describe("Calculator", () => { 9 | let calc; 10 | 11 | beforeEach(() => { 12 | calc = new Calculator(); 13 | }); 14 | 15 | test("adds two numbers", () => { 16 | assert.equal(calc.add(2, 3), 5); 17 | }); 18 | 19 | test.skip("subtracts two numbers", () => { 20 | assert.equal(calc.subtract(5, 3), 2); 21 | }); 22 | 23 | test("multiplies two numbers", () => { 24 | assert.equal(calc.multiply(2, 3), 6); 25 | }); 26 | 27 | test("divides two numbers", () => { 28 | assert.equal(calc.divide(6, 2), 3); 29 | }); 30 | 31 | test("throws error when dividing by zero", () => { 32 | assert.throws(() => calc.divide(5, 0), Error); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /nodejs20_major_features/test-runner/http-request.js: -------------------------------------------------------------------------------- 1 | export default class HttpRequest { 2 | async get(url) { 3 | return await fetch(url, { redirect: "follow" }) 4 | } 5 | } -------------------------------------------------------------------------------- /nodejs20_major_features/test-runner/http-request.test.js: -------------------------------------------------------------------------------- 1 | import { describe, test, mock } from 'node:test' 2 | import assert from 'node:assert' 3 | import HttpRequest from './http-request.js'; 4 | 5 | describe('http-request test', () => { 6 | const obj = new HttpRequest(); 7 | // mock을 사용하여 get 메서드의 결괏값을 임의로 설정 8 | mock.method(obj, 'get', async () => { 9 | return await { 10 | text: () => 'Hello World' 11 | } 12 | }); 13 | 14 | test('get test', async () => { 15 | const response = await obj.get('http://www.naver.com') 16 | const text = await response.text() 17 | assert.equal(text, 'Hello World') 18 | }) 19 | 20 | test('get fail test', async () => { 21 | const response = await obj.get('http://www.google.com') 22 | const text = await response.text() 23 | assert.notEqual(text, 'Hello World!!') 24 | }) 25 | }); -------------------------------------------------------------------------------- /nodejs20_major_features/test-runner/logger.js: -------------------------------------------------------------------------------- 1 | function logOperation(operation, result) { 2 | console.log(`Operation: ${operation}, Result: ${result}`); 3 | } 4 | 5 | module.exports = logOperation; 6 | -------------------------------------------------------------------------------- /nodejs20_major_features/test-runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs20_major_features", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "type":"module", 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /nodejs20_major_features/v8_upgrade.js: -------------------------------------------------------------------------------- 1 | // v8 업그레이드 관련 2 | 3 | // String.prototype.isWellFormed(), String.prototype.toWellFormed() 메서드 추가 4 | let malformedString = "\ud800"; // 잘못 형성된 유니코드 문자열 5 | console.log(malformedString.isWellFormed()); // false 6 | // �로 변경됨 7 | // �는 원래의 데이터가 손상되었거나 유니코드로 변환될 수 없는 문자를 대체하는 데 사용됨 8 | 9 | console.log(malformedString.toWellFormed()); 10 | 11 | 12 | // Array 버퍼의 사이즈를 동적으로 변경가능 13 | const buffer = new ArrayBuffer(4, { maxByteLength: 10 }); 14 | if (buffer.resizable) { 15 | console.log("The Buffer can be resized!"); 16 | buffer.resize(8); // resize the buffer 17 | } 18 | console.log(`New Buffer Size: ${buffer.byteLength}`); 19 | 20 | // SharedArrayBuffer는 grow 메서드를 사용하여 동적으로 변경가능 21 | const shredBuffer = new SharedArrayBuffer(4, { maxByteLength: 10 }); 22 | 23 | if (shredBuffer.growable) { 24 | console.log("The SharedArrayBuffer can grow!"); 25 | shredBuffer.grow(8); // 이름은 grow인데 줄일수 있군?! 26 | } 27 | console.log(`New Shared Buffer Size: ${shredBuffer.byteLength}`); 28 | 29 | 30 | // Methods that change Array and TypedArray by copy 31 | // 원본 배열을 변경하지 않고 새 배열을 생성하는 메서드 32 | let array = [1, 2, 3, 4]; 33 | let newArray = array.with(0, 99); // index 0의 값을 99로 변경한 새 배열 생성 34 | console.log(newArray); // [99, 2, 3, 4] 35 | console.log(array); // 원본 배열은 변경되지 않음: [1, 2, 3, 4] 36 | 37 | // RegExp v flag with set notation + properties of strings: 38 | // 이 기능은 정규 표현식에서 새로운 v 플래그를 도입하여 문자 집합 표기법과 문자열의 속성을 확장합니다. 39 | // 하나의 코드로 된 이모지 40 | const re = /^\p{Emoji}$/u; 41 | console.log(re.test('⚽')); // '\u26BD' // true 42 | 43 | // 여러개의 유니코드가 결합된(multi code point)이모지 44 | // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F' 45 | // 👨 남성, 🏾 어두운피부색, 조합하는 이모지를 만들때 사용, ⚕의학기호, 컬러이모지임을 뜻함 46 | // \u{1F468}\u{1F3FE}\u200D 이렇게 하면 👨‍⚕️ 남성 의사 이모지가 나옴 47 | console.log(re.test('👨🏾‍⚕️')); // false 48 | const re2 = /^\p{RGI_Emoji}$/v; 49 | console.log(re2.test('⚽')); // true 50 | console.log(re2.test('👨🏾‍⚕️')); // true --------------------------------------------------------------------------------