Skip to content

Commit

Permalink
feat: Implement CLOVA Studio integration for drawing word generation (#…
Browse files Browse the repository at this point in the history
…121)

* feat: Install axios for HTTP requests

* feat: Implement CLOVA Studio integration for drawing word generation

* chore: Add CLOVA API keys to CI/CD workflow

---------

Co-authored-by: 유미라 <[email protected]>
  • Loading branch information
ssum1ra and 유미라 authored Nov 25, 2024
1 parent 0a20ad1 commit 6a94d34
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 9 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/server-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
run: |
echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env
echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env
echo "CLOVA_API_KEY=${{ secrets.CLOVA_API_KEY }}" >> .env
echo "CLOVA_GATEWAY_KEY=${{ secrets.CLOVA_GATEWAY_KEY }}" >> .env
- name: Run tests
run: pnpm --filter server test | true
Expand Down Expand Up @@ -62,6 +64,8 @@ jobs:
build-args: |
REDIS_HOST=${{ secrets.REDIS_HOST }}
REDIS_PORT=${{ secrets.REDIS_PORT }}
CLOVA_API_KEY=${{ secrets.CLOVA_API_KEY }}
CLOVA_GATEWAY_KEY=${{ secrets.CLOVA_GATEWAY_KEY }}
- name: Deploy to Server
uses: appleboy/[email protected]
Expand All @@ -77,4 +81,4 @@ jobs:
docker stop troublepainter || true
docker rm troublepainter || true
docker run -d --name troublepainter -p 3000:3000 -e REDIS_HOST=${{ secrets.REDIS_HOST }} -e REDIS_PORT=${{ secrets.REDIS_PORT }} --restart unless-stopped ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter:latest
docker run -d --name troublepainter -p 3000:3000 -e REDIS_HOST=${{ secrets.REDIS_HOST }} -e REDIS_PORT=${{ secrets.REDIS_PORT }} -e CLOVA_API_KEY=${{ secrets.CLOVA_API_KEY }} -e CLOVA_GATEWAY_KEY=${{ secrets.CLOVA_GATEWAY_KEY }} --restart unless-stopped ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter:latest
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@troublepainter/core": "workspace:*",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.7",
"@nestjs/websockets": "^10.4.7",
"@troublepainter/core": "workspace:*",
"axios": "^1.7.7",
"ioredis": "^5.4.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
Expand All @@ -37,6 +38,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/axios": "^0.14.4",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
Expand Down
60 changes: 60 additions & 0 deletions server/src/common/clova-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import { Difficulty } from './enums/game.status.enum';

@Injectable()
export class ClovaClient {
private readonly client: AxiosInstance;

constructor(private configService: ConfigService) {
const apiKey = this.configService.get<string>('CLOVA_API_KEY');
const gatewayKey = this.configService.get<string>('CLOVA_GATEWAY_KEY');

this.client = axios.create({
baseURL: 'https://clovastudio.stream.ntruss.com/testapp/v1',
headers: {
'X-NCP-CLOVASTUDIO-API-KEY': apiKey,
'X-NCP-APIGW-API-KEY': gatewayKey,
},
});
}
async getDrawingWords(difficulty: Difficulty, count: number) {
const request = {
messages: [
{
role: 'system',
content: '',
},
{
role: 'user',
content: `당신은 드로잉 게임의 출제자입니다. ${difficulty}난이도의 명사 ${count}개를 제시해주세요.
- 30초 안에 그릴 수 있는 단어만 선택
- 단어만 나열 (1. 2. 3. 형식)
- 설명이나 부연설명 없이 단어만 작성
`,
},
],
topP: 0.8,
topK: 0,
maxTokens: 256,
temperature: 0.8,
repeatPenalty: 5.0,
stopBefore: [],
includeAiFilters: true,
seed: Math.floor(Math.random() * 1000000),
};

try {
const response = await this.client.post('/chat-completions/HCX-003', request);
const result = response.data.result.message.content
.split('\n')
.map((line: string) => line.trim())
.filter((line: string) => line)
.map((line: string) => line.replace(/^\d+\.\s*/, ''));
return result;
} catch (error) {
throw new Error(`CLOVA API request failed: ${error.message}`);
}
}
}
6 changes: 6 additions & 0 deletions server/src/common/enums/game.status.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export enum RoomStatus {
DRAWING = 'DRAWING',
GUESSING = 'GUESSING',
}

export enum Difficulty {
EASY = 'EASY',
NORMAL = 'NORMAL',
HARD = 'HARD',
}
9 changes: 6 additions & 3 deletions server/src/game/game.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ export class GameGateway implements OnGatewayDisconnect {

const { room, roomSettings, roles, players } = await this.gameService.startGame(roomId, playerId);

this.timerService.startTimer(this.server, roomId, 35000, {
this.timerService.startTimer(this.server, roomId, roomSettings.drawTime * 1000, {
onTick: async (remaining: number) => {
this.server.to(roomId).emit('timerSync', { remaining });
}, // 35 seconds
},
onTimeUp: async () => {
console.log('Game ended');
this.server.to(roomId).emit('drawingTimeEnded');
Expand All @@ -108,7 +108,10 @@ export class GameGateway implements OnGatewayDisconnect {
};

if (player.role === PlayerRole.PAINTER || player.role === PlayerRole.DEVIL) {
this.server.to(playerSocket.id).emit('drawingGroupRoundStarted', basePayload);
this.server.to(playerSocket.id).emit('drawingGroupRoundStarted', {
...basePayload,
word: room.currentWord,
});
} else {
this.server.to(playerSocket.id).emit('guesserRoundStarted', {
...basePayload,
Expand Down
3 changes: 2 additions & 1 deletion server/src/game/game.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { GameGateway } from './game.gateway';
import { RedisModule } from 'src/redis/redis.module';
import { GameRepository } from './game.repository';
import { TimerService } from 'src/common/services/timer.service';
import { ClovaClient } from 'src/common/clova-client';

@Module({
imports: [RedisModule],
providers: [GameService, GameGateway, GameRepository, TimerService],
providers: [GameService, GameGateway, GameRepository, TimerService, ClovaClient],
controllers: [GameController],
})
export class GameModule {}
13 changes: 10 additions & 3 deletions server/src/game/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
BadRequestException,
InsufficientPlayersException,
} from 'src/exceptions/game.exception';
import { RoomStatus, PlayerStatus, PlayerRole } from 'src/common/enums/game.status.enum';
import { RoomStatus, PlayerStatus, PlayerRole, Difficulty } from 'src/common/enums/game.status.enum';
import { ClovaClient } from 'src/common/clova-client';

@Injectable()
export class GameService {
Expand All @@ -18,8 +19,12 @@ export class GameService {
totalRounds: 5,
drawTime: 35,
};
private words: string[] = [];

constructor(private readonly gameRepository: GameRepository) {}
constructor(
private readonly gameRepository: GameRepository,
private readonly clovaClient: ClovaClient,
) {}

async createRoom(): Promise<string> {
const roomId = v4();
Expand Down Expand Up @@ -138,10 +143,12 @@ export class GameService {

const roomSettings = await this.gameRepository.getRoomSettings(roomId);

this.words = await this.clovaClient.getDrawingWords(Difficulty.HARD, roomSettings.totalRounds);

const roomUpdates = {
status: RoomStatus.DRAWING,
currentRound: room.currentRound + 1,
currentWord: '바보',
currentWord: this.words.shift(),
};
await this.gameRepository.updateRoom(roomId, roomUpdates);

Expand Down

0 comments on commit 6a94d34

Please sign in to comment.