├── .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 |
40 | 41 |
42 | 43 |
44 | 45 |
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> { 469 | const similarMap = new Map(); 470 | const tagsOfNews: Tag[] = news.tagsForRecommend; 471 | const allNews: News[] = await this.newsRepository.find(); 472 | for (const newsTarget of allNews) { 473 | if (newsTarget.id == news.id) { 474 | continue; 475 | } 476 | const countOfSameTags: number = await this.calculateCountOfSameTags( 477 | tagsOfNews, 478 | newsTarget, 479 | ); 480 | similarMap.set(newsTarget, countOfSameTags); 481 | } 482 | console.log(similarMap); 483 | return similarMap; 484 | } 485 | 486 | async calculateCountOfSameTags( 487 | tagsOfNews: Tag[], 488 | news: News, 489 | ): Promise { 490 | let countOfSameTags = 0; 491 | const tagsOfTargetNews: Tag[] = news.tagsForRecommend; 492 | for (const tag of tagsOfNews) { 493 | for (const tagTarget of tagsOfTargetNews) { 494 | if (tag.id == tagTarget.id) { 495 | countOfSameTags += 1; 496 | } 497 | } 498 | } 499 | return countOfSameTags; 500 | } 501 | 502 | async sortSimilarMap(similarMap: Map): Promise { 503 | const mapArray: Object[] = [...similarMap.entries()].sort((prev, next) => { 504 | const prevCount: number = prev[1]; 505 | const nextCount: number = next[1]; 506 | const prevNews: News = prev[0]; 507 | const nextNews: News = next[0]; 508 | if (prevCount == nextCount) { 509 | return this.compareOrderOfTwoNews(prevNews, nextNews); 510 | } 511 | return nextCount - prevCount; 512 | }); 513 | return mapArray; 514 | } 515 | 516 | compareOrderOfTwoNews(prevNews: News, nextNews: News): number { 517 | if (+new Date(prevNews.reportDate) == +new Date(nextNews.reportDate)) { 518 | const condition = 519 | '[]{}*!@_.()#^&%-=+01234567989abcdefghijklmnopqrstuvwxyz'; 520 | const prev_condition = condition.indexOf(prevNews.title[0]); 521 | const next_condition = condition.indexOf(nextNews.title[0]); 522 | if (prev_condition === next_condition) { 523 | return prevNews.title < nextNews.title 524 | ? -1 525 | : prevNews.title > nextNews.title 526 | ? 1 527 | : 0; 528 | } 529 | return next_condition - prev_condition; 530 | } 531 | return +new Date(nextNews.reportDate) - +new Date(prevNews.reportDate); 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /src/news/utils/change-explore-news-list-to-dto.ts: -------------------------------------------------------------------------------- 1 | import { ExploreNewsDtoCollection } from '../dto/explore-news-collection.dto'; 2 | import { ExploreNewsDto } from '../dto/explore-news.dto'; 3 | import { News } from '../news.entity'; 4 | 5 | export const changeToExploreNewsList = (newsList: News[]): ExploreNewsDto[] => { 6 | const exploreNewsDtoList: ExploreNewsDto[] = newsList.map( 7 | (news) => new ExploreNewsDto(news), 8 | ); 9 | return exploreNewsDtoList; 10 | }; 11 | -------------------------------------------------------------------------------- /src/news/utils/change-return-news-list-to-dto.ts: -------------------------------------------------------------------------------- 1 | import { ReturnNewsDtoCollection } from '../dto/return-news-collection.dto'; 2 | import { ReturnNewsDto } from '../dto/return-news.dto'; 3 | import { News } from '../news.entity'; 4 | 5 | export const changeReturnNewsListToDto = ( 6 | newsList: News[], 7 | ): ReturnNewsDtoCollection => { 8 | const returnNewsDtoList: ReturnNewsDto[] = newsList.map( 9 | (news) => new ReturnNewsDto(news), 10 | ); 11 | const returnNewsDtoCollection: ReturnNewsDtoCollection = 12 | new ReturnNewsDtoCollection(returnNewsDtoList); 13 | return returnNewsDtoCollection; 14 | }; 15 | -------------------------------------------------------------------------------- /src/news/utils/check-news-dto-in-favorite-list.ts: -------------------------------------------------------------------------------- 1 | import { ExploreNewsDto } from '../dto/explore-news.dto'; 2 | import { ReturnNewsDto } from '../dto/return-news.dto'; 3 | 4 | export const checkNewsDtoInFavoriteList = ( 5 | newsDto: ExploreNewsDto | ReturnNewsDto, 6 | favoriteList: number[], 7 | ): ExploreNewsDto | ReturnNewsDto => { 8 | if (favoriteList.includes(newsDto.id)) { 9 | newsDto.isFavorite = true; 10 | return newsDto; 11 | } 12 | return newsDto; 13 | }; 14 | -------------------------------------------------------------------------------- /src/news/utils/check-user.ts: -------------------------------------------------------------------------------- 1 | export const checkUser = (user): boolean => { 2 | if (user === undefined) { 3 | return false; 4 | } 5 | return true; 6 | }; 7 | -------------------------------------------------------------------------------- /src/news/utils/convert-body-to-condition.ts: -------------------------------------------------------------------------------- 1 | import { PaginationCondition } from '../common/pagination-condition'; 2 | import { SearchCondition } from '../common/search-condition'; 3 | 4 | export const convertBodyToSearchCondition = (body: object): SearchCondition => { 5 | const channels = body['channel']; 6 | const categories = body['category']; 7 | const announcerGender = body['announcerGender']; 8 | const currentPage = body['currentPage']; 9 | const listSize = body['listSize']; 10 | const searchCondition: SearchCondition = new SearchCondition( 11 | channels, 12 | categories, 13 | announcerGender, 14 | currentPage, 15 | listSize, 16 | ); 17 | return searchCondition; 18 | }; 19 | 20 | export const convertBodyToPaginationCondition = ( 21 | body: object, 22 | ): PaginationCondition => { 23 | const currentPage = body['currentPage']; 24 | const listSize = body['listSize']; 25 | const paginationCondition: PaginationCondition = new PaginationCondition( 26 | currentPage, 27 | listSize, 28 | ); 29 | return paginationCondition; 30 | }; 31 | -------------------------------------------------------------------------------- /src/news/utils/get-last-page.ts: -------------------------------------------------------------------------------- 1 | export const getLastPage = (listSize: number, totalCount: number): number => { 2 | return Math.ceil(totalCount / listSize); 3 | }; 4 | -------------------------------------------------------------------------------- /src/news/utils/sort-by-date-and-title.ts: -------------------------------------------------------------------------------- 1 | export const sortByDateAndTitle = (newsData) => { 2 | const nowFilteringNewsData = newsData; 3 | nowFilteringNewsData.sort((prev, next) => { 4 | if (+new Date(prev.reportDate) == +new Date(next.reportDate)) { 5 | const condition = 6 | '[]{}*!@_.()#^&%-=+01234567989abcdefghijklmnopqrstuvwxyz'; 7 | const prev_condition = condition.indexOf(prev.title[0]); 8 | const next_condition = condition.indexOf(next.title[0]); 9 | 10 | if (prev_condition === next_condition) { 11 | return prev.title < next.title ? -1 : prev.title > next.title ? 1 : 0; 12 | } 13 | return prev_condition - next_condition; 14 | } 15 | 16 | return +new Date(next.reportDate) - +new Date(prev.reportDate); 17 | }); 18 | 19 | return nowFilteringNewsData; 20 | }; 21 | -------------------------------------------------------------------------------- /src/script/common/script-default-name.ts: -------------------------------------------------------------------------------- 1 | export const SCRIPT_DEFAULT_NAME = '스크립트 '; 2 | -------------------------------------------------------------------------------- /src/script/dto/create-memo.dto.ts: -------------------------------------------------------------------------------- 1 | import { Script } from '../entity/script.entity'; 2 | 3 | export class CreateMemoDto { 4 | userId: number; 5 | script: Script; 6 | order: number; 7 | startIndex: number; 8 | keyword: string; 9 | content: string; 10 | highlightId: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/script/dto/create-sentence.dto.ts: -------------------------------------------------------------------------------- 1 | import { Script } from '../entity/script.entity'; 2 | 3 | export class CreateSentenceDto { 4 | script: Script; 5 | order: number; 6 | startTime: number; 7 | endTime: number; 8 | text: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/script/dto/delete-memo.dto.ts: -------------------------------------------------------------------------------- 1 | export class DeleteMemoDto { 2 | userId: number; 3 | scriptId: number; 4 | memoId: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/script/dto/recording.dto.ts: -------------------------------------------------------------------------------- 1 | import { Script } from '../entity/script.entity'; 2 | 3 | export class RecordingDto { 4 | name: string; 5 | link: string; 6 | endTime: number; 7 | isDeleted: boolean; 8 | date: string; 9 | script: Script; 10 | 11 | constructor( 12 | name: string, 13 | link: string, 14 | endTime: number, 15 | isDeleted: boolean, 16 | date: string, 17 | length: number, 18 | ) { 19 | this.name = this.determineName(name, length); 20 | this.link = link; 21 | this.endTime = endTime; 22 | this.isDeleted = isDeleted; 23 | this.date = date; 24 | } 25 | 26 | determineName(nameInput: string, length: number): string { 27 | if (!nameInput) { 28 | return '녹음 ' + length; 29 | } 30 | return nameInput; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/script/dto/return-recording.dto.ts: -------------------------------------------------------------------------------- 1 | import { Recording } from '../entity/recording.entity'; 2 | 3 | export class ReturnRecordingDto { 4 | constructor(recording: Recording) { 5 | this.name = recording.name; 6 | this.link = recording.link; 7 | this.endTime = recording.endTime; 8 | this.isDeleted = recording.isDeleted; 9 | this.date = recording.date; 10 | } 11 | name: string; 12 | link: string; 13 | endTime: number; 14 | isDeleted: boolean; 15 | date: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/script/dto/return-script.dto.collection.ts: -------------------------------------------------------------------------------- 1 | import { ReturnScriptDto } from './return-script.dto'; 2 | 3 | export class ReturnScriptDtoCollection { 4 | constructor(returnScriptDtoList: ReturnScriptDto[]) { 5 | this.returnScriptDtoCollection = returnScriptDtoList; 6 | } 7 | 8 | returnScriptDtoCollection: ReturnScriptDto[] | []; 9 | } 10 | -------------------------------------------------------------------------------- /src/script/dto/return-script.dto.ts: -------------------------------------------------------------------------------- 1 | import { Memo } from '../entity/memo.entity'; 2 | import { Script } from '../entity/script.entity'; 3 | import { Sentence } from '../entity/sentence.entity'; 4 | 5 | export class ReturnScriptDto { 6 | constructor(script: Script) { 7 | this.id = script.id; 8 | this.userId = script.user.id; 9 | this.newsId = script.news.id; 10 | this.name = script.name; 11 | this.sentences = script.sentences; 12 | this.sortMemos(script); 13 | } 14 | id: number; 15 | userId: number; 16 | newsId: number; 17 | name: string; 18 | sentences: Sentence[]; 19 | memos: Memo[]; 20 | 21 | sortMemos(script: Script): void { 22 | const sortingMemos = script.memos; 23 | sortingMemos.sort((prev, next) => { 24 | if (prev.order == next.order) { 25 | return prev.startIndex - next.startIndex; 26 | } 27 | return prev.order - next.order; 28 | }); 29 | this.memos = sortingMemos; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/script/dto/return-sentence.dto.collection.ts: -------------------------------------------------------------------------------- 1 | import { ReturnSentenceDto } from './return-sentence.dto'; 2 | 3 | export class ReturnSentenceDtoCollection { 4 | constructor(returnSentenceDtoList: ReturnSentenceDto[]) { 5 | this.returnSentenceDtoCollection = returnSentenceDtoList; 6 | } 7 | 8 | returnSentenceDtoCollection: ReturnSentenceDto[] | []; 9 | } 10 | -------------------------------------------------------------------------------- /src/script/dto/return-sentence.dto.ts: -------------------------------------------------------------------------------- 1 | import { Sentence } from '../entity/sentence.entity'; 2 | 3 | export class ReturnSentenceDto { 4 | constructor(sentence: Sentence) { 5 | this.id = sentence.id; 6 | this.order = sentence.order; 7 | this.startTime = sentence.startTime; 8 | this.endTime = sentence.endTime; 9 | this.text = sentence.text; 10 | } 11 | id: number; 12 | order: number; 13 | startTime: number; 14 | endTime: number; 15 | text: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/script/dto/update-memo.dto.ts: -------------------------------------------------------------------------------- 1 | export class UpdateMemoDto { 2 | userId: number; 3 | scriptId: number; 4 | memoId: number; 5 | content: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/script/entity/memo.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { Script } from './script.entity'; 9 | 10 | @Entity() 11 | export class Memo extends BaseEntity { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @ManyToOne(() => Script, (script) => script.memos, { 16 | onDelete: 'CASCADE', 17 | }) 18 | script: Script; 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 | @Column({ type: 'varchar', length: 255 }) 33 | highlightId: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/script/entity/recording.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { Script } from './script.entity'; 9 | 10 | @Entity() 11 | export class Recording extends BaseEntity { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @Column({ type: 'varchar', length: 255 }) 16 | name: string; // 굳이 값 객체로 만들 필요는 없을 듯 17 | 18 | @Column({ type: 'varchar', length: 255 }) 19 | link: string; // 추후 URI 값 객체로 만들기 리팩토링 (e.g. aws s3 링크가 아니면 error throw) 20 | 21 | @ManyToOne(() => Script, (script) => script.recordings, { 22 | onDelete: 'CASCADE', 23 | }) 24 | script: Script; 25 | 26 | // seconds로 표기 23s -> 23 1 minute 57 seconds -> 117 27 | @Column({ type: 'int' }) 28 | endTime: number; 29 | 30 | // isdeleted 31 | @Column({ type: 'boolean' }) 32 | isDeleted: boolean; 33 | 34 | @Column({ type: 'varchar', length: 255 }) 35 | date: string; // 추후 Date 값 객체로 만들기 리팩토링 36 | } 37 | -------------------------------------------------------------------------------- /src/script/entity/script-count.entity.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'src/user/user.entity'; 2 | import { 3 | BaseEntity, 4 | Column, 5 | Entity, 6 | ManyToOne, 7 | PrimaryGeneratedColumn, 8 | } from 'typeorm'; 9 | 10 | @Entity() 11 | export class ScriptCount extends BaseEntity { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @ManyToOne(() => User, (user) => user.scriptCounts, { 16 | onDelete: 'CASCADE', 17 | }) 18 | user: User; 19 | 20 | @Column() 21 | newsId: number; 22 | 23 | @Column() 24 | count: number; 25 | } 26 | -------------------------------------------------------------------------------- /src/script/entity/script.entity.ts: -------------------------------------------------------------------------------- 1 | import { News } from 'src/news/news.entity'; 2 | import { User } from 'src/user/user.entity'; 3 | import { 4 | BaseEntity, 5 | Column, 6 | Entity, 7 | ManyToOne, 8 | OneToMany, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | import { Memo } from './memo.entity'; 12 | import { Sentence } from './sentence.entity'; 13 | import { Recording } from './recording.entity'; 14 | 15 | @Entity() 16 | export class Script extends BaseEntity { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column({ type: 'varchar', length: 255 }) 21 | name: string; 22 | 23 | // create json type 24 | @Column({ 25 | type: 'longblob', 26 | nullable: true, 27 | }) 28 | recordingblob: string; 29 | 30 | @ManyToOne(() => User, (user) => user.scripts, { 31 | eager: true, 32 | }) 33 | user: User; 34 | 35 | @ManyToOne(() => News, (news) => news.scripts, { 36 | eager: true, 37 | }) 38 | news: News; 39 | 40 | @OneToMany(() => Sentence, (sentence) => sentence.script, { 41 | eager: true, 42 | }) 43 | sentences: Sentence[]; 44 | 45 | @OneToMany(() => Memo, (memo) => memo.script, { 46 | eager: true, 47 | }) 48 | memos: Memo[]; 49 | 50 | @OneToMany(() => Recording, (recording) => recording.script, { 51 | eager: true, 52 | }) 53 | recordings: Recording[]; 54 | 55 | public addNewRecording = (recording: Recording) => { 56 | if (this.recordings == undefined) { 57 | this.recordings = []; 58 | } 59 | this.recordings.push(recording); 60 | return this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/script/entity/sentence.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { Script } from './script.entity'; 9 | 10 | @Entity() 11 | export class Sentence extends BaseEntity { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @ManyToOne(() => Script, (script) => script.sentences, { 16 | onDelete: 'CASCADE', 17 | }) 18 | script: Script; 19 | 20 | @Column() 21 | order: number; 22 | 23 | @Column({ type: 'float' }) 24 | startTime: number; 25 | 26 | @Column({ type: 'float' }) 27 | endTime: number; 28 | 29 | @Column({ type: 'varchar', length: 5000 }) 30 | text: string; 31 | } 32 | -------------------------------------------------------------------------------- /src/script/repository/memo.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import { CreateMemoDto } from '../dto/create-memo.dto'; 3 | import { UpdateMemoDto } from '../dto/update-memo.dto'; 4 | import { Memo } from '../entity/memo.entity'; 5 | 6 | @EntityRepository(Memo) 7 | export class MemoRepository extends Repository { 8 | async createMemo(createMemoDto: CreateMemoDto): Promise { 9 | const memo: Memo = new Memo(); 10 | memo.script = createMemoDto.script; 11 | memo.order = createMemoDto.order; 12 | memo.startIndex = createMemoDto.startIndex; 13 | memo.keyword = createMemoDto.keyword; 14 | memo.content = createMemoDto.content; 15 | memo.highlightId = createMemoDto.highlightId; 16 | 17 | await memo.save(); 18 | return memo; 19 | } 20 | 21 | async deleteMemo(memoId: number): Promise { 22 | const memoDeleted: Memo = await this.findOneOrFail(memoId); 23 | await this.createQueryBuilder() 24 | .delete() 25 | .from(Memo) 26 | .where('id = :memoId', { memoId }) 27 | .execute(); 28 | return memoDeleted; 29 | } 30 | 31 | async updateMemo(updateMemoDto: UpdateMemoDto): Promise { 32 | const memoId: number = updateMemoDto.memoId; 33 | const content: string = updateMemoDto.content; 34 | const memo: Memo = await this.findOneOrFail(memoId); 35 | memo.content = content; 36 | await memo.save(); 37 | return memo; 38 | } 39 | 40 | async getMemoJoinScript(memoId: number): Promise { 41 | return await this.createQueryBuilder('memo') 42 | .leftJoinAndSelect('memo.script', 'script') 43 | .where('memo.id = :memoId', { memoId: memoId }) 44 | .getOneOrFail(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/script/repository/recording.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | import { Recording } from "../entity/recording.entity"; 3 | import { RecordingDto } from "../dto/recording.dto"; 4 | 5 | @EntityRepository(Recording) 6 | export class RecordingRepository extends Repository { 7 | async createRecording(recordingDto: RecordingDto): Promise { 8 | const recording: Recording = new Recording(); 9 | recording.name = recordingDto.name; 10 | recording.link = recordingDto.link; 11 | recording.endTime = recordingDto.endTime; 12 | recording.isDeleted = recordingDto.isDeleted; 13 | recording.date = recordingDto.date; 14 | recording.script = recordingDto.script; 15 | 16 | await recording.save(); 17 | 18 | console.log( 19 | '>>>>>>> LINK >>>>>>>>>>>>> ', 20 | await this.getRecordingByLink(recording.link), 21 | ); 22 | return recording; 23 | } 24 | 25 | // find by recording using link 26 | async getRecordingByLink(link: string): Promise { 27 | return await this.createQueryBuilder('recording') 28 | .where('recording.link = :link', { link: link }) 29 | .getOneOrFail(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/script/repository/script-count.repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'src/user/user.entity'; 2 | import { EntityRepository, Repository } from 'typeorm'; 3 | import { ScriptCount } from '../entity/script-count.entity'; 4 | 5 | @EntityRepository(ScriptCount) 6 | export class ScriptCountRepository extends Repository { 7 | async createScriptCount(user: User, newsId: number): Promise { 8 | const scriptCount: ScriptCount = new ScriptCount(); 9 | 10 | scriptCount.user = user; 11 | scriptCount.newsId = newsId; 12 | scriptCount.count = 1; 13 | 14 | scriptCount.save(); 15 | return scriptCount; 16 | } 17 | 18 | async getScriptCount(userId: number, newsId: number): Promise { 19 | return await this.createQueryBuilder('scriptCount') 20 | .leftJoinAndSelect('scriptCount.user', 'user') 21 | .where('user.id = :userId', { userId: userId }) 22 | .andWhere('newsId = :newsId', { newsId: newsId }) 23 | .getOne(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/script/repository/script.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 { Script } from '../entity/script.entity'; 5 | import { Recording } from '../entity/recording.entity'; 6 | 7 | @EntityRepository(Script) 8 | export class ScriptRepository extends Repository