diff --git a/BE/package-lock.json b/BE/package-lock.json index 2f28e82..60ee557 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -31,6 +31,7 @@ "rxjs": "^7.8.1", "typeorm": "^0.3.17", "typeorm-transactional": "^0.5.0", + "uuid": "^9.0.1", "winston": "^3.11.0", "winston-daily-rotate-file": "^4.7.1" }, @@ -44,6 +45,7 @@ "@types/multer": "^1.4.10", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", @@ -2279,6 +2281,12 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@types/validator": { "version": "13.11.6", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz", diff --git a/BE/package.json b/BE/package.json index 471c7ec..3170ccd 100644 --- a/BE/package.json +++ b/BE/package.json @@ -42,6 +42,7 @@ "rxjs": "^7.8.1", "typeorm": "^0.3.17", "typeorm-transactional": "^0.5.0", + "uuid": "^9.0.1", "winston": "^3.11.0", "winston-daily-rotate-file": "^4.7.1" }, @@ -55,6 +56,7 @@ "@types/multer": "^1.4.10", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", diff --git a/BE/src/storage/storage.module.ts b/BE/src/storage/storage.module.ts index 9183e1b..90953e8 100644 --- a/BE/src/storage/storage.module.ts +++ b/BE/src/storage/storage.module.ts @@ -1,7 +1,9 @@ -import { Module } from '@nestjs/common'; import { StorageService } from './storage.service'; +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; @Module({ + imports: [HttpModule], providers: [StorageService], exports: [StorageService], }) diff --git a/BE/src/storage/storage.service.ts b/BE/src/storage/storage.service.ts index 3d6be9a..96d584a 100644 --- a/BE/src/storage/storage.service.ts +++ b/BE/src/storage/storage.service.ts @@ -1,9 +1,13 @@ -import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; import * as AWS from 'aws-sdk'; @Injectable() export class StorageService { + constructor(private readonly httpService: HttpService) {} + private readonly s3: AWS.S3 = new AWS.S3({ endpoint: 'https://kr.object.ncloudstorage.com', region: process.env.AWS_REGION, @@ -21,6 +25,12 @@ export class StorageService { } async upload(path: string, file: Express.Multer.File) { + const analyzeImage = await this.analyzeImage( + file.originalname, + file.buffer.toString('base64') + ); + this.validateAnalyzedImage(analyzeImage); + const uploadParams: AWS.S3.PutObjectRequest = { Bucket: this.bucketName, Key: path + this.generateFilename(file.originalname), @@ -54,4 +64,44 @@ export class StorageService { return await this.s3.deleteObject(deleteParams).promise(); } + + private async analyzeImage(filename: string, fileData: string) { + const url = `https://clovagreeneye.apigw.ntruss.com/custom/v1/${process.env.GREENEYE_DOMAIN_ID}/${process.env.GREENEYE_SIGNATURE}/predict`; + const { + data: { images }, + } = await firstValueFrom( + this.httpService.post( + url, + { + version: 'V1', + requestId: uuidv4(), + timestamp: Date.now(), + images: [{ name: filename, data: fileData }], + }, + { + headers: { + 'X-GREEN-EYE-SECRET': process.env.GREENEYE_SECRET_KEY, + 'Content-Type': 'application/json', + }, + } + ) + ); + + return images[0].result; + } + + private validateAnalyzedImage(result: { + adult: { confidence: number }; + normal: { confidence: number }; + sexy: { confidence: number }; + porn: { confidence: number }; + }) { + const normal = result.normal.confidence; + const sumOfOthers = + result.adult.confidence + result.porn.confidence + result.sexy.confidence; + + if (normal < sumOfOthers) { + throw new BadRequestException('유해한 이미지가 포함되어 있습니다.'); + } + } } diff --git a/BE/src/timelines/timelines.constants.ts b/BE/src/timelines/timelines.constants.ts index 9d89a3e..bffeb3a 100644 --- a/BE/src/timelines/timelines.constants.ts +++ b/BE/src/timelines/timelines.constants.ts @@ -2,3 +2,5 @@ export const DATA_SOURCE = 'DATA_SOURCE'; export const TIMELINES_REPOSITORY = 'TIMELINES_REPOSITORY'; export const KAKAO_KEYWORD_SEARCH = 'https://dapi.kakao.com/v2/local/search/keyword'; +export const PAPAGO_URL = + 'https://naveropenapi.apigw.ntruss.com/nmt/v1/translation'; diff --git a/BE/src/timelines/timelines.controller.ts b/BE/src/timelines/timelines.controller.ts index 0a69d44..7cd6a34 100644 --- a/BE/src/timelines/timelines.controller.ts +++ b/BE/src/timelines/timelines.controller.ts @@ -20,6 +20,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { CreateTimelineDto } from './dto/create-timeline.dto'; import { UpdateTimelineDto } from './dto/update-timeline.dto'; import { + ApiBadRequestResponse, ApiBearerAuth, ApiConsumes, ApiCreatedResponse, @@ -88,6 +89,15 @@ export class TimelinesController { }, }, }) + @ApiBadRequestResponse({ + schema: { + example: { + message: '유해한 이미지가 포함되어 있습니다.', + error: 'Bad Request', + statusCode: 400, + }, + }, + }) async create( @Req() request, @UploadedFile() image: Express.Multer.File, @@ -168,6 +178,15 @@ export class TimelinesController { }) @ApiConsumes('multipart/form-data') @ApiOkResponse({ schema: { example: update_OK } }) + @ApiBadRequestResponse({ + schema: { + example: { + message: '유해한 이미지가 포함되어 있습니다.', + error: 'Bad Request', + statusCode: 400, + }, + }, + }) async update( @Req() request, @Param('id', ParseUUIDPipe) id: string, diff --git a/BE/src/timelines/timelines.service.ts b/BE/src/timelines/timelines.service.ts index 312b034..5f0a99b 100644 --- a/BE/src/timelines/timelines.service.ts +++ b/BE/src/timelines/timelines.service.ts @@ -12,7 +12,7 @@ import { TimelinesRepository } from './timelines.repository'; import { Timeline } from './entities/timeline.entity'; import { StorageService } from '../storage/storage.service'; import { PostingsService } from '../postings/postings.service'; -import { KAKAO_KEYWORD_SEARCH } from './timelines.constants'; +import { KAKAO_KEYWORD_SEARCH, PAPAGO_URL } from './timelines.constants'; import { PostingsRepository } from '../postings/repositories/postings.repository'; @Injectable() @@ -166,7 +166,6 @@ export class TimelinesService { async translate(id: string) { const { description } = await this.findOne(id); - const url = 'https://naveropenapi.apigw.ntruss.com/nmt/v1/translation'; const body = { source: 'ko', target: 'en', @@ -177,7 +176,7 @@ export class TimelinesService { message: { result }, }, } = await firstValueFrom( - this.httpService.post(url, body, { + this.httpService.post(PAPAGO_URL, body, { headers: { 'Content-Type': 'application/json', 'X-NCP-APIGW-API-KEY-ID': process.env.X_NCP_APIGW_API_KEY_ID, diff --git a/BE/src/users/users.module.ts b/BE/src/users/users.module.ts index 3a6dfd0..05b2878 100644 --- a/BE/src/users/users.module.ts +++ b/BE/src/users/users.module.ts @@ -1,15 +1,15 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -import { StorageService } from 'src/storage/storage.service'; import { usersProvider } from './users.providers'; import { UserRepository } from './users.repository'; -import { DatabaseModule } from 'src/database/database.module'; +import { StorageModule } from '../storage/storage.module'; +import { DatabaseModule } from '../database/database.module'; @Module({ - imports: [DatabaseModule], + imports: [DatabaseModule, StorageModule], controllers: [UsersController], - providers: [UsersService, StorageService, ...usersProvider, UserRepository], + providers: [UsersService, ...usersProvider, UserRepository], exports: [UserRepository, UsersService], }) export class UsersModule {}