├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── LICENSE
├── Makefile
├── README.md
├── 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.module.ts
│ ├── auth.passport.jwt.strategy.ts
│ ├── auth.service.spec.ts
│ ├── auth.service.ts
│ ├── common
│ │ └── Social.ts
│ ├── dto
│ │ └── payload.ts
│ └── utils
│ │ ├── invalid.token.error.ts
│ │ └── transform.kakao.user.info.ts
├── configs
│ └── typeorm.config.ts
├── dummy
│ ├── common
│ │ ├── dummy-script-default-name.ts
│ │ └── dummy-script-type.enum.ts
│ ├── dto
│ │ ├── create-memo-guide.dto.ts
│ │ ├── create-sentence-default.dto.ts
│ │ ├── create-sentence-guide.dto.ts
│ │ ├── return-memo-guide.dto.ts
│ │ ├── return-script-default.dto.ts
│ │ ├── return-script-guide.dto.ts
│ │ ├── return-sentence-default.dto.ts
│ │ ├── return-sentence-guide.dto.ts
│ │ ├── update-keyword-memo-guide.dto.ts
│ │ ├── update-memo-guide.dto.ts
│ │ ├── update-sentence-default.dto.ts
│ │ └── update-sentence-guide.dto.ts
│ ├── dummy.controller.ts
│ ├── dummy.module.ts
│ ├── dummy.service.spec.ts
│ ├── dummy.service.ts
│ ├── entity
│ │ ├── memo-guide.entity.ts
│ │ ├── script-default.entity.ts
│ │ ├── script-guide.entity.ts
│ │ ├── sentence-default.entity.ts
│ │ └── sentence-guide.entity.ts
│ ├── repository
│ │ ├── memo-guide.repository.ts
│ │ ├── script-default.repository.ts
│ │ ├── script-guide.repository.ts
│ │ ├── sentence-default.repository.ts
│ │ └── sentence-guide.repository.ts
│ └── utils
│ │ └── convert-body-to-dto.ts
├── event
│ ├── dto
│ │ ├── create-event-user.dto.ts
│ │ ├── event-user.repository.ts
│ │ ├── return-event-user.dto.ts
│ │ └── return-news-collection.dto copy.ts
│ ├── event-user.entity.ts
│ ├── event.controller.ts
│ ├── event.module.ts
│ ├── event.service.ts
│ └── utils
│ │ ├── change-return-event-user-list-to-dto.ts
│ │ └── check-password-of-event-information.ts
├── history
│ ├── history.controller.ts
│ ├── history.entity.ts
│ ├── history.module.ts
│ ├── history.repository.ts
│ ├── history.service.spec.ts
│ └── history.service.ts
├── main.ts
├── modules
│ ├── Time.ts
│ └── response
│ │ ├── response.message.ts
│ │ ├── response.status.code.ts
│ │ └── response.util.ts
├── news
│ ├── common
│ │ ├── category.enum.ts
│ │ ├── channel.enum.ts
│ │ ├── condition-list.ts
│ │ ├── gender.enum.ts
│ │ ├── pagination-condition.ts
│ │ ├── pagination-info.ts
│ │ ├── search-condition.ts
│ │ └── suitability.enum.ts
│ ├── dto
│ │ ├── create-news.dto.ts
│ │ ├── explore-news-collection.dto.ts
│ │ ├── explore-news.dto.ts
│ │ ├── return-news-collection.dto.ts
│ │ ├── return-news.dto.ts
│ │ └── update-news.dto.ts
│ ├── news.controller.ts
│ ├── news.entity.ts
│ ├── news.module.ts
│ ├── news.repository.ts
│ ├── news.service.spec.ts
│ ├── news.service.ts
│ └── utils
│ │ ├── change-explore-news-list-to-dto.ts
│ │ ├── change-return-news-list-to-dto.ts
│ │ ├── check-news-dto-in-favorite-list.ts
│ │ ├── check-user.ts
│ │ ├── convert-body-to-condition.ts
│ │ ├── get-last-page.ts
│ │ └── sort-by-date-and-title.ts
├── script
│ ├── common
│ │ └── script-default-name.ts
│ ├── dto
│ │ ├── create-memo.dto.ts
│ │ ├── create-sentence.dto.ts
│ │ ├── delete-memo.dto.ts
│ │ ├── recording.dto.ts
│ │ ├── return-recording.dto.ts
│ │ ├── return-script.dto.collection.ts
│ │ ├── return-script.dto.ts
│ │ ├── return-sentence.dto.collection.ts
│ │ ├── return-sentence.dto.ts
│ │ └── update-memo.dto.ts
│ ├── entity
│ │ ├── memo.entity.ts
│ │ ├── recording.entity.ts
│ │ ├── script-count.entity.ts
│ │ ├── script.entity.ts
│ │ └── sentence.entity.ts
│ ├── repository
│ │ ├── memo.repository.ts
│ │ ├── recording.repository.ts
│ │ ├── script-count.repository.ts
│ │ ├── script.repository.ts
│ │ └── sentence.repository.ts
│ ├── script.controller.ts
│ ├── script.module.ts
│ ├── script.service.spec.ts
│ ├── script.service.ts
│ └── utils
│ │ ├── change-scripts-to-return.ts
│ │ └── scripts-count-check.ts
├── tag
│ ├── dto
│ │ ├── create-tag.dto.ts
│ │ ├── return-tag-collection.dto.ts
│ │ └── return-tag.dto.ts
│ ├── tag.controller.ts
│ ├── tag.entity.ts
│ ├── tag.module.ts
│ ├── tag.repository.ts
│ ├── tag.service.spec.ts
│ └── tag.service.ts
└── user
│ ├── common
│ └── toggle-favorite.type.ts
│ ├── dto
│ ├── return-user.dto.ts
│ └── user-for-view.dto.ts
│ ├── user.controller.ts
│ ├── user.entity.ts
│ ├── user.module.ts
│ ├── user.repository.ts
│ ├── user.service.spec.ts
│ ├── user.service.ts
│ └── utils
│ └── get-toggle-info.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | 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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # OS
15 | .DS_Store
16 |
17 | # Tests
18 | /coverage
19 | /.nyc_output
20 |
21 | # IDEs and editors
22 | /.idea
23 | .project
24 | .classpath
25 | .c9/
26 | *.launch
27 | .settings/
28 | *.sublime-workspace
29 |
30 | # IDE - VSCode
31 | .vscode/*
32 | !.vscode/settings.json
33 | !.vscode/tasks.json
34 | !.vscode/launch.json
35 | !.vscode/extensions.json
36 |
37 | /dist
38 | .env
39 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 DeliverBle
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | redeploy:
2 | @echo "building..."
3 | npm run build
4 |
5 | @echo "restarting pm2..."
6 | pm2 restart main
7 |
8 | redeploy-pull:
9 | @echo "pulling..."
10 | git pull origin develop
11 |
12 | # run make redeploy
13 | make redeploy
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | ## 🔉 프로젝트 소개
6 | > 아나운서 쉐도잉으로 키우는 스피치 자신감, 딜리버블
7 |
8 | 딜리버블은 발성, 발음 등을 개선하여 “잘” 말하고 싶은 사람들이
9 | **아나운서 쉐도잉**과 **뉴스 리딩**을 통해 스피치 자신감을 키우고, 더 세련된 이미지를 만들도록 도와주는 서비스입니다.
10 |
11 | **프로젝트 기간 (1차)**: SOPT 30th AppJam (2022. 7. 3. ~ 2022. 7. 22.)
12 |
13 | **프로젝트 기간 (2차)**: Nest.js 프로젝트로 마이그레이션 (2022. 7. 22 ~ 10.18)
14 |
15 | **프로젝트 기간 (3차)**: 신규 기능 구현 및 릴리즈 준비 (2022. 10.18 ~ 12.31)
16 |
17 |
18 |
19 | ## 👩🏻💻 팀원 소개
20 | | 이우진 | 박진형 |
21 | | :----------------------------------------------------------: | :----------------------------------------------------------: |
22 | |
|
|
23 | | [@horsehair](https://github.com/horsehair) | [@sigridjineth](https://github.com/sigridjineth) |
24 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-board-app",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:debug": "nest start --debug --watch",
15 | "start:prod": "node dist/main",
16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17 | "test": "jest",
18 | "test:watch": "jest --watch",
19 | "test:cov": "jest --coverage",
20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21 | "test:e2e": "jest --config ./test/jest-e2e.json"
22 | },
23 | "dependencies": {
24 | "@nestjs/common": "^8.0.0",
25 | "@nestjs/core": "^8.0.0",
26 | "@nestjs/jwt": "^8.0.1",
27 | "@nestjs/passport": "^8.2.2",
28 | "@nestjs/platform-express": "^8.0.0",
29 | "@nestjs/typeorm": "^8.0.1",
30 | "@types/passport-jwt": "^3.0.6",
31 | "axios": "^0.27.2",
32 | "bcryptjs": "^2.4.3",
33 | "class-transformer": "^0.4.0",
34 | "class-validator": "^0.13.1",
35 | "config": "^3.3.6",
36 | "form-data": "^4.0.0",
37 | "mysql2": "^2.3.3",
38 | "passport": "^0.4.1",
39 | "passport-jwt": "^4.0.0",
40 | "passport-kakao": "^1.0.1",
41 | "pg": "^8.7.1",
42 | "reflect-metadata": "^0.1.13",
43 | "rimraf": "^3.0.2",
44 | "rxjs": "^7.2.0",
45 | "typeorm": "^0.2.34",
46 | "uuid": "^8.3.2"
47 | },
48 | "devDependencies": {
49 | "@nestjs/cli": "^8.0.0",
50 | "@nestjs/schematics": "^8.0.0",
51 | "@nestjs/testing": "^8.0.0",
52 | "@types/express": "^4.17.13",
53 | "@types/jest": "^26.0.24",
54 | "@types/multer": "^1.4.7",
55 | "@types/node": "^16.0.0",
56 | "@types/passport-kakao": "^1.0.0",
57 | "@types/supertest": "^2.0.11",
58 | "@typescript-eslint/eslint-plugin": "^4.28.2",
59 | "@typescript-eslint/parser": "^4.28.2",
60 | "eslint": "^7.30.0",
61 | "eslint-config-prettier": "^8.3.0",
62 | "eslint-plugin-prettier": "^3.4.0",
63 | "jest": "27.0.6",
64 | "prettier": "^2.3.2",
65 | "supertest": "^6.1.3",
66 | "ts-jest": "^27.0.3",
67 | "ts-loader": "^9.2.3",
68 | "ts-node": "^10.0.0",
69 | "tsconfig-paths": "^3.10.1",
70 | "typescript": "^4.3.5"
71 | },
72 | "jest": {
73 | "moduleNameMapper": {
74 | "^src/(.*)$": "/$1"
75 | },
76 | "moduleFileExtensions": [
77 | "js",
78 | "json",
79 | "ts"
80 | ],
81 | "rootDir": "src",
82 | "testRegex": ".*\\.spec\\.ts$",
83 | "transform": {
84 | "^.+\\.(t|j)s$": "ts-jest"
85 | },
86 | "collectCoverageFrom": [
87 | "**/*.(t|j)s"
88 | ],
89 | "coverageDirectory": "../coverage",
90 | "testEnvironment": "node",
91 | "modulePaths": [
92 | ""
93 | ]
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { NewsModule } from './news/news.module';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { typeORMConfig } from './configs/typeorm.config';
5 | import { AuthModule } from './auth/auth.module';
6 | import { UserModule } from './user/user.module';
7 | import { TagModule } from './tag/tag.module';
8 | import { ScriptModule } from './script/script.module';
9 | import { DummyModule } from './dummy/dummy.module';
10 | import { HistoryModule } from './history/history.module';
11 | import { EventModule } from './event/event.module';
12 |
13 | @Module({
14 | imports: [
15 | TypeOrmModule.forRoot(typeORMConfig),
16 | NewsModule,
17 | AuthModule,
18 | UserModule,
19 | TagModule,
20 | ScriptModule,
21 | DummyModule,
22 | HistoryModule,
23 | EventModule,
24 | ],
25 | })
26 | export class AppModule {}
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Header,
5 | HttpCode,
6 | Logger,
7 | Post,
8 | Query,
9 | Res,
10 | UseGuards,
11 | } from '@nestjs/common';
12 | import { message } from 'src/modules/response/response.message';
13 | import { statusCode } from 'src/modules/response/response.status.code';
14 | import { util } from 'src/modules/response/response.util';
15 | import { JwtAuthGuard } from './auth.guard';
16 | import { AuthService } from './auth.service';
17 |
18 | require('dotenv').config();
19 | const kakaoClientId = process.env.KAKAO_CLIENT_ID;
20 | const kakaoCallbackURL = process.env.KAKAO_CALLBACK_URL;
21 |
22 | const logger = new Logger('auth.controller');
23 |
24 | @Controller('auth')
25 | export class AuthController {
26 | constructor(private readonly authService: AuthService) {}
27 |
28 | /**
29 | * 인가 코드 받아오는 로직 (프론트)
30 | */
31 | @Get('/kakao')
32 | @Header('Content-Type', 'text/html')
33 | @HttpCode(200)
34 | kakaoLoginPage(): string {
35 | return `
36 |
37 |
카카오 로그인
38 |
39 |
42 |
43 |
46 | `;
47 | }
48 |
49 | @Get('/kakaoLoginLogic')
50 | @Header('Content-Type', 'text/html')
51 | kakaoLoginGetCode(@Res() res): void {
52 | const _hostName = 'https://kauth.kakao.com';
53 | const _restApiKey = kakaoClientId;
54 | // 카카오 로그인 redirectURI 등록
55 | const _redirectUrl = kakaoCallbackURL;
56 | const url = `${_hostName}/oauth/authorize?client_id=${_restApiKey}&redirect_uri=${_redirectUrl}&response_type=code`;
57 | return res.redirect(url);
58 | }
59 |
60 | /**
61 | * 새로 로직 작성 중
62 | */
63 |
64 | // 사용자 정보 존재 확인(인가코드로)
65 | @Post('/authentication/kakao')
66 | @Header('Content-Type', 'application/json; charset=utf-8')
67 | async kakaoLoginGetUserIsByCode(@Query() qs, @Res() res): Promise
{
68 | const code = qs.code;
69 |
70 | try {
71 | const jwt = await this.authService.kakaoAuthentication(code);
72 | res.setHeader('Authorization', 'Bearer ' + jwt['accessToken']);
73 |
74 | return res
75 | .status(statusCode.OK)
76 | .send(util.success(statusCode.OK, message.AUTHENTICATION_SUCCESS, jwt));
77 | } catch (error) {
78 | logger.error(error);
79 | if (error.response.statusCode === statusCode.UNAUTHORIZED) {
80 | return res
81 | .status(statusCode.UNAUTHORIZED)
82 | .send(
83 | util.fail(statusCode.UNAUTHORIZED, message.AUTHENTICATION_FAIL),
84 | );
85 | }
86 | return res
87 | .status(statusCode.INTERNAL_SERVER_ERROR)
88 | .send(
89 | util.fail(
90 | statusCode.INTERNAL_SERVER_ERROR,
91 | message.INTERNAL_SERVER_ERROR,
92 | ),
93 | );
94 | }
95 | }
96 |
97 | @Get('test')
98 | @UseGuards(JwtAuthGuard)
99 | authTest(@Res() res): void {
100 | return res.send('auth test work');
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/auth/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext, Injectable, Logger, Res, UnauthorizedException, UseFilters } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 | import { InvalidTokenError } from './utils/invalid.token.error';
4 |
5 | const logger: Logger = new Logger();
6 |
7 | @Injectable()
8 | export class JwtAuthGuard extends AuthGuard('jwt') {
9 | canActivate(context: ExecutionContext) {
10 | return super.canActivate(context);
11 | }
12 |
13 | handleRequest(err, user, info) {
14 | if (err || !user) {
15 | throw err || new InvalidTokenError();
16 | }
17 | return user;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { JwtModule } from '@nestjs/jwt';
3 | import { PassportModule } from '@nestjs/passport';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { UserRepository } from 'src/user/user.repository';
6 | import { AuthController } from './auth.controller';
7 | import { JwtStrategy } from './auth.passport.jwt.strategy';
8 | import { AuthService } from './auth.service';
9 |
10 | @Module({
11 | imports: [
12 | TypeOrmModule.forFeature([UserRepository]),
13 | JwtModule.register({
14 | secret: process.env.JWT_SECRET,
15 | signOptions: { expiresIn: '18000s' },
16 | }),
17 | PassportModule,
18 | ],
19 | controllers: [AuthController],
20 | providers: [AuthService, JwtStrategy],
21 | exports: [AuthService],
22 | })
23 | export class AuthModule {}
24 |
--------------------------------------------------------------------------------
/src/auth/auth.passport.jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
2 | import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt';
3 | import { AuthGuard, PassportStrategy } from '@nestjs/passport';
4 | import { AuthService } from './auth.service';
5 | import { Payload } from './dto/payload';
6 |
7 | const logger: Logger = new Logger();
8 |
9 | @Injectable()
10 | export class JwtStrategy extends PassportStrategy(Strategy) {
11 | constructor(private authService: AuthService) {
12 | super({
13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
14 | ignoreExpiration: false,
15 | signOptions: { expiresIn: '18000s' },
16 | secretOrKey: process.env.JWT_SECRET,
17 | });
18 | }
19 |
20 | async validate(payload: Payload, done: VerifiedCallback): Promise {
21 | const user = await this.authService.tokenValidateUser(payload);
22 | console.log(user);
23 | if (!user) {
24 | logger.debug('auth test fail >>>>', user);
25 | return done(
26 | new UnauthorizedException({
27 | message: 'user does not exist (in JwtStrategy)',
28 | }),
29 | false,
30 | );
31 | }
32 |
33 | return done(null, user);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/auth/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { JwtModule, JwtService } from '@nestjs/jwt';
2 | import { Test, TestingModule } from '@nestjs/testing';
3 | import { getRepositoryToken } from '@nestjs/typeorm';
4 | import { Gender } from 'src/news/common/Gender';
5 | import { UserRepository } from 'src/user/user.repository';
6 | import { AuthService } from './auth.service';
7 | import { Social } from './common/Social';
8 | import { Payload } from './dto/payload';
9 | import { User } from '../user/user.entity';
10 |
11 | const mockUser: User = new User(
12 | '222222223',
13 | 'test',
14 | 'test@test.test',
15 | Gender.MEN,
16 | Social.KAKAO,
17 | );
18 | mockUser.id = 1;
19 |
20 | const MockUserRepository = () => ({
21 | async findOne(socialIdCondition) {
22 | const socialId: string = socialIdCondition.where.socialId;
23 | if (socialId === '222222222') {
24 | return undefined;
25 | }
26 | const user: User = new User(
27 | socialId,
28 | '테스트',
29 | 'test@test.test',
30 | Gender.MEN,
31 | Social.KAKAO,
32 | );
33 | return user;
34 | },
35 |
36 | async createUser(user: User) {
37 | return user;
38 | },
39 | });
40 |
41 | describe('AuthService', () => {
42 | let authService: AuthService;
43 | let jwtService: JwtService;
44 |
45 | beforeEach(async () => {
46 | const module: TestingModule = await Test.createTestingModule({
47 | imports: [
48 | JwtModule.register({
49 | secret: 'SECRET',
50 | signOptions: { expiresIn: '300s' },
51 | }),
52 | ],
53 | providers: [
54 | AuthService,
55 | JwtService,
56 | {
57 | provide: getRepositoryToken(UserRepository),
58 | useValue: MockUserRepository(),
59 | },
60 | ],
61 | }).compile();
62 |
63 | authService = module.get(AuthService);
64 | jwtService = module.get(JwtService);
65 | });
66 |
67 | it('should be defined', () => {
68 | expect(authService).toBeDefined();
69 | });
70 |
71 | describe('checkUserIs() : Kakao id로 DB로부터 유저 존재 여부 판별', () => {
72 | // 존재하는 경우 : socialId = "222222223"
73 | // 존재하지 않는 경우 : socialId = "222222222"
74 | it('SUCCESS: 존재하는 경우 - User 객체 반환', async () => {
75 | const socialId = '222222223';
76 | const result = await authService.checkUserIs(socialId);
77 | expect(result.socialId).toEqual(socialId);
78 | });
79 |
80 | it('SUCCESS: 존재하지 않는 경우 - undefined 객체 반환', async () => {
81 | const socialId = '222222222';
82 | const result = await authService.checkUserIs(socialId);
83 | expect(result).toEqual(undefined);
84 | });
85 | });
86 |
87 | describe('signUpWithKakao() : 회원가입', () => {
88 | it('SUCCESS: 회원가입 성공', async () => {
89 | // 카카오에서 넘어오는 유저 데이터 형식
90 | const kakaoUserInfo: Object = {
91 | id: 222222223,
92 | connected_at: '2022-08-30T04:52:39Z',
93 | properties: {
94 | nickname: '테스트',
95 | },
96 | kakao_account: {
97 | profile_nickname_needs_agreement: false,
98 | profile: {
99 | nickname: '테스트',
100 | },
101 | has_email: true,
102 | email_needs_agreement: false,
103 | is_email_valid: true,
104 | is_email_verified: true,
105 | email: 'test@test.test',
106 | has_gender: true,
107 | gender_needs_agreement: false,
108 | gender: 'male',
109 | },
110 | };
111 | const result = await authService.signUpWithKakao(kakaoUserInfo);
112 | expect(result.socialId).toEqual('222222223');
113 | });
114 | });
115 |
116 | describe('signIn() : 로그인', () => {
117 | it('SUCCESS: 로그인 성공', async () => {
118 | const id = mockUser['id'];
119 | const socialId = mockUser['socialId'];
120 | const nickname = mockUser['nickname'];
121 | const email = mockUser['email'];
122 | const gender = mockUser['gender'];
123 | const social = mockUser['social'];
124 |
125 | const payload: Payload = {
126 | id: id,
127 | socialId: socialId,
128 | nickname: nickname,
129 | email: email,
130 | gender: gender,
131 | social: social,
132 | };
133 |
134 | const accessToken = await jwtService.sign(payload); //여기서 알아서 payload를합쳐서 만들어준다.
135 | expect(typeof accessToken).toEqual('string');
136 | });
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HttpService,
3 | Injectable,
4 | Logger,
5 | UnauthorizedException,
6 | } from '@nestjs/common';
7 | import { JwtService } from '@nestjs/jwt';
8 | import { InjectRepository } from '@nestjs/typeorm';
9 | import axios, { AxiosResponse } from 'axios';
10 | import qs from 'qs';
11 | import { lastValueFrom } from 'rxjs';
12 | import { Gender } from '../news/common/gender.enum';
13 | import { Payload } from './dto/payload';
14 | import { Social } from './common/Social';
15 | import { User } from '../user/user.entity';
16 | import {
17 | transformKakaoEmail,
18 | transformKakaoGender,
19 | } from './utils/transform.kakao.user.info';
20 | import { UserRepository } from 'src/user/user.repository';
21 | import { InvalidTokenError } from './utils/invalid.token.error';
22 | require('dotenv').config();
23 |
24 | const kakaoClientId = process.env.KAKAO_CLIENT_ID;
25 | const kakaoCallbackURL = process.env.KAKAO_CALLBACK_URL;
26 |
27 | const logger = new Logger('auth.service');
28 |
29 | @Injectable()
30 | export class AuthService {
31 | constructor(
32 | @InjectRepository(UserRepository)
33 | private userRepository: UserRepository,
34 | private readonly jwtService: JwtService,
35 | ) {}
36 |
37 | // 토큰 발급 받기
38 | async getTokenFromKakao(code: string): Promise {
39 | const url = `https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=${kakaoClientId}&redirect_uri=${kakaoCallbackURL}&code=${code}`;
40 | const header = {
41 | 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
42 | };
43 | try {
44 | const response = await axios({
45 | method: 'GET',
46 | url: url,
47 | timeout: 18000,
48 | headers: header,
49 | });
50 | return response;
51 | } catch (error) {
52 | logger.error(error);
53 | throw new UnauthorizedException();
54 | }
55 | }
56 |
57 | // 유저 정보 받아오기
58 | async getUserInfoFromKakao(access_token: string): Promise {
59 | const url = 'https://kapi.kakao.com/v2/user/me';
60 | const headerUserInfo = {
61 | 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
62 | Authorization: 'Bearer ' + access_token,
63 | };
64 | try {
65 | const response = await axios({
66 | method: 'GET',
67 | url: url,
68 | timeout: 30000,
69 | headers: headerUserInfo,
70 | });
71 | return response;
72 | } catch (error) {
73 | logger.error(error);
74 | throw new UnauthorizedException();
75 | }
76 | }
77 |
78 | // socialId -> 유저 존재 여부
79 | async checkUserIs(socialId: string): Promise {
80 | return await this.userRepository.findOne({
81 | where: { socialId: socialId },
82 | });
83 | }
84 |
85 | async signUpWithKakao(kakaoUserInfo: Object): Promise {
86 | logger.debug(kakaoUserInfo);
87 | const kakaoId: string = kakaoUserInfo['id'].toString();
88 | const nickname: string = kakaoUserInfo['kakao_account'].profile.nickname;
89 | const emailRaw: string | undefined = kakaoUserInfo['kakao_account'].email;
90 | const genderRaw: string | undefined = kakaoUserInfo['kakao_account'].gender;
91 |
92 | const email: string = transformKakaoEmail(emailRaw);
93 | const gender: Gender = transformKakaoGender(genderRaw);
94 | logger.debug('gender in auth.service', gender);
95 | const user = new User(kakaoId, nickname, email, gender, Social.KAKAO);
96 | return this.userRepository.createUser(user);
97 | }
98 |
99 | // 인가 코드 -> 토큰 -> 유저 정보 -> 유저 존재 여부 확인 -> 가입
100 | async kakaoAuthentication(
101 | code: string,
102 | ): Promise {
103 | // 토큰 받아오기
104 | const responseGetToken = await this.getTokenFromKakao(code);
105 | const access_token = responseGetToken.data.access_token;
106 |
107 | // 유저 정보 받아오기
108 | const responseGetUserInfo = await this.getUserInfoFromKakao(access_token);
109 | const kakaoUserInfo = responseGetUserInfo.data;
110 | logger.debug('kakao user data >>>>', kakaoUserInfo);
111 |
112 | // 등록된 ID 찾기
113 | const socialId = kakaoUserInfo.id;
114 | const registeredUser = await this.checkUserIs(socialId);
115 | console.log('user after checkUserId >>>>>>', registeredUser);
116 |
117 | // 등록된 ID가 없다면, 가입 후 해당 ID 로그인
118 | if (registeredUser === undefined) {
119 | const newUser = await this.signUpWithKakao(kakaoUserInfo);
120 | logger.debug(
121 | 'socialId after signUpWithKakao >>>>>>',
122 | typeof newUser.socialId,
123 | newUser.socialId,
124 | );
125 | return this.signIn(newUser);
126 | }
127 | // 등록된 ID가 있다면, 해당 ID 로그인
128 | return this.signIn(registeredUser);
129 | }
130 |
131 | async signIn(userInfo: User): Promise<{ accessToken: string }> {
132 | logger.debug('user in signIn >>>>', userInfo);
133 |
134 | const id = userInfo['id'];
135 | const socialId = userInfo['socialId'];
136 | const nickname = userInfo['nickname'];
137 | const email = userInfo['email'];
138 | const gender = userInfo['gender'];
139 | const social = userInfo['social'];
140 |
141 | const payload: Payload = {
142 | id: id,
143 | socialId: socialId,
144 | nickname: nickname,
145 | email: email,
146 | gender: gender,
147 | social: social,
148 | };
149 |
150 | logger.debug('payload right before signIn >>>>', payload);
151 | const accessToken = await this.jwtService.sign(payload); //여기서 알아서 payload를 합쳐서 만들어준다.
152 | return { accessToken };
153 | }
154 |
155 | async tokenValidateUser(payload: Payload): Promise {
156 | return await this.userRepository.findOne({
157 | where: { id: payload.id },
158 | });
159 | }
160 |
161 | async verifyJWTReturnUser(bearerToken: string): Promise {
162 | try {
163 | // JWT Secret key 불러오기
164 | const secretKey = process.env.JWT_SECRET;
165 | // Bearer의 token 부분만 파싱하기
166 | const jwt = bearerToken.split('Bearer ')[1];
167 | const payload: Payload = this.jwtService.verify(jwt, {
168 | secret: secretKey,
169 | });
170 |
171 | return await this.userRepository.findOne({
172 | where: { socialId: payload.socialId },
173 | });
174 | } catch (e) {
175 | throw new InvalidTokenError();
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/auth/common/Social.ts:
--------------------------------------------------------------------------------
1 | export enum Social {
2 | KAKAO = '카카오',
3 | GOOGLE = '구글',
4 | NAVER = '네이버',
5 | ETC = '기타',
6 | }
7 |
--------------------------------------------------------------------------------
/src/auth/dto/payload.ts:
--------------------------------------------------------------------------------
1 | import { Gender } from 'src/news/common/gender.enum';
2 | import { Social } from '../common/Social';
3 |
4 | export class Payload {
5 | id: number;
6 | socialId: string;
7 | nickname: string;
8 | email: string;
9 | gender: Gender;
10 | social: Social;
11 | }
12 |
--------------------------------------------------------------------------------
/src/auth/utils/invalid.token.error.ts:
--------------------------------------------------------------------------------
1 | import { Catch, HttpException } from "@nestjs/common";
2 | import { message } from 'src/modules/response/response.message';
3 | import { statusCode } from 'src/modules/response/response.status.code';
4 |
5 | @Catch(HttpException)
6 | export class InvalidTokenError extends HttpException {
7 | constructor() {
8 | super(message.INVALID_TOKEN, statusCode.UNAUTHORIZED);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/auth/utils/transform.kakao.user.info.ts:
--------------------------------------------------------------------------------
1 | import { Gender } from 'src/news/common/gender.enum';
2 |
3 | export const transformKakaoGender = (genderRaw: string | undefined): Gender => {
4 | const genderMap = new Map([
5 | [undefined, Gender.UNSPECIFIED],
6 | ['male', Gender.MEN],
7 | ['female', Gender.WOMEN],
8 | ]);
9 |
10 | const gender: Gender = genderMap.get(genderRaw);
11 | return gender;
12 | };
13 |
14 | export const transformKakaoEmail = (emailRaw: string | undefined): string => {
15 | if (emailRaw === undefined) {
16 | return 'NO_EMAIL';
17 | }
18 | return emailRaw;
19 | };
20 |
--------------------------------------------------------------------------------
/src/configs/typeorm.config.ts:
--------------------------------------------------------------------------------
1 | import { TypeOrmModuleOptions } from "@nestjs/typeorm";
2 | require("dotenv").config();
3 |
4 | // TEST CONFIG
5 | // type: 'mysql',
6 | // host: process.env.DB_HOST,
7 | // port: parseInt(process.env.DBPORT),
8 | // username: process.env.DB_USERNAME,
9 | // password: process.env.DB_PASSWORD,
10 | // database: process.env.DATABASE,
11 | // charset: 'utf8mb4',
12 | // entities: [__dirname + '/../**/*.entity.{js,ts}'],
13 | // synchronize: true,
14 |
15 | export const typeORMConfig: TypeOrmModuleOptions = {
16 | type: 'mysql',
17 | host: process.env.HOST,
18 | port: parseInt(process.env.DBPORT),
19 | username: process.env.USERNAME,
20 | password: process.env.PASSWORD,
21 | database: process.env.DATABASE,
22 | charset: 'utf8mb4',
23 | entities: [__dirname + '/../**/*.entity.{js,ts}'],
24 | synchronize: true,
25 | }
26 |
--------------------------------------------------------------------------------
/src/dummy/common/dummy-script-default-name.ts:
--------------------------------------------------------------------------------
1 | export const DUMMY_SCRIPT_DEFAULT_NAME = '스크립트 1';
2 | export const DUMMY_SCRIPT_GUIDE_NAME = '스피치 가이드';
3 |
--------------------------------------------------------------------------------
/src/dummy/common/dummy-script-type.enum.ts:
--------------------------------------------------------------------------------
1 | export const DUMMY_SCRIPT_TYPE = {
2 | Default: 'default',
3 | Guide: 'guide',
4 | } as const;
5 |
6 | export type DUMMY_SCRIPT_TYPE =
7 | typeof DUMMY_SCRIPT_TYPE[keyof typeof DUMMY_SCRIPT_TYPE];
8 |
--------------------------------------------------------------------------------
/src/dummy/dto/create-memo-guide.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateMemoGuideDto {
2 | newsId: number;
3 | order: number;
4 | startIndex: number;
5 | content: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/dummy/dto/create-sentence-default.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateSentenceDefaultDto {
2 | newsId: number;
3 | order: number;
4 | startTime: number;
5 | endTime: number;
6 | text: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/dummy/dto/create-sentence-guide.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateSentenceGuideDto {
2 | newsId: number;
3 | order: number;
4 | startTime: number;
5 | endTime: number;
6 | text: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/dummy/dto/return-memo-guide.dto.ts:
--------------------------------------------------------------------------------
1 | import { MemoGuide } from '../entity/memo-guide.entity';
2 |
3 | export class ReturnMemoGuideDto {
4 | constructor(memoGuide: MemoGuide) {
5 | this.id = memoGuide.id;
6 | this.order = memoGuide.order;
7 | this.startIndex = memoGuide.startIndex;
8 | this.keyword = memoGuide.keyword;
9 | this.content = memoGuide.content;
10 | }
11 | id: number;
12 | order: number;
13 | startIndex: number;
14 | keyword: string;
15 | content: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/dummy/dto/return-script-default.dto.ts:
--------------------------------------------------------------------------------
1 | import { ScriptDefault } from '../entity/script-default.entity';
2 | import { SentenceDefault } from '../entity/sentence-default.entity';
3 |
4 | export class ReturnScriptDefaultDto {
5 | constructor(scriptDefault: ScriptDefault) {
6 | this.id = scriptDefault.id;
7 | this.newsId = scriptDefault.news.id;
8 | this.name = scriptDefault.name;
9 | this.sentences = scriptDefault.sentenceDefaults;
10 | }
11 | id: number;
12 | newsId: number;
13 | name: string;
14 | sentences: SentenceDefault[];
15 | }
16 |
--------------------------------------------------------------------------------
/src/dummy/dto/return-script-guide.dto.ts:
--------------------------------------------------------------------------------
1 | import { MemoGuide } from '../entity/memo-guide.entity';
2 | import { ScriptGuide } from '../entity/script-guide.entity';
3 | import { SentenceGuide } from '../entity/sentence-guide.entity';
4 |
5 | export class ReturnScriptGuideDto {
6 | constructor(scriptGuide: ScriptGuide) {
7 | this.id = scriptGuide.id;
8 | this.newsId = scriptGuide.news.id;
9 | this.name = scriptGuide.name;
10 | this.sentences = scriptGuide.sentenceGuides;
11 | this.saveSortedMemoGuides(scriptGuide);
12 | }
13 | id: number;
14 | newsId: number;
15 | name: string;
16 | sentences: SentenceGuide[];
17 | memoGuides: MemoGuide[];
18 |
19 | saveSortedMemoGuides(scriptGuide: ScriptGuide): void {
20 | const sortingMemos = scriptGuide.memoGuides;
21 | sortingMemos.sort((prev, next) => {
22 | if (prev.order == next.order) {
23 | return prev.startIndex - next.startIndex;
24 | }
25 | return prev.order - next.order;
26 | });
27 | this.memoGuides = sortingMemos;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/dummy/dto/return-sentence-default.dto.ts:
--------------------------------------------------------------------------------
1 | import { SentenceDefault } from '../entity/sentence-default.entity';
2 |
3 | export class ReturnSentenceDefaultDto {
4 | constructor(sentenceDefault: SentenceDefault) {
5 | this.id = sentenceDefault.id;
6 | this.order = sentenceDefault.order;
7 | this.startTime = sentenceDefault.startTime;
8 | this.endTime = sentenceDefault.endTime;
9 | this.text = sentenceDefault.text;
10 | }
11 | id: number;
12 | order: number;
13 | startTime: number;
14 | endTime: number;
15 | text: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/dummy/dto/return-sentence-guide.dto.ts:
--------------------------------------------------------------------------------
1 | import { SentenceGuide } from '../entity/sentence-guide.entity';
2 |
3 | export class ReturnSentenceGuideDto {
4 | constructor(sentenceGuide: SentenceGuide) {
5 | this.id = sentenceGuide.id;
6 | this.order = sentenceGuide.order;
7 | this.startTime = sentenceGuide.startTime;
8 | this.endTime = sentenceGuide.endTime;
9 | this.text = sentenceGuide.text;
10 | }
11 | id: number;
12 | order: number;
13 | startTime: number;
14 | endTime: number;
15 | text: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/dummy/dto/update-keyword-memo-guide.dto.ts:
--------------------------------------------------------------------------------
1 | export class UpdateKeywordMemoGuideDto {
2 | constructor(memoGuideId: number, keyword: string) {
3 | this.memoGuideId = memoGuideId;
4 | this.keyword = keyword;
5 | }
6 | memoGuideId: number;
7 | keyword: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/dummy/dto/update-memo-guide.dto.ts:
--------------------------------------------------------------------------------
1 | export class UpdateMemoGuideDto {
2 | constructor(
3 | memoGuideId: number,
4 | order: number,
5 | startIndex: number,
6 | keyword: string,
7 | content: string
8 | ) {
9 | this.memoGuideId = memoGuideId;
10 | this.order = order;
11 | this.startIndex = startIndex;
12 | this.keyword = keyword;
13 | this.content = content;
14 | }
15 | memoGuideId: number;
16 | order: number;
17 | startIndex: number;
18 | keyword: string;
19 | content: string;
20 | }
21 |
--------------------------------------------------------------------------------
/src/dummy/dto/update-sentence-default.dto.ts:
--------------------------------------------------------------------------------
1 | export class UpdateSentenceDefaultDto {
2 | sentenceDefaultId: number;
3 | order: number;
4 | startTime: number;
5 | endTime: number;
6 | text: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/dummy/dto/update-sentence-guide.dto.ts:
--------------------------------------------------------------------------------
1 | export class UpdateSentenceGuideDto {
2 | sentenceGuideId: number;
3 | order: number;
4 | startTime: number;
5 | endTime: number;
6 | text: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/dummy/dummy.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Delete,
4 | Get,
5 | Logger,
6 | Param,
7 | Patch,
8 | Post,
9 | Put,
10 | Req,
11 | Res,
12 | } from '@nestjs/common';
13 | import { message } from 'src/modules/response/response.message';
14 | import { statusCode } from 'src/modules/response/response.status.code';
15 | import { util } from 'src/modules/response/response.util';
16 | import { UpdateMemoDto } from 'src/script/dto/update-memo.dto';
17 | import { DUMMY_SCRIPT_TYPE } from './common/dummy-script-type.enum';
18 | import { CreateMemoGuideDto } from './dto/create-memo-guide.dto';
19 | import { CreateSentenceDefaultDto } from './dto/create-sentence-default.dto';
20 | import { CreateSentenceGuideDto } from './dto/create-sentence-guide.dto';
21 | import { ReturnMemoGuideDto } from './dto/return-memo-guide.dto';
22 | import { ReturnScriptDefaultDto } from './dto/return-script-default.dto';
23 | import { ReturnScriptGuideDto } from './dto/return-script-guide.dto';
24 | import { ReturnSentenceDefaultDto } from './dto/return-sentence-default.dto';
25 | import { ReturnSentenceGuideDto } from './dto/return-sentence-guide.dto';
26 | import { UpdateKeywordMemoGuideDto } from './dto/update-keyword-memo-guide.dto';
27 | import { UpdateMemoGuideDto } from './dto/update-memo-guide.dto';
28 | import { UpdateSentenceDefaultDto } from './dto/update-sentence-default.dto';
29 | import { UpdateSentenceGuideDto } from './dto/update-sentence-guide.dto';
30 | import { DummyService } from './dummy.service';
31 | import { ScriptDefault } from './entity/script-default.entity';
32 | import { ScriptGuide } from './entity/script-guide.entity';
33 | import { SentenceDefault } from './entity/sentence-default.entity';
34 | import {
35 | convertBodyToCreateMemoGuideDto,
36 | convertBodyToCreateSentenceDefaultDto,
37 | convertBodyToCreateSentenceGuideDto,
38 | convertBodyToUpdateSentenceDefaultDto,
39 | convertBodyToUpdateSentenceGuideDto,
40 | } from './utils/convert-body-to-dto';
41 |
42 | const logger: Logger = new Logger('dummy controller');
43 |
44 | @Controller('dummy')
45 | export class DummyController {
46 | constructor(private dummyService: DummyService) {}
47 |
48 | // 기본 스크립트
49 | @Post('default/script/create')
50 | async createScriptDefault(@Req() req, @Res() res): Promise {
51 | try {
52 | const newsId: number = req.body.newsId;
53 |
54 | const returnScriptDefaultDto: ReturnScriptDefaultDto =
55 | await this.dummyService.createScriptDefault(newsId);
56 | return res
57 | .status(statusCode.OK)
58 | .send(
59 | util.success(
60 | statusCode.OK,
61 | message.CREATE_SCRIPT_DEFAULT_SUCCESS,
62 | returnScriptDefaultDto,
63 | ),
64 | );
65 | } catch (error) {
66 | logger.error(error);
67 | if (error.name === 'EntityNotFound') {
68 | return res
69 | .status(statusCode.NOT_FOUND)
70 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
71 | }
72 | return res
73 | .status(statusCode.INTERNAL_SERVER_ERROR)
74 | .send(
75 | util.fail(
76 | statusCode.INTERNAL_SERVER_ERROR,
77 | message.INTERNAL_SERVER_ERROR,
78 | ),
79 | );
80 | }
81 | }
82 |
83 | @Get('default/script/get/:newsId')
84 | async getScriptDefault(
85 | @Res() res,
86 | @Param('newsId') newsId: number,
87 | ): Promise {
88 | try {
89 | const returnScriptDefaultDto: ReturnScriptDefaultDto =
90 | await this.dummyService.getScriptDefault(newsId);
91 | return res
92 | .status(statusCode.OK)
93 | .send(
94 | util.success(
95 | statusCode.OK,
96 | message.READ_SCRIPT_DEFAULT_SUCCESS,
97 | returnScriptDefaultDto,
98 | ),
99 | );
100 | } catch (error) {
101 | logger.error(error);
102 | if (error.name === 'TypeError') {
103 | return res
104 | .status(statusCode.NOT_FOUND)
105 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
106 | }
107 | return res
108 | .status(statusCode.INTERNAL_SERVER_ERROR)
109 | .send(
110 | util.fail(
111 | statusCode.INTERNAL_SERVER_ERROR,
112 | message.INTERNAL_SERVER_ERROR,
113 | ),
114 | );
115 | }
116 | }
117 |
118 | @Delete('default/script/delete/:newsId')
119 | async deleteScriptDefault(
120 | @Res() res,
121 | @Param('newsId') newsId: number,
122 | ): Promise {
123 | try {
124 | const returnScriptDefaultDto: ReturnScriptDefaultDto =
125 | await this.dummyService.deleteScriptDefault(newsId);
126 | return res
127 | .status(statusCode.OK)
128 | .send(
129 | util.success(
130 | statusCode.OK,
131 | message.DELETE_SCRIPT_DEFAULT_SUCCESS,
132 | returnScriptDefaultDto,
133 | ),
134 | );
135 | } catch (error) {
136 | logger.error(error);
137 | if (error.name === 'TypeError') {
138 | return res
139 | .status(statusCode.NOT_FOUND)
140 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
141 | }
142 | return res
143 | .status(statusCode.INTERNAL_SERVER_ERROR)
144 | .send(
145 | util.fail(
146 | statusCode.INTERNAL_SERVER_ERROR,
147 | message.INTERNAL_SERVER_ERROR,
148 | ),
149 | );
150 | }
151 | }
152 |
153 | @Post('default/sentence/create')
154 | async createSentenceDefault(@Req() req, @Res() res): Promise {
155 | try {
156 | const createSentenceDefaultDto: CreateSentenceDefaultDto =
157 | convertBodyToCreateSentenceDefaultDto(req.body);
158 | const returnSentenceDefaultDto: ReturnSentenceDefaultDto =
159 | await this.dummyService.createSentenceDefault(createSentenceDefaultDto);
160 | return res
161 | .status(statusCode.OK)
162 | .send(
163 | util.success(
164 | statusCode.OK,
165 | message.CREATE_SENTENCE_DEFAULT_SUCCESS,
166 | returnSentenceDefaultDto,
167 | ),
168 | );
169 | } catch (error) {
170 | logger.error(error);
171 | if (error.name === 'NotFoundErrorImpl') {
172 | return res
173 | .status(statusCode.NOT_FOUND)
174 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
175 | }
176 | return res
177 | .status(statusCode.INTERNAL_SERVER_ERROR)
178 | .send(
179 | util.fail(
180 | statusCode.INTERNAL_SERVER_ERROR,
181 | message.INTERNAL_SERVER_ERROR,
182 | ),
183 | );
184 | }
185 | }
186 |
187 | @Put('default/sentence/update')
188 | async updateSentenceDefault(@Req() req, @Res() res): Promise {
189 | try {
190 | const updateSentenceDefaultDto: UpdateSentenceDefaultDto =
191 | convertBodyToUpdateSentenceDefaultDto(req.body);
192 | const returnSentenceDefaultDto: ReturnSentenceDefaultDto =
193 | await this.dummyService.updateSentenceDefault(updateSentenceDefaultDto);
194 | return res
195 | .status(statusCode.OK)
196 | .send(
197 | util.success(
198 | statusCode.OK,
199 | message.UPDATE_SENTENCE_DEFAULT_SUCCESS,
200 | returnSentenceDefaultDto,
201 | ),
202 | );
203 | } catch (error) {
204 | logger.error(error);
205 | if (error.name === 'NotFoundErrorImpl') {
206 | return res
207 | .status(statusCode.NOT_FOUND)
208 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
209 | }
210 | return res
211 | .status(statusCode.INTERNAL_SERVER_ERROR)
212 | .send(
213 | util.fail(
214 | statusCode.INTERNAL_SERVER_ERROR,
215 | message.INTERNAL_SERVER_ERROR,
216 | ),
217 | );
218 | }
219 | }
220 |
221 | @Delete('default/sentence/delete/:sentenceDefaultId')
222 | async deleteSentenceDefault(
223 | @Res() res,
224 | @Param('sentenceDefaultId') sentenceDefaultId: number,
225 | ): Promise {
226 | try {
227 | const returnSentenceDefaultDto: ReturnSentenceDefaultDto =
228 | await this.dummyService.deleteSentenceDefault(sentenceDefaultId);
229 | return res
230 | .status(statusCode.OK)
231 | .send(
232 | util.success(
233 | statusCode.OK,
234 | message.DELETE_SENTENCE_DEFAULT_SUCCESS,
235 | returnSentenceDefaultDto,
236 | ),
237 | );
238 | } catch (error) {
239 | logger.error(error);
240 | if (error.name === 'NotFoundErrorImpl') {
241 | return res
242 | .status(statusCode.NOT_FOUND)
243 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
244 | }
245 | return res
246 | .status(statusCode.INTERNAL_SERVER_ERROR)
247 | .send(
248 | util.fail(
249 | statusCode.INTERNAL_SERVER_ERROR,
250 | message.INTERNAL_SERVER_ERROR,
251 | ),
252 | );
253 | }
254 | }
255 |
256 | // 스피치 가이드
257 | @Post('guide/script/create')
258 | async createScriptGuide(@Req() req, @Res() res): Promise {
259 | try {
260 | const newsId: number = req.body.newsId;
261 |
262 | const returnScriptGuideDto: ReturnScriptGuideDto =
263 | await this.dummyService.createScriptGuide(newsId);
264 | return res
265 | .status(statusCode.OK)
266 | .send(
267 | util.success(
268 | statusCode.OK,
269 | message.CREATE_SCRIPT_DEFAULT_SUCCESS,
270 | returnScriptGuideDto,
271 | ),
272 | );
273 | } catch (error) {
274 | logger.error(error);
275 | if (error.name === 'EntityNotFound') {
276 | return res
277 | .status(statusCode.NOT_FOUND)
278 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
279 | }
280 | return res
281 | .status(statusCode.INTERNAL_SERVER_ERROR)
282 | .send(
283 | util.fail(
284 | statusCode.INTERNAL_SERVER_ERROR,
285 | message.INTERNAL_SERVER_ERROR,
286 | ),
287 | );
288 | }
289 | }
290 |
291 | @Get('guide/script/get/:newsId')
292 | async getScriptGuide(
293 | @Res() res,
294 | @Param('newsId') newsId: number,
295 | ): Promise {
296 | try {
297 | const returnScriptGuideDto: ReturnScriptGuideDto =
298 | await this.dummyService.getScriptGuide(newsId);
299 | return res
300 | .status(statusCode.OK)
301 | .send(
302 | util.success(
303 | statusCode.OK,
304 | message.READ_SCRIPT_GUIDE_SUCCESS,
305 | returnScriptGuideDto,
306 | ),
307 | );
308 | } catch (error) {
309 | logger.error(error);
310 | if (error.name === 'TypeError') {
311 | return res
312 | .status(statusCode.NOT_FOUND)
313 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
314 | }
315 | return res
316 | .status(statusCode.INTERNAL_SERVER_ERROR)
317 | .send(
318 | util.fail(
319 | statusCode.INTERNAL_SERVER_ERROR,
320 | message.INTERNAL_SERVER_ERROR,
321 | ),
322 | );
323 | }
324 | }
325 |
326 | @Delete('guide/script/delete/:newsId')
327 | async deleteScriptGuide(
328 | @Res() res,
329 | @Param('newsId') newsId: number,
330 | ): Promise {
331 | try {
332 | const returnScriptGuideDto: ReturnScriptGuideDto =
333 | await this.dummyService.deleteScriptGuide(newsId);
334 | return res
335 | .status(statusCode.OK)
336 | .send(
337 | util.success(
338 | statusCode.OK,
339 | message.DELETE_SCRIPT_GUIDE_SUCCESS,
340 | returnScriptGuideDto,
341 | ),
342 | );
343 | } catch (error) {
344 | logger.error(error);
345 | if (error.name === 'TypeError') {
346 | return res
347 | .status(statusCode.NOT_FOUND)
348 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
349 | }
350 | return res
351 | .status(statusCode.INTERNAL_SERVER_ERROR)
352 | .send(
353 | util.fail(
354 | statusCode.INTERNAL_SERVER_ERROR,
355 | message.INTERNAL_SERVER_ERROR,
356 | ),
357 | );
358 | }
359 | }
360 |
361 | @Post('guide/sentence/create')
362 | async createSentenceGuide(@Req() req, @Res() res): Promise {
363 | try {
364 | const createSentenceGuideDto: CreateSentenceGuideDto =
365 | convertBodyToCreateSentenceGuideDto(req.body);
366 | const returnSentenceGuideDto: ReturnSentenceGuideDto =
367 | await this.dummyService.createSentenceGuide(createSentenceGuideDto);
368 | return res
369 | .status(statusCode.OK)
370 | .send(
371 | util.success(
372 | statusCode.OK,
373 | message.CREATE_SENTENCE_GUIDE_SUCCESS,
374 | returnSentenceGuideDto,
375 | ),
376 | );
377 | } catch (error) {
378 | logger.error(error);
379 | if (error.name === 'NotFoundErrorImpl') {
380 | return res
381 | .status(statusCode.NOT_FOUND)
382 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
383 | }
384 | return res
385 | .status(statusCode.INTERNAL_SERVER_ERROR)
386 | .send(
387 | util.fail(
388 | statusCode.INTERNAL_SERVER_ERROR,
389 | message.INTERNAL_SERVER_ERROR,
390 | ),
391 | );
392 | }
393 | }
394 |
395 | @Put('guide/sentence/update')
396 | async updateSentenceGuide(@Req() req, @Res() res): Promise {
397 | try {
398 | const updateSentenceGuideDto: UpdateSentenceGuideDto =
399 | convertBodyToUpdateSentenceGuideDto(req.body);
400 | const returnSentenceGuideDto: ReturnSentenceGuideDto =
401 | await this.dummyService.updateSentenceGuide(updateSentenceGuideDto);
402 | return res
403 | .status(statusCode.OK)
404 | .send(
405 | util.success(
406 | statusCode.OK,
407 | message.UPDATE_SENTENCE_GUIDE_SUCCESS,
408 | returnSentenceGuideDto,
409 | ),
410 | );
411 | } catch (error) {
412 | logger.error(error);
413 | if (error.name === 'NotFoundErrorImpl') {
414 | return res
415 | .status(statusCode.NOT_FOUND)
416 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
417 | }
418 | return res
419 | .status(statusCode.INTERNAL_SERVER_ERROR)
420 | .send(
421 | util.fail(
422 | statusCode.INTERNAL_SERVER_ERROR,
423 | message.INTERNAL_SERVER_ERROR,
424 | ),
425 | );
426 | }
427 | }
428 |
429 | @Delete('guide/sentence/delete/:sentenceGuideId')
430 | async deleteSentenceGuide(
431 | @Res() res,
432 | @Param('sentenceGuideId') sentenceGuideId: number,
433 | ): Promise {
434 | try {
435 | const returnSentenceGuideDto: ReturnSentenceGuideDto =
436 | await this.dummyService.deleteSentenceGuide(sentenceGuideId);
437 | return res
438 | .status(statusCode.OK)
439 | .send(
440 | util.success(
441 | statusCode.OK,
442 | message.DELETE_SENTENCE_GUIDE_SUCCESS,
443 | returnSentenceGuideDto,
444 | ),
445 | );
446 | } catch (error) {
447 | logger.error(error);
448 | if (error.name === 'NotFoundErrorImpl') {
449 | return res
450 | .status(statusCode.NOT_FOUND)
451 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
452 | }
453 | return res
454 | .status(statusCode.INTERNAL_SERVER_ERROR)
455 | .send(
456 | util.fail(
457 | statusCode.INTERNAL_SERVER_ERROR,
458 | message.INTERNAL_SERVER_ERROR,
459 | ),
460 | );
461 | }
462 | }
463 |
464 | @Post('guide/memo/create')
465 | async createMemoGuide(@Req() req, @Res() res): Promise {
466 | try {
467 | const createMemoGuideDto: CreateMemoGuideDto =
468 | convertBodyToCreateMemoGuideDto(req.body);
469 | const returnMemoGuideDto: ReturnMemoGuideDto =
470 | await this.dummyService.createMemoGuide(createMemoGuideDto);
471 | return res
472 | .status(statusCode.OK)
473 | .send(
474 | util.success(
475 | statusCode.OK,
476 | message.CREATE_MEMO_GUIDE_SUCCESS,
477 | returnMemoGuideDto,
478 | ),
479 | );
480 | } catch (error) {
481 | logger.error(error);
482 | if (error.name === 'NotFoundErrorImpl') {
483 | return res
484 | .status(statusCode.NOT_FOUND)
485 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
486 | }
487 | return res
488 | .status(statusCode.INTERNAL_SERVER_ERROR)
489 | .send(
490 | util.fail(
491 | statusCode.INTERNAL_SERVER_ERROR,
492 | message.INTERNAL_SERVER_ERROR,
493 | ),
494 | );
495 | }
496 | }
497 |
498 | @Delete('guide/memo/delete/:memoGuideId')
499 | async deleteMemoGuide(
500 | @Res() res,
501 | @Param('memoGuideId') memoGuideId: number,
502 | ): Promise {
503 | try {
504 | const returnMemoGuideDto: ReturnMemoGuideDto =
505 | await this.dummyService.deleteMemoGuide(memoGuideId);
506 | return res
507 | .status(statusCode.OK)
508 | .send(
509 | util.success(
510 | statusCode.OK,
511 | message.DELETE_MEMO_GUIDE_SUCCESS,
512 | returnMemoGuideDto,
513 | ),
514 | );
515 | } catch (error) {
516 | logger.error(error);
517 | if (error.name === 'NotFoundErrorImpl') {
518 | return res
519 | .status(statusCode.NOT_FOUND)
520 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
521 | }
522 | return res
523 | .status(statusCode.INTERNAL_SERVER_ERROR)
524 | .send(
525 | util.fail(
526 | statusCode.INTERNAL_SERVER_ERROR,
527 | message.INTERNAL_SERVER_ERROR,
528 | ),
529 | );
530 | }
531 | }
532 |
533 | @Patch('guide/memo/update/keyword')
534 | async updateKeywordOfMemoGuide(@Req() req, @Res() res): Promise {
535 | try {
536 | const updateKeywordMemoGuideDto: UpdateKeywordMemoGuideDto = new UpdateKeywordMemoGuideDto(
537 | req.body.memoGuideId, req.body.keyword
538 | );
539 | const returnMemoGuideDto: ReturnMemoGuideDto =
540 | await this.dummyService.updateKeywordOfMemoGuide(updateKeywordMemoGuideDto);
541 | return res
542 | .status(statusCode.OK)
543 | .send(
544 | util.success(
545 | statusCode.OK,
546 | message.UPDATE_KEYWORD_OF_MEMO_GUIDE_SUCCESS,
547 | returnMemoGuideDto,
548 | ),
549 | );
550 | } catch (error) {
551 | logger.error(error);
552 | if (error.name === 'NotFoundErrorImpl') {
553 | return res
554 | .status(statusCode.NOT_FOUND)
555 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
556 | }
557 | return res
558 | .status(statusCode.INTERNAL_SERVER_ERROR)
559 | .send(
560 | util.fail(
561 | statusCode.INTERNAL_SERVER_ERROR,
562 | message.INTERNAL_SERVER_ERROR,
563 | ),
564 | );
565 | }
566 | }
567 |
568 | @Patch('guide/memo/update')
569 | async updateMemoGuide(@Req() req, @Res() res): Promise {
570 | try {
571 | const updateMemoGuide: UpdateMemoGuideDto = new UpdateMemoGuideDto(
572 | req.body.memoGuideId,
573 | req.body.order,
574 | req.body.startIndex,
575 | req.body.keyword,
576 | req.body.content
577 | );
578 | const returnMemoGuideDto: ReturnMemoGuideDto =
579 | await this.dummyService.updatefMemoGuide(updateMemoGuide);
580 | return res
581 | .status(statusCode.OK)
582 | .send(
583 | util.success(
584 | statusCode.OK,
585 | message.UPDATE_KEYWORD_OF_MEMO_GUIDE_SUCCESS,
586 | returnMemoGuideDto,
587 | ),
588 | );
589 | } catch (error) {
590 | logger.error(error);
591 | if (error.name === 'NotFoundErrorImpl') {
592 | return res
593 | .status(statusCode.NOT_FOUND)
594 | .send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
595 | }
596 | return res
597 | .status(statusCode.INTERNAL_SERVER_ERROR)
598 | .send(
599 | util.fail(
600 | statusCode.INTERNAL_SERVER_ERROR,
601 | message.INTERNAL_SERVER_ERROR,
602 | ),
603 | );
604 | }
605 | }
606 | }
607 |
--------------------------------------------------------------------------------
/src/dummy/dummy.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { NewsRepository } from 'src/news/news.repository';
4 | import { DummyController } from './dummy.controller';
5 | import { DummyService } from './dummy.service';
6 | import { MemoGuideRepository } from './repository/memo-guide.repository';
7 | import { ScriptDefaultRepository } from './repository/script-default.repository';
8 | import { ScriptGuideRepository } from './repository/script-guide.repository';
9 | import { SentenceDefaultRepository } from './repository/sentence-default.repository';
10 | import { SentenceGuideRepository } from './repository/sentence-guide.repository';
11 |
12 | @Module({
13 | imports: [
14 | TypeOrmModule.forFeature([
15 | ScriptDefaultRepository,
16 | NewsRepository,
17 | SentenceDefaultRepository,
18 | ScriptGuideRepository,
19 | SentenceGuideRepository,
20 | MemoGuideRepository,
21 | ]),
22 | ],
23 | controllers: [DummyController],
24 | providers: [DummyService],
25 | })
26 | export class DummyModule {}
27 |
--------------------------------------------------------------------------------
/src/dummy/dummy.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { DummyService } from './dummy.service';
3 |
4 | describe('DummyService', () => {
5 | let service: DummyService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [DummyService],
10 | }).compile();
11 |
12 | service = module.get(DummyService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/dummy/dummy.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { NotFoundError } from 'rxjs';
4 | import { News } from 'src/news/news.entity';
5 | import { NewsRepository } from 'src/news/news.repository';
6 | import { CreateMemoGuideDto } from './dto/create-memo-guide.dto';
7 | import { CreateSentenceDefaultDto } from './dto/create-sentence-default.dto';
8 | import { CreateSentenceGuideDto } from './dto/create-sentence-guide.dto';
9 | import { ReturnMemoGuideDto } from './dto/return-memo-guide.dto';
10 | import { ReturnScriptDefaultDto } from './dto/return-script-default.dto';
11 | import { ReturnScriptGuideDto } from './dto/return-script-guide.dto';
12 | import { ReturnSentenceDefaultDto } from './dto/return-sentence-default.dto';
13 | import { ReturnSentenceGuideDto } from './dto/return-sentence-guide.dto';
14 | import { UpdateKeywordMemoGuideDto } from './dto/update-keyword-memo-guide.dto';
15 | import { UpdateMemoGuideDto } from './dto/update-memo-guide.dto';
16 | import { UpdateSentenceDefaultDto } from './dto/update-sentence-default.dto';
17 | import { UpdateSentenceGuideDto } from './dto/update-sentence-guide.dto';
18 | import { MemoGuide } from './entity/memo-guide.entity';
19 | import { ScriptDefault } from './entity/script-default.entity';
20 | import { ScriptGuide } from './entity/script-guide.entity';
21 | import { SentenceDefault } from './entity/sentence-default.entity';
22 | import { SentenceGuide } from './entity/sentence-guide.entity';
23 | import { MemoGuideRepository } from './repository/memo-guide.repository';
24 | import { ScriptDefaultRepository } from './repository/script-default.repository';
25 | import { ScriptGuideRepository } from './repository/script-guide.repository';
26 | import { SentenceDefaultRepository } from './repository/sentence-default.repository';
27 | import { SentenceGuideRepository } from './repository/sentence-guide.repository';
28 |
29 | const logger: Logger = new Logger('dummy service');
30 |
31 | @Injectable()
32 | export class DummyService {
33 | constructor(
34 | @InjectRepository(NewsRepository)
35 | private newsRepository: NewsRepository,
36 | @InjectRepository(ScriptDefaultRepository)
37 | private scriptDefaultRepository: ScriptDefaultRepository,
38 | @InjectRepository(SentenceDefaultRepository)
39 | private sentenceDefaultRepository: SentenceDefaultRepository,
40 | @InjectRepository(ScriptGuideRepository)
41 | private scriptGuideRepository: ScriptGuideRepository,
42 | @InjectRepository(SentenceGuideRepository)
43 | private sentenceGuideRepository: SentenceGuideRepository,
44 | @InjectRepository(MemoGuideRepository)
45 | private memoGuideRepository: MemoGuideRepository,
46 | ) {}
47 |
48 | async createScriptDefault(newsId: number): Promise {
49 | const news: News = await this.newsRepository.getNewsById(newsId);
50 | const scriptDefault: ScriptDefault =
51 | await this.scriptDefaultRepository.createScriptDefault(news);
52 | const returnScriptDefaultDto: ReturnScriptDefaultDto =
53 | new ReturnScriptDefaultDto(scriptDefault);
54 | return returnScriptDefaultDto;
55 | }
56 |
57 | async createScriptGuide(newsId: number): Promise {
58 | const news: News = await this.newsRepository.getNewsById(newsId);
59 | const scriptGuide: ScriptGuide =
60 | await this.scriptGuideRepository.createScriptGuide(news);
61 | const returnScriptDefaultDto: ReturnScriptGuideDto =
62 | new ReturnScriptGuideDto(scriptGuide);
63 | return returnScriptDefaultDto;
64 | }
65 |
66 | async getScriptDefault(newsId: number): Promise {
67 | const news: News = await this.newsRepository.getNewsById(newsId);
68 | const scriptDefaultId: number = (await news.scriptDefault).id;
69 | const scriptDefault: ScriptDefault =
70 | await this.scriptDefaultRepository.findOneOrFail(scriptDefaultId);
71 | const returnScriptDefaultDto: ReturnScriptDefaultDto =
72 | new ReturnScriptDefaultDto(scriptDefault);
73 | return returnScriptDefaultDto;
74 | }
75 |
76 | async getScriptGuide(newsId: number): Promise {
77 | const news: News = await this.newsRepository.getNewsById(newsId);
78 | const scriptGuideId: number = (await news.scriptGuide).id;
79 | const scriptGuide: ScriptGuide =
80 | await this.scriptGuideRepository.findOneOrFail(scriptGuideId);
81 | const returnScriptGuideDto: ReturnScriptGuideDto = new ReturnScriptGuideDto(
82 | scriptGuide,
83 | );
84 | return returnScriptGuideDto;
85 | }
86 |
87 | async deleteScriptDefault(newsId: number): Promise {
88 | const news: News = await this.newsRepository.getNewsById(newsId);
89 | const scriptDefaultId: number = (await news.scriptGuide).id;
90 | const scriptDefault: ScriptDefault =
91 | await this.scriptDefaultRepository.deleteScriptDefault(scriptDefaultId);
92 | const returnScriptDefaultDto: ReturnScriptDefaultDto =
93 | new ReturnScriptDefaultDto(scriptDefault);
94 | return returnScriptDefaultDto;
95 | }
96 |
97 | async deleteScriptGuide(newsId: number): Promise {
98 | const news: News = await this.newsRepository.getNewsById(newsId);
99 | const scriptGuideId: number = (await news.scriptGuide).id;
100 | const scriptGuide: ScriptGuide =
101 | await this.scriptGuideRepository.deleteScriptGuide(scriptGuideId);
102 | const returnScriptGuideDto: ReturnScriptGuideDto = new ReturnScriptGuideDto(
103 | scriptGuide,
104 | );
105 | return returnScriptGuideDto;
106 | }
107 |
108 | async createSentenceDefault(
109 | createSentenceDefaultDto: CreateSentenceDefaultDto,
110 | ): Promise {
111 | const newsId: number = createSentenceDefaultDto.newsId;
112 | const news: News = await this.newsRepository.getNewsById(newsId);
113 | const scriptDefault: ScriptDefault = await news.scriptDefault;
114 | if (!scriptDefault) {
115 | throw NotFoundError;
116 | }
117 | const sentenceDefault: SentenceDefault =
118 | await this.sentenceDefaultRepository.createSentenceDefault(
119 | scriptDefault,
120 | createSentenceDefaultDto,
121 | );
122 | const returnSentenceDefaultDto: ReturnSentenceDefaultDto =
123 | new ReturnSentenceDefaultDto(sentenceDefault);
124 | return returnSentenceDefaultDto;
125 | }
126 |
127 | async createSentenceGuide(
128 | createSentenceGuideDto: CreateSentenceGuideDto,
129 | ): Promise {
130 | const newsId: number = createSentenceGuideDto.newsId;
131 | const news: News = await this.newsRepository.getNewsById(newsId);
132 | const scriptGuide: ScriptGuide = await news.scriptGuide;
133 | if (!scriptGuide) {
134 | throw NotFoundError;
135 | }
136 | const sentenceGuide: SentenceGuide =
137 | await this.sentenceGuideRepository.createSentenceGuide(
138 | scriptGuide,
139 | createSentenceGuideDto,
140 | );
141 | const returnSentenceGuideDto: ReturnSentenceGuideDto =
142 | new ReturnSentenceGuideDto(sentenceGuide);
143 | return returnSentenceGuideDto;
144 | }
145 |
146 | async updateSentenceDefault(
147 | updateSentenceDefaultDto: UpdateSentenceDefaultDto,
148 | ): Promise {
149 | const sentenceDefault: SentenceDefault =
150 | await this.sentenceDefaultRepository.updateSentenceDefault(
151 | updateSentenceDefaultDto,
152 | );
153 | const returnSentenceDefaultDto: ReturnSentenceDefaultDto =
154 | new ReturnSentenceDefaultDto(sentenceDefault);
155 | return returnSentenceDefaultDto;
156 | }
157 |
158 | async updateSentenceGuide(
159 | updateSentenceGuideDto: UpdateSentenceGuideDto,
160 | ): Promise {
161 | const sentenceGuide: SentenceGuide =
162 | await this.sentenceGuideRepository.updateSentenceGuide(
163 | updateSentenceGuideDto,
164 | );
165 | const returnSentenceGuideDto: ReturnSentenceGuideDto =
166 | new ReturnSentenceGuideDto(sentenceGuide);
167 | return returnSentenceGuideDto;
168 | }
169 |
170 | async deleteSentenceDefault(
171 | sentenceDefaultId: number,
172 | ): Promise {
173 | const sentenceDefault: SentenceDefault =
174 | await this.sentenceDefaultRepository.deleteSentenceDefault(
175 | sentenceDefaultId,
176 | );
177 | const returnSentenceDefaultDto: ReturnSentenceDefaultDto =
178 | new ReturnSentenceDefaultDto(sentenceDefault);
179 | return returnSentenceDefaultDto;
180 | }
181 |
182 | async deleteSentenceGuide(
183 | sentenceGuideId: number,
184 | ): Promise {
185 | const sentenceGuide: SentenceGuide =
186 | await this.sentenceGuideRepository.deleteSentenceGuide(sentenceGuideId);
187 | const returnSentenceGuideDto: ReturnSentenceGuideDto =
188 | new ReturnSentenceGuideDto(sentenceGuide);
189 | return returnSentenceGuideDto;
190 | }
191 |
192 | async createMemoGuide(
193 | createMemoGuideDto: CreateMemoGuideDto,
194 | ): Promise {
195 | const newsId: number = createMemoGuideDto.newsId;
196 | const news: News = await this.newsRepository.getNewsById(newsId);
197 | const scriptGuide: ScriptGuide = await news.scriptGuide;
198 | if (!scriptGuide) {
199 | throw NotFoundError;
200 | }
201 | const memoGuide: MemoGuide = await this.memoGuideRepository.createMemoGuide(
202 | scriptGuide,
203 | createMemoGuideDto,
204 | );
205 | const returnMemoGuideDto: ReturnMemoGuideDto = new ReturnMemoGuideDto(
206 | memoGuide,
207 | );
208 | return returnMemoGuideDto;
209 | }
210 |
211 | async deleteMemoGuide(memoGuideId: number): Promise {
212 | const memoGuide: MemoGuide = await this.memoGuideRepository.deleteMemoGuide(
213 | memoGuideId,
214 | );
215 | const returnMemoGuideDto: ReturnMemoGuideDto = new ReturnMemoGuideDto(
216 | memoGuide,
217 | );
218 | return returnMemoGuideDto;
219 | }
220 |
221 | async updateKeywordOfMemoGuide(updateKeywordMemoGuideDto: UpdateKeywordMemoGuideDto): Promise {
222 | const memoGuide: MemoGuide = await this.memoGuideRepository.updateKeywordOfMemoGuide(updateKeywordMemoGuideDto);
223 | const returnMemoGuideDto: ReturnMemoGuideDto = new ReturnMemoGuideDto(
224 | memoGuide,
225 | );
226 | return returnMemoGuideDto;
227 | }
228 |
229 | async updatefMemoGuide(updateMemoGuideDto: UpdateMemoGuideDto): Promise {
230 | const memoGuide: MemoGuide = await this.memoGuideRepository.updateMemoGuide(updateMemoGuideDto);
231 | const returnMemoGuideDto: ReturnMemoGuideDto = new ReturnMemoGuideDto(
232 | memoGuide,
233 | );
234 | return returnMemoGuideDto;
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/dummy/entity/memo-guide.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseEntity,
3 | Column,
4 | Entity,
5 | ManyToOne,
6 | PrimaryGeneratedColumn,
7 | } from 'typeorm';
8 | import { ScriptGuide } from './script-guide.entity';
9 |
10 | @Entity()
11 | export class MemoGuide extends BaseEntity {
12 | @PrimaryGeneratedColumn()
13 | id: number;
14 |
15 | @ManyToOne(() => ScriptGuide, (scriptGuide) => scriptGuide.memoGuides, {
16 | onDelete: 'CASCADE',
17 | })
18 | scriptGuide: ScriptGuide;
19 |
20 | @Column()
21 | order: number;
22 |
23 | @Column()
24 | startIndex: number;
25 |
26 | @Column({ type: 'varchar', length: 255 })
27 | keyword: string;
28 |
29 | @Column({ type: 'varchar', length: 255 })
30 | content: string;
31 | }
32 |
--------------------------------------------------------------------------------
/src/dummy/entity/script-default.entity.ts:
--------------------------------------------------------------------------------
1 | import { News } from 'src/news/news.entity';
2 | import {
3 | BaseEntity,
4 | Column,
5 | Entity,
6 | JoinColumn,
7 | ManyToOne,
8 | OneToMany,
9 | OneToOne,
10 | PrimaryGeneratedColumn,
11 | } from 'typeorm';
12 | import { SentenceDefault } from './sentence-default.entity';
13 | import { SentenceGuide } from './sentence-guide.entity';
14 |
15 | @Entity()
16 | export class ScriptDefault extends BaseEntity {
17 | @PrimaryGeneratedColumn()
18 | id: number;
19 |
20 | @OneToOne(() => News, (news) => news.scriptGuide, {
21 | eager: true,
22 | })
23 | @JoinColumn()
24 | news: News;
25 |
26 | @Column({ type: 'varchar', length: 255 })
27 | name: string;
28 |
29 | @OneToMany(() => SentenceDefault, (sentence) => sentence.scriptDefault, {
30 | eager: true,
31 | })
32 | sentenceDefaults: SentenceDefault[];
33 | }
34 |
--------------------------------------------------------------------------------
/src/dummy/entity/script-guide.entity.ts:
--------------------------------------------------------------------------------
1 | import { News } from 'src/news/news.entity';
2 | import {
3 | BaseEntity,
4 | Column,
5 | Entity,
6 | JoinColumn,
7 | ManyToOne,
8 | OneToMany,
9 | OneToOne,
10 | PrimaryGeneratedColumn,
11 | } from 'typeorm';
12 | import { MemoGuide } from './memo-guide.entity';
13 | import { SentenceDefault } from './sentence-default.entity';
14 | import { SentenceGuide } from './sentence-guide.entity';
15 |
16 | @Entity()
17 | export class ScriptGuide extends BaseEntity {
18 | @PrimaryGeneratedColumn()
19 | id: number;
20 |
21 | @OneToOne(() => News, (news) => news.scriptGuide, {
22 | eager: true,
23 | })
24 | @JoinColumn()
25 | news: News;
26 |
27 | @Column({ type: 'varchar', length: 255 })
28 | name: string;
29 |
30 | @OneToMany(() => SentenceGuide, (sentence) => sentence.scriptGuide, {
31 | eager: true,
32 | })
33 | sentenceGuides: SentenceGuide[];
34 |
35 | @OneToMany(() => MemoGuide, (memoGuide) => memoGuide.scriptGuide, {
36 | eager: true,
37 | })
38 | memoGuides: MemoGuide[];
39 | }
40 |
--------------------------------------------------------------------------------
/src/dummy/entity/sentence-default.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseEntity,
3 | Column,
4 | Entity,
5 | ManyToOne,
6 | PrimaryGeneratedColumn,
7 | } from 'typeorm';
8 | import { ScriptDefault } from './script-default.entity';
9 |
10 | @Entity()
11 | export class SentenceDefault extends BaseEntity {
12 | @PrimaryGeneratedColumn()
13 | id: number;
14 |
15 | @ManyToOne(
16 | () => ScriptDefault,
17 | (scriptDefault) => scriptDefault.sentenceDefaults,
18 | {
19 | onDelete: 'CASCADE',
20 | },
21 | )
22 | scriptDefault: ScriptDefault;
23 |
24 | @Column()
25 | order: number;
26 |
27 | @Column({ type: 'float' })
28 | startTime: number;
29 |
30 | @Column({ type: 'float' })
31 | endTime: number;
32 |
33 | @Column({ type: 'varchar', length: 255 })
34 | text: string;
35 | }
36 |
--------------------------------------------------------------------------------
/src/dummy/entity/sentence-guide.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseEntity,
3 | Column,
4 | Entity,
5 | ManyToOne,
6 | PrimaryGeneratedColumn,
7 | } from 'typeorm';
8 | import { ScriptDefault } from './script-default.entity';
9 | import { ScriptGuide } from './script-guide.entity';
10 |
11 | @Entity()
12 | export class SentenceGuide extends BaseEntity {
13 | @PrimaryGeneratedColumn()
14 | id: number;
15 |
16 | @ManyToOne(() => ScriptGuide, (scriptGuide) => scriptGuide.sentenceGuides, {
17 | onDelete: 'CASCADE',
18 | })
19 | scriptGuide: ScriptGuide;
20 |
21 | @Column()
22 | order: number;
23 |
24 | @Column({ type: 'float' })
25 | startTime: number;
26 |
27 | @Column({ type: 'float' })
28 | endTime: number;
29 |
30 | @Column({ type: 'varchar', length: 255 })
31 | text: string;
32 | }
33 |
--------------------------------------------------------------------------------
/src/dummy/repository/memo-guide.repository.ts:
--------------------------------------------------------------------------------
1 | import { NotFoundError } from 'rxjs';
2 | import { EntityRepository, Repository } from 'typeorm';
3 | import { CreateMemoGuideDto } from '../dto/create-memo-guide.dto';
4 | import { CreateSentenceGuideDto } from '../dto/create-sentence-guide.dto';
5 | import { UpdateKeywordMemoGuideDto } from '../dto/update-keyword-memo-guide.dto';
6 | import { UpdateMemoGuideDto } from '../dto/update-memo-guide.dto';
7 | import { UpdateSentenceGuideDto } from '../dto/update-sentence-guide.dto';
8 | import { MemoGuide } from '../entity/memo-guide.entity';
9 | import { ScriptGuide } from '../entity/script-guide.entity';
10 | import { SentenceGuide } from '../entity/sentence-guide.entity';
11 |
12 | @EntityRepository(MemoGuide)
13 | export class MemoGuideRepository extends Repository {
14 | async createMemoGuide(
15 | scriptGuide: ScriptGuide,
16 | createMemoGuideDto: CreateMemoGuideDto,
17 | ): Promise {
18 | const memoGuide = new MemoGuide();
19 |
20 | memoGuide.scriptGuide = scriptGuide;
21 | memoGuide.order = createMemoGuideDto.order;
22 | memoGuide.startIndex = createMemoGuideDto.startIndex;
23 | memoGuide.content = createMemoGuideDto.content;
24 |
25 | await this.save(memoGuide);
26 | return memoGuide;
27 | }
28 |
29 | async deleteMemoGuide(memoGuideId: number): Promise {
30 | const memoGuide: MemoGuide = await this.findOneOrFail(memoGuideId);
31 | if (!memoGuide) {
32 | throw NotFoundError;
33 | }
34 | await this.createQueryBuilder()
35 | .delete()
36 | .from(MemoGuide)
37 | .where('id = :memoGuideId', { memoGuideId: memoGuideId })
38 | .execute();
39 | return memoGuide;
40 | }
41 |
42 | async updateKeywordOfMemoGuide(updateKeywordMemoGuideDto: UpdateKeywordMemoGuideDto): Promise {
43 | const memoGuideId: number = updateKeywordMemoGuideDto.memoGuideId;
44 | const keyword: string = updateKeywordMemoGuideDto.keyword;
45 | const memoGuide: MemoGuide = await this.findOneOrFail(memoGuideId);
46 | if (!memoGuide) {
47 | throw NotFoundError;
48 | }
49 | memoGuide.keyword = keyword;
50 | await this.save(memoGuide);
51 | return memoGuide;
52 | }
53 |
54 | async updateMemoGuide(updateMemoGuideDto: UpdateMemoGuideDto): Promise {
55 | const memoGuideId: number = updateMemoGuideDto.memoGuideId;
56 | const memoGuide: MemoGuide = await this.findOneOrFail(memoGuideId);
57 | if (!memoGuide) {
58 | throw NotFoundError;
59 | }
60 | memoGuide.order = updateMemoGuideDto.order;
61 | memoGuide.startIndex = updateMemoGuideDto.startIndex;
62 | memoGuide.keyword = updateMemoGuideDto.keyword;
63 | memoGuide.content = updateMemoGuideDto.content;
64 | await this.save(memoGuide);
65 | return memoGuide;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/dummy/repository/script-default.repository.ts:
--------------------------------------------------------------------------------
1 | import { NotFoundError } from 'rxjs';
2 | import { News } from 'src/news/news.entity';
3 | import { EntityRepository, Repository } from 'typeorm';
4 | import { DUMMY_SCRIPT_DEFAULT_NAME } from '../common/dummy-script-default-name';
5 | import { ScriptDefault } from '../entity/script-default.entity';
6 |
7 | @EntityRepository(ScriptDefault)
8 | export class ScriptDefaultRepository extends Repository {
9 | async createScriptDefault(news: News): Promise {
10 | const scriptDefault = new ScriptDefault();
11 |
12 | scriptDefault.news = news;
13 | scriptDefault.name = DUMMY_SCRIPT_DEFAULT_NAME;
14 |
15 | await this.save(scriptDefault);
16 | return scriptDefault;
17 | }
18 |
19 | async deleteScriptDefault(scriptDefaultId: number): Promise {
20 | const scriptDefault: ScriptDefault = await this.findOneOrFail(
21 | scriptDefaultId,
22 | );
23 | await this.createQueryBuilder()
24 | .delete()
25 | .from(ScriptDefault)
26 | .where('id = :scriptDefaultId', { scriptDefaultId })
27 | .execute();
28 | return scriptDefault;
29 | }
30 |
31 | async getScriptDefaultByNewsId(newsId: number): Promise {
32 | const scriptDefault: ScriptDefault = await this.createQueryBuilder(
33 | 'scriptDefault',
34 | )
35 | .leftJoinAndSelect('scriptDefault.news', 'news')
36 | .leftJoinAndSelect('scriptDefault.sentenceDefaults', 'sentenceDefaults')
37 | .where('news.id = :newsId', { newsId: newsId })
38 | .getOne();
39 | return scriptDefault;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/dummy/repository/script-guide.repository.ts:
--------------------------------------------------------------------------------
1 | import { News } from 'src/news/news.entity';
2 | import { EntityRepository, Repository } from 'typeorm';
3 | import { DUMMY_SCRIPT_GUIDE_NAME } from '../common/dummy-script-default-name';
4 | import { ScriptGuide } from '../entity/script-guide.entity';
5 |
6 | @EntityRepository(ScriptGuide)
7 | export class ScriptGuideRepository extends Repository {
8 | async createScriptGuide(news: News): Promise {
9 | const scriptGuide = new ScriptGuide();
10 |
11 | scriptGuide.news = news;
12 | scriptGuide.name = DUMMY_SCRIPT_GUIDE_NAME;
13 |
14 | await this.save(scriptGuide);
15 | return scriptGuide;
16 | }
17 |
18 | async deleteScriptGuide(scriptGuideId: number): Promise {
19 | const scriptGuide: ScriptGuide = await this.findOneOrFail(scriptGuideId);
20 | await this.createQueryBuilder()
21 | .delete()
22 | .from(ScriptGuide)
23 | .where('id = :scriptGuideId', { scriptGuideId: scriptGuideId })
24 | .execute();
25 | return scriptGuide;
26 | }
27 |
28 | async getScriptGuideByNewsId(newsId: number): Promise {
29 | const scriptGuide: ScriptGuide = await this.createQueryBuilder(
30 | 'scriptGuide',
31 | )
32 | .leftJoinAndSelect('scriptGuide.news', 'news')
33 | .leftJoinAndSelect('scriptGuide.sentenceGuides', 'sentenceGuides')
34 | .where('news.id = :newsId', { newsId: newsId })
35 | .getOne();
36 | return scriptGuide;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/dummy/repository/sentence-default.repository.ts:
--------------------------------------------------------------------------------
1 | import { NotFoundError } from 'rxjs';
2 | import { EntityRepository, Repository } from 'typeorm';
3 | import { CreateSentenceDefaultDto } from '../dto/create-sentence-default.dto';
4 | import { UpdateSentenceDefaultDto } from '../dto/update-sentence-default.dto';
5 | import { ScriptDefault } from '../entity/script-default.entity';
6 | import { SentenceDefault } from '../entity/sentence-default.entity';
7 |
8 | @EntityRepository(SentenceDefault)
9 | export class SentenceDefaultRepository extends Repository {
10 | async createSentenceDefault(
11 | scriptDefault: ScriptDefault,
12 | createSentenceDefaultDto: CreateSentenceDefaultDto,
13 | ): Promise {
14 | const sentenceDefault = new SentenceDefault();
15 |
16 | sentenceDefault.scriptDefault = scriptDefault;
17 | sentenceDefault.order = createSentenceDefaultDto.order;
18 | sentenceDefault.startTime = createSentenceDefaultDto.startTime;
19 | sentenceDefault.endTime = createSentenceDefaultDto.endTime;
20 | sentenceDefault.text = createSentenceDefaultDto.text;
21 |
22 | await this.save(sentenceDefault);
23 | return sentenceDefault;
24 | }
25 |
26 | async updateSentenceDefault(
27 | updateSentenceDefaultDto: UpdateSentenceDefaultDto,
28 | ): Promise {
29 | const sentenceDefaultId: number =
30 | updateSentenceDefaultDto.sentenceDefaultId;
31 | await this.createQueryBuilder()
32 | .update()
33 | .set({
34 | order: updateSentenceDefaultDto.order,
35 | startTime: updateSentenceDefaultDto.startTime,
36 | endTime: updateSentenceDefaultDto.endTime,
37 | text: updateSentenceDefaultDto.text,
38 | })
39 | .where('id = :sentenceDefaultId', { sentenceDefaultId })
40 | .execute();
41 | const sentenceDefault: SentenceDefault = await this.findOneOrFail(
42 | sentenceDefaultId,
43 | );
44 | if (!sentenceDefault) {
45 | throw NotFoundError;
46 | }
47 | return sentenceDefault;
48 | }
49 |
50 | async deleteSentenceDefault(
51 | sentenceDefaultId: number,
52 | ): Promise {
53 | const sentenceDefault: SentenceDefault = await this.findOneOrFail(
54 | sentenceDefaultId,
55 | );
56 | if (!sentenceDefault) {
57 | throw NotFoundError;
58 | }
59 | await this.createQueryBuilder()
60 | .delete()
61 | .from(SentenceDefault)
62 | .where('id = :sentenceDefaultId', { sentenceDefaultId })
63 | .execute();
64 | return sentenceDefault;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/dummy/repository/sentence-guide.repository.ts:
--------------------------------------------------------------------------------
1 | import { NotFoundError } from 'rxjs';
2 | import { EntityRepository, Repository } from 'typeorm';
3 | import { CreateSentenceGuideDto } from '../dto/create-sentence-guide.dto';
4 | import { UpdateSentenceGuideDto } from '../dto/update-sentence-guide.dto';
5 | import { ScriptGuide } from '../entity/script-guide.entity';
6 | import { SentenceGuide } from '../entity/sentence-guide.entity';
7 |
8 | @EntityRepository(SentenceGuide)
9 | export class SentenceGuideRepository extends Repository {
10 | async createSentenceGuide(
11 | scriptGuide: ScriptGuide,
12 | createSentenceGuideDto: CreateSentenceGuideDto,
13 | ): Promise {
14 | const sentenceGuide = new SentenceGuide();
15 |
16 | sentenceGuide.scriptGuide = scriptGuide;
17 | sentenceGuide.order = createSentenceGuideDto.order;
18 | sentenceGuide.startTime = createSentenceGuideDto.startTime;
19 | sentenceGuide.endTime = createSentenceGuideDto.endTime;
20 | sentenceGuide.text = createSentenceGuideDto.text;
21 |
22 | await this.save(sentenceGuide);
23 | return sentenceGuide;
24 | }
25 |
26 | async updateSentenceGuide(
27 | updateSentenceGuideDto: UpdateSentenceGuideDto,
28 | ): Promise {
29 | const sentenceGuideId: number = updateSentenceGuideDto.sentenceGuideId;
30 | await this.createQueryBuilder()
31 | .update()
32 | .set({
33 | order: updateSentenceGuideDto.order,
34 | startTime: updateSentenceGuideDto.startTime,
35 | endTime: updateSentenceGuideDto.endTime,
36 | text: updateSentenceGuideDto.text,
37 | })
38 | .where('id = :sentenceGuideId', { sentenceGuideId: sentenceGuideId })
39 | .execute();
40 | const sentenceGuide: SentenceGuide = await this.findOneOrFail(
41 | sentenceGuideId,
42 | );
43 | if (!sentenceGuide) {
44 | throw NotFoundError;
45 | }
46 | return sentenceGuide;
47 | }
48 |
49 | async deleteSentenceGuide(sentenceGuideId: number): Promise {
50 | const sentenceGuide: SentenceGuide = await this.findOneOrFail(
51 | sentenceGuideId,
52 | );
53 | if (!sentenceGuide) {
54 | throw NotFoundError;
55 | }
56 | await this.createQueryBuilder()
57 | .delete()
58 | .from(SentenceGuide)
59 | .where('id = :sentenceGuideId', { sentenceGuideId: sentenceGuideId })
60 | .execute();
61 | return sentenceGuide;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/dummy/utils/convert-body-to-dto.ts:
--------------------------------------------------------------------------------
1 | import { CreateMemoGuideDto } from '../dto/create-memo-guide.dto';
2 | import { CreateSentenceDefaultDto } from '../dto/create-sentence-default.dto';
3 | import { CreateSentenceGuideDto } from '../dto/create-sentence-guide.dto';
4 | import { UpdateSentenceDefaultDto } from '../dto/update-sentence-default.dto';
5 | import { UpdateSentenceGuideDto } from '../dto/update-sentence-guide.dto';
6 |
7 | export const convertBodyToCreateSentenceDefaultDto = (
8 | body: any,
9 | ): CreateSentenceDefaultDto => {
10 | const createSentenceDefaultDto: CreateSentenceDefaultDto =
11 | new CreateSentenceDefaultDto();
12 | createSentenceDefaultDto.newsId = body.newsId;
13 | createSentenceDefaultDto.order = body.order;
14 | createSentenceDefaultDto.startTime = body.startTime;
15 | createSentenceDefaultDto.endTime = body.endTime;
16 | createSentenceDefaultDto.text = body.text;
17 | return createSentenceDefaultDto;
18 | };
19 |
20 | export const convertBodyToUpdateSentenceDefaultDto = (
21 | body: any,
22 | ): UpdateSentenceDefaultDto => {
23 | const updateSentenceDefaultDto: UpdateSentenceDefaultDto =
24 | new UpdateSentenceDefaultDto();
25 | updateSentenceDefaultDto.sentenceDefaultId = body.sentenceDefaultId;
26 | updateSentenceDefaultDto.order = body.order;
27 | updateSentenceDefaultDto.startTime = body.startTime;
28 | updateSentenceDefaultDto.endTime = body.endTime;
29 | updateSentenceDefaultDto.text = body.text;
30 | return updateSentenceDefaultDto;
31 | };
32 |
33 | export const convertBodyToCreateSentenceGuideDto = (
34 | body: any,
35 | ): CreateSentenceGuideDto => {
36 | const createSentenceGuideDto: CreateSentenceGuideDto =
37 | new CreateSentenceGuideDto();
38 | createSentenceGuideDto.newsId = body.newsId;
39 | createSentenceGuideDto.order = body.order;
40 | createSentenceGuideDto.startTime = body.startTime;
41 | createSentenceGuideDto.endTime = body.endTime;
42 | createSentenceGuideDto.text = body.text;
43 | return createSentenceGuideDto;
44 | };
45 |
46 | export const convertBodyToUpdateSentenceGuideDto = (
47 | body: any,
48 | ): UpdateSentenceGuideDto => {
49 | const updateSentenceGuideDto: UpdateSentenceGuideDto =
50 | new UpdateSentenceGuideDto();
51 | updateSentenceGuideDto.sentenceGuideId = body.sentenceGuideId;
52 | updateSentenceGuideDto.order = body.order;
53 | updateSentenceGuideDto.startTime = body.startTime;
54 | updateSentenceGuideDto.endTime = body.endTime;
55 | updateSentenceGuideDto.text = body.text;
56 | return updateSentenceGuideDto;
57 | };
58 |
59 | export const convertBodyToCreateMemoGuideDto = (
60 | body: any,
61 | ): CreateMemoGuideDto => {
62 | const createMemoGuideDto: CreateMemoGuideDto = new CreateMemoGuideDto();
63 | createMemoGuideDto.newsId = body.newsId;
64 | createMemoGuideDto.order = body.order;
65 | createMemoGuideDto.startIndex = body.startIndex;
66 | createMemoGuideDto.content = body.content;
67 | return createMemoGuideDto;
68 | };
69 |
--------------------------------------------------------------------------------
/src/event/dto/create-event-user.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateEventUserDto {
2 | nickname: string;
3 | email: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/event/dto/event-user.repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from "typeorm";
2 | import { EventUser } from "../event-user.entity";
3 | import { CreateEventUserDto } from "./create-event-user.dto";
4 |
5 | @EntityRepository(EventUser)
6 | export class EventUserRepository extends Repository {
7 | async createEventUser(createEventUserDto: CreateEventUserDto): Promise {
8 | const {
9 | nickname,
10 | email
11 | } = createEventUserDto;
12 |
13 | const eventUser = new EventUser(
14 | nickname,
15 | email
16 | );
17 |
18 | await this.save(eventUser);
19 | return eventUser;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/event/dto/return-event-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { EventUser } from "../event-user.entity";
2 |
3 | export class ReturnEventUserDto {
4 | constructor(eventUser: EventUser) {
5 | this.id = eventUser.id;
6 | this.nickname = eventUser.nickname;
7 | this.email = eventUser.email;
8 | this.date = eventUser.date;
9 | }
10 | id: number;
11 | nickname: string;
12 | email: string;
13 | date: Date;
14 | }
15 |
--------------------------------------------------------------------------------
/src/event/dto/return-news-collection.dto copy.ts:
--------------------------------------------------------------------------------
1 | import { ReturnEventUserDto } from "./return-event-user.dto";
2 |
3 |
4 | export class ReturnEventUserDtoCollection {
5 | constructor(returnEventUserList: ReturnEventUserDto[]) {
6 | this.returnEventUserDtoCollection = returnEventUserList;
7 | }
8 |
9 | returnEventUserDtoCollection: ReturnEventUserDto[] | [];
10 | }
11 |
--------------------------------------------------------------------------------
/src/event/event-user.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
2 |
3 | @Entity()
4 | export class EventUser extends BaseEntity {
5 | constructor(
6 | _nickname: string,
7 | _email: string,
8 | ) {
9 | super();
10 | this.nickname = _nickname;
11 | this.email = _email;
12 | this.date = new Date();
13 | }
14 | @PrimaryGeneratedColumn()
15 | id: number;
16 |
17 | @Column({ type: 'varchar', length: 5 })
18 | nickname: string;
19 |
20 | @Column({ type: 'varchar', length: 200 })
21 | email: string;
22 |
23 | @Column({
24 | nullable: true,
25 | })
26 | date: Date;
27 | }
28 |
--------------------------------------------------------------------------------
/src/event/event.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Logger, Param, Post, Res } from '@nestjs/common';
2 | import { message } from 'src/modules/response/response.message';
3 | import { statusCode } from 'src/modules/response/response.status.code';
4 | import { util } from 'src/modules/response/response.util';
5 | import { CreateEventUserDto } from './dto/create-event-user.dto';
6 | import { EventService } from './event.service';
7 | import { ReturnEventUserDto } from './dto/return-event-user.dto';
8 | import { ReturnEventUserDtoCollection } from './dto/return-news-collection.dto copy';
9 |
10 | const logger: Logger = new Logger('event controller');
11 |
12 | @Controller('event')
13 | export class EventController {
14 | constructor(
15 | private eventService: EventService,
16 | ) {}
17 |
18 | @Post('user')
19 | async createEventUser(
20 | @Body() createEventUserDto: CreateEventUserDto,
21 | @Res() res,
22 | ): Promise {
23 | try {
24 | const data: ReturnEventUserDto =
25 | await this.eventService.createEventUser(createEventUserDto);
26 | return res
27 | .status(statusCode.CREATED)
28 | .send(
29 | util.success(statusCode.CREATED, message.CREATE_EVENT_USER_SUCCESS, data),
30 | );
31 | } catch (error) {
32 | logger.error(error);
33 | if (error.name === "QueryFailedError") {
34 | return res
35 | .status(statusCode.BAD_REQUEST)
36 | .send(util.fail(statusCode.BAD_REQUEST, message.TOO_LONG_NICKNAME));
37 | }
38 | return res
39 | .status(statusCode.INTERNAL_SERVER_ERROR)
40 | .send(
41 | util.fail(
42 | statusCode.INTERNAL_SERVER_ERROR,
43 | message.INTERNAL_SERVER_ERROR,
44 | ),
45 | );
46 | }
47 | }
48 |
49 | @Get('user/all/:password')
50 | async getAllEventUser(
51 | @Res() res,
52 | @Param('password') password: string,
53 | ): Promise {
54 | try {
55 | const data: ReturnEventUserDtoCollection =
56 | await this.eventService.getAllEventUserAfterCheckPassword(password);
57 | return res
58 | .status(statusCode.OK)
59 | .send(
60 | util.success(statusCode.OK, message.READ_ALL_EVENT_USER, data),
61 | );
62 | } catch (error) {
63 | logger.error(error);
64 | return res
65 | .status(statusCode.INTERNAL_SERVER_ERROR)
66 | .send(
67 | util.fail(
68 | statusCode.INTERNAL_SERVER_ERROR,
69 | message.INTERNAL_SERVER_ERROR,
70 | ),
71 | );
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/event/event.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { EventUserRepository } from './dto/event-user.repository';
3 | import { EventController } from './event.controller';
4 | import { EventService } from './event.service';
5 | import { TypeOrmModule } from '@nestjs/typeorm';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([
9 | EventUserRepository
10 | ])],
11 | controllers: [EventController],
12 | providers: [EventService],
13 | })
14 | export class EventModule {}
15 |
--------------------------------------------------------------------------------
/src/event/event.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { changeReturnNewsListToDto } from 'src/news/utils/change-return-news-list-to-dto';
4 | import { CreateEventUserDto } from './dto/create-event-user.dto';
5 | import { EventUserRepository } from './dto/event-user.repository';
6 | import { ReturnEventUserDto } from './dto/return-event-user.dto';
7 | import { ReturnEventUserDtoCollection } from './dto/return-news-collection.dto copy';
8 | import { EventUser } from './event-user.entity';
9 | import { changeReturnEventUserListToDto } from './utils/change-return-event-user-list-to-dto';
10 | import { checkPasswordOfEventInformation } from './utils/check-password-of-event-information';
11 |
12 | const logger: Logger = new Logger('event service');
13 |
14 | @Injectable()
15 | export class EventService {
16 | constructor(
17 | @InjectRepository(EventUserRepository)
18 | private eventUserRepository: EventUserRepository,
19 | ) {}
20 |
21 | async createEventUser(createEventUserDto: CreateEventUserDto): Promise {
22 | const eventUser: EventUser = await this.eventUserRepository.createEventUser(createEventUserDto);
23 | const returnEventUserDto: ReturnEventUserDto = new ReturnEventUserDto(eventUser);
24 | return returnEventUserDto;
25 | }
26 |
27 | async getAllEventUserAfterCheckPassword(password: string): Promise {
28 | checkPasswordOfEventInformation(password);
29 | const eventUsers: EventUser[] = await this.eventUserRepository.find();
30 | const returnEventUserDtoCollection: ReturnEventUserDtoCollection = changeReturnEventUserListToDto(eventUsers);
31 | return returnEventUserDtoCollection;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/event/utils/change-return-event-user-list-to-dto.ts:
--------------------------------------------------------------------------------
1 | import { ReturnEventUserDto } from "../dto/return-event-user.dto";
2 | import { ReturnEventUserDtoCollection } from "../dto/return-news-collection.dto copy";
3 | import { EventUser } from "../event-user.entity";
4 |
5 | export const changeReturnEventUserListToDto = (
6 | eventUsers: EventUser[],
7 | ): ReturnEventUserDtoCollection => {
8 | const returnEventDtoList: ReturnEventUserDto[] = eventUsers.map(
9 | (eventUser) => new ReturnEventUserDto(eventUser),
10 | );
11 | const returnEventUserDtoCollection: ReturnEventUserDtoCollection =
12 | new ReturnEventUserDtoCollection(returnEventDtoList);
13 | return returnEventUserDtoCollection;
14 | };
15 |
--------------------------------------------------------------------------------
/src/event/utils/check-password-of-event-information.ts:
--------------------------------------------------------------------------------
1 | import { ForbiddenException } from "@nestjs/common";
2 |
3 | export const checkPasswordOfEventInformation = (password: string): void => {
4 | if (password !== process.env.EVENT_PASSWORD) {
5 | throw new ForbiddenException;
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/history/history.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 |
3 | @Controller('history')
4 | export class HistoryController {}
5 |
--------------------------------------------------------------------------------
/src/history/history.entity.ts:
--------------------------------------------------------------------------------
1 | import { Time } from 'src/modules/Time';
2 | import { News } from 'src/news/news.entity';
3 | import { User } from 'src/user/user.entity';
4 | import {
5 | BaseEntity,
6 | Column,
7 | Entity,
8 | ManyToOne,
9 | PrimaryGeneratedColumn,
10 | } from 'typeorm';
11 |
12 | @Entity()
13 | export class History extends BaseEntity {
14 | constructor(_user: User, _news: News) {
15 | super();
16 | this.user = _user;
17 | this.news = _news;
18 | this.date = new Date();
19 | }
20 |
21 | @PrimaryGeneratedColumn()
22 | id: number;
23 |
24 | @ManyToOne(() => User, (user) => user.histories, {
25 | eager: true,
26 | })
27 | user: User;
28 |
29 | @ManyToOne(() => News, (news) => news.histories, {
30 | eager: true,
31 | })
32 | news: News;
33 |
34 | @Column()
35 | date: Date;
36 | }
37 |
--------------------------------------------------------------------------------
/src/history/history.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { AuthService } from 'src/auth/auth.service';
5 | import { NewsRepository } from 'src/news/news.repository';
6 | import { NewsService } from 'src/news/news.service';
7 | import { ScriptRepository } from 'src/script/repository/script.repository';
8 | import { TagRepository } from 'src/tag/tag.repository';
9 | import { UserRepository } from 'src/user/user.repository';
10 | import { HistoryController } from './history.controller';
11 | import { HistoryRepository } from './history.repository';
12 | import { HistoryService } from './history.service';
13 |
14 | @Module({
15 | imports: [
16 | TypeOrmModule.forFeature([
17 | HistoryRepository,
18 | NewsRepository,
19 | TagRepository,
20 | ScriptRepository,
21 | UserRepository,
22 | ]),
23 | ],
24 | controllers: [HistoryController],
25 | providers: [HistoryService, NewsService, AuthService, JwtService],
26 | })
27 | export class HistoryModule {}
28 |
--------------------------------------------------------------------------------
/src/history/history.repository.ts:
--------------------------------------------------------------------------------
1 | import { News } from 'src/news/news.entity';
2 | import { User } from 'src/user/user.entity';
3 | import { EntityRepository, Repository } from 'typeorm';
4 | import { History } from './history.entity';
5 |
6 | @EntityRepository(History)
7 | export class HistoryRepository extends Repository {
8 | async createHistory(user: User, news: News): Promise {
9 | const history: History = new History(user, news);
10 |
11 | await this.save(history);
12 | return history;
13 | }
14 |
15 | async getHistory(user: User, news: News): Promise {
16 | const userId: number = user.id;
17 | const newsId: number = news.id;
18 | const history: History = await this.createQueryBuilder('history')
19 | .leftJoinAndSelect('history.user', 'user')
20 | .leftJoinAndSelect('history.news', 'news')
21 | .where('user.id = :userId', { userId: userId })
22 | .andWhere('news.id = :newsId', { newsId: newsId })
23 | .getOne();
24 | return history;
25 | }
26 |
27 | async getHistoryById(historyId: number): Promise {
28 | return await this.findOne(historyId);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/history/history.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { HistoryService } from './history.service';
3 |
4 | describe('HistoryService', () => {
5 | let service: HistoryService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [HistoryService],
10 | }).compile();
11 |
12 | service = module.get(HistoryService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/history/history.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { News } from 'src/news/news.entity';
4 | import { NewsRepository } from 'src/news/news.repository';
5 | import { User } from 'src/user/user.entity';
6 | import { History } from './history.entity';
7 | import { HistoryRepository } from './history.repository';
8 |
9 | @Injectable()
10 | export class HistoryService {
11 | constructor(
12 | @InjectRepository(HistoryRepository)
13 | private historyRepository: HistoryRepository,
14 | @InjectRepository(NewsRepository)
15 | private newsRepository: NewsRepository,
16 | ) {}
17 |
18 | async fetchHistory(user: User, newsId: number): Promise {
19 | const news: News = await this.newsRepository.getNewsById(newsId);
20 | const history: History = await this.historyRepository.getHistory(
21 | user,
22 | news,
23 | );
24 | console.log(history);
25 | if (!history) {
26 | await this.historyRepository.createHistory(user, news);
27 | return;
28 | }
29 | history.date = new Date();
30 | history.save();
31 | }
32 |
33 | async getNewsByHistoryId(historyId: number) {
34 | const history: History = await this.historyRepository.getHistoryById(
35 | historyId,
36 | );
37 | return history.news;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { typeORMConfig } from './configs/typeorm.config';
4 |
5 | const KakaoStrategy = require('passport-kakao').Strategy;
6 | const passport = require('passport');
7 | const kakaoClientId = process.env.KAKAO_CLIENT_ID;
8 | const kakaoCallbackURL = process.env.KAKAO_CALLBACK_URL;
9 |
10 | async function bootstrap() {
11 | const app = await NestFactory.create(AppModule, { cors: true });
12 | app.enableCors();
13 | await app.listen(process.env.PORT);
14 | }
15 | bootstrap();
16 |
17 | passport.use(
18 | 'kakao',
19 | new KakaoStrategy(
20 | {
21 | clientID: kakaoClientId,
22 | callbackURL: kakaoCallbackURL, // 위에서 설정한 Redirect URI
23 | },
24 | async (accessToken, refreshToken, profile, done) => {
25 | //console.log(profile);
26 | console.log(accessToken);
27 | console.log(refreshToken);
28 | },
29 | ),
30 | );
31 |
--------------------------------------------------------------------------------
/src/modules/Time.ts:
--------------------------------------------------------------------------------
1 | import { Column } from 'typeorm';
2 |
3 | export class Time {
4 | constructor(private _seconds: number, private _milliseconds: number) {
5 | this.seconds = _seconds;
6 | this.milliseconds = _milliseconds;
7 | }
8 |
9 | @Column({ type: 'integer' })
10 | seconds!: number;
11 |
12 | @Column({ type: 'integer' })
13 | milliseconds!: number;
14 |
15 | static toNumber(_time: Time): number {
16 | return (
17 | Number(parseFloat(_time.seconds.toString()).toFixed(2)) +
18 | Number(parseFloat((_time.milliseconds / 100).toFixed(2)))
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/response/response.message.ts:
--------------------------------------------------------------------------------
1 | export const message = {
2 | // 기본 에러 메시지
3 | NULL_VALUE: '필요한 값이 없습니다.',
4 | FORBIDDEN: '접근 권한이 없습니다.',
5 | DUPLICATED: '중복된 자원입니다.',
6 | NOT_FOUND: '존재하지 않는 자원',
7 | BAD_REQUEST: '잘못된 요청',
8 | INTERNAL_SERVER_ERROR: '서버 내부 오류',
9 | EXCEED_PAGE_INDEX:
10 | '요청한 페이지 번호가 존재하는 인덱스 개수를 넘어섰습니다.',
11 | NULL_VALUE_TOKEN: '토큰이 없습니다.',
12 | INVALID_TOKEN: '유효하지 않은 토큰입니다.',
13 | INVALID_PASSWORD: '비밀번호 오류',
14 |
15 | // 인증, 인가 로직
16 | AUTHENTICATION_SUCCESS: '유저 인증 성공',
17 | AUTHENTICATION_FAIL: '유저 인증 실패',
18 |
19 | // 유저, Favorite
20 | READ_USER_SUCCESS: '유저 조회 성공',
21 | TOGGLE_FAVORITE_NEWS_SUCCESS: '좋아하는 뉴스 토글 성공',
22 |
23 | // 뉴스
24 | CREATE_NEWS_SUCCESS: '뉴스 생성 성공',
25 | READ_ALL_NEWS_SUCCESS: '모든 뉴스 조회 성공',
26 | UPDATE_NEWS_SUCCESS: '뉴스 수정 성공',
27 | DELETE_NEWS_SUCCESS: '뉴스 삭제 성공',
28 | SEARCH_NEWS_SUCCESS: '영상 검색 결과 조회 성공',
29 | RECOMMENDED_NEWS_SUCCESS: '추천 영상 조회 성공',
30 | FAVORITE_NEWS_SUCCESS: '즐겨찾는 영상 조회 성공',
31 | DETAIL_NEWS_SUCCESS: '영상 조회 성공',
32 | ADD_TAG_TO_NEWS_SUCCESS: '뉴스 태그 추가 성공',
33 | READ_NEWS_DETAIL_SUCCESS: '뉴스 학습 상세 정보 조회 성공',
34 | SPEECH_GUIDE_NEWS_SUCCESS: '스피치 가이드 영상 조회 성공',
35 | SPEECH_GUIDE_NEWS_DETAIL_SUCCESS: '스피치 가이드 상세 정보 조회 성공',
36 | SAVE_SIMILAR_NEWS: '비슷한 영상 조회 성공',
37 | HISTORY_SUCCESS: '내 학습 기록 조회 성공',
38 | NOT_EXISTING_NEWS: '존재하지 않는 뉴스의 id입니다.',
39 |
40 | // 태그
41 | CREATE_TAG_SUCCESS: '태그 생성 성공',
42 | READ_ALL_TAGS_SUCCESS: '모든 태그 조회 성공',
43 | DELETE_TAG_SUCCESS: '태그 삭제 성공',
44 |
45 | // 스크립트
46 | CREATE_SCRIPT_SUCCESS: '스크립트 생성 성공',
47 | READ_ALL_SCRIPTS_SUCCESS: '모든 스크립트 조회 성공',
48 | DELETE_SCRIPT_SUCCESS: '스크립트 삭제 성공',
49 | UPDATE_SCRIPT_NAME_SUCCESS: '스크립트 이름 수정 성공',
50 |
51 | // 스크립트 에러
52 | NOT_EXISTING_SCRIPT: '없는 스크립트의 id로 요청했습니다.',
53 | FULL_SCRIPTS_COUNT: '스크립트의 개수가 너무 많아, 더이상 생성할 수 없습니다.',
54 | NOT_OWNER_OF_SCRIPT: '로그인한 유저가 해당 스크립트를 가지고 있지 않습니다.',
55 | NOT_REMOVABLE_SCRIPT: '스크립트가 1개이므로 삭제할 수 없습니다.',
56 |
57 | // 녹음 에러
58 | NO_RECORDING_FILE: '정상적인 녹음 파일이 아니거나, 녹음 파일이 없습니다.',
59 | NOT_FOUND_SCRIPT: '존재하는 스크립트의 아이디가 아닙니다.',
60 | UNAUTHORIZED_SCRIPT_OF_USER: '사용자에 해당하는 스크립트 아이디가 아닙니다.',
61 | NOT_FOUND_RECORDING: '해당 녹음이 없습니다.',
62 | NOT_FOUND_RECORDING_ALL: '사용자가 가지고 있는 녹음이 없습니다.',
63 | NOT_FOUND_SCRIPT_OR_RECORDING:
64 | '사용자에 해당 스크립트가 없거나 스크립트에 녹음이 없습니다.',
65 |
66 | // 녹음
67 | FOUND_RECORDING_ALL: '사용자 전체 녹음 조회 성공',
68 | FOUND_RECORDING: '녹음 조회 성공',
69 | CREATE_RECORDING_SUCCESS: '녹음 생성 성공',
70 | DELETE_RECORDING_SUCCESS: '녹음 삭제 성공',
71 | CHANGE_NAME_OF_RECORDING_SUCCESS: '녹음 이름 변경 성공',
72 | CHANGE_NAME_OF_RECORDING_FAIL: '녹음 이름 변경 실패',
73 |
74 | // 문장
75 | CREATE_SENTENCE_SUCCESS: '문장 생성 성공',
76 | UPDATE_SENTENCE_SUCCESS: '문장 수정 성공',
77 | // 문장 에러
78 | NOT_EXISTING_ORDER: '없는 문장 번호로 요청했습니다.',
79 |
80 | // 메모
81 | CREATE_MEMO_SUCCESS: '메모 생성 성공',
82 | UPDATE_MEMO_SUCCESS: '메모 수정 성공',
83 | DELETE_MEMO_SUCCESS: '메모 삭제 성공',
84 |
85 | // 메모 에러
86 | NOT_EXISTING_MEMO: '없는 메모의 id로 요청했습니다.',
87 |
88 | // 더미
89 | CREATE_SCRIPT_DEFAULT_SUCCESS: '기본 스크립트 생성 성공',
90 | READ_SCRIPT_DEFAULT_SUCCESS: '기본 스크립트 조회 성공',
91 | DELETE_SCRIPT_DEFAULT_SUCCESS: '기본 스크립트 삭제 성공',
92 | CREATE_SENTENCE_DEFAULT_SUCCESS: '기본 문장 생성 성공',
93 | UPDATE_SENTENCE_DEFAULT_SUCCESS: '기본 문장 수정 성공',
94 | DELETE_SENTENCE_DEFAULT_SUCCESS: '기본 문장 삭제 성공',
95 |
96 | CREATE_SCRIPT_GUIDE_SUCCESS: '스피치 가이드 스크립트 생성 성공',
97 | READ_SCRIPT_GUIDE_SUCCESS: '스피치 가이드 스크립트 조회 성공',
98 | DELETE_SCRIPT_GUIDE_SUCCESS: '스피치 가이드 스크립트 삭제 성공',
99 | CREATE_SENTENCE_GUIDE_SUCCESS: '스피치 가이드 문장 생성 성공',
100 | UPDATE_SENTENCE_GUIDE_SUCCESS: '스피치 가이드 문장 수정 성공',
101 | DELETE_SENTENCE_GUIDE_SUCCESS: '스피치 가이드 문장 삭제 성공',
102 |
103 | CREATE_MEMO_GUIDE_SUCCESS: '스피치 가이드 메모 생성 성공',
104 | DELETE_MEMO_GUIDE_SUCCESS: '스피치 가이드 메모 삭제 성공',
105 | UPDATE_KEYWORD_OF_MEMO_GUIDE_SUCCESS: '스피치 가이드 메모 키워드 수정 성공',
106 |
107 | // 이벤트
108 | CREATE_EVENT_USER_SUCCESS: '이벤트 유저 정보 생성 성공',
109 | TOO_LONG_NICKNAME: '닉네임은 5자 이하여야 합니다.',
110 | READ_ALL_EVENT_USER: '이벤트 유저 정보 조회 성공',
111 | };
112 |
--------------------------------------------------------------------------------
/src/modules/response/response.status.code.ts:
--------------------------------------------------------------------------------
1 | export const statusCode = {
2 | OK: 200,
3 | CREATED: 201,
4 | NO_CONTENT: 204,
5 | BAD_REQUEST: 400,
6 | UNAUTHORIZED: 401,
7 | FORBIDDEN: 403,
8 | NOT_FOUND: 404,
9 | CONFLICT: 409,
10 | INTERNAL_SERVER_ERROR: 500,
11 | SERVICE_UNAVAILABLE: 503,
12 | DB_ERROR: 600,
13 | };
14 |
--------------------------------------------------------------------------------
/src/modules/response/response.util.ts:
--------------------------------------------------------------------------------
1 | export const util = {
2 | success: (statusCode: number, message: string, data?: any, data2?: any) => {
3 | return {
4 | statusCode,
5 | message,
6 | data,
7 | data2,
8 | };
9 | },
10 | fail: (statusCode: number, message: string, data?: any) => {
11 | return {
12 | statusCode,
13 | message,
14 | data,
15 | };
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/news/common/category.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Category {
2 | SOCIETY = '사회',
3 | POLITICS = '정치',
4 | ECONOMY = '경제',
5 | ENTERTAINMENT = '연예',
6 | WORLD = '세계',
7 | SCIENCE = '과학',
8 | HEALTH = '건강',
9 | TECHNOLOGY = '기술',
10 | ETC = '기타',
11 | UNSPECIFIED = '분류 안됨',
12 | }
13 |
--------------------------------------------------------------------------------
/src/news/common/channel.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Channel {
2 | SBS = 'SBS',
3 | KBS = 'KBS',
4 | MBC = 'MBC',
5 | ETC = '기타',
6 | UNSPECIFIED = '분류안됨',
7 | }
8 |
--------------------------------------------------------------------------------
/src/news/common/condition-list.ts:
--------------------------------------------------------------------------------
1 | export interface ConditionList {
2 | channels: boolean;
3 | categories: boolean;
4 | announcerGender: boolean;
5 | findAll: boolean;
6 | }
7 |
8 | export const hasCategories = (conditionList: ConditionList): boolean => {
9 | return conditionList.categories;
10 | };
11 |
12 | export const hasChannels = (conditionList: ConditionList): boolean => {
13 | return conditionList.channels;
14 | };
15 |
16 | export const hasAnnouncerGender = (conditionList: ConditionList): boolean => {
17 | return conditionList.announcerGender;
18 | };
19 |
20 | export const hasFindAll = (conditionList: ConditionList): boolean => {
21 | return conditionList.findAll;
22 | };
23 |
--------------------------------------------------------------------------------
/src/news/common/gender.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Gender {
2 | MEN = '남성',
3 | WOMEN = '여성',
4 | UNSPECIFIED = '분류 안됨',
5 | }
6 |
--------------------------------------------------------------------------------
/src/news/common/pagination-condition.ts:
--------------------------------------------------------------------------------
1 | export class PaginationCondition {
2 | constructor(_currentPage, _listSize) {
3 | this.currentPage = _currentPage;
4 | this.listSize = _listSize;
5 | }
6 |
7 | currentPage: number | 1;
8 | listSize: number | 12;
9 |
10 | getOffset(): number {
11 | return (this.currentPage - 1) * this.listSize;
12 | }
13 |
14 | getLimit(): number {
15 | return this.listSize;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/news/common/pagination-info.ts:
--------------------------------------------------------------------------------
1 | export class PaginationInfo {
2 | constructor(_totalCount: number, _lastPage: number) {
3 | this.totalCount = _totalCount;
4 | this.lastPage = _lastPage;
5 | }
6 | totalCount: number;
7 | lastPage: number;
8 | }
9 |
--------------------------------------------------------------------------------
/src/news/common/search-condition.ts:
--------------------------------------------------------------------------------
1 | export class SearchCondition {
2 | constructor(_channel, _category, _announcerGender, _currentPage, _listSize) {
3 | this.channel = _channel;
4 | this.category = _category;
5 | this.announcerGender = _announcerGender;
6 | this.currentPage = _currentPage;
7 | this.listSize = _listSize;
8 | }
9 |
10 | channel: string[];
11 | category: string[];
12 | announcerGender: string[];
13 | currentPage: number | 1;
14 | listSize: number | 12;
15 |
16 | getOffset(): number {
17 | return (this.currentPage - 1) * this.listSize;
18 | }
19 |
20 | getLimit(): number {
21 | return this.listSize;
22 | }
23 |
24 | checkIfChannelIs(): boolean {
25 | if (this.channel.length === 0) {
26 | return false;
27 | }
28 | return true;
29 | }
30 |
31 | checkIfCategoryIs(): boolean {
32 | if (this.category.length === 0) {
33 | return false;
34 | }
35 | return true;
36 | }
37 |
38 | checkIfAnnouncerGenderIs(): boolean {
39 | if (this.announcerGender.length === 0) {
40 | return false;
41 | }
42 | return true;
43 | }
44 |
45 | checkFindAllOrNot(): boolean {
46 | if (
47 | !(
48 | this.checkIfChannelIs() ||
49 | this.checkIfCategoryIs() ||
50 | this.checkIfAnnouncerGenderIs()
51 | )
52 | ) {
53 | return true;
54 | }
55 | return false;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/news/common/suitability.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Suitability {
2 | HIGH = '상',
3 | MEDIUM = '중',
4 | LOW = '하',
5 | UNSPECIFIED = '분류 안됨',
6 | }
7 |
--------------------------------------------------------------------------------
/src/news/dto/create-news.dto.ts:
--------------------------------------------------------------------------------
1 | import { Time } from 'src/modules/Time';
2 | import { Category } from '../common/category.enum';
3 | import { Channel } from '../common/channel.enum';
4 | import { Gender } from '../common/gender.enum';
5 | import { Suitability } from '../common/suitability.enum';
6 |
7 | export class CreateNewsDto {
8 | title: string;
9 | category: Category;
10 | script: string;
11 | announcerGender: Gender;
12 | channel: Channel;
13 | link: string;
14 | thumbnail: string;
15 | startTime: number;
16 | endTime: number;
17 | suitability: Suitability;
18 | isEmbeddable: boolean;
19 | reportDate: Date;
20 | }
21 |
--------------------------------------------------------------------------------
/src/news/dto/explore-news-collection.dto.ts:
--------------------------------------------------------------------------------
1 | import { ExploreNewsDto } from './explore-news.dto';
2 |
3 | export class ExploreNewsDtoCollection {
4 | constructor(exploreNewsDtoList: ExploreNewsDto[]) {
5 | this.exploreNewsDtoCollection = exploreNewsDtoList;
6 | }
7 |
8 | exploreNewsDtoCollection: ExploreNewsDto[] | [];
9 | }
10 |
--------------------------------------------------------------------------------
/src/news/dto/explore-news.dto.ts:
--------------------------------------------------------------------------------
1 | import { ScriptGuide } from 'src/dummy/entity/script-guide.entity';
2 | import { Category } from '../common/category.enum';
3 | import { Channel } from '../common/channel.enum';
4 | import { News } from '../news.entity';
5 |
6 | export class ExploreNewsDto {
7 | constructor(news: News) {
8 | this.id = news.id;
9 | this.title = news.title;
10 | this.category = news.category;
11 | this.channel = news.channel;
12 | this.thumbnail = news.thumbnail;
13 | this.reportDate = news.reportDate;
14 | this.isFavorite = false;
15 | }
16 | id: number;
17 | title: string;
18 | category: Category;
19 | channel: Channel;
20 | thumbnail: string;
21 | reportDate: Date;
22 | isFavorite: boolean;
23 | haveGuide: boolean;
24 |
25 | async checkHaveGuide(news: News): Promise {
26 | const scriptGuide: ScriptGuide = await news.scriptGuide;
27 | if (!scriptGuide) {
28 | this.haveGuide = false;
29 | return;
30 | }
31 | this.haveGuide = true;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/news/dto/return-news-collection.dto.ts:
--------------------------------------------------------------------------------
1 | import { ReturnNewsDto } from './return-news.dto';
2 |
3 | export class ReturnNewsDtoCollection {
4 | constructor(returnNewsDtoList: ReturnNewsDto[]) {
5 | this.returnNewsDtoCollection = returnNewsDtoList;
6 | }
7 |
8 | returnNewsDtoCollection: ReturnNewsDto[] | [];
9 | }
10 |
--------------------------------------------------------------------------------
/src/news/dto/return-news.dto.ts:
--------------------------------------------------------------------------------
1 | import { ScriptGuide } from 'src/dummy/entity/script-guide.entity';
2 | import { Tag } from 'src/tag/tag.entity';
3 | import { Category } from '../common/category.enum';
4 | import { Channel } from '../common/channel.enum';
5 | import { Gender } from '../common/gender.enum';
6 | import { Suitability } from '../common/suitability.enum';
7 | import { News } from '../news.entity';
8 |
9 | export class ReturnNewsDto {
10 | constructor(news: News) {
11 | this.checkHaveGuide(news);
12 | this.id = news.id;
13 | this.title = news.title;
14 | this.category = news.category;
15 | this.announcerGender = news.announcerGender;
16 | this.channel = news.channel;
17 | this.link = news.link;
18 | this.thumbnail = news.thumbnail;
19 | this.startTime = news.startTime;
20 | this.endTime = news.endTime;
21 | this.suitability = news.suitability;
22 | this.isEmbeddable = news.isEmbeddable;
23 | this.reportDate = news.reportDate;
24 | this.isFavorite = false;
25 | this.tagsForView = news.tagsForView;
26 | }
27 | id: number;
28 | title: string;
29 | category: Category;
30 | announcerGender: Gender;
31 | channel: Channel;
32 | link: string;
33 | thumbnail: string;
34 | startTime: number;
35 | endTime: number;
36 | suitability: Suitability;
37 | isEmbeddable: boolean;
38 | reportDate: Date;
39 | isFavorite: boolean;
40 | tagsForView?: Tag[];
41 | haveGuide: boolean;
42 |
43 | async checkHaveGuide(news: News): Promise {
44 | const scriptGuide: ScriptGuide = await news.scriptGuide;
45 | if (!scriptGuide) {
46 | this.haveGuide = false;
47 | return;
48 | }
49 | this.haveGuide = true;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/news/dto/update-news.dto.ts:
--------------------------------------------------------------------------------
1 | import { Time } from 'src/modules/Time';
2 | import { Category } from '../common/category.enum';
3 | import { Channel } from '../common/channel.enum';
4 | import { Gender } from '../common/gender.enum';
5 | import { Suitability } from '../common/suitability.enum';
6 |
7 | export class UpdateNewsDto {
8 | title: string;
9 | category: Category;
10 | announcerGender: Gender;
11 | channel: Channel;
12 | link: string;
13 | thumbnail: string;
14 | startTime: number;
15 | endTime: number;
16 | suitability: Suitability;
17 | isEmbeddable: boolean;
18 | reportDate: Date;
19 | }
20 |
--------------------------------------------------------------------------------
/src/news/news.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | Logger,
7 | Param,
8 | Post,
9 | Req,
10 | Res,
11 | UseGuards,
12 | } from '@nestjs/common';
13 | // import { JwtAuthGuard } from 'src/auth/auth.guard';
14 | import { JwtAuthGuard } from 'src/auth/auth.guard';
15 | import { ReturnScriptDefaultDto } from 'src/dummy/dto/return-script-default.dto';
16 | import { ReturnScriptGuideDto } from 'src/dummy/dto/return-script-guide.dto';
17 | import { DummyService } from 'src/dummy/dummy.service';
18 | import { HistoryService } from 'src/history/history.service';
19 | import { message } from 'src/modules/response/response.message';
20 | import { statusCode } from 'src/modules/response/response.status.code';
21 | import { util } from 'src/modules/response/response.util';
22 | import { ReturnScriptDtoCollection } from 'src/script/dto/return-script.dto.collection';
23 | import { ScriptService } from 'src/script/script.service';
24 | import { User } from 'src/user/user.entity';
25 | import { PaginationCondition } from './common/pagination-condition';
26 | import { PaginationInfo } from './common/pagination-info';
27 | import { SearchCondition } from './common/search-condition';
28 | import { CreateNewsDto } from './dto/create-news.dto';
29 | import { ExploreNewsDtoCollection } from './dto/explore-news-collection.dto';
30 | import { ReturnNewsDtoCollection } from './dto/return-news-collection.dto';
31 | import { ReturnNewsDto } from './dto/return-news.dto';
32 | import { UpdateNewsDto } from './dto/update-news.dto';
33 | import { News } from './news.entity';
34 | import { NewsService } from './news.service';
35 | import { convertBodyToPaginationCondition } from './utils/convert-body-to-condition';
36 | import { convertBodyToSearchCondition } from './utils/convert-body-to-condition';
37 |
38 | const logger: Logger = new Logger('news controller');
39 |
40 | @Controller('news')
41 | export class NewsController {
42 | constructor(
43 | private newsService: NewsService,
44 | private scriptService: ScriptService,
45 | private dummyService: DummyService,
46 | private historyService: HistoryService,
47 | ) {}
48 |
49 | @Post('create')
50 | async createNews(
51 | @Body() createNewsDto: CreateNewsDto,
52 | @Res() res,
53 | ): Promise {
54 | try {
55 | const data: ReturnNewsDtoCollection =
56 | await this.newsService.createAndGetAllNews(createNewsDto);
57 | return res
58 | .status(statusCode.CREATED)
59 | .send(
60 | util.success(statusCode.CREATED, message.CREATE_NEWS_SUCCESS, data),
61 | );
62 | } catch (error) {
63 | logger.error(error);
64 | return res
65 | .status(statusCode.INTERNAL_SERVER_ERROR)
66 | .send(
67 | util.fail(
68 | statusCode.INTERNAL_SERVER_ERROR,
69 | message.INTERNAL_SERVER_ERROR,
70 | ),
71 | );
72 | }
73 | }
74 |
75 | @Get('all')
76 | async getAllNews(@Res() res): Promise {
77 | try {
78 | const data: ReturnNewsDtoCollection = await this.newsService.getAllNews();
79 | return res
80 | .status(statusCode.OK)
81 | .send(util.success(statusCode.OK, message.READ_ALL_NEWS_SUCCESS, data));
82 | } catch (error) {
83 | logger.error(error);
84 | return res
85 | .status(statusCode.INTERNAL_SERVER_ERROR)
86 | .send(
87 | util.fail(
88 | statusCode.INTERNAL_SERVER_ERROR,
89 | message.INTERNAL_SERVER_ERROR,
90 | ),
91 | );
92 | }
93 | }
94 |
95 | @Post('update/:id')
96 | async updateNews(
97 | @Body() updateNewsDto: UpdateNewsDto,
98 | @Param('id') id: number,
99 | @Res() res,
100 | ): Promise {
101 | try {
102 | const data: ReturnNewsDtoCollection =
103 | await this.newsService.updateAndGetAllNews(id, updateNewsDto);
104 | return res
105 | .status(statusCode.OK)
106 | .send(util.success(statusCode.OK, message.UPDATE_NEWS_SUCCESS, data));
107 | } catch (error) {
108 | logger.error(error);
109 | return res
110 | .status(statusCode.INTERNAL_SERVER_ERROR)
111 | .send(
112 | util.fail(
113 | statusCode.INTERNAL_SERVER_ERROR,
114 | message.INTERNAL_SERVER_ERROR,
115 | ),
116 | );
117 | }
118 | }
119 |
120 | @Delete('delete/:id')
121 | async deleteNews(@Param('id') id: number, @Res() res): Promise {
122 | try {
123 | const data: ReturnNewsDtoCollection =
124 | await this.newsService.deleteAndGetAllNews(id);
125 | return res
126 | .status(statusCode.OK)
127 | .send(util.success(statusCode.OK, message.DELETE_NEWS_SUCCESS, data));
128 | } catch (error) {
129 | logger.error(error);
130 | return res
131 | .status(statusCode.INTERNAL_SERVER_ERROR)
132 | .send(
133 | util.fail(
134 | statusCode.INTERNAL_SERVER_ERROR,
135 | message.INTERNAL_SERVER_ERROR,
136 | ),
137 | );
138 | }
139 | }
140 |
141 | @Post('tag/add/:newsId')
142 | async addTagsToNews(
143 | @Req() req,
144 | @Res() res,
145 | @Param('newsId') newsId: number,
146 | ): Promise {
147 | try {
148 | const tagListForView: string[] = req.body.tagListForView;
149 | const tagListForRecommend: string[] = req.body.tagListForRecommend;
150 | const data: ReturnNewsDto = await this.newsService.addTagsToNews(
151 | newsId,
152 | tagListForView,
153 | tagListForRecommend,
154 | );
155 | return res
156 | .status(statusCode.OK)
157 | .send(
158 | util.success(statusCode.OK, message.ADD_TAG_TO_NEWS_SUCCESS, data),
159 | );
160 | } catch (error) {
161 | logger.error(error);
162 | return res
163 | .status(statusCode.INTERNAL_SERVER_ERROR)
164 | .send(
165 | util.fail(
166 | statusCode.INTERNAL_SERVER_ERROR,
167 | message.INTERNAL_SERVER_ERROR,
168 | ),
169 | );
170 | }
171 | }
172 |
173 | @Post('search')
174 | async searchNews(@Req() req, @Res() res): Promise {
175 | try {
176 | const body: object = req.body;
177 | const searchCondition: SearchCondition =
178 | convertBodyToSearchCondition(body);
179 | const bearerToken: string = req.headers['authorization'];
180 |
181 | let data: ExploreNewsDtoCollection;
182 | let paginationInfo: PaginationInfo;
183 |
184 | // 검색 조건과 토큰 입력 -> 토큰으로 user.favorite 가져오기
185 | [data, paginationInfo] = await this.newsService.searchByConditions(
186 | searchCondition,
187 | bearerToken,
188 | );
189 | const paginationInfoObject: object = { paginationInfo: paginationInfo };
190 |
191 | return res
192 | .status(statusCode.OK)
193 | .send(
194 | util.success(
195 | statusCode.OK,
196 | message.SEARCH_NEWS_SUCCESS,
197 | data,
198 | paginationInfoObject,
199 | ),
200 | );
201 | } catch (error) {
202 | logger.error(error);
203 | if (error.name == "InvalidTokenError") {
204 | return res
205 | .status(statusCode.UNAUTHORIZED)
206 | .send(
207 | util.fail(
208 | statusCode.UNAUTHORIZED,
209 | message.INVALID_TOKEN,
210 | ),
211 | );
212 | }
213 | return res
214 | .status(statusCode.INTERNAL_SERVER_ERROR)
215 | .send(
216 | util.fail(
217 | statusCode.INTERNAL_SERVER_ERROR,
218 | message.INTERNAL_SERVER_ERROR,
219 | ),
220 | );
221 | }
222 | }
223 |
224 | @Post('favorite')
225 | @UseGuards(JwtAuthGuard)
226 | async getFavoriteNews(@Res() res, @Req() req): Promise {
227 | try {
228 | const user: User = req.user;
229 | const body: object = req.body;
230 | const paginationCondition: PaginationCondition =
231 | convertBodyToPaginationCondition(body);
232 |
233 | let data: ExploreNewsDtoCollection;
234 | let paginationInfo;
235 |
236 | [data, paginationInfo] = await this.newsService.getFavoriteNews(
237 | paginationCondition,
238 | user,
239 | );
240 | const paginationInfoObject: object = { paginationInfo: paginationInfo };
241 |
242 | return res
243 | .status(statusCode.OK)
244 | .send(
245 | util.success(
246 | statusCode.OK,
247 | message.FAVORITE_NEWS_SUCCESS,
248 | data,
249 | paginationInfoObject,
250 | ),
251 | );
252 | } catch (error) {
253 | logger.error(error);
254 | return res
255 | .status(statusCode.INTERNAL_SERVER_ERROR)
256 | .send(
257 | util.fail(
258 | statusCode.INTERNAL_SERVER_ERROR,
259 | message.INTERNAL_SERVER_ERROR,
260 | ),
261 | );
262 | }
263 | }
264 |
265 | @Get('recommend')
266 | async getRecommendedNews(@Req() req, @Res() res): Promise {
267 | try {
268 | const bearerToken: string = req.headers['authorization'];
269 | const data: ExploreNewsDtoCollection =
270 | await this.newsService.getRecommendedNews(bearerToken);
271 | return res
272 | .status(statusCode.OK)
273 | .send(
274 | util.success(statusCode.OK, message.RECOMMENDED_NEWS_SUCCESS, data),
275 | );
276 | } catch (error) {
277 | logger.error(error);
278 | if (error.name == "InvalidTokenError") {
279 | return res
280 | .status(statusCode.UNAUTHORIZED)
281 | .send(
282 | util.fail(
283 | statusCode.UNAUTHORIZED,
284 | message.INVALID_TOKEN,
285 | ),
286 | );
287 | }
288 | return res
289 | .status(statusCode.INTERNAL_SERVER_ERROR)
290 | .send(
291 | util.fail(
292 | statusCode.INTERNAL_SERVER_ERROR,
293 | message.INTERNAL_SERVER_ERROR,
294 | ),
295 | );
296 | }
297 | }
298 |
299 | @Get('detail/not-authentication/:newsId')
300 | async newsDetailNotAuthenticated(
301 | @Res() res,
302 | @Param('newsId') newsId: number,
303 | ): Promise {
304 | try {
305 | const data: ReturnNewsDto = await this.newsService.getNews(newsId);
306 | const data2: ReturnScriptDefaultDto[] = [
307 | await this.dummyService.getScriptDefault(newsId),
308 | ];
309 | return res
310 | .status(statusCode.OK)
311 | .send(
312 | util.success(
313 | statusCode.OK,
314 | message.READ_NEWS_DETAIL_SUCCESS,
315 | data,
316 | data2,
317 | ),
318 | );
319 | } catch (error) {
320 | logger.error(error);
321 | if (error.name === 'EntityNotFound') {
322 | return res
323 | .status(statusCode.BAD_REQUEST)
324 | .send(util.fail(statusCode.BAD_REQUEST, message.NOT_EXISTING_NEWS));
325 | }
326 | return res
327 | .status(statusCode.INTERNAL_SERVER_ERROR)
328 | .send(
329 | util.fail(
330 | statusCode.INTERNAL_SERVER_ERROR,
331 | message.INTERNAL_SERVER_ERROR,
332 | ),
333 | );
334 | }
335 | }
336 |
337 | @Get('detail/:newsId')
338 | @UseGuards(JwtAuthGuard)
339 | async newsDetailAuthenticated(
340 | @Req() req,
341 | @Res() res,
342 | @Param('newsId') newsId: number,
343 | ): Promise {
344 | const userId: number = req.user.id;
345 | const user: User = req.user;
346 | try {
347 | const returnNewsDto: ReturnNewsDto = await this.newsService.getNews(
348 | newsId,
349 | );
350 | const data: ReturnNewsDto =
351 | await this.newsService.checkReturnNewsDtoIsFavorite(
352 | returnNewsDto,
353 | user,
354 | );
355 | const data2: ReturnScriptDtoCollection =
356 | await this.scriptService.getScripts(userId, newsId);
357 | this.historyService.fetchHistory(user, newsId);
358 | return res
359 | .status(statusCode.OK)
360 | .send(
361 | util.success(
362 | statusCode.OK,
363 | message.READ_NEWS_DETAIL_SUCCESS,
364 | data,
365 | data2.returnScriptDtoCollection,
366 | ),
367 | );
368 | } catch (error) {
369 | logger.error(error);
370 | if (error.name === 'EntityNotFound') {
371 | return res
372 | .status(statusCode.BAD_REQUEST)
373 | .send(util.fail(statusCode.BAD_REQUEST, message.NOT_EXISTING_NEWS));
374 | }
375 | return res
376 | .status(statusCode.INTERNAL_SERVER_ERROR)
377 | .send(
378 | util.fail(
379 | statusCode.INTERNAL_SERVER_ERROR,
380 | message.INTERNAL_SERVER_ERROR,
381 | ),
382 | );
383 | }
384 | }
385 |
386 | @Get('test/get/:newsId')
387 | async getNewsTest(
388 | @Res() res,
389 | @Param('newsId') newsId: number,
390 | ): Promise {
391 | const data: News = await this.newsService.getNewsTest(newsId);
392 | return res
393 | .status(statusCode.OK)
394 | .send(
395 | util.success(statusCode.OK, message.READ_NEWS_DETAIL_SUCCESS, data),
396 | );
397 | }
398 |
399 | @Get('guide')
400 | async getSpeechGuideNews(@Req() req, @Res() res): Promise {
401 | try {
402 | const bearerToken: string = req.headers['authorization'];
403 | const data: ExploreNewsDtoCollection =
404 | await this.newsService.getSpeechGuideNews(bearerToken);
405 | return res
406 | .status(statusCode.OK)
407 | .send(
408 | util.success(statusCode.OK, message.SPEECH_GUIDE_NEWS_SUCCESS, data),
409 | );
410 | } catch (error) {
411 | logger.error(error);
412 | if (error.name == "InvalidTokenError") {
413 | return res
414 | .status(statusCode.UNAUTHORIZED)
415 | .send(
416 | util.fail(
417 | statusCode.UNAUTHORIZED,
418 | message.INVALID_TOKEN,
419 | ),
420 | );
421 | }
422 | return res
423 | .status(statusCode.INTERNAL_SERVER_ERROR)
424 | .send(
425 | util.fail(
426 | statusCode.INTERNAL_SERVER_ERROR,
427 | message.INTERNAL_SERVER_ERROR,
428 | ),
429 | );
430 | }
431 | }
432 |
433 | @Get('guide/detail/:newsId')
434 | async newsDetailOfSpeechGuide(
435 | @Req() req,
436 | @Res() res,
437 | @Param('newsId') newsId: number,
438 | ): Promise {
439 | try {
440 | const bearerToken: string = req.headers['authorization'];
441 | // const data: ReturnNewsDto = await this.newsService.getNews(newsId);
442 | const data: ReturnNewsDto = await this.newsService.getNewsIncludeFavorite(newsId, bearerToken);
443 | const data2: ReturnScriptGuideDto[] = [
444 | await this.dummyService.getScriptGuide(newsId),
445 | ];
446 | return res
447 | .status(statusCode.OK)
448 | .send(
449 | util.success(
450 | statusCode.OK,
451 | message.SPEECH_GUIDE_NEWS_DETAIL_SUCCESS,
452 | data,
453 | data2,
454 | ),
455 | );
456 | } catch (error) {
457 | logger.error(error);
458 | if (error.name == "InvalidTokenError") {
459 | return res
460 | .status(statusCode.UNAUTHORIZED)
461 | .send(
462 | util.fail(
463 | statusCode.UNAUTHORIZED,
464 | message.INVALID_TOKEN,
465 | ),
466 | );
467 | }
468 | return res
469 | .status(statusCode.INTERNAL_SERVER_ERROR)
470 | .send(
471 | util.fail(
472 | statusCode.INTERNAL_SERVER_ERROR,
473 | message.INTERNAL_SERVER_ERROR,
474 | ),
475 | );
476 | }
477 | }
478 |
479 | @Get('similar/:newsId')
480 | async saveSimilarNews(
481 | @Req() req,
482 | @Res() res,
483 | @Param('newsId') newsId: number,
484 | ): Promise {
485 | try {
486 | const bearerToken: string = req.headers['authorization'];
487 | // await this.newsService.saveSimilarNews(bearerToken);
488 | const data: ExploreNewsDtoCollection =
489 | await this.newsService.getSimilarNews(newsId, bearerToken);
490 | return res
491 | .status(statusCode.OK)
492 | .send(util.success(statusCode.OK, message.SAVE_SIMILAR_NEWS, data));
493 | } catch (error) {
494 | logger.error(error);
495 | if (error.name == "InvalidTokenError") {
496 | return res
497 | .status(statusCode.UNAUTHORIZED)
498 | .send(
499 | util.fail(
500 | statusCode.UNAUTHORIZED,
501 | message.INVALID_TOKEN,
502 | ),
503 | );
504 | }
505 | return res
506 | .status(statusCode.INTERNAL_SERVER_ERROR)
507 | .send(
508 | util.fail(
509 | statusCode.INTERNAL_SERVER_ERROR,
510 | message.INTERNAL_SERVER_ERROR,
511 | ),
512 | );
513 | }
514 | }
515 |
516 | @Post('history')
517 | @UseGuards(JwtAuthGuard)
518 | async getHistory(@Res() res, @Req() req): Promise {
519 | try {
520 | const user: User = req.user;
521 | const body: object = req.body;
522 | const paginationCondition: PaginationCondition =
523 | convertBodyToPaginationCondition(body);
524 |
525 | let data: ExploreNewsDtoCollection;
526 | let paginationInfo;
527 |
528 | [data, paginationInfo] = await this.newsService.getHistory(
529 | paginationCondition,
530 | user,
531 | );
532 | const paginationInfoObject: object = { paginationInfo: paginationInfo };
533 |
534 | return res
535 | .status(statusCode.OK)
536 | .send(
537 | util.success(
538 | statusCode.OK,
539 | message.HISTORY_SUCCESS,
540 | data,
541 | paginationInfoObject,
542 | ),
543 | );
544 | } catch (error) {
545 | logger.error(error);
546 | return res
547 | .status(statusCode.INTERNAL_SERVER_ERROR)
548 | .send(
549 | util.fail(
550 | statusCode.INTERNAL_SERVER_ERROR,
551 | message.INTERNAL_SERVER_ERROR,
552 | ),
553 | );
554 | }
555 | }
556 | }
557 |
--------------------------------------------------------------------------------
/src/news/news.entity.ts:
--------------------------------------------------------------------------------
1 | import { ScriptDefault } from 'src/dummy/entity/script-default.entity';
2 | import { ScriptGuide } from 'src/dummy/entity/script-guide.entity';
3 | import { History } from 'src/history/history.entity';
4 | import { Script } from 'src/script/entity/script.entity';
5 | import { Tag } from 'src/tag/tag.entity';
6 | import { User } from 'src/user/user.entity';
7 | import {
8 | BaseEntity,
9 | Column,
10 | Entity,
11 | JoinTable,
12 | ManyToMany,
13 | ManyToOne,
14 | OneToMany,
15 | OneToOne,
16 | PrimaryGeneratedColumn,
17 | } from 'typeorm';
18 | import { Category } from './common/category.enum';
19 | import { Channel } from './common/channel.enum';
20 | import { Gender } from './common/gender.enum';
21 | import { Suitability } from './common/suitability.enum';
22 |
23 | @Entity()
24 | export class News extends BaseEntity {
25 | constructor(
26 | _title: string,
27 | _category: Category,
28 | _announcerGender: Gender,
29 | _channel: Channel,
30 | _link: string,
31 | _thumbnail: string,
32 | _startTime: number,
33 | _endTime: number,
34 | _suitability: Suitability,
35 | _isEmbeddable: boolean,
36 | _reportDate: Date,
37 | ) {
38 | super();
39 | this.title = _title;
40 | this.category = _category;
41 | this.announcerGender = _announcerGender;
42 | this.channel = _channel;
43 | this.link = _link;
44 | this.thumbnail = _thumbnail;
45 | this.startTime = _startTime;
46 | this.endTime = _endTime;
47 | this.suitability = _suitability;
48 | this.isEmbeddable = _isEmbeddable;
49 | this.reportDate = _reportDate;
50 | }
51 | @PrimaryGeneratedColumn()
52 | id: number;
53 |
54 | @Column({ type: 'varchar', length: 100 })
55 | title: string;
56 |
57 | @Column({
58 | type: 'enum',
59 | name: 'category',
60 | enum: Category,
61 | default: Category.UNSPECIFIED,
62 | })
63 | category: Category;
64 |
65 | @Column({
66 | type: 'enum',
67 | name: 'announcer_gender',
68 | enum: Gender,
69 | default: Gender.UNSPECIFIED,
70 | })
71 | announcerGender: Gender;
72 |
73 | @Column({
74 | type: 'enum',
75 | name: 'channel',
76 | enum: Channel,
77 | default: Channel.UNSPECIFIED,
78 | })
79 | channel: Channel;
80 |
81 | @Column({ type: 'varchar', length: 1000 })
82 | link: string;
83 |
84 | @Column({ type: 'varchar', length: 1000 })
85 | thumbnail: string;
86 |
87 | @Column('float')
88 | startTime: number;
89 |
90 | @Column('float')
91 | endTime: number;
92 |
93 | @Column({
94 | type: 'enum',
95 | name: 'suitability',
96 | enum: Suitability,
97 | default: Suitability.MEDIUM,
98 | })
99 | suitability: Suitability;
100 |
101 | @Column('varchar')
102 | isEmbeddable: boolean;
103 |
104 | @Column('date')
105 | reportDate: Date;
106 |
107 | @ManyToMany(() => User, (user) => user.favorites)
108 | favorites: User[];
109 |
110 | @ManyToMany(() => Tag, (tag) => tag.forView, {
111 | eager: true,
112 | })
113 | @JoinTable()
114 | tagsForView: Tag[];
115 |
116 | @ManyToMany(() => Tag, (tag) => tag.forRecommend, {
117 | eager: true,
118 | })
119 | @JoinTable()
120 | tagsForRecommend: Tag[];
121 |
122 | @OneToMany(() => Script, (script) => script.news)
123 | scripts: Script[];
124 |
125 | @OneToOne(() => ScriptDefault, (scriptDefault) => scriptDefault.news)
126 | scriptDefault: Promise;
127 |
128 | @OneToOne(() => ScriptGuide, (scriptGuide) => scriptGuide.news)
129 | scriptGuide: Promise;
130 |
131 | @OneToMany(() => History, (history) => history.news)
132 | histories: History[];
133 | }
134 |
--------------------------------------------------------------------------------
/src/news/news.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { AuthModule } from 'src/auth/auth.module';
4 | import { DummyService } from 'src/dummy/dummy.service';
5 | import { MemoGuideRepository } from 'src/dummy/repository/memo-guide.repository';
6 | import { ScriptDefaultRepository } from 'src/dummy/repository/script-default.repository';
7 | import { ScriptGuideRepository } from 'src/dummy/repository/script-guide.repository';
8 | import { SentenceDefaultRepository } from 'src/dummy/repository/sentence-default.repository';
9 | import { SentenceGuideRepository } from 'src/dummy/repository/sentence-guide.repository';
10 | import { HistoryRepository } from 'src/history/history.repository';
11 | import { HistoryService } from 'src/history/history.service';
12 | import { MemoRepository } from 'src/script/repository/memo.repository';
13 | import { ScriptCountRepository } from 'src/script/repository/script-count.repository';
14 | import { ScriptRepository } from 'src/script/repository/script.repository';
15 | import { SentenceRepository } from 'src/script/repository/sentence.repository';
16 | import { ScriptService } from 'src/script/script.service';
17 | import { TagRepository } from 'src/tag/tag.repository';
18 | import { UserRepository } from 'src/user/user.repository';
19 | import { NewsController } from './news.controller';
20 | import { NewsRepository } from './news.repository';
21 | import { NewsService } from './news.service';
22 | import { RecordingRepository } from '../script/repository/recording.repository';
23 |
24 | @Module({
25 | imports: [
26 | TypeOrmModule.forFeature([
27 | NewsRepository,
28 | TagRepository,
29 | ScriptRepository,
30 | SentenceRepository,
31 | UserRepository,
32 | ScriptDefaultRepository,
33 | SentenceDefaultRepository,
34 | MemoRepository,
35 | ScriptCountRepository,
36 | ScriptGuideRepository,
37 | SentenceGuideRepository,
38 | MemoGuideRepository,
39 | RecordingRepository,
40 | HistoryRepository,
41 | ]),
42 | AuthModule,
43 | ],
44 | controllers: [NewsController],
45 | providers: [NewsService, ScriptService, DummyService, HistoryService],
46 | })
47 | export class NewsModule {}
48 |
--------------------------------------------------------------------------------
/src/news/news.repository.ts:
--------------------------------------------------------------------------------
1 | import { Tag } from 'src/tag/tag.entity';
2 | import { EntityRepository, Repository, UpdateResult } from 'typeorm';
3 | import { SearchCondition } from './common/search-condition';
4 | import { CreateNewsDto } from './dto/create-news.dto';
5 | import { ReturnNewsDtoCollection } from './dto/return-news-collection.dto';
6 | import { ReturnNewsDto } from './dto/return-news.dto';
7 | import { UpdateNewsDto } from './dto/update-news.dto';
8 | import { News } from './news.entity';
9 | import { changeReturnNewsListToDto } from './utils/change-return-news-list-to-dto';
10 |
11 | @EntityRepository(News)
12 | export class NewsRepository extends Repository {
13 | async createNews(createNewsDto: CreateNewsDto): Promise {
14 | const {
15 | title,
16 | category,
17 | announcerGender,
18 | channel,
19 | link,
20 | thumbnail,
21 | startTime,
22 | endTime,
23 | suitability,
24 | isEmbeddable,
25 | reportDate,
26 | } = createNewsDto;
27 |
28 | const news = new News(
29 | title,
30 | category,
31 | announcerGender,
32 | channel,
33 | link,
34 | thumbnail,
35 | startTime,
36 | endTime,
37 | suitability,
38 | isEmbeddable,
39 | reportDate,
40 | );
41 |
42 | await this.save(news);
43 | return news;
44 | }
45 |
46 | async getAllNews(): Promise {
47 | return await this.find();
48 | }
49 |
50 | async getNewsById(id: number): Promise {
51 | const news: News = await this.findOneOrFail({
52 | id: id,
53 | });
54 | return news;
55 | }
56 |
57 | async resetTagsOfNews(news: News): Promise {
58 | news.tagsForView = [];
59 | news.tagsForRecommend = [];
60 | news.save();
61 | return news;
62 | }
63 |
64 | async addTagsForViewToNews(news: News, tagsForView: Tag[]): Promise {
65 | tagsForView.forEach((tag) => {
66 | news.tagsForView.push(tag);
67 | });
68 | news.save();
69 | return news;
70 | }
71 |
72 | async addTagsForRecommendToNews(
73 | news: News,
74 | tagsForRecommend: Tag[],
75 | ): Promise {
76 | tagsForRecommend.forEach((tag) => {
77 | news.tagsForRecommend.push(tag);
78 | });
79 | news.save();
80 | return news;
81 | }
82 |
83 | async updateNews(id: number, updateNewsDto: UpdateNewsDto): Promise {
84 | await this.update({ id: id }, updateNewsDto);
85 | return await this.getNewsById(id);
86 | }
87 |
88 | async deleteNews(id: number): Promise {
89 | const news: News = await this.getNewsById(id);
90 | await this.delete({ id: id });
91 | return news;
92 | }
93 |
94 | async findByChannel(searchCondition: SearchCondition): Promise {
95 | const channels: string[] = searchCondition.channel;
96 | return await this.createQueryBuilder('news')
97 | .where('news.channel IN (:...channels)', { channels })
98 | .getMany();
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/news/news.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { JwtModule } from '@nestjs/jwt';
2 | import { Test, TestingModule } from '@nestjs/testing';
3 | import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm';
4 | import { Category } from './common/category.enum';
5 | import { Channel } from './common/channel.enum';
6 | import { Gender } from './common/gender.enum';
7 | import { Suitability } from './common/suitability.enum';
8 | import { CreateNewsDto } from './dto/create-news.dto';
9 | import { ReturnNewsDto } from './dto/return-news.dto';
10 | import { UpdateNewsDto } from './dto/update-news.dto';
11 | import { NewsController } from './news.controller';
12 | import { News } from './news.entity';
13 | import { NewsRepository } from './news.repository';
14 | import { NewsService } from './news.service';
15 |
16 | const mockNews: News = new News(
17 | 'test title',
18 | Category.SOCIETY,
19 | Gender.WOMEN,
20 | Channel.KBS,
21 | 'test link',
22 | 'test thumbnail',
23 | 3,
24 | 53,
25 | Suitability.HIGH,
26 | true,
27 | new Date(),
28 | );
29 | mockNews.id = 1;
30 |
31 | const MockNewsRepository = () => ({
32 | async createNews(createNewsDto: CreateNewsDto): Promise {
33 | const {
34 | title,
35 | category,
36 | script,
37 | announcerGender,
38 | channel,
39 | link,
40 | thumbnail,
41 | startTime,
42 | endTime,
43 | suitability,
44 | isEmbeddable,
45 | reportDate,
46 | } = createNewsDto;
47 |
48 | const news = new News(
49 | title,
50 | category,
51 | announcerGender,
52 | channel,
53 | link,
54 | thumbnail,
55 | startTime,
56 | endTime,
57 | suitability,
58 | isEmbeddable,
59 | reportDate,
60 | );
61 |
62 | return news;
63 | },
64 |
65 | async updateNews(
66 | id: number,
67 | updateNewsDto: UpdateNewsDto,
68 | ): Promise {
69 | const {
70 | title,
71 | category,
72 | announcerGender,
73 | channel,
74 | link,
75 | thumbnail,
76 | startTime,
77 | endTime,
78 | suitability,
79 | isEmbeddable,
80 | reportDate,
81 | } = updateNewsDto;
82 |
83 | const news = new News(
84 | title,
85 | category,
86 | announcerGender,
87 | channel,
88 | link,
89 | thumbnail,
90 | startTime,
91 | endTime,
92 | suitability,
93 | isEmbeddable,
94 | reportDate,
95 | );
96 | const returnNewsDto: ReturnNewsDto = new ReturnNewsDto(news);
97 | return returnNewsDto;
98 | },
99 | });
100 |
101 | describe('NewsService', () => {
102 | let newsService: NewsService;
103 |
104 | beforeEach(async () => {
105 | const module: TestingModule = await Test.createTestingModule({
106 | providers: [
107 | NewsService,
108 | {
109 | provide: getRepositoryToken(NewsRepository),
110 | useValue: MockNewsRepository(),
111 | },
112 | ],
113 | }).compile();
114 |
115 | newsService = module.get(NewsService);
116 | });
117 |
118 | it('should be defined', () => {
119 | expect(newsService).toBeDefined();
120 | });
121 |
122 | describe('createNews() : 뉴스 생성', () => {
123 | it('SUCCESS: 뉴스 생성 성공', async () => {
124 | const createNewsDto: CreateNewsDto = {
125 | title: 'test title',
126 | category: Category.SOCIETY,
127 | script: 'test script',
128 | announcerGender: Gender.WOMEN,
129 | channel: Channel.KBS,
130 | link: 'test link',
131 | thumbnail: 'test thumbnail',
132 | startTime: 3,
133 | endTime: 53,
134 | suitability: Suitability.HIGH,
135 | isEmbeddable: true,
136 | reportDate: new Date(),
137 | };
138 | const result = await newsService.createNews(createNewsDto);
139 | expect(result.title).toStrictEqual(createNewsDto.title);
140 | });
141 | });
142 |
143 | describe('updateNews() : 뉴스 수정 조회', () => {
144 | it('SUCCESS: 뉴스 수정 성공', async () => {
145 | const updateNewsDto: UpdateNewsDto = {
146 | title: 'test2 title',
147 | category: Category.SOCIETY,
148 | announcerGender: Gender.WOMEN,
149 | channel: Channel.KBS,
150 | link: 'test2 link',
151 | thumbnail: 'test2 thumbnail',
152 | startTime: 3,
153 | endTime: 53,
154 | suitability: Suitability.HIGH,
155 | isEmbeddable: true,
156 | reportDate: new Date(),
157 | };
158 | const result = await newsService.updateNews(mockNews.id, updateNewsDto);
159 | expect(result.title).toStrictEqual(updateNewsDto.title);
160 | });
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/news/news.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { AuthService } from 'src/auth/auth.service';
4 | import { JwtStrategy } from 'src/auth/auth.passport.jwt.strategy';
5 | import { message } from 'src/modules/response/response.message';
6 | import { User } from 'src/user/user.entity';
7 | import { UpdateResult } from 'typeorm';
8 | import {
9 | ConditionList,
10 | hasChannels,
11 | hasFindAll,
12 | } from './common/condition-list';
13 | import { PaginationInfo } from './common/pagination-info';
14 | import { SearchCondition } from './common/search-condition';
15 | import { CreateNewsDto } from './dto/create-news.dto';
16 | import { ExploreNewsDto } from './dto/explore-news.dto';
17 | import { ReturnNewsDtoCollection } from './dto/return-news-collection.dto';
18 | import { ReturnNewsDto } from './dto/return-news.dto';
19 | import { UpdateNewsDto } from './dto/update-news.dto';
20 | import { News } from './news.entity';
21 | import { NewsRepository } from './news.repository';
22 | import { changeReturnNewsListToDto } from './utils/change-return-news-list-to-dto';
23 | import { getLastPage } from './utils/get-last-page';
24 | import { sortByDateAndTitle } from './utils/sort-by-date-and-title';
25 | import { VerifiedCallback } from 'passport-jwt';
26 | import { Payload } from 'src/auth/dto/payload';
27 | import { ExploreNewsDtoCollection } from './dto/explore-news-collection.dto';
28 | import { changeToExploreNewsList } from './utils/change-explore-news-list-to-dto';
29 | import { Tag } from 'src/tag/tag.entity';
30 | import { TagRepository } from 'src/tag/tag.repository';
31 | import { PaginationCondition } from './common/pagination-condition';
32 | import { Script } from 'src/script/entity/script.entity';
33 | import { ScriptRepository } from 'src/script/repository/script.repository';
34 | import { checkUser } from './utils/check-user';
35 | import { checkNewsDtoInFavoriteList } from './utils/check-news-dto-in-favorite-list';
36 | import { History } from 'src/history/history.entity';
37 | import { HistoryService } from 'src/history/history.service';
38 |
39 | const logger: Logger = new Logger('news service');
40 |
41 | @Injectable()
42 | export class NewsService {
43 | constructor(
44 | @InjectRepository(NewsRepository)
45 | private newsRepository: NewsRepository,
46 | @InjectRepository(TagRepository)
47 | private tagRepository: TagRepository,
48 | @InjectRepository(ScriptRepository)
49 | private scriptRepository: ScriptRepository,
50 | private authService: AuthService,
51 | private historyService: HistoryService,
52 | ) {}
53 |
54 | async createNews(createNewsDto: CreateNewsDto): Promise {
55 | const news: News = await this.newsRepository.createNews(createNewsDto);
56 | const returnNewsDto: ReturnNewsDto = new ReturnNewsDto(news);
57 | return returnNewsDto;
58 | }
59 |
60 | async getAllNews(): Promise {
61 | const returnNewsDtoCollection: ReturnNewsDtoCollection =
62 | changeReturnNewsListToDto(await this.newsRepository.getAllNews());
63 | return returnNewsDtoCollection;
64 | }
65 |
66 | async updateNews(
67 | id: number,
68 | updateNewsDto: UpdateNewsDto,
69 | ): Promise {
70 | const news: News = await this.newsRepository.updateNews(id, updateNewsDto);
71 | const returnNewsDto: ReturnNewsDto = new ReturnNewsDto(news);
72 | return returnNewsDto;
73 | }
74 |
75 | async deleteNews(id: number): Promise {
76 | const deletedNews: News = await this.newsRepository.deleteNews(id);
77 | const returnNewsDto: ReturnNewsDto = new ReturnNewsDto(deletedNews);
78 | return returnNewsDto;
79 | }
80 |
81 | async createAndGetAllNews(
82 | createNewsDto: CreateNewsDto,
83 | ): Promise {
84 | await this.createNews(createNewsDto);
85 | return await this.getAllNews();
86 | }
87 |
88 | async updateAndGetAllNews(
89 | id: number,
90 | updateNewsDto: UpdateNewsDto,
91 | ): Promise {
92 | await this.updateNews(id, updateNewsDto);
93 | return await this.getAllNews();
94 | }
95 |
96 | async deleteAndGetAllNews(id: number): Promise {
97 | await this.deleteNews(id);
98 | return await this.getAllNews();
99 | }
100 |
101 | async addTagsToNews(
102 | newsId: number,
103 | tagListForView: string[],
104 | tagListForRecommend: string[],
105 | ): Promise {
106 | // 태그, 뉴스 불러오기
107 | const tagsForView: Tag[] = await this.tagRepository.getTagsByNameList(
108 | tagListForView,
109 | );
110 | const tagsForRecommend: Tag[] = await this.tagRepository.getTagsByNameList(
111 | tagListForRecommend,
112 | );
113 | const news: News = await this.newsRepository.getNewsById(newsId);
114 | // 해당 뉴스의 태그(화면 표시용) 초기화
115 | const newsResetTag: News = await this.newsRepository.resetTagsOfNews(news);
116 | // 태그 추가
117 | await this.newsRepository.addTagsForViewToNews(news, tagsForView);
118 | await this.newsRepository.addTagsForRecommendToNews(news, tagsForRecommend);
119 | // 뉴스 불러오기
120 | const newsAfterAddTags: News = await this.newsRepository.getNewsById(
121 | newsId,
122 | );
123 | const returnNewsDto: ReturnNewsDto = new ReturnNewsDto(newsAfterAddTags);
124 | return returnNewsDto;
125 | }
126 |
127 | async filterNewsByCategory(
128 | newsList: News[],
129 | searchCondition: SearchCondition,
130 | ) {
131 | if (searchCondition.checkIfCategoryIs() === true) {
132 | const category: string[] = searchCondition.category;
133 | newsList = newsList.filter((news) => {
134 | if (category.includes(news.category)) {
135 | return news;
136 | }
137 | });
138 | }
139 | return newsList;
140 | }
141 |
142 | async filterNewsByAnnouncerGender(
143 | newsList: News[],
144 | searchCondition: SearchCondition,
145 | ) {
146 | if (searchCondition.checkIfAnnouncerGenderIs() === true) {
147 | const announcerGender: string[] = searchCondition.announcerGender;
148 | newsList = newsList.filter((news) => {
149 | if (announcerGender.includes(news.announcerGender)) {
150 | return news;
151 | }
152 | });
153 | }
154 | return newsList;
155 | }
156 |
157 | async validateNewsDataLength(newsData: News[], offset: number) {
158 | if (offset > newsData.length) {
159 | throw new Error(message.EXCEED_PAGE_INDEX);
160 | }
161 | }
162 |
163 | async paginateWithOffsetAndLimit(
164 | newsList: News[],
165 | condition: SearchCondition | PaginationCondition,
166 | ): Promise {
167 | const offset: number = condition.getOffset();
168 | const limit: number = condition.getLimit();
169 | const endIndex: number = offset + limit;
170 |
171 | this.validateNewsDataLength(newsList, offset);
172 | return newsList.slice(offset, endIndex);
173 | }
174 |
175 | async checkExploreNewsDtoListIsFavorite(
176 | exploreNewsDtoList: ExploreNewsDto[],
177 | user: User,
178 | ): Promise {
179 | if (!checkUser(user)) {
180 | return exploreNewsDtoList;
181 | }
182 | const favoriteList: number[] = (await user.favorites).map(
183 | (news) => news.id,
184 | );
185 | exploreNewsDtoList = exploreNewsDtoList.map((news) => {
186 | return checkNewsDtoInFavoriteList(news, favoriteList);
187 | });
188 | return exploreNewsDtoList;
189 | }
190 |
191 | async checkReturnNewsDtoIsFavorite(
192 | returnNewsDto: ReturnNewsDto,
193 | user: User,
194 | ): Promise {
195 | if (!checkUser(user)) {
196 | return returnNewsDto;
197 | }
198 | const favoriteList: number[] = (await user.favorites).map(
199 | (news) => news.id,
200 | );
201 | checkNewsDtoInFavoriteList(returnNewsDto, favoriteList);
202 | return returnNewsDto;
203 | }
204 |
205 | async searchByConditions(
206 | searchCondition: SearchCondition,
207 | bearerToken: string | undefined,
208 | ): Promise<[ExploreNewsDtoCollection, PaginationInfo]> {
209 | let newsList: News[];
210 | // channel 조건에 따라 불러오기
211 | if (searchCondition.checkIfChannelIs() === false) {
212 | newsList = await this.newsRepository.getAllNews();
213 | } else {
214 | newsList = await this.newsRepository.findByChannel(searchCondition);
215 | }
216 | // category 조건으로 필터링
217 | newsList = await this.filterNewsByCategory(newsList, searchCondition);
218 | // announcerGender 조건으로 필터링
219 | newsList = await this.filterNewsByAnnouncerGender(
220 | newsList,
221 | searchCondition,
222 | );
223 |
224 | // 페이지네이션 정보 생성
225 | const totalCount: number = newsList.length;
226 | const lastPage = getLastPage(12, totalCount);
227 | const paginationInfo = new PaginationInfo(totalCount, lastPage);
228 |
229 | // 정렬
230 | newsList = sortByDateAndTitle(newsList);
231 | // 페이지네이션
232 | newsList = await this.paginateWithOffsetAndLimit(newsList, searchCondition);
233 |
234 | // 탐색창(검색 등)에 보여지는 형식으로 수정
235 | let exploreNewsDtoList: ExploreNewsDto[] =
236 | await this.changeToExploreNewsList(newsList);
237 |
238 | // 즐겨찾기 체크 (로그인 된 유저라면)
239 | if (bearerToken !== undefined) {
240 | const user: User = await this.authService.verifyJWTReturnUser(
241 | bearerToken,
242 | );
243 | exploreNewsDtoList = await this.checkExploreNewsDtoListIsFavorite(
244 | exploreNewsDtoList,
245 | user,
246 | );
247 | }
248 |
249 | const exploreNewsDtoCollection: ExploreNewsDtoCollection =
250 | new ExploreNewsDtoCollection(exploreNewsDtoList);
251 | return [exploreNewsDtoCollection, paginationInfo];
252 | }
253 |
254 | async getFavoriteNews(
255 | paginationCondition: PaginationCondition,
256 | user: User,
257 | ): Promise<[ExploreNewsDtoCollection, PaginationInfo]> {
258 | let favoriteNewsList: News[] = await user.favorites;
259 | // 페이지네이션 정보 생성
260 | const totalCount: number = favoriteNewsList.length;
261 | const lastPage = getLastPage(12, totalCount);
262 | const paginationInfo = new PaginationInfo(totalCount, lastPage);
263 |
264 | // 정렬
265 | favoriteNewsList = sortByDateAndTitle(favoriteNewsList);
266 | // 페이지네이션
267 | favoriteNewsList = await this.paginateWithOffsetAndLimit(
268 | favoriteNewsList,
269 | paginationCondition,
270 | );
271 |
272 | // 탐색창(검색 등)에 보여지는 형식으로 수정
273 | const exploreNewsDtoList: ExploreNewsDto[] =
274 | await this.changeToExploreNewsList(favoriteNewsList);
275 | // 즐겨찾기 여부 true로 수정
276 | exploreNewsDtoList.map((news) => (news.isFavorite = true));
277 | const exploreNewsDtoCollection: ExploreNewsDtoCollection =
278 | new ExploreNewsDtoCollection(exploreNewsDtoList);
279 | return [exploreNewsDtoCollection, paginationInfo];
280 | }
281 |
282 | async getHistory(
283 | paginationCondition: PaginationCondition,
284 | user: User,
285 | ): Promise<[ExploreNewsDtoCollection, PaginationInfo]> {
286 | const historyList: History[] = this.sortHistoryListByDate(
287 | await user.histories,
288 | );
289 | let historyNewsList: News[] = await this.getNewsListFromHistoryList(
290 | historyList,
291 | );
292 | // 페이지네이션 정보 생성
293 | const totalCount: number = historyNewsList.length;
294 | const lastPage = getLastPage(12, totalCount);
295 | const paginationInfo = new PaginationInfo(totalCount, lastPage);
296 | // 페이지네이션
297 | historyNewsList = await this.paginateWithOffsetAndLimit(
298 | historyNewsList,
299 | paginationCondition,
300 | );
301 | // 탐색창(검색 등)에 보여지는 형식으로 수정
302 | let exploreNewsDtoList: ExploreNewsDto[] =
303 | await this.changeToExploreNewsList(historyNewsList);
304 | // 즐겨찾기 체크
305 | exploreNewsDtoList = await this.checkExploreNewsDtoListIsFavorite(
306 | exploreNewsDtoList,
307 | user,
308 | );
309 | const exploreNewsDtoCollection: ExploreNewsDtoCollection =
310 | new ExploreNewsDtoCollection(exploreNewsDtoList);
311 | return [exploreNewsDtoCollection, paginationInfo];
312 | }
313 |
314 | async getNewsListFromHistoryList(historyList: History[]): Promise {
315 | const newsList: News[] = [];
316 | for (const history of historyList) {
317 | const news: News = await this.historyService.getNewsByHistoryId(
318 | history.id,
319 | );
320 | newsList.push(news);
321 | }
322 | return newsList;
323 | }
324 |
325 | sortHistoryListByDate(historyList: History[]): History[] {
326 | historyList.sort((prev, next) => {
327 | const prevDate: Date = prev.date;
328 | const nextDate: Date = next.date;
329 | if (prevDate < nextDate) {
330 | return 1;
331 | }
332 | return -1;
333 | });
334 | return historyList;
335 | }
336 |
337 | async getRecommendedNews(
338 | bearerToken: string,
339 | ): Promise {
340 | // 추천 태그에 포함된 뉴스 리스트 가져오기
341 | const recommendedTag: Tag = await this.tagRepository.getRecommendedTag();
342 | let recommendedNewsList: News[] = await recommendedTag.forView;
343 | // 정렬 후 8개 슬라이싱
344 | recommendedNewsList = await sortByDateAndTitle(recommendedNewsList);
345 | recommendedNewsList = recommendedNewsList.slice(0, 8);
346 | // 타입 변경 후 반환
347 | let exploreNewsDtoList: ExploreNewsDto[] =
348 | await this.changeToExploreNewsList(recommendedNewsList);
349 | // 즐겨찾기 체크 (로그인 된 유저라면)
350 | if (bearerToken !== undefined) {
351 | const user: User = await this.authService.verifyJWTReturnUser(
352 | bearerToken,
353 | );
354 | exploreNewsDtoList = await this.checkExploreNewsDtoListIsFavorite(
355 | exploreNewsDtoList,
356 | user,
357 | );
358 | }
359 | const exploreNewsDtoCollection: ExploreNewsDtoCollection =
360 | new ExploreNewsDtoCollection(exploreNewsDtoList);
361 | return exploreNewsDtoCollection;
362 | }
363 |
364 | async getSpeechGuideNews(
365 | bearerToken: string,
366 | ): Promise {
367 | // 스피치 가이드 태그에 포함된 뉴스 리스트 가져오기
368 | const speechGuideTag: Tag = await this.tagRepository.getSpeechGuideTag();
369 | let speechGuideNewsList: News[] = await speechGuideTag.forRecommend;
370 | // 정렬 후 4개 슬라이싱
371 | speechGuideNewsList = await sortByDateAndTitle(speechGuideNewsList);
372 | speechGuideNewsList = speechGuideNewsList.slice(0, 4);
373 | // 타입 변경 후 반환
374 | let exploreNewsDtoList: ExploreNewsDto[] =
375 | await this.changeToExploreNewsList(speechGuideNewsList);
376 | // 즐겨찾기 체크 (로그인 된 유저라면)
377 | if (bearerToken !== undefined) {
378 | const user: User = await this.authService.verifyJWTReturnUser(
379 | bearerToken,
380 | );
381 | exploreNewsDtoList = await this.checkExploreNewsDtoListIsFavorite(
382 | exploreNewsDtoList,
383 | user,
384 | );
385 | }
386 | const exploreNewsDtoCollection: ExploreNewsDtoCollection =
387 | new ExploreNewsDtoCollection(exploreNewsDtoList);
388 | return exploreNewsDtoCollection;
389 | }
390 |
391 | async getNews(newsId: number): Promise {
392 | const news: News = await this.newsRepository.getNewsById(newsId);
393 | const returnNewsDto: ReturnNewsDto = new ReturnNewsDto(news);
394 | return returnNewsDto;
395 | }
396 |
397 | async getNewsIncludeFavorite(newsId: number, bearerToken: string): Promise {
398 | const news: News = await this.newsRepository.getNewsById(newsId);
399 | let returnNewsDto: ReturnNewsDto = new ReturnNewsDto(news);
400 | // 즐겨찾기 체크 (로그인 된 유저라면)
401 | if (bearerToken !== undefined) {
402 | const user: User = await this.authService.verifyJWTReturnUser(
403 | bearerToken,
404 | );
405 | returnNewsDto = await this.checkReturnNewsDtoIsFavorite(
406 | returnNewsDto,
407 | user,
408 | );
409 | }
410 | console.log(returnNewsDto);
411 | return returnNewsDto;
412 | }
413 |
414 | async getNewsByScriptId(scriptId: number): Promise {
415 | const script: Script = await this.scriptRepository.findOneOrFail(scriptId);
416 | const newsId: number = script.news.id;
417 | return await this.getNews(newsId);
418 | }
419 |
420 | async getNewsTest(newsId: number): Promise {
421 | return this.newsRepository.findOne(newsId);
422 | }
423 |
424 | async changeToExploreNewsList(newsList: News[]): Promise {
425 | const exploreNewsDtoList: ExploreNewsDto[] = [];
426 | for (const news of newsList) {
427 | const exploreNewsDto: ExploreNewsDto = await new ExploreNewsDto(news);
428 | await exploreNewsDto.checkHaveGuide(news);
429 | exploreNewsDtoList.push(exploreNewsDto);
430 | }
431 | return exploreNewsDtoList;
432 | }
433 |
434 | async getSimilarNews(
435 | newsId: number,
436 | bearerToken: string,
437 | ): Promise {
438 | const news: News = await this.newsRepository.getNewsById(newsId);
439 | const similarNewsList: News[] = await this.getSimilarNewsList(news);
440 | let exploreNewsDtoList: ExploreNewsDto[] =
441 | await this.changeToExploreNewsList(similarNewsList);
442 | // 즐겨찾기 체크 (로그인 된 유저라면)
443 | if (bearerToken !== undefined) {
444 | const user: User = await this.authService.verifyJWTReturnUser(
445 | bearerToken,
446 | );
447 | exploreNewsDtoList = await this.checkExploreNewsDtoListIsFavorite(
448 | exploreNewsDtoList,
449 | user,
450 | );
451 | }
452 | const exploreNewsDtoCollection: ExploreNewsDtoCollection =
453 | new ExploreNewsDtoCollection(exploreNewsDtoList);
454 | return exploreNewsDtoCollection;
455 | }
456 |
457 | async getSimilarNewsList(news: News): Promise {
458 | const similarMap: Map = await this.calculateSimilarMap(news);
459 | const similarMapArray = await this.sortSimilarMap(similarMap);
460 | // 비슷한 영상 4개 자르기
461 | const similarNewsList: News[] = [];
462 | for (let i = 0; i < 4; i++) {
463 | similarNewsList.push(similarMapArray[i][0]);
464 | }
465 | return similarNewsList;
466 | }
467 |
468 | async calculateSimilarMap(news: News): Promise