Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions apps/backend/apps/admin/src/polygon/file/file.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common'
import type { ToolType } from '@prisma/client'
import type { FileUpload } from 'graphql-upload/processRequest.mjs'
import { UnprocessableDataException } from '@libs/exception'
import { PrismaService } from '@libs/prisma'

const MAX_TOOL_FILE_SIZE = 10 * 1024 * 1024 // 10MB

@Injectable()
export class FileService {
constructor(private readonly prisma: PrismaService) {}

async uploadPolygonToolFile(
problemId: number,
toolType: ToolType,
file: FileUpload
) {
const { filename, createReadStream } = file

//ReadStream → [chunk1, chunk2, chunk3, ...] → Buffer.concat
//→ 최종 Buffer로 변환해 → DB(PostgreSQL)에 저장
const chunks: Buffer[] = []
let total = 0
for await (const chunk of createReadStream()) {
total += chunk.length
if (total > MAX_TOOL_FILE_SIZE) {
throw new UnprocessableDataException('File size exceeds maximum limit')
}
chunks.push(chunk)
}
const fileContent = Buffer.concat(chunks).toString('utf-8')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

파일 내용을 utf-8 문자열로 변환하여 DB에 저장하고 있습니다. PR 설명에 따르면 .cpp 소스코드를 상정하고 있으나, 사용자가 바이너리 파일 등 의도하지 않은 형식의 파일을 업로드할 경우 데이터가 손상될 수 있습니다. 업로드된 파일의 mimetype이나 확장자를 검증하는 로직을 추가하여 의도한 형식의 파일만 처리하도록 하는 것이 안전합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확장자 검증 추가하겠습니다..!


// (problemId, toolType) unique — 재업로드 시 갱신
const tool = await this.prisma.polygonTool.upsert({
// eslint-disable-next-line @typescript-eslint/naming-convention
where: { problemId_toolType: { problemId, toolType } },
update: { fileName: filename, fileContent },
create: { problemId, toolType, fileName: filename, fileContent }
})
return tool
}

async deletePolygonFile(problemId: number, toolType: ToolType) {
return await this.prisma.polygonTool.delete({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

prisma.polygonTool.delete는 삭제하려는 레코드가 존재하지 않을 경우 에러를 발생시킵니다. 해당 도구가 이미 삭제되었거나 존재하지 않는 경우를 대비하여 예외 처리를 추가하거나, 존재 여부를 먼저 확인하는 것이 좋습니다. 단순히 삭제 여부만 중요하다면 deleteMany를 사용하는 것도 방법이지만, 현재 리턴 타입이 PolygonTool이므로 에러 발생 시 적절한 GraphQL 에러(예: NotFoundException)를 던지도록 처리하는 것이 더 명확합니다.

// eslint-disable-next-line @typescript-eslint/naming-convention
where: { problemId_toolType: { problemId, toolType } }
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface GeneratorRequest {
problemId: number
generatorLanguage: string
generatorCode: string
generatorArgs: string[]
solutionLanguage: string
solutionCode: string
testCaseCount: number
}

export interface ValidatorRequest {
problemId: number
language: string
validatorCode: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export interface GeneratorResultMessage {
submissionId: number
resultCode: number
judgeResult: {
generatedTestCases: number
totalTestCases: number
}
error: string
}

export interface ValidatorResultMessage {
submissionId: number
resultCode: number
judgeResult: {
isValid: boolean
testcaseCount: number
results: Array<{
id: number
isValid: boolean
}>
}
error: string
}
55 changes: 55 additions & 0 deletions apps/backend/apps/admin/src/polygon/polygon-pub.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Language, ToolType } from '@prisma/client'
import type { PolygonAMQPService } from '@libs/amqp'
import type { PrismaService } from '@libs/prisma'

export class PolygonPublicationService {
constructor(
private readonly prisma: PrismaService,
private readonly amqpService: PolygonAMQPService
) {}

async publishGeneratorMessage(
problemId: number,
generatorArgs: string[],
testCaseCount: number
) {
//DB에서 generator, solution 조회
const [generator, solution] = await Promise.all([
this.prisma.polygonTool.findUniqueOrThrow({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
problemId_toolType: { problemId, toolType: ToolType.Generator }
}
}),
this.prisma.polygonSolution.findUniqueOrThrow({
where: { problemId }
})
])

//실행 요청 메시지 publish
await this.amqpService.publishGeneratorMessage({
problemId,
generatorLanguage: Language.Cpp,
generatorCode: generator.fileContent,
generatorArgs,
solutionLanguage: solution.language,
solutionCode: solution.fileContent,
testCaseCount
})
}

async publishValidatorMessage(problemId: number) {
const validator = await this.prisma.polygonTool.findUniqueOrThrow({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
problemId_toolType: { problemId, toolType: ToolType.Validator }
}
})

await this.amqpService.publishValidatorMessage({
problemId,
language: Language.Cpp,
validatorCode: validator.fileContent
})
}
}
6 changes: 4 additions & 2 deletions apps/backend/apps/admin/src/polygon/polygon.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common'
import { AMQPModule } from '@libs/amqp'
import { RolesModule } from '@libs/auth'
import { FileService } from './file/file.service'
import { PolygonResolver } from './polygon.resolver'
import { PolygonService } from './polygon.service'

@Module({
imports: [RolesModule],
providers: [PolygonResolver, PolygonService]
imports: [RolesModule, AMQPModule],
providers: [PolygonResolver, PolygonService, FileService]
})
export class PolygonModule {}
28 changes: 26 additions & 2 deletions apps/backend/apps/admin/src/polygon/polygon.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import { Resolver } from '@nestjs/graphql'
import { Args, Int, Mutation, Resolver } from '@nestjs/graphql'
import { ToolType } from '@prisma/client'
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'
import type { FileUpload } from 'graphql-upload/processRequest.mjs'
import { UseDisableAdminGuard } from '@libs/auth'
import { PolygonProblem } from '@admin/@generated'
import { PolygonProblem, PolygonTool } from '@admin/@generated'
import { PolygonService } from './polygon.service'

@Resolver(() => PolygonProblem)
@UseDisableAdminGuard()
export class PolygonResolver {
constructor(private readonly polygonService: PolygonService) {}

@Mutation(() => PolygonTool)
async uploadPolygonTool(
@Args('problemId', { type: () => Int }) problemId: number,
@Args('toolType', { type: () => ToolType }) toolType: ToolType,
@Args('file', { type: () => GraphQLUpload }) file: Promise<FileUpload>
) {
return this.polygonService.uploadPolygonTool(
problemId,
toolType,
await file
)
}

@Mutation(() => PolygonTool)
async deletePolygonTool(
@Args('problemId', { type: () => Int }) problemId: number,
@Args('toolType', { type: () => ToolType }) toolType: ToolType
) {
return this.polygonService.deletePolygonTool(problemId, toolType)
}
}
42 changes: 41 additions & 1 deletion apps/backend/apps/admin/src/polygon/polygon.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
import { Injectable } from '@nestjs/common'
import { ToolType } from '@prisma/client'
import type { FileUpload } from 'graphql-upload/processRequest.mjs'
import { PrismaService } from '@libs/prisma'
import { FileService } from './file/file.service'
import { PolygonPublicationService } from './polygon-pub.service'

@Injectable()
export class PolygonService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly fileService: FileService,
private readonly publicationService: PolygonPublicationService
) {}

async uploadPolygonTool(
problemId: number,
toolType: ToolType,
file: FileUpload
) {
//DB에 파일 저장
await this.fileService.uploadPolygonToolFile(problemId, toolType, file)
}

async deletePolygonTool(problemId: number, toolType: ToolType) {
return this.fileService.deletePolygonFile(problemId, toolType)
}

//파일 실행
async runGenerator(
problemId: number,
generatorArgs: string[],
testCaseCount: number
) {
await this.publicationService.publishGeneratorMessage(
problemId,
generatorArgs,
testCaseCount
)
}

async runValidator(problemId: number) {
await this.publicationService.publishValidatorMessage(problemId)
}

//테스트케이스 저장
}
10 changes: 7 additions & 3 deletions apps/backend/libs/amqp/src/amqp.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'
import { CONSUME_CHANNEL, PUBLISH_CHANNEL } from '@libs/constants'
import { CheckAMQPService, JudgeAMQPService } from './amqp.service'
import {
CheckAMQPService,
JudgeAMQPService,
PolygonAMQPService
Comment thread
zero1177 marked this conversation as resolved.
} from './amqp.service'

@Module({
imports: [
Expand Down Expand Up @@ -41,7 +45,7 @@ import { CheckAMQPService, JudgeAMQPService } from './amqp.service'
inject: [ConfigService]
})
],
providers: [JudgeAMQPService, CheckAMQPService],
exports: [JudgeAMQPService, CheckAMQPService]
providers: [JudgeAMQPService, CheckAMQPService, PolygonAMQPService],
exports: [JudgeAMQPService, CheckAMQPService, PolygonAMQPService]
})
export class AMQPModule {}
Loading
Loading