diff --git a/be/algo-with-me-api/package.json b/be/algo-with-me-api/package.json index eaeb6d6..cdfaf68 100644 --- a/be/algo-with-me-api/package.json +++ b/be/algo-with-me-api/package.json @@ -20,11 +20,14 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs/common": "^10.0.0", + "@nestjs/common": "^10.2.8", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "pg": "^8.11.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", diff --git a/be/algo-with-me-api/pnpm-lock.yaml b/be/algo-with-me-api/pnpm-lock.yaml index 737c168..2f37c20 100644 --- a/be/algo-with-me-api/pnpm-lock.yaml +++ b/be/algo-with-me-api/pnpm-lock.yaml @@ -6,20 +6,29 @@ settings: dependencies: '@nestjs/common': - specifier: ^10.0.0 - version: 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: ^10.2.8 + version: 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/config': specifier: ^3.1.1 version: 3.1.1(@nestjs/common@10.2.8)(reflect-metadata@0.1.13) '@nestjs/core': specifier: ^10.0.0 version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/mapped-types': + specifier: '*' + version: 2.0.3(@nestjs/common@10.2.8)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) '@nestjs/platform-express': specifier: ^10.0.0 version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) '@nestjs/typeorm': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.0 + version: 0.14.0 pg: specifier: ^8.11.3 version: 8.11.3 @@ -903,7 +912,7 @@ packages: - webpack-cli dev: true - /@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1): + /@nestjs/common@10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==} peerDependencies: class-transformer: '*' @@ -916,6 +925,8 @@ packages: class-validator: optional: true dependencies: + class-transformer: 0.5.1 + class-validator: 0.14.0 iterare: 1.2.1 reflect-metadata: 0.1.13 rxjs: 7.8.1 @@ -928,7 +939,7 @@ packages: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 reflect-metadata: ^0.1.13 dependencies: - '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) dotenv: 16.3.1 dotenv-expand: 10.0.0 lodash: 4.17.21 @@ -954,7 +965,7 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 @@ -967,13 +978,32 @@ packages: transitivePeerDependencies: - encoding + /@nestjs/mapped-types@2.0.3(@nestjs/common@10.2.8)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + class-transformer: 0.5.1 + class-validator: 0.14.0 + reflect-metadata: 0.1.13 + dev: false + /@nestjs/platform-express@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8): resolution: {integrity: sha512-WoSSVtwIRc5AdGMHWVzWZK4JZLT0f4o2xW8P9gQvcX+omL8W1kXCfY8GQYXNBG84XmBNYH8r0FtC8oMe/lH5NQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 dependencies: - '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 @@ -1011,7 +1041,7 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) tslib: 2.6.2 @@ -1026,7 +1056,7 @@ packages: rxjs: ^7.2.0 typeorm: ^0.3.0 dependencies: - '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) reflect-metadata: 0.1.13 rxjs: 7.8.1 @@ -1297,6 +1327,9 @@ packages: '@types/superagent': 4.1.21 dev: true + /@types/validator@13.11.6: + resolution: {integrity: sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==} + /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true @@ -2092,6 +2125,16 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + /class-validator@0.14.0: + resolution: {integrity: sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==} + dependencies: + '@types/validator': 13.11.6 + libphonenumber-js: 1.10.49 + validator: 13.11.0 + /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -4313,6 +4356,9 @@ packages: type-check: 0.4.0 dev: true + /libphonenumber-js@1.10.49: + resolution: {integrity: sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ==} + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true @@ -6065,6 +6111,10 @@ packages: convert-source-map: 2.0.0 dev: true + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} diff --git a/be/algo-with-me-api/src/app.controller.spec.ts b/be/algo-with-me-api/src/app.controller.spec.ts deleted file mode 100644 index ccea57f..0000000 --- a/be/algo-with-me-api/src/app.controller.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/be/algo-with-me-api/src/app.controller.ts b/be/algo-with-me-api/src/app.controller.ts deleted file mode 100644 index 0a8e973..0000000 --- a/be/algo-with-me-api/src/app.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -import { AppService } from '@src/app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/be/algo-with-me-api/src/app.module.ts b/be/algo-with-me-api/src/app.module.ts index b6a83c8..e4d68c9 100644 --- a/be/algo-with-me-api/src/app.module.ts +++ b/be/algo-with-me-api/src/app.module.ts @@ -2,8 +2,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AppController } from '@src/app.controller'; -import { AppService } from '@src/app.service'; +import { CompetitionModule } from './competition/competition.module'; +import { Problem } from './competition/entities/problem.entity'; @Module({ imports: [ @@ -19,10 +19,9 @@ import { AppService } from '@src/app.service'; password: process.env.DB_PASSWORD, database: process.env.DB_NAME, synchronize: true, - entities: [], + entities: [Problem], }), + CompetitionModule, ], - controllers: [AppController], - providers: [AppService], }) export class AppModule {} diff --git a/be/algo-with-me-api/src/app.service.ts b/be/algo-with-me-api/src/app.service.ts deleted file mode 100644 index 8c5c12b..0000000 --- a/be/algo-with-me-api/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello api!'; - } -} diff --git a/be/algo-with-me-api/src/competition/competition.controller.ts b/be/algo-with-me-api/src/competition/competition.controller.ts new file mode 100644 index 0000000..8dd928d --- /dev/null +++ b/be/algo-with-me-api/src/competition/competition.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Param } from '@nestjs/common'; + +import { CompetitionService } from './competition.service'; + +@Controller('competitions') +export class CompetitionController { + constructor(private readonly competitionService: CompetitionService) {} + + @Get('problems/:id') + findOne(@Param('id') id: number) { + return this.competitionService.findOneProblem(id); + } +} diff --git a/be/algo-with-me-api/src/competition/competition.module.ts b/be/algo-with-me-api/src/competition/competition.module.ts new file mode 100644 index 0000000..75197a5 --- /dev/null +++ b/be/algo-with-me-api/src/competition/competition.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { CompetitionController } from './competition.controller'; +import { CompetitionService } from './competition.service'; +import { Problem } from './entities/problem.entity'; +import { ProblemController } from './problem.controller'; +import { ProblemService } from './problem.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Problem])], + controllers: [ProblemController, CompetitionController], + providers: [ProblemService, CompetitionService], +}) +export class CompetitionModule {} diff --git a/be/algo-with-me-api/src/competition/competition.service.ts b/be/algo-with-me-api/src/competition/competition.service.ts new file mode 100644 index 0000000..8cc3d2a --- /dev/null +++ b/be/algo-with-me-api/src/competition/competition.service.ts @@ -0,0 +1,31 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { existsSync, readFileSync } from 'fs'; +import * as path from 'path'; + +import { Problem } from './entities/problem.entity'; + +@Injectable() +export class CompetitionService { + constructor(@InjectRepository(Problem) private readonly problemRepository: Repository) {} + + async findOneProblem(id: number) { + const problem = await this.problemRepository.findOneBy({ id }); + const fileName = id.toString() + '.md'; + const paths = path.join(process.env.PROBLEM_PATH, id.toString(), fileName); + if (!existsSync(paths)) throw new NotFoundException('문제 파일을 찾을 수 없습니다.'); + const content = readFileSync(paths).toString(); + return { + id: problem.id, + title: problem.title, + timeLimit: problem.timeLimit, + memoryLimit: problem.memoryLimit, + content: content, + solutionCode: problem.solutionCode, + testcases: '임시', + createdAt: problem.createdAt, + }; + } +} diff --git a/be/algo-with-me-api/src/competition/dto/create-problem.dto.ts b/be/algo-with-me-api/src/competition/dto/create-problem.dto.ts new file mode 100644 index 0000000..76f2f8f --- /dev/null +++ b/be/algo-with-me-api/src/competition/dto/create-problem.dto.ts @@ -0,0 +1,34 @@ +import { IsNotEmpty } from 'class-validator'; + +import { Problem } from '../entities/problem.entity'; + +export class CreateProblemDto { + @IsNotEmpty() + title: string; + + @IsNotEmpty() + timeLimit: number; + + @IsNotEmpty() + memoryLimit: number; + + @IsNotEmpty() + testcaseNum: number; + + @IsNotEmpty() + frameCode: string; + + @IsNotEmpty() + solutionCode: string; + + toEntity(): Problem { + const problem = new Problem(); + problem.title = this.title; + problem.timeLimit = this.timeLimit; + problem.memoryLimit = this.memoryLimit; + problem.testcaseNum = this.testcaseNum; + problem.frameCode = this.frameCode; + problem.solutionCode = this.solutionCode; + return problem; + } +} diff --git a/be/algo-with-me-api/src/competition/entities/problem.entity.ts b/be/algo-with-me-api/src/competition/entities/problem.entity.ts new file mode 100644 index 0000000..381d293 --- /dev/null +++ b/be/algo-with-me-api/src/competition/entities/problem.entity.ts @@ -0,0 +1,37 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Problem { + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @Column() + timeLimit: number; + + @Column() + memoryLimit: number; + + @Column() + testcaseNum: number; + + @Column('text') + frameCode: string; + + @Column('text') + solutionCode: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/be/algo-with-me-api/src/competition/problem.controller.ts b/be/algo-with-me-api/src/competition/problem.controller.ts new file mode 100644 index 0000000..c926d12 --- /dev/null +++ b/be/algo-with-me-api/src/competition/problem.controller.ts @@ -0,0 +1,44 @@ +import { + Controller, + Get, + Post, + Body, + UsePipes, + ValidationPipe, + Param, + Delete, +} from '@nestjs/common'; + +import { CreateProblemDto } from './dto/create-problem.dto'; +import { ProblemService } from './problem.service'; + +@Controller('problems') +export class ProblemController { + constructor(private readonly problemService: ProblemService) {} + + @Post() + @UsePipes(new ValidationPipe({ transform: true })) + create(@Body() createProblemDto: CreateProblemDto) { + return this.problemService.create(createProblemDto); + } + + @Get() + findAll() { + return this.problemService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: number) { + return this.problemService.findOne(id); + } + + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateCompetitionDto: UpdateCompetitionDto) { + // return this.competitionService.update(+id, updateCompetitionDto); + // } + + @Delete(':id') + remove(@Param('id') id: number) { + this.problemService.remove(id); + } +} diff --git a/be/algo-with-me-api/src/competition/problem.service.ts b/be/algo-with-me-api/src/competition/problem.service.ts new file mode 100644 index 0000000..f4dfb8a --- /dev/null +++ b/be/algo-with-me-api/src/competition/problem.service.ts @@ -0,0 +1,55 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { existsSync, readFileSync } from 'fs'; +import * as path from 'path'; + +import { CreateProblemDto } from './dto/create-problem.dto'; +import { Problem } from './entities/problem.entity'; + +@Injectable() +export class ProblemService { + constructor(@InjectRepository(Problem) private readonly problemRepository: Repository) {} + + create(createProblemDto: CreateProblemDto) { + const problem: Problem = createProblemDto.toEntity(); + + const savedProblem = this.problemRepository.save(problem); + return savedProblem; + } + + async findAll() { + const problems = await this.problemRepository.find(); + return problems.map((problem: Problem) => { + return { + id: problem.id, + title: problem.title, + }; + }); + } + + async findOne(id: number) { + const problem = await this.problemRepository.findOneBy({ id }); + const fileName = id.toString() + '.md'; + const paths = path.join(process.env.PROBLEM_PATH, id.toString(), fileName); + if (!existsSync(paths)) throw new NotFoundException('문제 파일을 찾을 수 없습니다.'); + const content = readFileSync(paths).toString(); + return { + id: problem.id, + title: problem.title, + timeLimit: problem.timeLimit, + memoryLimit: problem.memoryLimit, + content: content, + createdAt: problem.createdAt, + }; + } + + // update(id: number, updateCompetitionDto: UpdateCompetitionDto) { + // return `This action updates a #${id} competition`; + // } + + remove(id: number) { + this.problemRepository.delete({ id }); + } +} diff --git a/be/algo-with-me-api/tsconfig.json b/be/algo-with-me-api/tsconfig.json index f52a0b1..5627a41 100644 --- a/be/algo-with-me-api/tsconfig.json +++ b/be/algo-with-me-api/tsconfig.json @@ -18,7 +18,7 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, "paths": { - "@src/*": ["./src/*"], + "@src/*": ["./src/*"] } } }