diff --git a/apps/backend/package.json b/apps/backend/package.json index d2025e02..e5cafc30 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,91 +1,91 @@ -{ - "name": "backend", - "version": "0.0.1", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" - }, - "lint-staged": { - "*.ts": [ - "eslint --fix", - "prettier --write" - ] - }, - "dependencies": { - "@nestjs/bull": "^10.2.2", - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", - "@nestjs/jwt": "^10.2.0", - "@nestjs/platform-express": "^10.0.0", - "@nestjs/swagger": "^8.0.7", - "@nestjs/typeorm": "^10.0.2", - "bull": "^4.16.4", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", - "dockerode": "^4.0.2", - "dotenv": "^16.4.5", - "mysql2": "^3.11.4", - "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "swagger-ui-express": "^5.0.1", - "tar-stream": "^3.1.7", - "typeorm": "^0.3.20" - }, - "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/config": "^3.3.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", - "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.11", - "@typescript-eslint/parser": "^5.59.11", - "eslint": "^8.42.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-no-relative-import-paths": "^1.5.5", - "eslint-plugin-prettier": "^4.2.1", - "jest": "^29.5.0", - "prettier": "^2.8.8", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" - } -} +{ + "name": "backend", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "lint-staged": { + "*.ts": [ + "eslint --fix", + "prettier --write" + ] + }, + "dependencies": { + "@nestjs/bull": "^10.2.2", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.0.7", + "@nestjs/typeorm": "^10.0.2", + "bull": "^4.16.4", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dockerode": "^4.0.2", + "dotenv": "^16.4.5", + "mysql2": "^3.11.4", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1", + "tar-stream": "^3.1.7", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/config": "^3.3.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^2.0.12", + "@typescript-eslint/eslint-plugin": "^5.59.11", + "@typescript-eslint/parser": "^5.59.11", + "eslint": "^8.42.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-no-relative-import-paths": "^1.5.5", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "prettier": "^2.8.8", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 2981e2f8..d336ec60 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -1,42 +1,42 @@ -import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { AuthModule } from './auth/auth.module'; -import { queueConfig } from './config/queue.config'; -import { typeORMConfig } from './config/typeorm.config'; -import { DockerModule } from './docker/docker.module'; -import { GistModule } from './gist/gist.module'; -import { HistoryModule } from './history/history.module'; -import { LotusModule } from './lotus/lotus.module'; -import { TagModule } from './tag/tag.module'; -import { UserModule } from './user/user.module'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.env' - }), - TypeOrmModule.forRootAsync({ - inject: [ConfigService], - useFactory: typeORMConfig - }), - BullModule.forRootAsync({ - inject: [ConfigService], - useFactory: queueConfig - }), - DockerModule, - GistModule, - HistoryModule, - UserModule, - AuthModule, - LotusModule, - TagModule - ], - controllers: [AppController], - providers: [AppService] -}) -export class AppModule {} +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { AuthModule } from './auth/auth.module'; +import { queueConfig } from './config/queue.config'; +import { typeORMConfig } from './config/typeorm.config'; +import { DockerModule } from './docker/docker.module'; +import { GistModule } from './gist/gist.module'; +import { HistoryModule } from './history/history.module'; +import { LotusModule } from './lotus/lotus.module'; +import { TagModule } from './tag/tag.module'; +import { UserModule } from './user/user.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: '.env' + }), + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: typeORMConfig + }), + BullModule.forRootAsync({ + inject: [ConfigService], + useFactory: queueConfig + }), + DockerModule, + GistModule, + HistoryModule, + UserModule, + AuthModule, + LotusModule, + TagModule + ], + controllers: [AppController], + providers: [AppService] +}) +export class AppModule {} diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 8daf9c2e..708166a5 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,20 +1,20 @@ -import { Global, Module, forwardRef } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { AuthService } from './auth.service'; -import { UserModule } from '@/user/user.module'; - -@Global() -@Module({ - imports: [ - JwtModule.register({ - signOptions: { - algorithm: 'HS256', - expiresIn: '1h' - } - }), - forwardRef(() => UserModule) - ], - providers: [AuthService], - exports: [AuthService] -}) -export class AuthModule {} +import { Global, Module, forwardRef } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { AuthService } from './auth.service'; +import { UserModule } from '@/user/user.module'; + +@Global() +@Module({ + imports: [ + JwtModule.register({ + signOptions: { + algorithm: 'HS256', + expiresIn: '1h' + } + }), + forwardRef(() => UserModule) + ], + providers: [AuthService], + exports: [AuthService] +}) +export class AuthModule {} diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index d1defa14..06949828 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -1,58 +1,58 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; -import { isString } from 'class-validator'; -import { Request } from 'express'; -import { UserService } from '@/user/user.service'; - -@Injectable() -export class AuthService { - constructor( - private readonly userService: UserService, - private configService: ConfigService, - private jwtService: JwtService - ) {} - - private JWT_SECRET_KEY = this.configService.get('JWT_SECRET_KEY'); - createJwt(userId: string): string { - const payload = { userId }; - return this.jwtService.sign(payload, { - secret: this.JWT_SECRET_KEY - }); - } - - verifyJwt(token: string): string { - try { - if (!token) { - throw new Error('no token'); - } - const decoded = this.jwtService.verify(token, { - secret: this.JWT_SECRET_KEY - }); - if (!decoded.userId) throw new Error('invalid token'); - return decoded.userId; - } catch (e) { - if (e.name === 'TokenExpiredError') throw new HttpException('token expired', HttpStatus.UNAUTHORIZED); - else if (e.message === 'no token') throw new HttpException('token is not found', HttpStatus.UNAUTHORIZED); - else { - throw new HttpException('invalid token', HttpStatus.UNAUTHORIZED); - } - } - } - - getIdFromRequest(req: Request): string { - const auth = req.header('Authorization'); - if (!auth) { - throw new HttpException('token is not found', HttpStatus.UNAUTHORIZED); - } - const token = req.header('Authorization').split(' ')[1].trim(); - if (!isString(token)) { - throw new HttpException('invalid token', HttpStatus.UNAUTHORIZED); - } - return this.verifyJwt(token); - } - - async getUserGitToken(userId: string): Promise { - return await this.userService.findUserGistToken(userId); - } -} +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { isString } from 'class-validator'; +import { Request } from 'express'; +import { UserService } from '@/user/user.service'; + +@Injectable() +export class AuthService { + constructor( + private readonly userService: UserService, + private configService: ConfigService, + private jwtService: JwtService + ) {} + + private JWT_SECRET_KEY = this.configService.get('JWT_SECRET_KEY'); + createJwt(userId: string): string { + const payload = { userId }; + return this.jwtService.sign(payload, { + secret: this.JWT_SECRET_KEY + }); + } + + verifyJwt(token: string): string { + try { + if (!token) { + throw new Error('no token'); + } + const decoded = this.jwtService.verify(token, { + secret: this.JWT_SECRET_KEY + }); + if (!decoded.userId) throw new Error('invalid token'); + return decoded.userId; + } catch (e) { + if (e.name === 'TokenExpiredError') throw new HttpException('token expired', HttpStatus.UNAUTHORIZED); + else if (e.message === 'no token') throw new HttpException('token is not found', HttpStatus.UNAUTHORIZED); + else { + throw new HttpException('invalid token', HttpStatus.UNAUTHORIZED); + } + } + } + + getIdFromRequest(req: Request): string { + const auth = req.header('Authorization'); + if (!auth) { + throw new HttpException('token is not found', HttpStatus.UNAUTHORIZED); + } + const token = req.header('Authorization').split(' ')[1].trim(); + if (!isString(token)) { + throw new HttpException('invalid token', HttpStatus.UNAUTHORIZED); + } + return this.verifyJwt(token); + } + + async getUserGitToken(userId: string): Promise { + return await this.userService.findUserGistToken(userId); + } +} diff --git a/apps/backend/src/comment/comment.entity.ts b/apps/backend/src/comment/comment.entity.ts index fca54cd0..49cb72e2 100644 --- a/apps/backend/src/comment/comment.entity.ts +++ b/apps/backend/src/comment/comment.entity.ts @@ -17,11 +17,11 @@ export class Comment { @CreateDateColumn({ name: 'created_at' }) createdAt: Date; - @ManyToOne(() => User, (user) => user.comments) + @ManyToOne(() => User, (user) => user.comments, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) // 외래키 이름 설정 user: User; - @ManyToOne(() => Lotus, (lotus) => lotus.comments) + @ManyToOne(() => Lotus, (lotus) => lotus.comments, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'lotus_id' }) // 외래키 이름 설정 lotus: Lotus; } diff --git a/apps/backend/src/config/queue.config.ts b/apps/backend/src/config/queue.config.ts index 95b4d79a..4b515e0d 100644 --- a/apps/backend/src/config/queue.config.ts +++ b/apps/backend/src/config/queue.config.ts @@ -1,9 +1,10 @@ -import { BullRootModuleOptions } from '@nestjs/bull'; -import { ConfigService } from '@nestjs/config'; - -export const queueConfig = (configService: ConfigService): BullRootModuleOptions => ({ - redis: { - host: configService.get('REDIS_HOST', { infer: true }), - port: configService.get('REDIS_PORT', { infer: true }) - } -}); +import { BullRootModuleOptions } from '@nestjs/bull'; +import { ConfigService } from '@nestjs/config'; + +export const queueConfig = (configService: ConfigService): BullRootModuleOptions => ({ + redis: { + host: configService.get('REDIS_HOST', { infer: true }), + port: configService.get('REDIS_PORT', { infer: true }), + password: configService.get('REDIS_PASSWORD', { infer: true }) + } +}); diff --git a/apps/backend/src/config/typeorm.config.ts b/apps/backend/src/config/typeorm.config.ts index d6962a4b..0e9fab48 100644 --- a/apps/backend/src/config/typeorm.config.ts +++ b/apps/backend/src/config/typeorm.config.ts @@ -1,18 +1,19 @@ -import { ConfigService } from '@nestjs/config'; -import { TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { Comment } from '@/comment/comment.entity'; -import { History } from '@/history/history.entity'; -import { Lotus } from '@/lotus/lotus.entity'; -import { Tag } from '@/tag/tag.entity'; -import { User } from '@/user/user.entity'; - -export const typeORMConfig = async (configService: ConfigService): Promise => ({ - type: 'mysql', - host: configService.get('MYSQL_HOST'), - port: configService.get('MYSQL_PORT'), - username: configService.get('MYSQL_USER'), - password: configService.get('MYSQL_PASSWORD'), - database: configService.get('MYSQL_DATABASE'), - entities: [User, Lotus, Comment, Tag, History], - synchronize: true //todo: env로 release에서는 false가 되도록 해야함 -}); +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { Comment } from '@/comment/comment.entity'; +import { History } from '@/history/history.entity'; +import { Lotus } from '@/lotus/lotus.entity'; +import { LotusTag } from '@/relation/lotus.tag.entity'; +import { Tag } from '@/tag/tag.entity'; +import { User } from '@/user/user.entity'; + +export const typeORMConfig = async (configService: ConfigService): Promise => ({ + type: 'mysql', + host: configService.get('MYSQL_HOST'), + port: configService.get('MYSQL_PORT'), + username: configService.get('MYSQL_USER'), + password: configService.get('MYSQL_PASSWORD'), + database: configService.get('MYSQL_DATABASE'), + entities: [User, Lotus, Comment, Tag, History, LotusTag] + //synchronize: true //todo: env로 release에서는 false가 되도록 해야함 +}); diff --git a/apps/backend/src/constants/constants.ts b/apps/backend/src/constants/constants.ts index b3b274dd..7935b8db 100644 --- a/apps/backend/src/constants/constants.ts +++ b/apps/backend/src/constants/constants.ts @@ -1,19 +1,19 @@ -export const GIST_AUTH_HEADER = (gitToken: string = null) => { - return { - Accept: 'application/vnd.github+json', - Authorization: `Bearer ${gitToken}`, - 'X-GitHub-Api-Version': '2022-11-28', - 'Content-Type': 'application/json' - }; -}; -export enum HISTORY_STATUS { - PENDING = 'PENDING', - ERROR = 'ERROR', - SUCCESS = 'SUCCESS' -} - -export enum SUPPORTED_LANGUAGES { - JS = '.js' -} - -export const MAX_CONTAINER_CNT = 6; +export const GIST_AUTH_HEADER = (gitToken: string = null) => { + return { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${gitToken}`, + 'X-GitHub-Api-Version': '2022-11-28', + 'Content-Type': 'application/json' + }; +}; +export enum HISTORY_STATUS { + PENDING = 'PENDING', + ERROR = 'ERROR', + SUCCESS = 'SUCCESS' +} + +export enum SUPPORTED_LANGUAGES { + JS = '.js' +} + +export const MAX_CONTAINER_CNT = 6; diff --git a/apps/backend/src/docker/docker.consumer.ts b/apps/backend/src/docker/docker.consumer.ts index c1f03864..d0c73209 100644 --- a/apps/backend/src/docker/docker.consumer.ts +++ b/apps/backend/src/docker/docker.consumer.ts @@ -1,205 +1,205 @@ -import { Process, Processor } from '@nestjs/bull'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { Job } from 'bull'; -import { Container } from 'dockerode'; -import * as tar from 'tar-stream'; -import { DockerContainerPool } from './docker.pool'; -import { MAX_CONTAINER_CNT } from '@/constants/constants'; -import { GistApiFileDto } from '@/gist/dto/gistApiFile.dto'; -import { GistApiFileListDto } from '@/gist/dto/gistApiFileList.dto'; -import { GistService } from '@/gist/gist.service'; - -interface GistFileAttributes { - filename: string; - type: string; - language: string; - raw_url: string; - size: number; - truncated?: boolean; - content?: string; -} - -interface GistFile { - filename: string; - attr: GistFileAttributes; -} - -@Processor('docker-queue') -@Injectable() -export class DockerConsumer { - constructor(private gistService: GistService, private dockerContainerPool: DockerContainerPool) {} - - @Process({ name: 'docker-run', concurrency: MAX_CONTAINER_CNT }) - async handleDockerRun(job: Job) { - const { gitToken, gistId, commitId, mainFileName, inputs } = job.data; - let container; - try { - container = await this.dockerContainerPool.getContainer(); - await container.start(); - const result = await this.runGistFiles(container, gitToken, gistId, commitId, mainFileName, inputs); - return result; - } catch (error) { - throw new Error(`Execution failed: ${error.message}`); - } finally { - await this.dockerContainerPool.returnContainer(container); - } - } - async runGistFiles( - container: Container, - gitToken: string, - gistId: string, - commitId: string, - mainFileName: string, - inputs: any[] - ): Promise { - const gistData: GistApiFileListDto = await this.gistService.getCommit(gistId, commitId, gitToken); - const files: GistApiFileDto[] = gistData.files; - if (!files || !files.some((file) => file.fileName === mainFileName)) { - throw new HttpException('execFile is not found', HttpStatus.NOT_FOUND); - } - //desciption: 컨테이너 시작 - const tarBuffer = await this.parseTarBuffer(files); - - //desciption: tarBuffer를 Docker 컨테이너에 업로드 - await container.putArchive(tarBuffer, { path: '/tmp' }); - if (files.some((file) => file.fileName === 'package.json')) { - await this.packageInstall(container); - } - const stream = await this.dockerExcution(inputs, mainFileName, container); - let output = ''; - const timeout = setTimeout(async () => { - console.log('timeout'); - stream.destroy(new Error('Timeout')); - }, 5000); - //desciption: 스트림 종료 후 결과 반환 - return new Promise((resolve, reject) => { - //desciption: 스트림에서 데이터 수집 - stream.on('data', (chunk) => { - output += chunk.toString(); - }); - stream.on('close', async () => { - let result = await this.filterAnsiCode(output); - clearTimeout(timeout); - if (inputs.length !== 0) { - result = result.split('\n').slice(1).join('\n'); - } - this.initWorkDir(container); - resolve(result); - }); - stream.on('error', reject); - }); - } - - async fetchGistFiles(gitToken: string, gistId: string): Promise<{ name: string; content: string }[]> { - try { - const response = await fetch(`https://api.github.com/gists/${gistId}`, { - headers: { - Authorization: `Bearer ${gitToken}` - }, - method: 'GET' - }); - const json = await response.json(); - const files: GistFile = json.files; - - const fileData: { name: string; content: string }[] = []; - for (const [fileName, file] of Object.entries(files)) { - fileData.push({ name: fileName, content: file.content }); - } - return fileData; - } catch (error) { - throw new Error('Failed to fetch Gist files'); - } - } - - async parseTarBuffer(files: GistApiFileDto[]): Promise { - //desciption: tar 아카이브를 생성 - return new Promise((resolve, reject) => { - const pack = tar.pack(); - - for (const file of files) { - //desciption: 파일 이름과 내용을 tar 아카이브에 추가 - pack.entry({ name: file.fileName }, file.content, (err) => { - if (err) reject(err); - }); - } - - //desciption: 아카이브 완료 - pack.finalize(); - - //desciption: Buffer로 변환 - const buffers: Buffer[] = []; - pack.on('data', (data) => buffers.push(data)); - pack.on('end', () => resolve(Buffer.concat(buffers))); - pack.on('error', reject); - }); - } - async dockerExcution(inputs: any[], mainFileName: string, container: Container) { - const exec = await container.exec({ - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Tty: inputs.length !== 0, //true - Cmd: ['node', mainFileName], - workingDir: '/tmp' - }); - //todo: 입력값이 없으면 스킵 - const stream = await exec.start({ hijack: true, stdin: true }); - for (const input of inputs) { - await stream.write(input + '\n'); - await this.delay(100); //각 입력 term - } - // stream.end(); - return stream; - } - - async packageInstall(container: Container): Promise { - const exec = await container.exec({ - AttachStdin: false, - AttachStdout: true, - AttachStderr: true, - Cmd: ['npm', 'install'], - workingDir: '/tmp' - }); - - const stream = await exec.start(); - return new Promise((resolve, reject) => { - stream.on('data', (chunk) => { - const c = chunk; - }); - stream.on('end', resolve); - stream.on('error', reject); - }); - } - - async initWorkDir(container: Container): Promise { - try { - const exec = await container.exec({ - AttachStdin: false, - AttachStdout: true, - AttachStderr: true, - Cmd: ['rm', '-rf', '/tmp/*'] - }); - const stream = await exec.start(); - return new Promise((resolve, reject) => { - stream.on('data', (chunk) => { - const c = chunk; - }); - stream.on('end', resolve); - stream.on('error', reject); - }); - } catch (error) { - console.log(error.message); - throw new Error('container tmp init failed'); - } - } - - filterAnsiCode(output: string): string { - return output - .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\r]/g, '') - .replaceAll('\n)', '\n') - .trim(); - } - delay(ms = 1000) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} +import { Process, Processor } from '@nestjs/bull'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { Job } from 'bull'; +import { Container } from 'dockerode'; +import * as tar from 'tar-stream'; +import { DockerContainerPool } from './docker.pool'; +import { MAX_CONTAINER_CNT } from '@/constants/constants'; +import { GistApiFileDto } from '@/gist/dto/gistApiFile.dto'; +import { GistApiFileListDto } from '@/gist/dto/gistApiFileList.dto'; +import { GistService } from '@/gist/gist.service'; + +interface GistFileAttributes { + filename: string; + type: string; + language: string; + raw_url: string; + size: number; + truncated?: boolean; + content?: string; +} + +interface GistFile { + filename: string; + attr: GistFileAttributes; +} + +@Processor('docker-queue') +@Injectable() +export class DockerConsumer { + constructor(private gistService: GistService, private dockerContainerPool: DockerContainerPool) {} + + @Process({ name: 'docker-run', concurrency: MAX_CONTAINER_CNT }) + async handleDockerRun(job: Job) { + const { gitToken, gistId, commitId, mainFileName, inputs } = job.data; + let container; + try { + container = await this.dockerContainerPool.getContainer(); + await container.start(); + const result = await this.runGistFiles(container, gitToken, gistId, commitId, mainFileName, inputs); + return result; + } catch (error) { + throw new Error(`Execution failed: ${error.message}`); + } finally { + await this.dockerContainerPool.returnContainer(container); + } + } + async runGistFiles( + container: Container, + gitToken: string, + gistId: string, + commitId: string, + mainFileName: string, + inputs: any[] + ): Promise { + const gistData: GistApiFileListDto = await this.gistService.getCommit(gistId, commitId, gitToken); + const files: GistApiFileDto[] = gistData.files; + if (!files || !files.some((file) => file.fileName === mainFileName)) { + throw new HttpException('execFile is not found', HttpStatus.NOT_FOUND); + } + //desciption: 컨테이너 시작 + const tarBuffer = await this.parseTarBuffer(files); + + //desciption: tarBuffer를 Docker 컨테이너에 업로드 + await container.putArchive(tarBuffer, { path: '/tmp' }); + if (files.some((file) => file.fileName === 'package.json')) { + await this.packageInstall(container); + } + const stream = await this.dockerExcution(inputs, mainFileName, container); + let output = ''; + const timeout = setTimeout(async () => { + console.log('timeout'); + stream.destroy(new Error('Timeout')); + }, 5000); + //desciption: 스트림 종료 후 결과 반환 + return new Promise((resolve, reject) => { + //desciption: 스트림에서 데이터 수집 + stream.on('data', (chunk) => { + output += chunk.toString(); + }); + stream.on('close', async () => { + let result = await this.filterAnsiCode(output); + clearTimeout(timeout); + if (inputs.length !== 0) { + result = result.split('\n').slice(1).join('\n'); + } + this.initWorkDir(container); + resolve(result); + }); + stream.on('error', reject); + }); + } + + async fetchGistFiles(gitToken: string, gistId: string): Promise<{ name: string; content: string }[]> { + try { + const response = await fetch(`https://api.github.com/gists/${gistId}`, { + headers: { + Authorization: `Bearer ${gitToken}` + }, + method: 'GET' + }); + const json = await response.json(); + const files: GistFile = json.files; + + const fileData: { name: string; content: string }[] = []; + for (const [fileName, file] of Object.entries(files)) { + fileData.push({ name: fileName, content: file.content }); + } + return fileData; + } catch (error) { + throw new Error('Failed to fetch Gist files'); + } + } + + async parseTarBuffer(files: GistApiFileDto[]): Promise { + //desciption: tar 아카이브를 생성 + return new Promise((resolve, reject) => { + const pack = tar.pack(); + + for (const file of files) { + //desciption: 파일 이름과 내용을 tar 아카이브에 추가 + pack.entry({ name: file.fileName }, file.content, (err) => { + if (err) reject(err); + }); + } + + //desciption: 아카이브 완료 + pack.finalize(); + + //desciption: Buffer로 변환 + const buffers: Buffer[] = []; + pack.on('data', (data) => buffers.push(data)); + pack.on('end', () => resolve(Buffer.concat(buffers))); + pack.on('error', reject); + }); + } + async dockerExcution(inputs: any[], mainFileName: string, container: Container) { + const exec = await container.exec({ + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: inputs.length !== 0, //true + Cmd: ['node', mainFileName], + workingDir: '/tmp' + }); + //todo: 입력값이 없으면 스킵 + const stream = await exec.start({ hijack: true, stdin: true }); + for (const input of inputs) { + await stream.write(input + '\n'); + await this.delay(100); //각 입력 term + } + // stream.end(); + return stream; + } + + async packageInstall(container: Container): Promise { + const exec = await container.exec({ + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + Cmd: ['npm', 'install'], + workingDir: '/tmp' + }); + + const stream = await exec.start(); + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => { + const c = chunk; + }); + stream.on('end', resolve); + stream.on('error', reject); + }); + } + + async initWorkDir(container: Container): Promise { + try { + const exec = await container.exec({ + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + Cmd: ['rm', '-rf', '/tmp/*'] + }); + const stream = await exec.start(); + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => { + const c = chunk; + }); + stream.on('end', resolve); + stream.on('error', reject); + }); + } catch (error) { + console.log(error.message); + throw new Error('container tmp init failed'); + } + } + + filterAnsiCode(output: string): string { + return output + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\r]/g, '') + .replaceAll('\n)', '\n') + .trim(); + } + delay(ms = 1000) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/apps/backend/src/docker/docker.controller.ts b/apps/backend/src/docker/docker.controller.ts index e7486d32..124770cc 100644 --- a/apps/backend/src/docker/docker.controller.ts +++ b/apps/backend/src/docker/docker.controller.ts @@ -1,46 +1,46 @@ -import { Controller, Get, Param } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { DockerProducer } from './docker.producer.js'; - -@Controller('docker') -export class DockerController { - constructor(private readonly dockerProducer: DockerProducer, private configService: ConfigService) {} - - @Get('get') - async getDockersTest(): Promise { - const mainFileName = 'FunctionDivide.js'; - // const gitToken = this.configService.get('STATIC_GIST_ID'); - const gistId = this.configService.get('DYNAMIC_GIST_ID'); - const gitToken = this.configService.get('GIT_TOKEN'); - const inputs = ['1 1 1 1', '1 1 1 1', '1 1 1 1', '1 1 1 1']; - const commit = '654dd3f1d7f17d172132aebae283e73356197d18'; - console.log('docker Test'); - const value = await this.dockerProducer.getDocker( - gitToken, - '25cf4713b2386b4ad4ce7c8dbbecebe8', - 'e717102aefed1f1f8b27b63eb7f46ce1f1516c86', - 'main.js', - inputs - ); - return value; - } - - @Get('get2') - async getDockersTest2(): Promise { - const mainFileName = 'FunctionDivide.js'; - // const gitToken = this.configService.get('STATIC_GIST_ID'); - const gistId = this.configService.get('DYNAMIC_GIST_ID'); - const gitToken = this.configService.get('GIT_TOKEN'); - const inputs = ['1 1 1 1', '1 1 1 1', '1 1 1 1', '1 1 1 1']; - const commit = '654dd3f1d7f17d172132aebae283e73356197d18'; - console.log('docker Test2'); - const value = await this.dockerProducer.getDocker( - gitToken, - '7f93da28e2522409a2274eff51b5dc20', - '57944932d1ec6f05415b5e067f23c8a358e79d84', - 'main.js', - inputs - ); - return value; - } -} +import { Controller, Get, Param } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DockerProducer } from './docker.producer.js'; + +@Controller('docker') +export class DockerController { + constructor(private readonly dockerProducer: DockerProducer, private configService: ConfigService) {} + + @Get('get') + async getDockersTest(): Promise { + const mainFileName = 'FunctionDivide.js'; + // const gitToken = this.configService.get('STATIC_GIST_ID'); + const gistId = this.configService.get('DYNAMIC_GIST_ID'); + const gitToken = this.configService.get('GIT_TOKEN'); + const inputs = ['1 1 1 1', '1 1 1 1', '1 1 1 1', '1 1 1 1']; + const commit = '654dd3f1d7f17d172132aebae283e73356197d18'; + console.log('docker Test'); + const value = await this.dockerProducer.getDocker( + gitToken, + '25cf4713b2386b4ad4ce7c8dbbecebe8', + 'e717102aefed1f1f8b27b63eb7f46ce1f1516c86', + 'main.js', + inputs + ); + return value; + } + + @Get('get2') + async getDockersTest2(): Promise { + const mainFileName = 'FunctionDivide.js'; + // const gitToken = this.configService.get('STATIC_GIST_ID'); + const gistId = this.configService.get('DYNAMIC_GIST_ID'); + const gitToken = this.configService.get('GIT_TOKEN'); + const inputs = ['1 1 1 1', '1 1 1 1', '1 1 1 1', '1 1 1 1']; + const commit = '654dd3f1d7f17d172132aebae283e73356197d18'; + console.log('docker Test2'); + const value = await this.dockerProducer.getDocker( + gitToken, + '7f93da28e2522409a2274eff51b5dc20', + '57944932d1ec6f05415b5e067f23c8a358e79d84', + 'main.js', + inputs + ); + return value; + } +} diff --git a/apps/backend/src/docker/docker.module.ts b/apps/backend/src/docker/docker.module.ts index 1fe90d21..3e64cbd5 100644 --- a/apps/backend/src/docker/docker.module.ts +++ b/apps/backend/src/docker/docker.module.ts @@ -1,26 +1,26 @@ -import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; -import { DockerConsumer } from './docker.consumer.js'; -import { DockerController } from './docker.controller.js'; -import { DockerContainerPool } from './docker.pool.js'; -import { DockerProducer } from './docker.producer.js'; -import { GistModule } from '@/gist/gist.module'; - -@Module({ - imports: [ - GistModule, - BullModule.forRoot({ - redis: { - host: '211.188.48.24', // Redis 호스트 주소 - port: 6379 // Redis 포트 - } - }), - BullModule.registerQueue({ - name: 'docker-queue' // 큐 이름 - }) - ], - controllers: [DockerController], - providers: [DockerProducer, DockerConsumer, DockerContainerPool], - exports: [DockerProducer] -}) -export class DockerModule {} +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; +import { DockerConsumer } from './docker.consumer.js'; +import { DockerController } from './docker.controller.js'; +import { DockerContainerPool } from './docker.pool.js'; +import { DockerProducer } from './docker.producer.js'; +import { GistModule } from '@/gist/gist.module'; + +@Module({ + imports: [ + GistModule, + BullModule.forRoot({ + redis: { + host: '211.188.48.24', // Redis 호스트 주소 + port: 6379 // Redis 포트 + } + }), + BullModule.registerQueue({ + name: 'docker-queue' // 큐 이름 + }) + ], + controllers: [DockerController], + providers: [DockerProducer, DockerConsumer, DockerContainerPool], + exports: [DockerProducer] +}) +export class DockerModule {} diff --git a/apps/backend/src/docker/docker.pool.ts b/apps/backend/src/docker/docker.pool.ts index 781a6170..d9efc268 100644 --- a/apps/backend/src/docker/docker.pool.ts +++ b/apps/backend/src/docker/docker.pool.ts @@ -1,50 +1,50 @@ -import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; -import * as Docker from 'dockerode'; -import { Container } from 'dockerode'; -import { MAX_CONTAINER_CNT } from '@/constants/constants'; - -@Injectable() -export class DockerContainerPool implements OnApplicationBootstrap { - docker = new Docker(); - pool: Container[] = []; - lock = false; - async onApplicationBootstrap() { - await this.createContainer(); - } - - async createContainer() { - for (let i = 0; i < MAX_CONTAINER_CNT; i++) { - const container = await this.docker.createContainer({ - Image: 'node:latest', - Tty: false, - OpenStdin: true, - AttachStdout: true, - AttachStderr: true, - Env: [ - 'NODE_DISABLE_COLORS=true', // 색상 비활성화 - 'TERM=dumb' // dumb 터미널로 설정하여 색상 비활성화 - ] - }); - this.pool.push(container); - } - } - - async getContainer(): Container | null { - while (this.lock || this.pool.length === 0) { - await this.delay(10); // 풀 비어 있음 처리 - } - this.lock = true; - const container = this.pool.pop(); - this.lock = false; - return container; - } - - async returnContainer(container: Container) { - await container.stop(); - this.pool.push(container); - } - - delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} +import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; +import * as Docker from 'dockerode'; +import { Container } from 'dockerode'; +import { MAX_CONTAINER_CNT } from '@/constants/constants'; + +@Injectable() +export class DockerContainerPool implements OnApplicationBootstrap { + docker = new Docker(); + pool: Container[] = []; + lock = false; + async onApplicationBootstrap() { + await this.createContainer(); + } + + async createContainer() { + for (let i = 0; i < MAX_CONTAINER_CNT; i++) { + const container = await this.docker.createContainer({ + Image: 'node:latest', + Tty: false, + OpenStdin: true, + AttachStdout: true, + AttachStderr: true, + Env: [ + 'NODE_DISABLE_COLORS=true', // 색상 비활성화 + 'TERM=dumb' // dumb 터미널로 설정하여 색상 비활성화 + ] + }); + this.pool.push(container); + } + } + + async getContainer(): Container | null { + while (this.lock || this.pool.length === 0) { + await this.delay(10); // 풀 비어 있음 처리 + } + this.lock = true; + const container = this.pool.pop(); + this.lock = false; + return container; + } + + async returnContainer(container: Container) { + await container.stop(); + this.pool.push(container); + } + + delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/apps/backend/src/docker/docker.producer.ts b/apps/backend/src/docker/docker.producer.ts index 9348c0a0..bc0194ab 100644 --- a/apps/backend/src/docker/docker.producer.ts +++ b/apps/backend/src/docker/docker.producer.ts @@ -1,41 +1,41 @@ -import { InjectQueue } from '@nestjs/bull'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { DockerContainerPool } from './docker.pool'; - -@Injectable() -export class DockerProducer { - constructor( - @InjectQueue('docker-queue') - private readonly dockerQueue, - private dockerContainerPool: DockerContainerPool - ) {} - - async getDocker( - gitToken: string, - gistId: string, - commitId: string, - mainFileName: string, - inputs: any[] - ): Promise { - const job = await this.dockerQueue.add( - 'docker-run', - { - gitToken, - gistId, - commitId, - mainFileName, - inputs - }, - { removeOnComplete: true, removeOnFail: true } - ); - // Job 완료 대기 및 결과 반환 - return new Promise((resolve, reject) => { - job - .finished() - .then((result) => { - resolve(result); - }) - .catch((error) => reject(error)); - }); - } -} +import { InjectQueue } from '@nestjs/bull'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DockerContainerPool } from './docker.pool'; + +@Injectable() +export class DockerProducer { + constructor( + @InjectQueue('docker-queue') + private readonly dockerQueue, + private dockerContainerPool: DockerContainerPool + ) {} + + async getDocker( + gitToken: string, + gistId: string, + commitId: string, + mainFileName: string, + inputs: any[] + ): Promise { + const job = await this.dockerQueue.add( + 'docker-run', + { + gitToken, + gistId, + commitId, + mainFileName, + inputs + }, + { removeOnComplete: true, removeOnFail: true } + ); + // Job 완료 대기 및 결과 반환 + return new Promise((resolve, reject) => { + job + .finished() + .then((result) => { + resolve(result); + }) + .catch((error) => reject(error)); + }); + } +} diff --git a/apps/backend/src/gist/gist.service.ts b/apps/backend/src/gist/gist.service.ts index 65cbbe28..ae6707cc 100644 --- a/apps/backend/src/gist/gist.service.ts +++ b/apps/backend/src/gist/gist.service.ts @@ -1,188 +1,188 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CommentDto } from './dto/comment.dto'; -import { CommitDto } from './dto/gist.commit.dto'; -import { GistApiFileDto } from './dto/gistApiFile.dto'; -import { GistApiFileListDto } from './dto/gistApiFileList.dto'; -import { ResponseAllGistsDto } from './dto/response.allGists.dto'; -import { ResponseGistDto } from './dto/response.gist.dto'; -import { UserDto } from './dto/user.dto'; -import { GIST_AUTH_HEADER } from '@/constants/constants'; - -@Injectable() -export class GistService { - gitBaseUrl: string; - constructor() { - this.gitBaseUrl = 'https://api.github.com/'; - } - async getGistList(gitToken: string, page: number, perPage: number): Promise { - let hasNextPage = true; - const currentGistPage = await this.gistPageData(gitToken, page, perPage); - const nextGistPage = await this.gistPageData(gitToken, page + 1, perPage); - - if (nextGistPage.length === 0) { - hasNextPage = false; - } - - const gists = currentGistPage.map((gist) => { - return ResponseGistDto.of(gist); - }); - return ResponseAllGistsDto.of(gists, page, hasNextPage); - } - - async gistPageData(gitToken: string, page: number, per_page: number): Promise { - const params = { - page: page.toString(), - per_page: per_page.toString() - }; - const queryParam = new URLSearchParams(params).toString(); - const gistsData = await this.gistReq('GET', `${this.gitBaseUrl}gists`, gitToken, queryParam); - return await gistsData.json(); - } - - async getGistById(id: string, gitToken: string): Promise { - let response = null; - try { - response = await this.gistReq('GET', `${this.gitBaseUrl}gists/${id}`, gitToken); - } catch (e) { - throw new HttpException('gistId is not exist', HttpStatus.NOT_FOUND); - } - const data = await response.json(); - - const fileArr: GistApiFileDto[] = await this.parseGistApiFiles(data, gitToken); - return GistApiFileListDto.of(data, fileArr); - } - - async getMostRecentGistInUser(gitToken: string): Promise { - const params = { - page: '1', - per_page: '1' - }; - const queryParam = new URLSearchParams(params).toString(); - const response = await this.gistReq('GET', `${this.gitBaseUrl}gists`, gitToken, queryParam); - if (!response.length) { - throw new Error('404'); - } - const mostRecentData = response[0]; - - const fileArr: GistApiFileDto[] = await this.parseGistApiFiles(mostRecentData, gitToken); - return GistApiFileListDto.of(mostRecentData, fileArr); - } - - async getCommitsForAGist(gist_id: string, pageIdx = 1, gitToken: string): Promise { - const page = pageIdx; - const perPage = 5; - const params = { - page: page.toString(), - per_page: perPage.toString() - }; - const queryParam = new URLSearchParams(params).toString(); - const response = await this.gistReq('GET', `${this.gitBaseUrl}gists/${gist_id}/commits`, gitToken, queryParam); - const data = await response.json(); - const commits: CommitDto[] = data.map((history) => CommitDto.of(history)); - return commits; - } - - async getCommit(gist_id: string, commit_id: string, gitToken: string) { - const response = await this.getFilesFromCommit(this.getCommitUrl(gist_id, commit_id), gitToken); - return response; - } - - getCommitUrl(gist_id: string, commit_id: string) { - return `https://api.github.com/gists/${gist_id}/${commit_id}`; - } - - async getFilesFromCommit(commit_url: string, gittoken: string) { - const data = await this.getFileContent(commit_url, gittoken); - const dataJson = JSON.parse(data); - const fileArr: GistApiFileDto[] = await this.parseGistApiFiles(dataJson, gittoken); - return GistApiFileListDto.of(dataJson, fileArr); - } - - async getUserData(gitToken: string): Promise { - const response = await this.gistReq('GET', `${this.gitBaseUrl}user`, gitToken); - const userData = await response.json(); - if (!userData.id || !userData.avatar_url || !userData.login) { - throw new Error('404'); - } - const result: UserDto = UserDto.of(userData); - return result; - } - async getFileContent(raw_url: string, gittoken: string) { - const header = {}; - if (gittoken) { - header['Authorization'] = `Bearer ${gittoken}`; - } - const response = await fetch(raw_url, { - headers: header - }); - if (!response.ok) { - throw new Error('404'); - } - const data = await response.text(); - return data; - } - - async getComments(gitToken: string, gist_id: string): Promise { - const data = await this.gistReq('GET', `${this.gitBaseUrl}gists/${gist_id}/comments`, gitToken); - const comments: CommentDto[] = data.map((comment) => CommentDto.of(comment)); - return comments; - } - - async createComments(gitToken: string, gist_id: string, detail: string): Promise { - const response = await this.gistReq('POST', `${this.gitBaseUrl}gists/${gist_id}/comments`, gitToken, '', detail); - - const data = await response.json(); - const comment: CommentDto = CommentDto.of(data); - return comment; - } - - async updateComment(gitToken: string, gist_id: string, comment_id: string, detail: string): Promise { - const response = await this.gistReq( - 'PATCH', - `${this.gitBaseUrl}gists/${gist_id}/comments/${comment_id}`, - gitToken, - '', - detail - ); - const data = await response.json(); - return true; - } - - async deleteComment(gitToken: string, gist_id: string, comment_id: string): Promise { - const data = await this.gistReq('DELETE', `${this.gitBaseUrl}gists/${gist_id}/comments/${comment_id}`, gitToken); - return true; - } - - async gistReq( - method: string, - commend: string, - gitToken: string = null, - queryParam = '', - body: any = null - ): Promise { - const commendURL = queryParam ? commend + '?' + queryParam : commend; - const requestInit: RequestInit = { - method: method, - headers: GIST_AUTH_HEADER(gitToken) - }; - - if (body) { - requestInit.body = JSON.stringify({ body }); - } - const response = await fetch(commendURL, requestInit); - if (!response.ok) { - throw new HttpException('gist api throw error', HttpStatus.NOT_FOUND); - } - return response; - } - - async parseGistApiFiles(gistData: any, gitToken: string): Promise { - return await Promise.all( - Object.keys(gistData.files).map(async (filename) => { - //trunc 옵션 - const content = await this.getFileContent(gistData.files[filename].raw_url, gitToken); - return GistApiFileDto.of(filename, gistData, content); - }) - ); - } -} +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CommentDto } from './dto/comment.dto'; +import { CommitDto } from './dto/gist.commit.dto'; +import { GistApiFileDto } from './dto/gistApiFile.dto'; +import { GistApiFileListDto } from './dto/gistApiFileList.dto'; +import { ResponseAllGistsDto } from './dto/response.allGists.dto'; +import { ResponseGistDto } from './dto/response.gist.dto'; +import { UserDto } from './dto/user.dto'; +import { GIST_AUTH_HEADER } from '@/constants/constants'; + +@Injectable() +export class GistService { + gitBaseUrl: string; + constructor() { + this.gitBaseUrl = 'https://api.github.com/'; + } + async getGistList(gitToken: string, page: number, perPage: number): Promise { + let hasNextPage = true; + const currentGistPage = await this.gistPageData(gitToken, page, perPage); + const nextGistPage = await this.gistPageData(gitToken, page + 1, perPage); + + if (nextGistPage.length === 0) { + hasNextPage = false; + } + + const gists = currentGistPage.map((gist) => { + return ResponseGistDto.of(gist); + }); + return ResponseAllGistsDto.of(gists, page, hasNextPage); + } + + async gistPageData(gitToken: string, page: number, per_page: number): Promise { + const params = { + page: page.toString(), + per_page: per_page.toString() + }; + const queryParam = new URLSearchParams(params).toString(); + const gistsData = await this.gistReq('GET', `${this.gitBaseUrl}gists`, gitToken, queryParam); + return await gistsData.json(); + } + + async getGistById(id: string, gitToken: string): Promise { + let response = null; + try { + response = await this.gistReq('GET', `${this.gitBaseUrl}gists/${id}`, gitToken); + } catch (e) { + throw new HttpException('gistId is not exist', HttpStatus.NOT_FOUND); + } + const data = await response.json(); + + const fileArr: GistApiFileDto[] = await this.parseGistApiFiles(data, gitToken); + return GistApiFileListDto.of(data, fileArr); + } + + async getMostRecentGistInUser(gitToken: string): Promise { + const params = { + page: '1', + per_page: '1' + }; + const queryParam = new URLSearchParams(params).toString(); + const response = await this.gistReq('GET', `${this.gitBaseUrl}gists`, gitToken, queryParam); + if (!response.length) { + throw new Error('404'); + } + const mostRecentData = response[0]; + + const fileArr: GistApiFileDto[] = await this.parseGistApiFiles(mostRecentData, gitToken); + return GistApiFileListDto.of(mostRecentData, fileArr); + } + + async getCommitsForAGist(gist_id: string, pageIdx = 1, gitToken: string): Promise { + const page = pageIdx; + const perPage = 5; + const params = { + page: page.toString(), + per_page: perPage.toString() + }; + const queryParam = new URLSearchParams(params).toString(); + const response = await this.gistReq('GET', `${this.gitBaseUrl}gists/${gist_id}/commits`, gitToken, queryParam); + const data = await response.json(); + const commits: CommitDto[] = data.map((history) => CommitDto.of(history)); + return commits; + } + + async getCommit(gist_id: string, commit_id: string, gitToken: string) { + const response = await this.getFilesFromCommit(this.getCommitUrl(gist_id, commit_id), gitToken); + return response; + } + + getCommitUrl(gist_id: string, commit_id: string) { + return `https://api.github.com/gists/${gist_id}/${commit_id}`; + } + + async getFilesFromCommit(commit_url: string, gittoken: string) { + const data = await this.getFileContent(commit_url, gittoken); + const dataJson = JSON.parse(data); + const fileArr: GistApiFileDto[] = await this.parseGistApiFiles(dataJson, gittoken); + return GistApiFileListDto.of(dataJson, fileArr); + } + + async getUserData(gitToken: string): Promise { + const response = await this.gistReq('GET', `${this.gitBaseUrl}user`, gitToken); + const userData = await response.json(); + if (!userData.id || !userData.avatar_url || !userData.login) { + throw new Error('404'); + } + const result: UserDto = UserDto.of(userData); + return result; + } + async getFileContent(raw_url: string, gittoken: string) { + const header = {}; + if (gittoken) { + header['Authorization'] = `Bearer ${gittoken}`; + } + const response = await fetch(raw_url, { + headers: header + }); + if (!response.ok) { + throw new Error('404'); + } + const data = await response.text(); + return data; + } + + async getComments(gitToken: string, gist_id: string): Promise { + const data = await this.gistReq('GET', `${this.gitBaseUrl}gists/${gist_id}/comments`, gitToken); + const comments: CommentDto[] = data.map((comment) => CommentDto.of(comment)); + return comments; + } + + async createComments(gitToken: string, gist_id: string, detail: string): Promise { + const response = await this.gistReq('POST', `${this.gitBaseUrl}gists/${gist_id}/comments`, gitToken, '', detail); + + const data = await response.json(); + const comment: CommentDto = CommentDto.of(data); + return comment; + } + + async updateComment(gitToken: string, gist_id: string, comment_id: string, detail: string): Promise { + const response = await this.gistReq( + 'PATCH', + `${this.gitBaseUrl}gists/${gist_id}/comments/${comment_id}`, + gitToken, + '', + detail + ); + const data = await response.json(); + return true; + } + + async deleteComment(gitToken: string, gist_id: string, comment_id: string): Promise { + const data = await this.gistReq('DELETE', `${this.gitBaseUrl}gists/${gist_id}/comments/${comment_id}`, gitToken); + return true; + } + + async gistReq( + method: string, + commend: string, + gitToken: string = null, + queryParam = '', + body: any = null + ): Promise { + const commendURL = queryParam ? commend + '?' + queryParam : commend; + const requestInit: RequestInit = { + method: method, + headers: GIST_AUTH_HEADER(gitToken) + }; + + if (body) { + requestInit.body = JSON.stringify({ body }); + } + const response = await fetch(commendURL, requestInit); + if (!response.ok) { + throw new HttpException('gist api throw error', HttpStatus.NOT_FOUND); + } + return response; + } + + async parseGistApiFiles(gistData: any, gitToken: string): Promise { + return await Promise.all( + Object.keys(gistData.files).map(async (filename) => { + //trunc 옵션 + const content = await this.getFileContent(gistData.files[filename].raw_url, gitToken); + return GistApiFileDto.of(filename, gistData, content); + }) + ); + } +} diff --git a/apps/backend/src/history/dto/history.responseList.dto.ts b/apps/backend/src/history/dto/history.responseList.dto.ts index c1d5c5bd..01747443 100644 --- a/apps/backend/src/history/dto/history.responseList.dto.ts +++ b/apps/backend/src/history/dto/history.responseList.dto.ts @@ -16,12 +16,12 @@ export class HistoryResponseListDto { max: number; }; - static of(historys: History[], page: number, size: number, total: number) { + static of(historys: History[], page: number, size: number, maxPage: number) { return { list: historys.map((history) => HistoryPublicDto.of(history)), page: { current: page, - max: Math.ceil(total / size) + max: maxPage } }; } diff --git a/apps/backend/src/history/history.controller.ts b/apps/backend/src/history/history.controller.ts index de5df0e7..a7f6d4dc 100644 --- a/apps/backend/src/history/history.controller.ts +++ b/apps/backend/src/history/history.controller.ts @@ -1,80 +1,80 @@ -import { - Body, - Controller, - DefaultValuePipe, - Get, - Headers, - HttpCode, - HttpException, - HttpStatus, - Param, - ParseIntPipe, - Post, - Query, - Req -} from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ApiBody, ApiHeader, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { error } from 'console'; -import { Request } from 'express'; -import { HistoryExecRequestDto } from './dto/history.execRequest.dto'; -import { HistoryExecResponseDto } from './dto/history.execResponse.dto'; -import { HistoryGetResponseDto } from './dto/history.getReponse.dto'; -import { HistoryResponseListDto } from './dto/history.responseList.dto'; -import { HistoryService } from './history.service'; -import { AuthService } from '@/auth/auth.service'; -import { HISTORY_STATUS } from '@/constants/constants'; -import { UserService } from '@/user/user.service'; - -@Controller('lotus/:lotusId/history') -export class HistoryController { - constructor(private historyService: HistoryService, private authServer: AuthService) {} - - @Get('error') - async errorQuery() { - return this.historyService.errorQuery(); - } - - @Post() - @HttpCode(200) - @ApiOperation({ summary: '코드 실행 & history 추가' }) - @ApiBody({ type: HistoryExecRequestDto }) - @ApiResponse({ status: 200, description: '실행 성공', type: HistoryExecResponseDto }) - async execCode( - @Req() request: Request, - @Param('lotusId') lotusId: string, - @Body() historyExecRequestDto: HistoryExecRequestDto - ): Promise { - const { input, execFileName } = historyExecRequestDto; - try { - const gitToken = await this.authServer.getUserGitToken(this.authServer.getIdFromRequest(request)); - // const execFileName = 'FunctionDivide.js'; - // const input = ['1 1 1 1', '1 1 1 1', '1 1 1 1', '1 1 1 1']; - return await this.historyService.saveHistory(gitToken, lotusId, execFileName, input); - } catch (e) { - return await this.historyService.saveHistory(null, lotusId, execFileName, input); - } - } - - @Get() - @HttpCode(200) - @ApiOperation({ summary: '해당 lotus의 history 목록 조회' }) - @ApiResponse({ status: 200, description: '실행 성공', type: HistoryResponseListDto }) - @ApiQuery({ name: 'page', type: Number, example: 1 }) - @ApiQuery({ name: 'size', type: Number, example: 5 }) - getHistoryList( - @Param('lotusId') lotusId: string, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number - ): Promise { - return this.historyService.getHistoryList(lotusId, page, size); - } - - @Get(':historyId') - @HttpCode(200) - @ApiOperation({ summary: '해당 historyId의 상세 정보 조회' }) - @ApiResponse({ status: 200, description: '실행 성공', type: HistoryGetResponseDto }) - getHistory(@Param('historyId') historyId: string): Promise { - return this.historyService.getHistoryFromId(historyId); - } -} +import { + Body, + Controller, + DefaultValuePipe, + Get, + Headers, + HttpCode, + HttpException, + HttpStatus, + Param, + ParseIntPipe, + Post, + Query, + Req +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiBody, ApiHeader, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { error } from 'console'; +import { Request } from 'express'; +import { HistoryExecRequestDto } from './dto/history.execRequest.dto'; +import { HistoryExecResponseDto } from './dto/history.execResponse.dto'; +import { HistoryGetResponseDto } from './dto/history.getReponse.dto'; +import { HistoryResponseListDto } from './dto/history.responseList.dto'; +import { HistoryService } from './history.service'; +import { AuthService } from '@/auth/auth.service'; +import { HISTORY_STATUS } from '@/constants/constants'; +import { UserService } from '@/user/user.service'; + +@Controller('lotus/:lotusId/history') +export class HistoryController { + constructor(private historyService: HistoryService, private authServer: AuthService) {} + + @Get('error') + async errorQuery() { + return this.historyService.errorQuery(); + } + + @Post() + @HttpCode(200) + @ApiOperation({ summary: '코드 실행 & history 추가' }) + @ApiBody({ type: HistoryExecRequestDto }) + @ApiResponse({ status: 200, description: '실행 성공', type: HistoryExecResponseDto }) + async execCode( + @Req() request: Request, + @Param('lotusId') lotusId: string, + @Body() historyExecRequestDto: HistoryExecRequestDto + ): Promise { + const { input, execFileName } = historyExecRequestDto; + try { + const gitToken = await this.authServer.getUserGitToken(this.authServer.getIdFromRequest(request)); + // const execFileName = 'FunctionDivide.js'; + // const input = ['1 1 1 1', '1 1 1 1', '1 1 1 1', '1 1 1 1']; + return await this.historyService.saveHistory(gitToken, lotusId, execFileName, input); + } catch (e) { + return await this.historyService.saveHistory(null, lotusId, execFileName, input); + } + } + + @Get() + @HttpCode(200) + @ApiOperation({ summary: '해당 lotus의 history 목록 조회' }) + @ApiResponse({ status: 200, description: '실행 성공', type: HistoryResponseListDto }) + @ApiQuery({ name: 'page', type: Number, example: 1 }) + @ApiQuery({ name: 'size', type: Number, example: 5 }) + getHistoryList( + @Param('lotusId') lotusId: string, + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number + ): Promise { + return this.historyService.getHistoryList(lotusId, page, size); + } + + @Get(':historyId') + @HttpCode(200) + @ApiOperation({ summary: '해당 historyId의 상세 정보 조회' }) + @ApiResponse({ status: 200, description: '실행 성공', type: HistoryGetResponseDto }) + getHistory(@Param('historyId') historyId: string): Promise { + return this.historyService.getHistoryFromId(historyId); + } +} diff --git a/apps/backend/src/history/history.entity.ts b/apps/backend/src/history/history.entity.ts index 77e9002b..b370cd25 100644 --- a/apps/backend/src/history/history.entity.ts +++ b/apps/backend/src/history/history.entity.ts @@ -4,8 +4,7 @@ import { User } from '@/user/user.entity'; @Entity() export class History { - //@PrimaryGeneratedColumn('uuid', { type: 'bigint' }) - @PrimaryGeneratedColumn('increment', { type: 'bigint' }) + @PrimaryGeneratedColumn('uuid', { name: 'history_id' }) historyId: string; @CreateDateColumn({ name: 'created_at' }) @@ -23,7 +22,7 @@ export class History { @Column() status: string; - @ManyToOne(() => Lotus, (lotus) => lotus.historys) + @ManyToOne(() => Lotus, (lotus) => lotus.historys, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'lotus_id' }) // 외래키 이름 설정 lotus: Lotus; } diff --git a/apps/backend/src/history/history.service.ts b/apps/backend/src/history/history.service.ts index a1652451..96ee376a 100644 --- a/apps/backend/src/history/history.service.ts +++ b/apps/backend/src/history/history.service.ts @@ -1,81 +1,86 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import e from 'express'; -import { HistoryExecResponseDto } from './dto/history.execResponse.dto'; -import { HistoryGetResponseDto } from './dto/history.getReponse.dto'; -import { HistoryResponseListDto } from './dto/history.responseList.dto'; -import { HistoryRepository } from './history.repository'; -import { HISTORY_STATUS, SUPPORTED_LANGUAGES } from '@/constants/constants'; -import { DockerProducer } from '@/docker/docker.producer'; -import { GistApiFileListDto } from '@/gist/dto/gistApiFileList.dto'; -import { GistService } from '@/gist/gist.service'; -import { Lotus } from '@/lotus/lotus.entity'; -import { LotusRepository } from '@/lotus/lotus.repository'; - -@Injectable() -export class HistoryService { - constructor( - private historyRepository: HistoryRepository, - private dockerProducer: DockerProducer, - private lotusRepository: LotusRepository, - private gistService: GistService - ) {} - async saveHistory(gitToken: string, lotusId: string, execFilename: string, inputs: string[]): Promise { - const [lotus]: Lotus[] = await this.lotusRepository.findBy({ lotusId: lotusId }); - const file: GistApiFileListDto = await this.gistService.getCommit(lotus.gistRepositoryId, lotus.commitId, gitToken); - if (!execFilename.endsWith(SUPPORTED_LANGUAGES.JS)) { - throw new HttpException('not supported file extension', HttpStatus.BAD_REQUEST); - } - const history = await this.historyRepository.save({ - input: JSON.stringify(inputs), - execFilename: execFilename, - result: null, - status: 'PENDING', - lotus: lotus - }); - this.execContainer(gitToken, lotus.gistRepositoryId, lotus.commitId, execFilename, inputs, history.historyId); - return HistoryExecResponseDto.of(HISTORY_STATUS.PENDING); - } - - async execContainer( - gitToken: string, - lotusId: string, - commitId: string, - execFilename: string, - inputs: string[], - historyId: string - ) { - try { - const result = await this.dockerProducer.getDocker(gitToken, lotusId, commitId, execFilename, inputs); - await this.historyRepository.update(historyId, { status: HISTORY_STATUS.SUCCESS, result }); - } catch (error) { - console.log(error.message); - await this.historyRepository.update(historyId, { - status: HISTORY_STATUS.ERROR, - result: error.message - }); - } - } - - async getHistoryList(lotusId: string, page: number, size: number): Promise { - const result = await this.historyRepository.findAndCount({ - where: { lotus: { lotusId } }, - skip: (page - 1) * size, - take: size, - order: { createdAt: 'DESC' } - }); - const [historys, total] = result; - - return HistoryResponseListDto.of(historys, page, size, total); - } - async getHistoryFromId(historyId: string): Promise { - const history = await this.historyRepository.findOneBy({ historyId: historyId }); - if (!history) { - throw new HttpException('not exist history', HttpStatus.BAD_REQUEST); - } - return HistoryGetResponseDto.of(history); - } - - async errorQuery() { - return await this.historyRepository.save({ status: HISTORY_STATUS.SUCCESS }); - } -} +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import e from 'express'; +import { HistoryExecResponseDto } from './dto/history.execResponse.dto'; +import { HistoryGetResponseDto } from './dto/history.getReponse.dto'; +import { HistoryResponseListDto } from './dto/history.responseList.dto'; +import { HistoryRepository } from './history.repository'; +import { HISTORY_STATUS, SUPPORTED_LANGUAGES } from '@/constants/constants'; +import { DockerProducer } from '@/docker/docker.producer'; +import { GistApiFileListDto } from '@/gist/dto/gistApiFileList.dto'; +import { GistService } from '@/gist/gist.service'; +import { Lotus } from '@/lotus/lotus.entity'; +import { LotusRepository } from '@/lotus/lotus.repository'; + +@Injectable() +export class HistoryService { + constructor( + private historyRepository: HistoryRepository, + private dockerProducer: DockerProducer, + private lotusRepository: LotusRepository, + private gistService: GistService + ) {} + async saveHistory(gitToken: string, lotusId: string, execFilename: string, inputs: string[]): Promise { + const [lotus]: Lotus[] = await this.lotusRepository.findBy({ lotusId: lotusId }); + const file: GistApiFileListDto = await this.gistService.getCommit(lotus.gistRepositoryId, lotus.commitId, gitToken); + if (!execFilename.endsWith(SUPPORTED_LANGUAGES.JS)) { + throw new HttpException('not supported file extension', HttpStatus.BAD_REQUEST); + } + const history = await this.historyRepository.save({ + input: JSON.stringify(inputs), + execFilename: execFilename, + result: null, + status: 'PENDING', + lotus: lotus + }); + this.execContainer(gitToken, lotus.gistRepositoryId, lotus.commitId, execFilename, inputs, history.historyId); + return HistoryExecResponseDto.of(HISTORY_STATUS.PENDING); + } + + async execContainer( + gitToken: string, + lotusId: string, + commitId: string, + execFilename: string, + inputs: string[], + historyId: string + ) { + try { + const result = await this.dockerProducer.getDocker(gitToken, lotusId, commitId, execFilename, inputs); + await this.historyRepository.update(historyId, { status: HISTORY_STATUS.SUCCESS, result }); + } catch (error) { + await this.historyRepository.update(historyId, { + status: HISTORY_STATUS.ERROR, + result: error.message + }); + } + } + + async getHistoryList(lotusId: string, page: number, size: number): Promise { + const result = await this.historyRepository.findAndCount({ + where: { lotus: { lotusId } }, + skip: (page - 1) * size, + take: size, + order: { createdAt: 'DESC' } + }); + const [historys, total] = result; + const maxPage = Math.ceil(total / size); + if (page > maxPage && maxPage !== 0) { + throw new HttpException('page must be lower than max page', HttpStatus.NOT_FOUND); + } + if (page <= 0) { + throw new HttpException('page must be higher than 0', HttpStatus.NOT_FOUND); + } + return HistoryResponseListDto.of(historys, page, size, maxPage); + } + async getHistoryFromId(historyId: string): Promise { + const history = await this.historyRepository.findOneBy({ historyId: historyId }); + if (!history) { + throw new HttpException('not exist history', HttpStatus.BAD_REQUEST); + } + return HistoryGetResponseDto.of(history); + } + + async errorQuery() { + return await this.historyRepository.save({ status: HISTORY_STATUS.SUCCESS }); + } +} diff --git a/apps/backend/src/lotus/dto/lotus.detail.dto.ts b/apps/backend/src/lotus/dto/lotus.detail.dto.ts index 8696e922..bfbeebef 100644 --- a/apps/backend/src/lotus/dto/lotus.detail.dto.ts +++ b/apps/backend/src/lotus/dto/lotus.detail.dto.ts @@ -74,7 +74,7 @@ export class LotusDetailDto { SimpleFileResponseDto.ofFileApiDto(file) ); const simpleUser: SimpleUserResponseDto = SimpleUserResponseDto.ofUserDto(lotus.user); - const simpleTags: string[] = lotus.tags.map((tag) => tag.tagName); + const simpleTags: string[] = lotus.tags.map((tag) => tag.tag.tagName); return { id: lotus.lotusId, title: lotus.title, diff --git a/apps/backend/src/lotus/dto/lotus.dto.ts b/apps/backend/src/lotus/dto/lotus.dto.ts index 169dfb6d..e0216218 100644 --- a/apps/backend/src/lotus/dto/lotus.dto.ts +++ b/apps/backend/src/lotus/dto/lotus.dto.ts @@ -3,7 +3,7 @@ import { IsBoolean, IsString, ValidateNested } from 'class-validator'; import { LotusCreateRequestDto } from './lotus.createRequest.dto'; import { Comment } from '@/comment/comment.entity'; import { History } from '@/history/history.entity'; -import { Tag } from '@/tag/tag.entity'; +import { LotusTag } from '@/relation/lotus.tag.entity'; import { User } from '@/user/user.entity'; export class LotusDto { @@ -44,10 +44,10 @@ export class LotusDto { historys: History[]; @ValidateNested({ each: true }) - @Type(() => Tag) - tags: Tag[]; + @Type(() => LotusTag) + tags: LotusTag[]; - constructor(commitId: string, user: User, lotusInputData: LotusCreateRequestDto, tags: Tag[]) { + constructor(commitId: string, user: User, lotusInputData: LotusCreateRequestDto) { this.title = lotusInputData.title; this.isPublic = lotusInputData.isPublic; this.gistRepositoryId = lotusInputData.gistUuid; @@ -56,7 +56,6 @@ export class LotusDto { this.version = lotusInputData.version; this.comments = []; this.historys = []; - this.tags = tags; this.user = user; } } diff --git a/apps/backend/src/lotus/dto/lotus.response.dto.ts b/apps/backend/src/lotus/dto/lotus.response.dto.ts index 4676961d..da2767bd 100644 --- a/apps/backend/src/lotus/dto/lotus.response.dto.ts +++ b/apps/backend/src/lotus/dto/lotus.response.dto.ts @@ -3,6 +3,7 @@ import { Type } from 'class-transformer'; import { IsBoolean, IsDate, IsString, IsUrl, ValidateNested } from 'class-validator'; import { SimpleUserResponseDto } from './simple.user.response.dto'; import { Lotus } from '@/lotus/lotus.entity'; +import { Tag } from '@/tag/tag.entity'; export class LotusResponseDto { @IsString() @@ -53,8 +54,7 @@ export class LotusResponseDto { }) tags: string[]; - static ofSpreadData(user: SimpleUserResponseDto, lotus: Lotus): LotusResponseDto { - const tags = lotus.tags.map((tag) => tag.tagName); + static ofSpreadData(user: SimpleUserResponseDto, lotus: Lotus, tags: string[]): LotusResponseDto { return { id: lotus.lotusId, author: user, @@ -69,7 +69,7 @@ export class LotusResponseDto { static ofLotus(lotus: Lotus): LotusResponseDto { const simpleUser = SimpleUserResponseDto.ofUserDto(lotus.user); - const tags = lotus.tags.map((tag) => tag.tagName); + const tags = lotus.tags.map((tag) => tag.tag.tagName); return { id: lotus.lotusId, author: simpleUser, diff --git a/apps/backend/src/lotus/lotus.controller.ts b/apps/backend/src/lotus/lotus.controller.ts index 065833a4..6ea39dde 100644 --- a/apps/backend/src/lotus/lotus.controller.ts +++ b/apps/backend/src/lotus/lotus.controller.ts @@ -1,98 +1,98 @@ -import { - Body, - Controller, - DefaultValuePipe, - Delete, - Get, - HttpCode, - Param, - ParseIntPipe, - Patch, - Post, - Query, - Req -} from '@nestjs/common'; -import { ApiBody, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Request } from 'express'; -import { LotusCreateRequestDto } from './dto/lotus.createRequest.dto'; -import { LotusDetailDto } from './dto/lotus.detail.dto'; -import { LotusPublicDto } from './dto/lotus.public.dto'; -import { LotusResponseDto } from './dto/lotus.response.dto'; -import { LotusUpdateRequestDto } from './dto/lotus.updateRequest.dto'; -import { MessageDto } from './dto/message.dto'; -import { LotusService } from './lotus.service'; -import { AuthService } from '@/auth/auth.service'; - -@Controller('lotus') -export class LotusController { - constructor(private readonly lotusService: LotusService, private authService: AuthService) {} - - @Post() - @HttpCode(201) - @ApiOperation({ summary: 'lotus 생성 및 추가' }) - @ApiBody({ type: LotusCreateRequestDto }) - @ApiResponse({ status: 201, description: '실행 성공', type: LotusResponseDto }) - async createLotus( - @Req() request: Request, - @Body() lotusCreateRequestDto: LotusCreateRequestDto - ): Promise { - const userId = this.authService.getIdFromRequest(request); - const gitToken = await this.authService.getUserGitToken(userId); - return await this.lotusService.createLotus(userId, gitToken, lotusCreateRequestDto); - } - - @Patch('/:lotusId') - @HttpCode(200) - @ApiOperation({ summary: 'lotus 업데이트' }) - @ApiBody({ type: LotusCreateRequestDto }) - @ApiResponse({ status: 200, description: '실행 성공', type: LotusResponseDto }) - @ApiQuery({ name: 'lotusId', type: String, example: '25' }) - updateLotus( - @Req() request: Request, - @Param('lotusId') lotusId: string, - @Body() lotusUpdateRequestDto: LotusUpdateRequestDto - ): Promise { - const userId = this.authService.getIdFromRequest(request); - return this.lotusService.updateLotus(lotusId, lotusUpdateRequestDto, userId); - } - - @Delete('/:lotusId') - @HttpCode(204) - @ApiOperation({ summary: 'lotus 삭제' }) - @ApiResponse({ status: 204, description: '실행 성공', type: MessageDto }) - @ApiQuery({ name: 'lotusId', type: String, example: '25' }) - deleteLotus(@Req() request: Request, @Param('lotusId') lotusId: string): Promise { - const userId = this.authService.getIdFromRequest(request); - return this.lotusService.deleteLotus(lotusId, userId); - } - - @Get() - @HttpCode(200) - @ApiOperation({ summary: 'lotus public 목록 가져오기' }) - @ApiResponse({ status: 200, description: '실행 성공', type: LotusPublicDto }) - @ApiQuery({ name: 'page', type: String, example: '1', required: false }) - @ApiQuery({ name: 'size', type: String, example: '10', required: false }) - @ApiQuery({ name: 'search', type: String, example: 'Web', required: false }) - getPublicLotus( - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number, - @Query('search', new DefaultValuePipe('')) search: string - ): Promise { - return this.lotusService.getPublicLotus(page, size, search); - } - - @Get('/:lotusId') - @HttpCode(200) - @ApiOperation({ summary: 'lotus 상세 데이터 가져오기' }) - @ApiResponse({ status: 200, description: '실행 성공', type: LotusDetailDto }) - @ApiQuery({ name: 'lotusId', type: String, example: '25' }) - async getLotusDetail(@Req() request: Request, @Param('lotusId') lotusId: string): Promise { - try { - const userId = this.authService.getIdFromRequest(request); - const gitToken = await this.authService.getUserGitToken(userId); - return this.lotusService.getLotusFile(userId, gitToken, lotusId); - } catch (e) { - return this.lotusService.getLotusFile(null, null, lotusId); - } - } -} +import { + Body, + Controller, + DefaultValuePipe, + Delete, + Get, + HttpCode, + Param, + ParseIntPipe, + Patch, + Post, + Query, + Req +} from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { Request } from 'express'; +import { LotusCreateRequestDto } from './dto/lotus.createRequest.dto'; +import { LotusDetailDto } from './dto/lotus.detail.dto'; +import { LotusPublicDto } from './dto/lotus.public.dto'; +import { LotusResponseDto } from './dto/lotus.response.dto'; +import { LotusUpdateRequestDto } from './dto/lotus.updateRequest.dto'; +import { MessageDto } from './dto/message.dto'; +import { LotusService } from './lotus.service'; +import { AuthService } from '@/auth/auth.service'; + +@Controller('lotus') +export class LotusController { + constructor(private readonly lotusService: LotusService, private authService: AuthService) {} + + @Post() + @HttpCode(201) + @ApiOperation({ summary: 'lotus 생성 및 추가' }) + @ApiBody({ type: LotusCreateRequestDto }) + @ApiResponse({ status: 201, description: '실행 성공', type: LotusResponseDto }) + async createLotus( + @Req() request: Request, + @Body() lotusCreateRequestDto: LotusCreateRequestDto + ): Promise { + const userId = this.authService.getIdFromRequest(request); + const gitToken = await this.authService.getUserGitToken(userId); + return await this.lotusService.createLotus(userId, gitToken, lotusCreateRequestDto); + } + + @Patch('/:lotusId') + @HttpCode(200) + @ApiOperation({ summary: 'lotus 업데이트' }) + @ApiBody({ type: LotusCreateRequestDto }) + @ApiResponse({ status: 200, description: '실행 성공', type: LotusResponseDto }) + @ApiQuery({ name: 'lotusId', type: String, example: '25' }) + updateLotus( + @Req() request: Request, + @Param('lotusId') lotusId: string, + @Body() lotusUpdateRequestDto: LotusUpdateRequestDto + ): Promise { + const userId = this.authService.getIdFromRequest(request); + return this.lotusService.updateLotus(lotusId, lotusUpdateRequestDto, userId); + } + + @Delete('/:lotusId') + @HttpCode(204) + @ApiOperation({ summary: 'lotus 삭제' }) + @ApiResponse({ status: 204, description: '실행 성공', type: MessageDto }) + @ApiQuery({ name: 'lotusId', type: String, example: '25' }) + deleteLotus(@Req() request: Request, @Param('lotusId') lotusId: string): Promise { + const userId = this.authService.getIdFromRequest(request); + return this.lotusService.deleteLotus(lotusId, userId); + } + + @Get() + @HttpCode(200) + @ApiOperation({ summary: 'lotus public 목록 가져오기' }) + @ApiResponse({ status: 200, description: '실행 성공', type: LotusPublicDto }) + @ApiQuery({ name: 'page', type: String, example: '1', required: false }) + @ApiQuery({ name: 'size', type: String, example: '10', required: false }) + @ApiQuery({ name: 'search', type: String, example: 'Web', required: false }) + getPublicLotus( + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number, + @Query('search', new DefaultValuePipe('')) search: string + ): Promise { + return this.lotusService.getPublicLotus(page, size, search); + } + + @Get('/:lotusId') + @HttpCode(200) + @ApiOperation({ summary: 'lotus 상세 데이터 가져오기' }) + @ApiResponse({ status: 200, description: '실행 성공', type: LotusDetailDto }) + @ApiQuery({ name: 'lotusId', type: String, example: '25' }) + async getLotusDetail(@Req() request: Request, @Param('lotusId') lotusId: string): Promise { + try { + const userId = this.authService.getIdFromRequest(request); + const gitToken = await this.authService.getUserGitToken(userId); + return this.lotusService.getLotusFile(userId, gitToken, lotusId); + } catch (e) { + return this.lotusService.getLotusFile(null, null, lotusId); + } + } +} diff --git a/apps/backend/src/lotus/lotus.entity.ts b/apps/backend/src/lotus/lotus.entity.ts index b486d15d..d492a79e 100644 --- a/apps/backend/src/lotus/lotus.entity.ts +++ b/apps/backend/src/lotus/lotus.entity.ts @@ -11,13 +11,12 @@ import { } from 'typeorm'; import { Comment } from '@/comment/comment.entity'; import { History } from '@/history/history.entity'; -import { Tag } from '@/tag/tag.entity'; +import { LotusTag } from '@/relation/lotus.tag.entity'; import { User } from '@/user/user.entity'; @Entity() export class Lotus { - //@PrimaryGeneratedColumn('uuid', { type: 'bigint' }) - @PrimaryGeneratedColumn('increment', { type: 'bigint', name: 'lotus_id' }) + @PrimaryGeneratedColumn('uuid', { name: 'lotus_id' }) lotusId: string; @Column() @@ -41,7 +40,7 @@ export class Lotus { @CreateDateColumn({ name: 'created_at' }) createdAt: Date; - @ManyToOne(() => User, (user) => user.lotuses) + @ManyToOne(() => User, (user) => user.lotuses, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) // 외래키 이름 설정 user: User; @@ -51,17 +50,6 @@ export class Lotus { @OneToMany(() => History, (history) => history.lotus, { cascade: ['remove'] }) historys: History[]; - @ManyToMany(() => Tag, { cascade: ['insert', 'update', 'remove'] }) - @JoinTable({ - name: 'lotus_tags', // 교차 테이블 이름 지정 - joinColumn: { - name: 'lotus_id', // 이 컬럼은 Lotus 엔티티를 참조 - referencedColumnName: 'lotusId' - }, - inverseJoinColumn: { - name: 'tag_id', // 이 컬럼은 Tag 엔티티를 참조 - referencedColumnName: 'tagId' - } - }) - tags: Tag[]; + @OneToMany(() => LotusTag, (lotusTag) => lotusTag.lotus, { cascade: ['remove'] }) + tags: LotusTag[]; } diff --git a/apps/backend/src/lotus/lotus.module.ts b/apps/backend/src/lotus/lotus.module.ts index 393baf53..7555cd0f 100644 --- a/apps/backend/src/lotus/lotus.module.ts +++ b/apps/backend/src/lotus/lotus.module.ts @@ -6,6 +6,7 @@ import { LotusRepository } from './lotus.repository'; import { LotusService } from './lotus.service'; import { AuthModule } from '@/auth/auth.module'; import { GistModule } from '@/gist/gist.module'; +import { LotusTagModule } from '@/relation/lotus.tag.module'; import { TagModule } from '@/tag/tag.module'; import { UserModule } from '@/user/user.module'; @@ -15,7 +16,7 @@ import { UserModule } from '@/user/user.module'; GistModule, forwardRef(() => UserModule), forwardRef(() => AuthModule), - TagModule + LotusTagModule ], controllers: [LotusController], providers: [LotusService, LotusRepository], diff --git a/apps/backend/src/lotus/lotus.service.ts b/apps/backend/src/lotus/lotus.service.ts index 224df691..7f716e82 100644 --- a/apps/backend/src/lotus/lotus.service.ts +++ b/apps/backend/src/lotus/lotus.service.ts @@ -1,210 +1,213 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { title } from 'process'; -import { In, Like } from 'typeorm'; -import { LotusCreateRequestDto } from './dto/lotus.createRequest.dto'; -import { LotusDetailDto } from './dto/lotus.detail.dto'; -import { LotusDto } from './dto/lotus.dto'; -import { LotusPublicDto } from './dto/lotus.public.dto'; -import { LotusResponseDto } from './dto/lotus.response.dto'; -import { LotusUpdateRequestDto } from './dto/lotus.updateRequest.dto'; -import { MessageDto } from './dto/message.dto'; -import { SimpleUserResponseDto } from './dto/simple.user.response.dto'; -import { Lotus } from './lotus.entity'; -import { LotusRepository } from './lotus.repository'; -import { GistService } from '@/gist/gist.service'; -import { Tag } from '@/tag/tag.entity'; -import { TagService } from '@/tag/tag.service'; -import { UserService } from '@/user/user.service'; - -@Injectable() -export class LotusService { - constructor( - private readonly lotusRepository: LotusRepository, - private readonly gistService: GistService, - private readonly userService: UserService, - private readonly tagService: TagService - ) {} - async createLotus( - userId: string, - gitToken: string, - lotusInputData: LotusCreateRequestDto - ): Promise { - if (!lotusInputData.title) { - throw new HttpException("title can't use empty", HttpStatus.BAD_REQUEST); - } - if (!lotusInputData.language) { - lotusInputData.language = 'JavaScript'; - } - if (!lotusInputData.version) { - lotusInputData.version = 'NodeJs:v22.11.0'; - } - const commits = await this.gistService.getCommitsForAGist(lotusInputData.gistUuid, 1, gitToken); - if (!commits || commits.length < 1) { - throw new HttpException('gistId is not exist', HttpStatus.NOT_FOUND); - } - const currentCommitId = commits[0].commitId; - - if (await this.checkAlreadyExist(lotusInputData.gistUuid, currentCommitId)) { - throw new HttpException('same commit Lotus already exist.', HttpStatus.CONFLICT); - } - const userData = await this.userService.findOneByUserId(userId); - const tags: Tag[] = await Promise.all( - lotusInputData.tags.map((tag) => { - return this.tagService.getTag(tag); - }) - ); - await this.saveLotus(new LotusDto(currentCommitId, userData, lotusInputData, tags)); - const lotusData = await this.lotusRepository.findOne({ - where: { gistRepositoryId: lotusInputData.gistUuid, commitId: currentCommitId }, - relations: ['tags'] - }); - - return LotusResponseDto.ofSpreadData(SimpleUserResponseDto.ofUserDto(userData), lotusData); - } - - async updateLotus( - lotusId: string, - lotusUpdateRequestDto: LotusUpdateRequestDto, - userIdWhoWantToUpdate: string - ): Promise { - const foundUser = await this.userService.findOneByUserId(userIdWhoWantToUpdate); - if (!foundUser) throw new HttpException('user data not found', HttpStatus.NOT_FOUND); - const updateLotus = await this.lotusRepository.findOne({ - where: { lotusId }, - relations: ['user', 'tags'] - }); - if (!updateLotus) throw new HttpException('lotusId is not exist', HttpStatus.NOT_FOUND); - if (updateLotus.user.userId !== userIdWhoWantToUpdate) { - throw new HttpException("can't modify this lotus", HttpStatus.FORBIDDEN); - } - - if (lotusUpdateRequestDto.isPublic !== undefined) { - updateLotus.isPublic = lotusUpdateRequestDto.isPublic; - } else { - if (!lotusUpdateRequestDto.title) { - throw new HttpException("title can't use empty", HttpStatus.BAD_REQUEST); - } else { - updateLotus.title = lotusUpdateRequestDto.title; - } - if (lotusUpdateRequestDto.tags) { - const tags = await Promise.all(lotusUpdateRequestDto.tags.map((tag) => this.tagService.getTag(tag))); - updateLotus.tags = tags; - } - } - const result = await this.lotusRepository.save(updateLotus); - if (!result) throw new HttpException('update fail', HttpStatus.BAD_REQUEST); - return LotusResponseDto.ofSpreadData(SimpleUserResponseDto.ofUserDto(updateLotus.user), updateLotus); - } - - async deleteLotus(lotusId: string, userIdWhoWantToDelete: string): Promise { - const foundUser = await this.userService.findOneByUserId(userIdWhoWantToDelete); - if (!foundUser) throw new HttpException('user data not found', HttpStatus.NOT_FOUND); - const deleteLotus = await this.lotusRepository.findOne({ - where: { lotusId }, - relations: ['user'] - }); - if (!deleteLotus) throw new HttpException('lotusId is not exist', HttpStatus.NOT_FOUND); - if (deleteLotus.user.userId !== userIdWhoWantToDelete) { - throw new HttpException("can't remove this lotus", HttpStatus.FORBIDDEN); - } - - const result = await this.lotusRepository.delete({ lotusId }); - if (!result.affected) throw new HttpException('delete fail', HttpStatus.NOT_FOUND); - - return new MessageDto('ok'); - } - - async getLotusFile(userId: string, gitToken: string, lotusId: string): Promise { - const lotusData = await this.lotusRepository.findOne({ - where: { lotusId }, - relations: ['tags', 'user'] - }); - if (!lotusData) { - throw new HttpException('lotusId is not exist', HttpStatus.NOT_FOUND); - } - if (!lotusData.isPublic && lotusData.user.userId !== userId) { - throw new HttpException("can't view this lotus", HttpStatus.FORBIDDEN); - } - - const commitFiles = await this.gistService.getCommit(lotusData.gistRepositoryId, lotusData.commitId, gitToken); - - return LotusDetailDto.ofGistFileListDto(commitFiles, lotusData); - } - - async getPublicLotus(page: number, size: number, search: string): Promise { - //const [lotusData, totalNum] = await this.getLotusByTags(page, size, search); - const [lotusData, totalNum] = await this.getLotusByTitle(page, size, search); - const maxPage = Math.ceil(totalNum / size); - if (page > maxPage && maxPage !== 0) { - throw new HttpException('page must be lower than max page', HttpStatus.NOT_FOUND); - } - if (page <= 0) { - throw new HttpException('page must be higher than 0', HttpStatus.NOT_FOUND); - } - return LotusPublicDto.ofLotusList(lotusData, page, maxPage); - } - - async getLotusByTitle(page: number, size: number, search: string) { - const whereData = { - isPublic: true - }; - if (search) { - whereData['title'] = Like(`%${search}%`); - } - return await this.lotusRepository.findAndCount({ - where: whereData, - skip: (page - 1) * size, - take: size, - relations: ['tags', 'user'], - order: { createdAt: 'DESC' } - }); - } - - async getLotusByTags(page: number, size: number, search: string) { - const whereData = { - isPublic: true - }; - if (search) { - const tags = await this.tagService.searchTag(search); - whereData['tags'] = { tagId: In(tags) }; - } - return await this.lotusRepository.findAndCount({ - where: whereData, - skip: (page - 1) * size, - take: size, - relations: ['tags', 'user'], - order: { createdAt: 'DESC' } - }); - } - - async getUserLotus(userId: string, page: number, size: number) { - const user = this.userService.findOneByUserId(userId); - if (!user) { - throw new HttpException('user data is not found', HttpStatus.UNAUTHORIZED); - } - - const [lotusData, totalNum] = await this.lotusRepository.findAndCount({ - where: { user: { userId } }, - skip: (page - 1) * size, - take: size, - relations: ['tags', 'user'], - order: { createdAt: 'DESC' } - }); - const maxPage = Math.ceil(totalNum / size); - if (page > maxPage && maxPage !== 0) { - throw new HttpException('page must be lower than max page', HttpStatus.NOT_FOUND); - } - if (page <= 0) { - throw new HttpException('page must be higher than 0', HttpStatus.NOT_FOUND); - } - return LotusPublicDto.ofLotusList(lotusData, page, maxPage); - } - - async checkAlreadyExist(gistUuid: string, commitId: string) { - return await this.lotusRepository.exists({ where: { gistRepositoryId: gistUuid, commitId: commitId } }); - } - - async saveLotus(lotus: Lotus): Promise { - await this.lotusRepository.save(lotus); - } -} +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { title } from 'process'; +import { In, Like } from 'typeorm'; +import { LotusCreateRequestDto } from './dto/lotus.createRequest.dto'; +import { LotusDetailDto } from './dto/lotus.detail.dto'; +import { LotusDto } from './dto/lotus.dto'; +import { LotusPublicDto } from './dto/lotus.public.dto'; +import { LotusResponseDto } from './dto/lotus.response.dto'; +import { LotusUpdateRequestDto } from './dto/lotus.updateRequest.dto'; +import { MessageDto } from './dto/message.dto'; +import { SimpleUserResponseDto } from './dto/simple.user.response.dto'; +import { Lotus } from './lotus.entity'; +import { LotusRepository } from './lotus.repository'; +import { GistService } from '@/gist/gist.service'; +import { LotusTagService } from '@/relation/lotus.tag.service'; +import { UserService } from '@/user/user.service'; + +@Injectable() +export class LotusService { + constructor( + private readonly lotusRepository: LotusRepository, + private readonly gistService: GistService, + private readonly userService: UserService, + private readonly lotusTagService: LotusTagService + ) {} + async createLotus( + userId: string, + gitToken: string, + lotusInputData: LotusCreateRequestDto + ): Promise { + if (!lotusInputData.title) { + throw new HttpException("title can't use empty", HttpStatus.BAD_REQUEST); + } + if (!lotusInputData.language) { + lotusInputData.language = 'JavaScript'; + } + if (!lotusInputData.version) { + lotusInputData.version = 'NodeJs:v22.11.0'; + } + const commits = await this.gistService.getCommitsForAGist(lotusInputData.gistUuid, 1, gitToken); + if (!commits || commits.length < 1) { + throw new HttpException('gistId is not exist', HttpStatus.NOT_FOUND); + } + const currentCommitId = commits[0].commitId; + + if (await this.checkAlreadyExist(lotusInputData.gistUuid, currentCommitId)) { + throw new HttpException('same commit Lotus already exist.', HttpStatus.CONFLICT); + } + const userData = await this.userService.findOneByUserId(userId); + await this.saveLotus(new LotusDto(currentCommitId, userData, lotusInputData)); + const lotusData = await this.lotusRepository.findOne({ + where: { gistRepositoryId: lotusInputData.gistUuid, commitId: currentCommitId } + }); + await Promise.all( + lotusInputData.tags.map((tag) => { + return this.lotusTagService.getLotusTagRelation(lotusData, tag); + }) + ); + + return LotusResponseDto.ofSpreadData(SimpleUserResponseDto.ofUserDto(userData), lotusData, lotusInputData.tags); + } + + async updateLotus( + lotusId: string, + lotusUpdateRequestDto: LotusUpdateRequestDto, + userIdWhoWantToUpdate: string + ): Promise { + const foundUser = await this.userService.findOneByUserId(userIdWhoWantToUpdate); + if (!foundUser) throw new HttpException('user data not found', HttpStatus.NOT_FOUND); + const targetLotus = await this.lotusRepository.findOne({ + where: { lotusId }, + relations: ['user', 'tags', 'tags.tag'] + }); + if (!targetLotus) throw new HttpException('lotusId is not exist', HttpStatus.NOT_FOUND); + if (targetLotus.user.userId !== userIdWhoWantToUpdate) { + throw new HttpException("can't modify this lotus", HttpStatus.FORBIDDEN); + } + const updateContent = {}; + if (lotusUpdateRequestDto.isPublic !== undefined) { + updateContent['isPublic'] = lotusUpdateRequestDto.isPublic; + } else { + if (!lotusUpdateRequestDto.title) { + throw new HttpException("title can't use empty", HttpStatus.BAD_REQUEST); + } else { + updateContent['title'] = lotusUpdateRequestDto.title; + } + if (lotusUpdateRequestDto.tags !== undefined) { + await this.lotusTagService.updateRelation(targetLotus, lotusUpdateRequestDto.tags); + } + } + const result = await this.lotusRepository.update({ lotusId: targetLotus.lotusId }, updateContent); + if (!result) throw new HttpException('update fail', HttpStatus.BAD_REQUEST); + return LotusResponseDto.ofSpreadData( + SimpleUserResponseDto.ofUserDto(targetLotus.user), + targetLotus, + lotusUpdateRequestDto.tags !== undefined + ? lotusUpdateRequestDto.tags + : targetLotus.tags.map((tag) => tag.tag.tagName) + ); + } + + async deleteLotus(lotusId: string, userIdWhoWantToDelete: string): Promise { + const foundUser = await this.userService.findOneByUserId(userIdWhoWantToDelete); + if (!foundUser) throw new HttpException('user data not found', HttpStatus.NOT_FOUND); + const deleteLotus = await this.lotusRepository.findOne({ + where: { lotusId }, + relations: ['user'] + }); + if (!deleteLotus) throw new HttpException('lotusId is not exist', HttpStatus.NOT_FOUND); + if (deleteLotus.user.userId !== userIdWhoWantToDelete) { + throw new HttpException("can't remove this lotus", HttpStatus.FORBIDDEN); + } + + const result = await this.lotusRepository.delete({ lotusId }); + if (!result.affected) throw new HttpException('delete fail', HttpStatus.NOT_FOUND); + + return new MessageDto('ok'); + } + + async getLotusFile(userId: string, gitToken: string, lotusId: string): Promise { + const lotusData = await this.lotusRepository.findOne({ + where: { lotusId }, + relations: ['tags', 'tags.tag', 'user'] + }); + if (!lotusData) { + throw new HttpException('lotusId is not exist', HttpStatus.NOT_FOUND); + } + if (!lotusData.isPublic && lotusData.user.userId !== userId) { + throw new HttpException("can't view this lotus", HttpStatus.FORBIDDEN); + } + + const commitFiles = await this.gistService.getCommit(lotusData.gistRepositoryId, lotusData.commitId, gitToken); + + return LotusDetailDto.ofGistFileListDto(commitFiles, lotusData); + } + + async getPublicLotus(page: number, size: number, search: string): Promise { + //const [lotusData, totalNum] = await this.getLotusByTags(page, size, search); + const [lotusData, totalNum] = await this.getLotusByTitle(page, size, search); + const maxPage = Math.ceil(totalNum / size); + if (page > maxPage && maxPage !== 0) { + throw new HttpException('page must be lower than max page', HttpStatus.NOT_FOUND); + } + if (page <= 0) { + throw new HttpException('page must be higher than 0', HttpStatus.NOT_FOUND); + } + return LotusPublicDto.ofLotusList(lotusData, page, maxPage); + } + + async getLotusByTitle(page: number, size: number, search: string) { + const whereData = { + isPublic: true + }; + if (search) { + whereData['title'] = Like(`%${search}%`); + } + return await this.lotusRepository.findAndCount({ + where: whereData, + skip: (page - 1) * size, + take: size, + relations: ['tags', 'tags.tag', 'user'], + order: { createdAt: 'DESC' } + }); + } + + async getLotusByTags(page: number, size: number, search: string) { + const whereData = { + isPublic: true + }; + if (search) { + const lotuses = await this.lotusTagService.searchTag(search); + whereData['lotusId'] = In(lotuses); + } + return await this.lotusRepository.findAndCount({ + where: whereData, + skip: (page - 1) * size, + take: size, + relations: ['tags', 'tags.tag', 'user'], + order: { createdAt: 'DESC' } + }); + } + + async getUserLotus(userId: string, page: number, size: number) { + const user = this.userService.findOneByUserId(userId); + if (!user) { + throw new HttpException('user data is not found', HttpStatus.UNAUTHORIZED); + } + + const [lotusData, totalNum] = await this.lotusRepository.findAndCount({ + where: { user: { userId } }, + skip: (page - 1) * size, + take: size, + relations: ['tags', 'tags.tag', 'user'], + order: { createdAt: 'DESC' } + }); + const maxPage = Math.ceil(totalNum / size); + if (page > maxPage && maxPage !== 0) { + throw new HttpException('page must be lower than max page', HttpStatus.NOT_FOUND); + } + if (page <= 0) { + throw new HttpException('page must be higher than 0', HttpStatus.NOT_FOUND); + } + return LotusPublicDto.ofLotusList(lotusData, page, maxPage); + } + + async checkAlreadyExist(gistUuid: string, commitId: string) { + return await this.lotusRepository.exists({ where: { gistRepositoryId: gistUuid, commitId: commitId } }); + } + + async saveLotus(lotus: Lotus): Promise { + await this.lotusRepository.save(lotus); + } +} diff --git a/apps/backend/src/relation/lotus.tag.entity.ts b/apps/backend/src/relation/lotus.tag.entity.ts new file mode 100644 index 00000000..ac2f81ee --- /dev/null +++ b/apps/backend/src/relation/lotus.tag.entity.ts @@ -0,0 +1,17 @@ +import { Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Lotus } from '@/lotus/lotus.entity'; +import { Tag } from '@/tag/tag.entity'; + +@Entity() +export class LotusTag { + @PrimaryGeneratedColumn('increment', { type: 'bigint', name: 'lotus_tag_id' }) + lotusTagId: string; + + @ManyToOne(() => Lotus, (lotus) => lotus.tags, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'lotus_id' }) + lotus: Lotus; + + @ManyToOne(() => Tag, (tag) => tag.lotuses, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tag_id' }) + tag: Tag; +} diff --git a/apps/backend/src/relation/lotus.tag.module.ts b/apps/backend/src/relation/lotus.tag.module.ts new file mode 100644 index 00000000..419b5304 --- /dev/null +++ b/apps/backend/src/relation/lotus.tag.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LotusTag } from './lotus.tag.entity'; +import { LotusTagRepository } from './lotus.tag.repository'; +import { LotusTagService } from './lotus.tag.service'; +import { TagModule } from '@/tag/tag.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([LotusTag]), TagModule], + providers: [LotusTagService, LotusTagRepository], + exports: [LotusTagService] +}) +export class LotusTagModule {} diff --git a/apps/backend/src/relation/lotus.tag.repository.ts b/apps/backend/src/relation/lotus.tag.repository.ts new file mode 100644 index 00000000..4a0f8510 --- /dev/null +++ b/apps/backend/src/relation/lotus.tag.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, Like, Repository } from 'typeorm'; +import { LotusTag } from './lotus.tag.entity'; + +@Injectable() +export class LotusTagRepository extends Repository { + constructor(private dataSource: DataSource) { + super(LotusTag, dataSource.createEntityManager()); + } +} diff --git a/apps/backend/src/relation/lotus.tag.service.ts b/apps/backend/src/relation/lotus.tag.service.ts new file mode 100644 index 00000000..3756df7e --- /dev/null +++ b/apps/backend/src/relation/lotus.tag.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { In, Not } from 'typeorm'; +import { LotusTagRepository } from './lotus.tag.repository'; +import { Lotus } from '@/lotus/lotus.entity'; +import { Tag } from '@/tag/tag.entity'; +import { TagService } from '@/tag/tag.service'; + +@Injectable() +export class LotusTagService { + constructor(private readonly lotusTagRepository: LotusTagRepository, private readonly tagService: TagService) {} + + async createLotusTagRelation(lotus: Lotus, tag: Tag) { + await this.lotusTagRepository.save({ lotus, tag }); + return await this.lotusTagRepository.findOne({ + where: { lotus: { lotusId: lotus.lotusId }, tag: { tagId: tag.tagId } }, + relations: ['lotus', 'tag'] + }); + } + + async getLotusTagRelation(lotus: Lotus, tagName: string) { + const tag = await this.tagService.getTag(tagName); + let relation = await this.lotusTagRepository.findOne({ + where: { lotus: { lotusId: lotus.lotusId }, tag: { tagId: tag.tagId } }, + relations: ['lotus', 'tag'] + }); + if (!relation) { + relation = await this.createLotusTagRelation(lotus, tag); + } + return relation; + } + + async searchTag(search: string) { + const tags = await this.tagService.searchTag(search); + const lotusTags = await this.lotusTagRepository.find({ + where: { tag: In(tags) }, + relations: ['lotus'] + }); + return lotusTags.map((relation) => relation.lotus.lotusId); + } + + async updateRelation(lotus: Lotus, tagNames: string[]) { + const data = await Promise.all( + tagNames.map(async (tag) => { + return await this.getLotusTagRelation(lotus, tag); + }) + ); + const tagIds = data.map((tag) => tag.tag.tagId); + await this.lotusTagRepository.delete({ + lotus: { lotusId: lotus.lotusId }, + tag: { tagId: Not(In(tagIds)) } + }); + + await this.tagService.deleteNoRelationTags(await this.findUsingTags()); + } + + async findUsingTags() { + const allData = await this.lotusTagRepository.find({ relations: ['tag'] }); + return allData.map((data) => data.tag.tagId); + } +} diff --git a/apps/backend/src/tag/tag.controller.ts b/apps/backend/src/tag/tag.controller.ts index 7dd63af3..a31c4317 100644 --- a/apps/backend/src/tag/tag.controller.ts +++ b/apps/backend/src/tag/tag.controller.ts @@ -13,6 +13,6 @@ export class TagController { @Get() searchTag(@Query('keyword') keyword: string) { - return this.tagService.searchTag(keyword); + return this.tagService.searchTagNames(keyword); } } diff --git a/apps/backend/src/tag/tag.entity.ts b/apps/backend/src/tag/tag.entity.ts index c9d697a4..cc780ddc 100644 --- a/apps/backend/src/tag/tag.entity.ts +++ b/apps/backend/src/tag/tag.entity.ts @@ -1,6 +1,15 @@ -import { Column, CreateDateColumn, Entity, JoinColumn, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn +} from 'typeorm'; import { Lotus } from '@/lotus/lotus.entity'; -import { User } from '@/user/user.entity'; +import { LotusTag } from '@/relation/lotus.tag.entity'; @Entity() export class Tag { @@ -14,6 +23,6 @@ export class Tag { @CreateDateColumn({ name: 'created_at' }) createdAt: Date; - @ManyToMany(() => Lotus, (lotus) => lotus.tags) - lotuses: Lotus[]; + @OneToMany(() => LotusTag, (lotusTag) => lotusTag.tag, { cascade: ['remove'] }) + lotuses: LotusTag[]; } diff --git a/apps/backend/src/tag/tag.service.ts b/apps/backend/src/tag/tag.service.ts index c3a2e8d1..4d4a0ea9 100644 --- a/apps/backend/src/tag/tag.service.ts +++ b/apps/backend/src/tag/tag.service.ts @@ -1,4 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { In, Not } from 'typeorm'; import { Tag } from './tag.entity'; import { TagRepository } from './tag.repository'; @@ -35,4 +36,14 @@ export class TagService { const tagIds = tags.map((tag) => tag.tagId); return tagIds; } + + async searchTagNames(tagName: string): Promise { + const tags = await this.tagRepository.searchTagName(tagName); + const tagNames = tags.map((tag) => tag.tagName); + return tagNames; + } + + async deleteNoRelationTags(tagId: string[]) { + return await this.tagRepository.delete({ tagId: Not(In(tagId)) }); + } } diff --git a/apps/backend/src/user/user.controller.ts b/apps/backend/src/user/user.controller.ts index 1c6ae2bd..85a3235a 100644 --- a/apps/backend/src/user/user.controller.ts +++ b/apps/backend/src/user/user.controller.ts @@ -1,153 +1,153 @@ -import { - Body, - Controller, - DefaultValuePipe, - Get, - HttpCode, - Param, - ParseIntPipe, - Patch, - Query, - Redirect, - Req -} from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ApiBody, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Request } from 'express'; -import { FileDto } from './dto/file.dto'; -import { FileResponseDto } from './dto/file.response.dto'; -import { TokenDTO } from './dto/token.dto'; -import { UserCreateDto } from './dto/user.create.dto'; -import { UserPatchDTO } from './dto/user.patch.dto'; -import { UserService } from './user.service'; -import { AuthService } from '@/auth/auth.service'; -import { ResponseAllGistsDto } from '@/gist/dto/response.allGists.dto'; -import { GistService } from '@/gist/gist.service'; -import { LotusPublicDto } from '@/lotus/dto/lotus.public.dto'; -import { SimpleUserResponseDto } from '@/lotus/dto/simple.user.response.dto'; -import { LotusService } from '@/lotus/lotus.service'; - -@Controller('/user') -export class UserController { - constructor( - private readonly userService: UserService, - private readonly lotusService: LotusService, - private readonly authService: AuthService, - private readonly gistService: GistService, - private configService: ConfigService - ) {} - private OAUTH_CLIENT_ID = this.configService.get('OAUTH_CLIENT_ID'); - private OAUTH_CLIENT_SECRETS = this.configService.get('OAUTH_CLIENT_SECRETS'); - private OAUTH_LOGIN_CALLBACK_URL = this.configService.get('OAUTH_LOGIN_CALLBACK_URL'); - - private TEST_GIT_TOKEN = this.configService.get('TEST_GIT_TOKEN'); - private TEST_GIT_LOGIN = this.configService.get('TEST_GIT_LOGIN'); - private TEST_GIT_PROFILE = this.configService.get('TEST_GIT_PROFILE'); - private TEST_GIT_ID = this.configService.get('TEST_GIT_ID'); - - @Get('test') - @HttpCode(200) - @ApiOperation({ summary: 'test용 access token 발급' }) - @ApiResponse({ status: 200, description: '실행 성공', type: TokenDTO }) - async testLogin(): Promise { - let testUser = await this.userService.findOne(this.TEST_GIT_ID); - if (!testUser) { - await this.userService.saveUser( - new UserCreateDto( - { - login: this.TEST_GIT_LOGIN, - avatar_url: this.TEST_GIT_PROFILE, - id: this.TEST_GIT_ID - }, - this.TEST_GIT_TOKEN - ) - ); - testUser = await this.userService.findOne(this.TEST_GIT_ID); - } - return TokenDTO.of(await this.userService.makeTestUser(testUser)); - } - - @Get('login') - @Redirect() - getGithubLoginPage() { - const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${this.OAUTH_CLIENT_ID}&redirect_uri=${this.OAUTH_LOGIN_CALLBACK_URL}&scope=gist`; - return { url: githubAuthUrl, statusCode: 301 }; - } - - @Get('login/callback') - async githubCallback(@Query('code') code: string) { - const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - client_id: this.OAUTH_CLIENT_ID, - client_secret: this.OAUTH_CLIENT_SECRETS, - code - }) - }); - const tokenData = await tokenResponse.json(); - const token = await this.userService.loginUser(tokenData); - return { token }; - } - - @Get('/lotus') - @HttpCode(200) - @ApiOperation({ summary: '사용자 lotus 목록 가져오기' }) - @ApiResponse({ status: 200, description: '실행 성공', type: LotusPublicDto }) - @ApiQuery({ name: 'page', type: String, example: '1', required: false }) - @ApiQuery({ name: 'size', type: String, example: '10', required: false }) - getUserLotus( - @Req() request: Request, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number - ): Promise { - const userId = this.authService.getIdFromRequest(request); - return this.lotusService.getUserLotus(userId, page, size); - } - - @Get('') - @HttpCode(200) - @ApiOperation({ summary: '사용자 정보 가져오기' }) - @ApiResponse({ status: 200, description: '실행 성공', type: SimpleUserResponseDto }) - getUserInfo(@Req() request: Request): Promise { - const userId = this.authService.getIdFromRequest(request); - return this.userService.getSimpleUserInfoByUserId(userId); - } - - @Patch('') - @HttpCode(200) - @ApiOperation({ summary: '사용자 정보 수정하기' }) - @ApiBody({ type: UserPatchDTO }) - @ApiResponse({ status: 200, description: '실행 성공', type: UserPatchDTO }) - patchUserInfo(@Req() request: Request, @Body() userData: UserPatchDTO): Promise { - const userId = this.authService.getIdFromRequest(request); - return this.userService.patchUserDataByUserId(userId, userData); - } - - @Get('gist') - @HttpCode(200) - @ApiOperation({ summary: '사용자의 gist 목록 가져오기' }) - @ApiResponse({ status: 200, description: '실행 성공', type: ResponseAllGistsDto }) - async getGistPage( - @Req() request: Request, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number - ): Promise { - const gitToken = await this.authService.getUserGitToken(this.authService.getIdFromRequest(request)); - return await this.gistService.getGistList(gitToken, page, size); - } - - @Get('gist/:gistId') - @HttpCode(200) - @ApiOperation({ summary: '사용자의 특정 gist의 내부 파일 데이터 가져오기' }) - @ApiResponse({ status: 200, description: '실행 성공', type: FileResponseDto }) - async getCommitFile(@Req() request: Request, @Param('gistId') gistId: string) { - const gitToken = await this.authService.getUserGitToken(this.authService.getIdFromRequest(request)); - const Files = await this.gistService.getGistById(gistId, gitToken); - const result = Files.files.map((file) => FileDto.ofGistApiFile(file)); - return FileResponseDto.ofFiles(result); - } -} +import { + Body, + Controller, + DefaultValuePipe, + Get, + HttpCode, + Param, + ParseIntPipe, + Patch, + Query, + Redirect, + Req +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiBody, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { Request } from 'express'; +import { FileDto } from './dto/file.dto'; +import { FileResponseDto } from './dto/file.response.dto'; +import { TokenDTO } from './dto/token.dto'; +import { UserCreateDto } from './dto/user.create.dto'; +import { UserPatchDTO } from './dto/user.patch.dto'; +import { UserService } from './user.service'; +import { AuthService } from '@/auth/auth.service'; +import { ResponseAllGistsDto } from '@/gist/dto/response.allGists.dto'; +import { GistService } from '@/gist/gist.service'; +import { LotusPublicDto } from '@/lotus/dto/lotus.public.dto'; +import { SimpleUserResponseDto } from '@/lotus/dto/simple.user.response.dto'; +import { LotusService } from '@/lotus/lotus.service'; + +@Controller('/user') +export class UserController { + constructor( + private readonly userService: UserService, + private readonly lotusService: LotusService, + private readonly authService: AuthService, + private readonly gistService: GistService, + private configService: ConfigService + ) {} + private OAUTH_CLIENT_ID = this.configService.get('OAUTH_CLIENT_ID'); + private OAUTH_CLIENT_SECRETS = this.configService.get('OAUTH_CLIENT_SECRETS'); + private OAUTH_LOGIN_CALLBACK_URL = this.configService.get('OAUTH_LOGIN_CALLBACK_URL'); + + private TEST_GIT_TOKEN = this.configService.get('TEST_GIT_TOKEN'); + private TEST_GIT_LOGIN = this.configService.get('TEST_GIT_LOGIN'); + private TEST_GIT_PROFILE = this.configService.get('TEST_GIT_PROFILE'); + private TEST_GIT_ID = this.configService.get('TEST_GIT_ID'); + + @Get('test') + @HttpCode(200) + @ApiOperation({ summary: 'test용 access token 발급' }) + @ApiResponse({ status: 200, description: '실행 성공', type: TokenDTO }) + async testLogin(): Promise { + let testUser = await this.userService.findOne(this.TEST_GIT_ID); + if (!testUser) { + await this.userService.saveUser( + new UserCreateDto( + { + login: this.TEST_GIT_LOGIN, + avatar_url: this.TEST_GIT_PROFILE, + id: this.TEST_GIT_ID + }, + this.TEST_GIT_TOKEN + ) + ); + testUser = await this.userService.findOne(this.TEST_GIT_ID); + } + return TokenDTO.of(await this.userService.makeTestUser(testUser)); + } + + @Get('login') + @Redirect() + getGithubLoginPage() { + const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${this.OAUTH_CLIENT_ID}&redirect_uri=${this.OAUTH_LOGIN_CALLBACK_URL}&scope=gist`; + return { url: githubAuthUrl, statusCode: 301 }; + } + + @Get('login/callback') + async githubCallback(@Query('code') code: string) { + const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + client_id: this.OAUTH_CLIENT_ID, + client_secret: this.OAUTH_CLIENT_SECRETS, + code + }) + }); + const tokenData = await tokenResponse.json(); + const token = await this.userService.loginUser(tokenData); + return { token }; + } + + @Get('/lotus') + @HttpCode(200) + @ApiOperation({ summary: '사용자 lotus 목록 가져오기' }) + @ApiResponse({ status: 200, description: '실행 성공', type: LotusPublicDto }) + @ApiQuery({ name: 'page', type: String, example: '1', required: false }) + @ApiQuery({ name: 'size', type: String, example: '10', required: false }) + getUserLotus( + @Req() request: Request, + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number + ): Promise { + const userId = this.authService.getIdFromRequest(request); + return this.lotusService.getUserLotus(userId, page, size); + } + + @Get('') + @HttpCode(200) + @ApiOperation({ summary: '사용자 정보 가져오기' }) + @ApiResponse({ status: 200, description: '실행 성공', type: SimpleUserResponseDto }) + getUserInfo(@Req() request: Request): Promise { + const userId = this.authService.getIdFromRequest(request); + return this.userService.getSimpleUserInfoByUserId(userId); + } + + @Patch('') + @HttpCode(200) + @ApiOperation({ summary: '사용자 정보 수정하기' }) + @ApiBody({ type: UserPatchDTO }) + @ApiResponse({ status: 200, description: '실행 성공', type: UserPatchDTO }) + patchUserInfo(@Req() request: Request, @Body() userData: UserPatchDTO): Promise { + const userId = this.authService.getIdFromRequest(request); + return this.userService.patchUserDataByUserId(userId, userData); + } + + @Get('gist') + @HttpCode(200) + @ApiOperation({ summary: '사용자의 gist 목록 가져오기' }) + @ApiResponse({ status: 200, description: '실행 성공', type: ResponseAllGistsDto }) + async getGistPage( + @Req() request: Request, + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number + ): Promise { + const gitToken = await this.authService.getUserGitToken(this.authService.getIdFromRequest(request)); + return await this.gistService.getGistList(gitToken, page, size); + } + + @Get('gist/:gistId') + @HttpCode(200) + @ApiOperation({ summary: '사용자의 특정 gist의 내부 파일 데이터 가져오기' }) + @ApiResponse({ status: 200, description: '실행 성공', type: FileResponseDto }) + async getCommitFile(@Req() request: Request, @Param('gistId') gistId: string) { + const gitToken = await this.authService.getUserGitToken(this.authService.getIdFromRequest(request)); + const Files = await this.gistService.getGistById(gistId, gitToken); + const result = Files.files.map((file) => FileDto.ofGistApiFile(file)); + return FileResponseDto.ofFiles(result); + } +} diff --git a/apps/backend/src/user/user.entity.ts b/apps/backend/src/user/user.entity.ts index cc4bca8a..74ae3207 100644 --- a/apps/backend/src/user/user.entity.ts +++ b/apps/backend/src/user/user.entity.ts @@ -4,8 +4,7 @@ import { Lotus } from '@/lotus/lotus.entity'; @Entity() export class User { - //@PrimaryGeneratedColumn('uuid', { type: 'bigint' }) - @PrimaryGeneratedColumn('increment', { type: 'bigint', name: 'user_id' }) + @PrimaryGeneratedColumn('uuid', { name: 'user_id' }) userId: string; @Column() @@ -23,9 +22,9 @@ export class User { @Column({ name: 'git_id', unique: true }) gitId: number; - @OneToMany(() => Lotus, (lotus) => lotus.user) + @OneToMany(() => Lotus, (lotus) => lotus.user, { cascade: true }) lotuses: Lotus[]; - @OneToMany(() => Comment, (comment) => comment.user) + @OneToMany(() => Comment, (comment) => comment.user, { cascade: true }) comments: Comment[]; } diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts index ba8df4a9..dc8c9b29 100644 --- a/apps/backend/src/user/user.service.ts +++ b/apps/backend/src/user/user.service.ts @@ -1,93 +1,93 @@ -import { HttpException, HttpStatus, Inject, Injectable, forwardRef } from '@nestjs/common'; -import { isString } from 'class-validator'; -import { UserCreateDto } from './dto/user.create.dto'; -import { UserPatchDTO } from './dto/user.patch.dto'; -import { User } from './user.entity'; -import { UserRepository } from './user.repository'; -import { AuthService } from '@/auth/auth.service'; -import { SimpleUserResponseDto } from '@/lotus/dto/simple.user.response.dto'; - -@Injectable() -export class UserService { - constructor( - private readonly userRepository: UserRepository, - @Inject(forwardRef(() => AuthService)) - private authService: AuthService - ) {} - - async findOne(gitId: number): Promise { - return this.userRepository.findOneBy({ gitId }); - } - - async findOneByUserId(userId: string): Promise { - return this.userRepository.findOneBy({ userId }); - } - - async getSimpleUserInfoByUserId(userId: string): Promise { - const user = await this.userRepository.findOneBy({ userId }); - if (!user) { - throw new HttpException('user data is not found', HttpStatus.NOT_FOUND); - } - return SimpleUserResponseDto.ofUserDto(user); - } - - async patchUserDataByUserId(userId: string, updateData: UserPatchDTO): Promise { - const modifyingData = this.getObjUser(updateData); - - const result = await this.userRepository.update({ userId }, modifyingData); - if (!result.affected) { - throw new HttpException('user info not found', HttpStatus.NOT_FOUND); - } - const user = await this.userRepository.findOneBy({ userId }); - return UserPatchDTO.ofUser(user); - } - - getObjUser(updateData: UserPatchDTO) { - const obj: any = {}; - - if (updateData.nickname && isString(updateData.nickname)) { - obj.nickname = updateData.nickname; - } - if (updateData.profile) { - obj.profilePath = updateData.profile; - } - if (Object.keys(obj).length === 0) { - throw new HttpException('wrong user data', HttpStatus.BAD_REQUEST); - } - - return obj; - } - - async loginUser(tokenData) { - const accessToken = tokenData.access_token; - const userResponse = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${accessToken}` - } - }); - const inputUser = await userResponse.json(); - let user = await this.findOne(inputUser.id); - if (!user) { - await this.saveUser(new UserCreateDto(inputUser, accessToken)); - user = await this.findOne(inputUser.id); - } else { - await this.userRepository.update({ gitId: inputUser.id }, { gitToken: accessToken }); - } - const token = this.authService.createJwt(user.userId); - return token; - } - - async makeTestUser(user: User) { - return this.authService.createJwt(user.userId); - } - - async saveUser(user: User): Promise { - await this.userRepository.save(user); - } - - async findUserGistToken(userId: string): Promise { - const foundUser = await this.userRepository.findOneBy({ userId }); - if (!foundUser) throw new HttpException('user data not found', HttpStatus.NOT_FOUND); - return foundUser.gitToken; - } -} +import { HttpException, HttpStatus, Inject, Injectable, forwardRef } from '@nestjs/common'; +import { isString } from 'class-validator'; +import { UserCreateDto } from './dto/user.create.dto'; +import { UserPatchDTO } from './dto/user.patch.dto'; +import { User } from './user.entity'; +import { UserRepository } from './user.repository'; +import { AuthService } from '@/auth/auth.service'; +import { SimpleUserResponseDto } from '@/lotus/dto/simple.user.response.dto'; + +@Injectable() +export class UserService { + constructor( + private readonly userRepository: UserRepository, + @Inject(forwardRef(() => AuthService)) + private authService: AuthService + ) {} + + async findOne(gitId: number): Promise { + return this.userRepository.findOneBy({ gitId }); + } + + async findOneByUserId(userId: string): Promise { + return this.userRepository.findOneBy({ userId }); + } + + async getSimpleUserInfoByUserId(userId: string): Promise { + const user = await this.userRepository.findOneBy({ userId }); + if (!user) { + throw new HttpException('user data is not found', HttpStatus.NOT_FOUND); + } + return SimpleUserResponseDto.ofUserDto(user); + } + + async patchUserDataByUserId(userId: string, updateData: UserPatchDTO): Promise { + const modifyingData = this.getObjUser(updateData); + + const result = await this.userRepository.update({ userId }, modifyingData); + if (!result.affected) { + throw new HttpException('user info not found', HttpStatus.NOT_FOUND); + } + const user = await this.userRepository.findOneBy({ userId }); + return UserPatchDTO.ofUser(user); + } + + getObjUser(updateData: UserPatchDTO) { + const obj: any = {}; + + if (updateData.nickname && isString(updateData.nickname)) { + obj.nickname = updateData.nickname; + } + if (updateData.profile) { + obj.profilePath = updateData.profile; + } + if (Object.keys(obj).length === 0) { + throw new HttpException('wrong user data', HttpStatus.BAD_REQUEST); + } + + return obj; + } + + async loginUser(tokenData) { + const accessToken = tokenData.access_token; + const userResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + const inputUser = await userResponse.json(); + let user = await this.findOne(inputUser.id); + if (!user) { + await this.saveUser(new UserCreateDto(inputUser, accessToken)); + user = await this.findOne(inputUser.id); + } else { + await this.userRepository.update({ gitId: inputUser.id }, { gitToken: accessToken }); + } + const token = this.authService.createJwt(user.userId); + return token; + } + + async makeTestUser(user: User) { + return this.authService.createJwt(user.userId); + } + + async saveUser(user: User): Promise { + await this.userRepository.save(user); + } + + async findUserGistToken(userId: string): Promise { + const foundUser = await this.userRepository.findOneBy({ userId }); + if (!foundUser) throw new HttpException('user data not found', HttpStatus.NOT_FOUND); + return foundUser.gitToken; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac853589..6040c870 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,6 +313,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + embla-carousel-autoplay: + specifier: ^8.5.1 + version: 8.5.1(embla-carousel@8.3.1) embla-carousel-react: specifier: ^8.3.1 version: 8.3.1(react@18.3.1) @@ -3383,6 +3386,11 @@ packages: electron-to-chromium@1.5.50: resolution: {integrity: sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==} + embla-carousel-autoplay@8.5.1: + resolution: {integrity: sha512-FnZklFpePfp8wbj177UwVaGFehgs+ASVcJvYLWTtHuYKURynCc3IdDn2qrn0E5Qpa3g9yeGwCS4p8QkrZmO8xg==} + peerDependencies: + embla-carousel: 8.5.1 + embla-carousel-react@8.3.1: resolution: {integrity: sha512-gBY0zM+2ASvKFwRpTIOn2SLifFqOKKap9R/y0iCpJWS3bc8OHVEn2gAThGYl2uq0N+hu9aBiswffL++OYZOmDQ==} peerDependencies: @@ -9883,6 +9891,10 @@ snapshots: electron-to-chromium@1.5.50: {} + embla-carousel-autoplay@8.5.1(embla-carousel@8.3.1): + dependencies: + embla-carousel: 8.3.1 + embla-carousel-react@8.3.1(react@18.3.1): dependencies: embla-carousel: 8.3.1