Skip to content

Commit 3b3e7cd

Browse files
authored
Merge pull request #299 from boostcampwm2023/release-1.0.0
2 parents a01d533 + 9e0e102 commit 3b3e7cd

File tree

188 files changed

+9489
-879
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

188 files changed

+9489
-879
lines changed

โ€Ž.github/workflows/backend-deploy.yml

+7
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,12 @@ jobs:
7474
-e CONTAINER_SSH_PORT=${{ secrets.CONTAINER_SSH_PORT }} \
7575
-e CONTAINER_SSH_USERNAME=${{ secrets.CONTAINER_SSH_USERNAME }} \
7676
-e CONTAINER_SSH_PASSWORD=${{ secrets.CONTAINER_SSH_PASSWORD }} \
77+
-e CONTAINER_GIT_USERNAME=${{ secrets.CONTAINER_GIT_USERNAME }} \
7778
-e MONGODB_HOST=${{ secrets.MONGODB_HOST }} \
79+
-e SECRET_KEY=${{ secrets.SECRET_KEY }} \
80+
-e X_NCP_CLOVASTUDIO_API_KEY=${{ secrets.X_NCP_CLOVASTUDIO_API_KEY }} \
81+
-e X_NCP_APIGW_API_KEY=${{ secrets.X_NCP_APIGW_API_KEY }} \
82+
-e X_NCP_CLOVASTUDIO_REQUEST_ID=${{ secrets.X_NCP_CLOVASTUDIO_REQUEST_ID }} \
83+
-e CONTAINER_SERVER_HOST=${{ secrets.CONTAINER_SERVER_HOST }} \
84+
-e CONTAINER_POOL_MAX=${{ secrets.CONTAINER_POOL_MAX }} \
7885
${{ secrets.DOCKERHUB_USERNAME }}/git-challenge-backend:0.1

โ€Ž.github/workflows/frontend-deploy.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: "frontend-docker-build"
33
on:
44
push:
55
branches: [ "dev-fe" ]
6-
6+
77
jobs:
88
build:
99
name: Build and Test
@@ -25,7 +25,7 @@ jobs:
2525
run: yarn install
2626

2727
- name: Build
28-
run: |
28+
run: |
2929
cd packages/frontend
3030
yarn build
3131
@@ -53,6 +53,8 @@ jobs:
5353
file: ./packages/frontend/Dockerfile
5454
push: true
5555
tags: ${{ secrets.DOCKERHUB_USERNAME }}/git-challenge-frontend:0.1
56+
build-args: |
57+
NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
5658
5759
deploy:
5860
name: Deploy Frontend

โ€Žpackages/backend/git-challenge-quiz.csv

+500-40
Large diffs are not rendered by default.

โ€Žpackages/backend/package.json

+12-2
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,22 @@
3333
"@nestjs/platform-express": "^10.0.0",
3434
"@nestjs/swagger": "^7.1.16",
3535
"@nestjs/typeorm": "^10.0.1",
36+
"axios": "^1.6.2",
3637
"class-transformer": "^0.5.1",
3738
"class-validator": "^0.14.0",
3839
"cookie-parser": "^1.4.6",
40+
"dotenv": "^16.3.1",
41+
"jest": "^29.7.0",
3942
"mongoose": "^8.0.1",
4043
"nest-winston": "^1.9.4",
4144
"papaparse": "^5.4.1",
4245
"reflect-metadata": "^0.1.13",
4346
"rxjs": "^7.8.1",
47+
"shell-escape": "^0.2.0",
4448
"sqlite3": "^5.1.6",
4549
"ssh2": "^1.14.0",
4650
"typeorm": "^0.3.17",
51+
"uuid": "^9.0.1",
4752
"winston": "^3.11.0",
4853
"winston-daily-rotate-file": "^4.7.1"
4954
},
@@ -56,14 +61,15 @@
5661
"@types/jest": "^29.5.2",
5762
"@types/node": "^20.3.1",
5863
"@types/papaparse": "^5",
64+
"@types/shell-escape": "^0.2.3",
5965
"@types/ssh2": "^1",
6066
"@types/supertest": "^2.0.12",
67+
"@types/uuid": "^9",
6168
"@typescript-eslint/eslint-plugin": "^6.0.0",
6269
"@typescript-eslint/parser": "^6.0.0",
6370
"eslint": "^8.53.0",
6471
"eslint-config-prettier": "^9.0.0",
6572
"eslint-plugin-prettier": "^5.0.0",
66-
"jest": "^29.5.0",
6773
"lint-staged": "^15.1.0",
6874
"prettier": "^3.1.0",
6975
"source-map-support": "^0.5.21",
@@ -89,6 +95,10 @@
8995
"**/*.(t|j)s"
9096
],
9197
"coverageDirectory": "../coverage",
92-
"testEnvironment": "node"
98+
"testEnvironment": "node",
99+
"testTimeout": 20000,
100+
"globals": {
101+
"NODE_ENV": "test"
102+
}
93103
}
94104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AiController } from './ai.controller';
3+
4+
describe('AiController', () => {
5+
let controller: AiController;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
controllers: [AiController],
10+
}).compile();
11+
12+
controller = module.get<AiController>(AiController);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(controller).toBeDefined();
17+
});
18+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Body, Controller, Post } from '@nestjs/common';
2+
import { AiRequestDto, AiResponseDto } from './dto/ai.dto';
3+
import { AiService } from './ai.service';
4+
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
5+
6+
@Controller('api/v1/ai')
7+
export class AiController {
8+
constructor(private readonly aiService: AiService) {}
9+
@Post()
10+
@ApiOperation({ summary: 'AI ๋‹ต๋ณ€์„ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.' })
11+
@ApiResponse({
12+
status: 200,
13+
description: 'AI ๋‹ต๋ณ€์„ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.',
14+
type: AiResponseDto,
15+
})
16+
async ai(@Body() aiDto: AiRequestDto): Promise<AiResponseDto> {
17+
return await this.aiService.getApiResponse(aiDto.message);
18+
}
19+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
import { AiController } from './ai.controller';
3+
import { AiService } from './ai.service';
4+
5+
@Module({
6+
controllers: [AiController],
7+
providers: [AiService],
8+
})
9+
export class AiModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AiService } from './ai.service';
3+
4+
describe('AiService', () => {
5+
let service: AiService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [AiService],
10+
}).compile();
11+
12+
service = module.get<AiService>(AiService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import axios from 'axios';
3+
import { ConfigService } from '@nestjs/config';
4+
import { AiResponseDto } from './dto/ai.dto';
5+
import { Logger } from 'winston';
6+
import { preview } from '../common/util';
7+
8+
@Injectable()
9+
export class AiService {
10+
private readonly headers = {
11+
'Content-Type': 'application/json',
12+
Accept: 'application/json',
13+
};
14+
private readonly instance = axios.create({
15+
baseURL:
16+
'https://clovastudio.stream.ntruss.com/testapp/v1/chat-completions/HCX-002',
17+
timeout: 50000,
18+
headers: this.headers,
19+
});
20+
21+
constructor(
22+
private configService: ConfigService,
23+
@Inject('winston') private readonly logger: Logger,
24+
) {
25+
this.instance.interceptors.request.use((config) => {
26+
config.headers['X-NCP-CLOVASTUDIO-API-KEY'] = this.configService.get(
27+
'X_NCP_CLOVASTUDIO_API_KEY',
28+
);
29+
config.headers['X-NCP-APIGW-API-KEY'] = this.configService.get(
30+
'X_NCP_APIGW_API_KEY',
31+
);
32+
config.headers['X-NCP-CLOVASTUDIO-REQUEST-ID'] = this.configService.get(
33+
'X_NCP_CLOVASTUDIO_REQUEST_ID',
34+
);
35+
return config;
36+
});
37+
}
38+
async getApiResponse(message: string): Promise<AiResponseDto> {
39+
const response = await this.instance.post('/', {
40+
messages: [
41+
{
42+
role: 'system',
43+
content:
44+
'- Git ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.\\n- Git์— ๋Œ€ํ•œ ์งˆ๋ฌธ๋งŒ ๋Œ€๋‹ตํ•ฉ๋‹ˆ๋‹ค.\\n- Git ์‚ฌ์šฉ์ด ๋‚ฏ์„  ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์งˆ๋ฌธ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.\\n- Git ์„ค์น˜๋Š” ์ด๋ฏธ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค.\\n- ์„ค๋ช…์€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋ช…๋ฃŒํ•˜๊ณ  ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ช…๋ น์–ด ์œ„์ฃผ๋กœ ๋Œ€๋‹ตํ•ฉ๋‹ˆ๋‹ค.\\n- ์งˆ๋ฌธํ•œ ๊ฒƒ๋งŒ ๋Œ€๋‹ตํ•ฉ๋‹ˆ๋‹ค.\\n- Git ๋ช…๋ น์–ด๋กœ๋งŒ ํ•ด๋‹ต์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.\\n- ์˜ˆ๋ฅผ ๋“ค์–ด ์„ค๋ช…ํ•˜์ง€ ์•Š๋Š”๋‹ค.',
45+
},
46+
{
47+
role: 'user',
48+
content: message,
49+
},
50+
],
51+
topP: 0.8,
52+
topK: 0,
53+
maxTokens: 512,
54+
temperature: 0.3,
55+
repeatPenalty: 5.0,
56+
stopBefore: [],
57+
includeAiFilters: true,
58+
});
59+
60+
this.logger.log(
61+
'info',
62+
`AI response: ${preview(response.data.result.message.content)}`,
63+
);
64+
65+
return { message: response.data.result.message.content };
66+
}
67+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class AiRequestDto {
4+
@ApiProperty({
5+
description: '์งˆ๋ฌธํ•  ๋‚ด์šฉ',
6+
example: 'git์ด ๋ญ์•ผ?',
7+
})
8+
message: string;
9+
}
10+
11+
export class AiResponseDto {
12+
@ApiProperty({
13+
description: '๋‹ต๋ณ€ ๋‚ด์šฉ',
14+
example:
15+
'Git์€ ๋ถ„์‚ฐ ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ(Distributed Version Control System)์œผ๋กœ, ์†Œ์Šค ์ฝ”๋“œ์˜ ๋ฒ„์ „์„ ๊ด€๋ฆฌํ•˜๊ณ  ํ˜‘์—…์„ ์ง€์›ํ•˜๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. Git์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.\\n\\n1. **๋ถ„์‚ฐ ์ €์žฅ์†Œ**: Git์€ ์ค‘์•™ ์ง‘์ค‘์‹ ์ €์žฅ์†Œ๊ฐ€ ์•„๋‹Œ ๋ถ„์‚ฐ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์‚ฌ์šฉ์ž๋Š” ์ž์‹ ์˜ ์ปดํ“จํ„ฐ์— ์ €์žฅ์†Œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ๋กœ์ปฌ ์ €์žฅ์†Œ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.\\n2. **๋น ๋ฅธ ์†๋„**: Git์€ ๋น ๋ฅธ ์†๋„๋กœ ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” Git์ด ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ•˜๊ณ , ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ ํŒŒ์ผ์„ ๋น ๋ฅด๊ฒŒ ๊ฒ€์ƒ‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.\\n3. **๋ฒ„์ „ ๊ด€๋ฆฌ**: Git์€ ์†Œ์Šค ์ฝ”๋“œ์˜ ๋ฒ„์ „์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๊ณ  ์ปค๋ฐ‹(commit)ํ•˜๋ฉด, ํ•ด๋‹น ํŒŒ์ผ์˜ ์ด์ „ ๋ฒ„์ „๊ณผ ์ดํ›„ ๋ฒ„์ „์„ ๋ชจ๋‘ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\\n4. **ํ˜‘์—… ์ง€์›**: Git์€ ํ˜‘์—…์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์™€ ํ•จ๊ป˜ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์„œ๋กœ์˜ ์ž‘์—… ๋‚ด์šฉ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\\n5. **๋ช…๋ น์–ด ๊ธฐ๋ฐ˜**: Git์€ ๋ช…๋ น์–ด ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” Git ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์ €์žฅ์†Œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ํŒŒ์ผ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\\n\\nGit์€ ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์™€ ์šด์˜์ฒด์ œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด Git์„ ์ด์šฉํ•˜์—ฌ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.',
16+
})
17+
message: string;
18+
}

โ€Žpackages/backend/src/app.module.ts

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { format } from 'winston';
1010
import { typeOrmConfig } from './configs/typeorm.config';
1111
import { QuizzesModule } from './quizzes/quizzes.module';
1212
import { LoggingInterceptor } from './common/logging.interceptor';
13+
import { QuizWizardModule } from './quiz-wizard/quiz-wizard.module';
14+
import { AiModule } from './ai/ai.module';
15+
import { CommandModule } from './command/command.module';
1316

1417
@Module({
1518
imports: [
@@ -38,6 +41,9 @@ import { LoggingInterceptor } from './common/logging.interceptor';
3841
),
3942
),
4043
}),
44+
QuizWizardModule,
45+
AiModule,
46+
CommandModule,
4147
],
4248
controllers: [AppController],
4349
providers: [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from '@nestjs/common';
2+
import { CommandService } from './command.service';
3+
4+
@Module({
5+
providers: [CommandService],
6+
exports: [CommandService],
7+
})
8+
export class CommandModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { CommandService } from './command.service';
3+
4+
describe('CommandService', () => {
5+
let service: CommandService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [CommandService],
10+
}).compile();
11+
12+
service = module.get<CommandService>(CommandService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import axios from 'axios';
4+
import { Logger } from 'winston';
5+
import { preview, processCarriageReturns } from '../common/util';
6+
7+
@Injectable()
8+
export class CommandService {
9+
private readonly host: string;
10+
private readonly instance;
11+
constructor(
12+
private readonly configService: ConfigService,
13+
@Inject('winston') private readonly logger: Logger,
14+
) {
15+
this.host = this.configService.get<string>('CONTAINER_SERVER_HOST');
16+
this.instance = axios.create({
17+
baseURL: this.host,
18+
timeout: 10000,
19+
});
20+
}
21+
22+
async executeCommand(
23+
...commands: string[]
24+
): Promise<{ stdoutData: string; stderrData: string }> {
25+
try {
26+
const command = commands.join('; ');
27+
this.logger.log('info', `command: ${preview(command, 40)}`);
28+
const response = await this.instance.post('/', { command });
29+
return {
30+
stdoutData: processCarriageReturns(response.data.stdoutData),
31+
stderrData: processCarriageReturns(response.data.stderrData),
32+
};
33+
} catch (error) {
34+
this.logger.log('info', error);
35+
}
36+
}
37+
38+
async executeCron(
39+
...commands: string[]
40+
): Promise<{ stdoutData: string; stderrData: string }> {
41+
try {
42+
const command = commands.join('; ');
43+
this.logger.log('info', `command: ${preview(command, 40)}`);
44+
const response = await this.instance.post('/cron', { command });
45+
return response.data;
46+
} catch (error) {
47+
this.logger.log('info', error);
48+
}
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
CanActivate,
3+
ExecutionContext,
4+
ForbiddenException,
5+
Injectable,
6+
} from '@nestjs/common';
7+
8+
@Injectable()
9+
export class CommandGuard implements CanActivate {
10+
canActivate(context: ExecutionContext): boolean {
11+
const request = context.switchToHttp().getRequest<Request>();
12+
const mode = request.body['mode'];
13+
const message = request.body['message'];
14+
if (
15+
!(
16+
typeof mode === 'string' &&
17+
typeof message === 'string' &&
18+
(mode === 'editor' ||
19+
(mode === 'command' &&
20+
message.startsWith('git') &&
21+
!this.isMessageIncluded(message, [
22+
';',
23+
'>',
24+
'|',
25+
'<',
26+
'&',
27+
'$',
28+
'(',
29+
')',
30+
'{',
31+
'}',
32+
])))
33+
)
34+
) {
35+
throw new ForbiddenException('๊ธˆ์ง€๋œ ๋ช…๋ น์ž…๋‹ˆ๋‹ค');
36+
}
37+
return true;
38+
}
39+
private isMessageIncluded(message: string, keywords: string[]): boolean {
40+
return keywords.some((keyword) => message.includes(keyword));
41+
}
42+
}

0 commit comments

Comments
ย (0)