Skip to content

Commit

Permalink
Merge pull request #299 from boostcampwm2023/release-1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
YuHyun-P authored Dec 18, 2023
2 parents a01d533 + 9e0e102 commit 3b3e7cd
Show file tree
Hide file tree
Showing 188 changed files with 9,489 additions and 879 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/backend-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,12 @@ jobs:
-e CONTAINER_SSH_PORT=${{ secrets.CONTAINER_SSH_PORT }} \
-e CONTAINER_SSH_USERNAME=${{ secrets.CONTAINER_SSH_USERNAME }} \
-e CONTAINER_SSH_PASSWORD=${{ secrets.CONTAINER_SSH_PASSWORD }} \
-e CONTAINER_GIT_USERNAME=${{ secrets.CONTAINER_GIT_USERNAME }} \
-e MONGODB_HOST=${{ secrets.MONGODB_HOST }} \
-e SECRET_KEY=${{ secrets.SECRET_KEY }} \
-e X_NCP_CLOVASTUDIO_API_KEY=${{ secrets.X_NCP_CLOVASTUDIO_API_KEY }} \
-e X_NCP_APIGW_API_KEY=${{ secrets.X_NCP_APIGW_API_KEY }} \
-e X_NCP_CLOVASTUDIO_REQUEST_ID=${{ secrets.X_NCP_CLOVASTUDIO_REQUEST_ID }} \
-e CONTAINER_SERVER_HOST=${{ secrets.CONTAINER_SERVER_HOST }} \
-e CONTAINER_POOL_MAX=${{ secrets.CONTAINER_POOL_MAX }} \
${{ secrets.DOCKERHUB_USERNAME }}/git-challenge-backend:0.1
6 changes: 4 additions & 2 deletions .github/workflows/frontend-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: "frontend-docker-build"
on:
push:
branches: [ "dev-fe" ]

jobs:
build:
name: Build and Test
Expand All @@ -25,7 +25,7 @@ jobs:
run: yarn install

- name: Build
run: |
run: |
cd packages/frontend
yarn build
Expand Down Expand Up @@ -53,6 +53,8 @@ jobs:
file: ./packages/frontend/Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/git-challenge-frontend:0.1
build-args: |
NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
deploy:
name: Deploy Frontend
Expand Down
540 changes: 500 additions & 40 deletions packages/backend/git-challenge-quiz.csv

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,22 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.16",
"@nestjs/typeorm": "^10.0.1",
"axios": "^1.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"jest": "^29.7.0",
"mongoose": "^8.0.1",
"nest-winston": "^1.9.4",
"papaparse": "^5.4.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"shell-escape": "^0.2.0",
"sqlite3": "^5.1.6",
"ssh2": "^1.14.0",
"typeorm": "^0.3.17",
"uuid": "^9.0.1",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1"
},
Expand All @@ -56,14 +61,15 @@
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/papaparse": "^5",
"@types/shell-escape": "^0.2.3",
"@types/ssh2": "^1",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"lint-staged": "^15.1.0",
"prettier": "^3.1.0",
"source-map-support": "^0.5.21",
Expand All @@ -89,6 +95,10 @@
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
"testEnvironment": "node",
"testTimeout": 20000,
"globals": {
"NODE_ENV": "test"
}
}
}
18 changes: 18 additions & 0 deletions packages/backend/src/ai/ai.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AiController } from './ai.controller';

describe('AiController', () => {
let controller: AiController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AiController],
}).compile();

controller = module.get<AiController>(AiController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
19 changes: 19 additions & 0 deletions packages/backend/src/ai/ai.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Body, Controller, Post } from '@nestjs/common';
import { AiRequestDto, AiResponseDto } from './dto/ai.dto';
import { AiService } from './ai.service';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';

@Controller('api/v1/ai')
export class AiController {
constructor(private readonly aiService: AiService) {}
@Post()
@ApiOperation({ summary: 'AI ๋‹ต๋ณ€์„ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.' })
@ApiResponse({
status: 200,
description: 'AI ๋‹ต๋ณ€์„ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.',
type: AiResponseDto,
})
async ai(@Body() aiDto: AiRequestDto): Promise<AiResponseDto> {
return await this.aiService.getApiResponse(aiDto.message);
}
}
9 changes: 9 additions & 0 deletions packages/backend/src/ai/ai.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AiController } from './ai.controller';
import { AiService } from './ai.service';

@Module({
controllers: [AiController],
providers: [AiService],
})
export class AiModule {}
18 changes: 18 additions & 0 deletions packages/backend/src/ai/ai.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AiService } from './ai.service';

describe('AiService', () => {
let service: AiService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AiService],
}).compile();

service = module.get<AiService>(AiService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
67 changes: 67 additions & 0 deletions packages/backend/src/ai/ai.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Inject, Injectable } from '@nestjs/common';
import axios from 'axios';
import { ConfigService } from '@nestjs/config';
import { AiResponseDto } from './dto/ai.dto';
import { Logger } from 'winston';
import { preview } from '../common/util';

@Injectable()
export class AiService {
private readonly headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
private readonly instance = axios.create({
baseURL:
'https://clovastudio.stream.ntruss.com/testapp/v1/chat-completions/HCX-002',
timeout: 50000,
headers: this.headers,
});

constructor(
private configService: ConfigService,
@Inject('winston') private readonly logger: Logger,
) {
this.instance.interceptors.request.use((config) => {
config.headers['X-NCP-CLOVASTUDIO-API-KEY'] = this.configService.get(
'X_NCP_CLOVASTUDIO_API_KEY',
);
config.headers['X-NCP-APIGW-API-KEY'] = this.configService.get(
'X_NCP_APIGW_API_KEY',
);
config.headers['X-NCP-CLOVASTUDIO-REQUEST-ID'] = this.configService.get(
'X_NCP_CLOVASTUDIO_REQUEST_ID',
);
return config;
});
}
async getApiResponse(message: string): Promise<AiResponseDto> {
const response = await this.instance.post('/', {
messages: [
{
role: 'system',
content:
'- Git ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.\\n- Git์— ๋Œ€ํ•œ ์งˆ๋ฌธ๋งŒ ๋Œ€๋‹ตํ•ฉ๋‹ˆ๋‹ค.\\n- Git ์‚ฌ์šฉ์ด ๋‚ฏ์„  ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์งˆ๋ฌธ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.\\n- Git ์„ค์น˜๋Š” ์ด๋ฏธ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค.\\n- ์„ค๋ช…์€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋ช…๋ฃŒํ•˜๊ณ  ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ช…๋ น์–ด ์œ„์ฃผ๋กœ ๋Œ€๋‹ตํ•ฉ๋‹ˆ๋‹ค.\\n- ์งˆ๋ฌธํ•œ ๊ฒƒ๋งŒ ๋Œ€๋‹ตํ•ฉ๋‹ˆ๋‹ค.\\n- Git ๋ช…๋ น์–ด๋กœ๋งŒ ํ•ด๋‹ต์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.\\n- ์˜ˆ๋ฅผ ๋“ค์–ด ์„ค๋ช…ํ•˜์ง€ ์•Š๋Š”๋‹ค.',
},
{
role: 'user',
content: message,
},
],
topP: 0.8,
topK: 0,
maxTokens: 512,
temperature: 0.3,
repeatPenalty: 5.0,
stopBefore: [],
includeAiFilters: true,
});

this.logger.log(
'info',
`AI response: ${preview(response.data.result.message.content)}`,
);

return { message: response.data.result.message.content };
}
}
18 changes: 18 additions & 0 deletions packages/backend/src/ai/dto/ai.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';

export class AiRequestDto {
@ApiProperty({
description: '์งˆ๋ฌธํ•  ๋‚ด์šฉ',
example: 'git์ด ๋ญ์•ผ?',
})
message: string;
}

export class AiResponseDto {
@ApiProperty({
description: '๋‹ต๋ณ€ ๋‚ด์šฉ',
example:
'Git์€ ๋ถ„์‚ฐ ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ(Distributed Version Control System)์œผ๋กœ, ์†Œ์Šค ์ฝ”๋“œ์˜ ๋ฒ„์ „์„ ๊ด€๋ฆฌํ•˜๊ณ  ํ˜‘์—…์„ ์ง€์›ํ•˜๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. Git์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.\\n\\n1. **๋ถ„์‚ฐ ์ €์žฅ์†Œ**: Git์€ ์ค‘์•™ ์ง‘์ค‘์‹ ์ €์žฅ์†Œ๊ฐ€ ์•„๋‹Œ ๋ถ„์‚ฐ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์‚ฌ์šฉ์ž๋Š” ์ž์‹ ์˜ ์ปดํ“จํ„ฐ์— ์ €์žฅ์†Œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ๋กœ์ปฌ ์ €์žฅ์†Œ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.\\n2. **๋น ๋ฅธ ์†๋„**: Git์€ ๋น ๋ฅธ ์†๋„๋กœ ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” Git์ด ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ•˜๊ณ , ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ ํŒŒ์ผ์„ ๋น ๋ฅด๊ฒŒ ๊ฒ€์ƒ‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.\\n3. **๋ฒ„์ „ ๊ด€๋ฆฌ**: Git์€ ์†Œ์Šค ์ฝ”๋“œ์˜ ๋ฒ„์ „์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๊ณ  ์ปค๋ฐ‹(commit)ํ•˜๋ฉด, ํ•ด๋‹น ํŒŒ์ผ์˜ ์ด์ „ ๋ฒ„์ „๊ณผ ์ดํ›„ ๋ฒ„์ „์„ ๋ชจ๋‘ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\\n4. **ํ˜‘์—… ์ง€์›**: Git์€ ํ˜‘์—…์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์™€ ํ•จ๊ป˜ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์„œ๋กœ์˜ ์ž‘์—… ๋‚ด์šฉ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\\n5. **๋ช…๋ น์–ด ๊ธฐ๋ฐ˜**: Git์€ ๋ช…๋ น์–ด ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” Git ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์ €์žฅ์†Œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ํŒŒ์ผ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\\n\\nGit์€ ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์™€ ์šด์˜์ฒด์ œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด Git์„ ์ด์šฉํ•˜์—ฌ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.',
})
message: string;
}
6 changes: 6 additions & 0 deletions packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { format } from 'winston';
import { typeOrmConfig } from './configs/typeorm.config';
import { QuizzesModule } from './quizzes/quizzes.module';
import { LoggingInterceptor } from './common/logging.interceptor';
import { QuizWizardModule } from './quiz-wizard/quiz-wizard.module';
import { AiModule } from './ai/ai.module';
import { CommandModule } from './command/command.module';

@Module({
imports: [
Expand Down Expand Up @@ -38,6 +41,9 @@ import { LoggingInterceptor } from './common/logging.interceptor';
),
),
}),
QuizWizardModule,
AiModule,
CommandModule,
],
controllers: [AppController],
providers: [
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/command/command.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { CommandService } from './command.service';

@Module({
providers: [CommandService],
exports: [CommandService],
})
export class CommandModule {}
18 changes: 18 additions & 0 deletions packages/backend/src/command/command.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommandService } from './command.service';

describe('CommandService', () => {
let service: CommandService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CommandService],
}).compile();

service = module.get<CommandService>(CommandService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
50 changes: 50 additions & 0 deletions packages/backend/src/command/command.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { Logger } from 'winston';
import { preview, processCarriageReturns } from '../common/util';

@Injectable()
export class CommandService {
private readonly host: string;
private readonly instance;
constructor(
private readonly configService: ConfigService,
@Inject('winston') private readonly logger: Logger,
) {
this.host = this.configService.get<string>('CONTAINER_SERVER_HOST');
this.instance = axios.create({
baseURL: this.host,
timeout: 10000,
});
}

async executeCommand(
...commands: string[]
): Promise<{ stdoutData: string; stderrData: string }> {
try {
const command = commands.join('; ');
this.logger.log('info', `command: ${preview(command, 40)}`);
const response = await this.instance.post('/', { command });
return {
stdoutData: processCarriageReturns(response.data.stdoutData),
stderrData: processCarriageReturns(response.data.stderrData),
};
} catch (error) {
this.logger.log('info', error);
}
}

async executeCron(
...commands: string[]
): Promise<{ stdoutData: string; stderrData: string }> {
try {
const command = commands.join('; ');
this.logger.log('info', `command: ${preview(command, 40)}`);
const response = await this.instance.post('/cron', { command });
return response.data;
} catch (error) {
this.logger.log('info', error);
}
}
}
42 changes: 42 additions & 0 deletions packages/backend/src/common/command.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';

@Injectable()
export class CommandGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const mode = request.body['mode'];
const message = request.body['message'];
if (
!(
typeof mode === 'string' &&
typeof message === 'string' &&
(mode === 'editor' ||
(mode === 'command' &&
message.startsWith('git') &&
!this.isMessageIncluded(message, [
';',
'>',
'|',
'<',
'&',
'$',
'(',
')',
'{',
'}',
])))
)
) {
throw new ForbiddenException('๊ธˆ์ง€๋œ ๋ช…๋ น์ž…๋‹ˆ๋‹ค');
}
return true;
}
private isMessageIncluded(message: string, keywords: string[]): boolean {
return keywords.some((keyword) => message.includes(keyword));
}
}
Loading

0 comments on commit 3b3e7cd

Please sign in to comment.