diff --git a/.github/workflows/back-center-cd.yml b/.github/workflows/back-center-cd.yml index f2a3c62..b14ac0b 100644 --- a/.github/workflows/back-center-cd.yml +++ b/.github/workflows/back-center-cd.yml @@ -48,4 +48,6 @@ jobs: echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> /root/.env echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> /root/.env echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> /root/.env + echo "CHAT_SOCKET_URL=${{ secrets.CHAT_SOCKET_URL }}" >> /root/.env + echo "SIGNAL_SOCKET_URL=${{ secrets.SERVER_A_SOCKET_URL }}" >> /root/.env ./deploy.sh diff --git a/.github/workflows/back-chat-cd.yml b/.github/workflows/back-chat-cd.yml index 14f73b5..f5c72a0 100644 --- a/.github/workflows/back-chat-cd.yml +++ b/.github/workflows/back-chat-cd.yml @@ -56,4 +56,5 @@ jobs: echo "Accept=${{ secrets.Accept }}" >> /root/.env echo "ContentType=${{ secrets.ContentType }}" >> /root/.env echo "MONGO_PROD=${{ secrets.MONGO_PROD }}" >> /root/.env + echo "SOCKET_URL=${{ secrets.CHAT_SOCKET_URL }}" >> /root/.env ./deploy.sh diff --git a/README.md b/README.md index 72fb0e1..30b7633 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,137 @@ -# AlgoITNi -#### 동료들과 함께 소통하며(화상, 음성, 채팅) 알고리즘 학습을 할 수 있는 플랫폼 - +
{
- const session = await this.connection.startSession();
- session.startTransaction();
- try {
- const code = await this.codeModel.create(saveCodeDto);
- await session.commitTransaction();
- return code;
- } catch (e) {
- await session.abortTransaction();
- this.logger.error(e);
- throw new TransactionRollback();
- } finally {
- await session.endSession();
- }
+ @Transactional('mongoose')
+ async save(saveCodeDto: SaveCodeDto) {
+ const session = getSession();
+ const code = await this.codeModel.create([saveCodeDto], {
+ session: session,
+ });
+ return code[0];
}
async getAll(userID: number) {
@@ -36,38 +34,20 @@ export class CodesService {
async getOne(userID: number, objectID: string) {
return this.codeModel.find({ userID: userID, _id: objectID }).exec();
}
-
+ @Transactional('mongoose')
async update(userID: number, objectID: string, saveCodeDto: SaveCodeDto) {
const query = { userID: userID, _id: objectID };
- const session = await this.connection.startSession();
- session.startTransaction();
- try {
- const result = await this.codeModel.updateOne(query, saveCodeDto);
- await session.commitTransaction();
- return result;
- } catch (e) {
- await session.abortTransaction();
- this.logger.error(e);
- throw new TransactionRollback();
- } finally {
- await session.endSession();
- }
- return;
+ const session: ClientSession = getSession();
+ const result = await this.codeModel
+ .updateOne(query, saveCodeDto)
+ .session(session);
+ return result;
}
- async delete(userID: number, objectID: string) {
+ @Transactional('mongoose')
+ async delete(userID: number, objectID: string): Promise {
const query = { userID: userID, _id: objectID };
- const session = await this.connection.startSession();
- session.startTransaction();
- try {
- await this.codeModel.deleteOne(query);
- await session.commitTransaction();
- } catch (e) {
- await session.abortTransaction();
- this.logger.error(e);
- throw new TransactionRollback();
- } finally {
- await session.endSession();
- }
+ const session = getSession();
+ return this.codeModel.deleteOne(query).session(session);
}
}
diff --git a/backEnd/api/src/common/decorator/typeormTransactional.decorator.ts b/backEnd/api/src/common/decorator/typeormTransactional.decorator.ts
deleted file mode 100644
index ee981b4..0000000
--- a/backEnd/api/src/common/decorator/typeormTransactional.decorator.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { getNamespace } from 'cls-hooked';
-export const typeormTransactional = () => {
- return (target: any, key: string, descriptor: PropertyDescriptor) => {
- const nameSpace = getNamespace('namespace');
- nameSpace.set();
- // console.log("target instance:", target);
-
- const originalMethod = descriptor.value;
- descriptor.value = async function (...args: any[]) {
- // 비동기 작업을 수행하거나 비동기 함수를 호출할 수 있습니다.
- const [res, req] = args;
- if (!req.authorized) return res.redirect('/user/login');
- // console.log(originalMethod)
- await originalMethod.apply(this, args);
- };
- };
-};
diff --git a/backEnd/api/src/common/exception/exception.filter.ts b/backEnd/api/src/common/exception/exception.filter.ts
index 0a4ed83..dec2202 100644
--- a/backEnd/api/src/common/exception/exception.filter.ts
+++ b/backEnd/api/src/common/exception/exception.filter.ts
@@ -24,7 +24,10 @@ export class HttpExceptionFilter implements ExceptionFilter {
);
response.status(status).json({
statusCode: status,
- message: exception.message,
+ message:
+ status !== HttpStatus.INTERNAL_SERVER_ERROR
+ ? exception.message
+ : ResponseMessage.INTERNAL_SERVER_ERROR,
timestamp: new Date().toISOString(),
});
}
diff --git a/backEnd/api/src/common/transaction/transaction.decorator.ts b/backEnd/api/src/common/transaction/transaction.decorator.ts
new file mode 100644
index 0000000..4844c48
--- /dev/null
+++ b/backEnd/api/src/common/transaction/transaction.decorator.ts
@@ -0,0 +1,28 @@
+import { applyDecorators, SetMetadata } from '@nestjs/common';
+import { AsyncLocalStorage } from 'async_hooks';
+import { ObjectLiteral, QueryRunner, Repository } from 'typeorm';
+import { ClientSession } from 'mongoose';
+
+export const TRANSACTIONAL_KEY = Symbol('TRANSACTION');
+export type ORM = 'typeorm' | 'mongoose';
+export function Transactional(orm: ORM): MethodDecorator {
+ return applyDecorators(SetMetadata(TRANSACTIONAL_KEY, orm));
+}
+
+export const queryRunnerLocalStorage = new AsyncLocalStorage<{
+ qr: QueryRunner;
+}>();
+export const sessionLocalStorage = new AsyncLocalStorage<{
+ session: ClientSession;
+}>();
+
+export function getLocalStorageRepository(
+ target,
+): Repository {
+ const queryRunner = queryRunnerLocalStorage.getStore();
+ return queryRunner?.qr?.manager.getRepository(target);
+}
+export function getSession(): ClientSession {
+ const session = sessionLocalStorage.getStore();
+ return session?.session;
+}
diff --git a/backEnd/api/src/common/transaction/transaction.module.ts b/backEnd/api/src/common/transaction/transaction.module.ts
new file mode 100644
index 0000000..3211638
--- /dev/null
+++ b/backEnd/api/src/common/transaction/transaction.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+import { DiscoveryModule } from '@nestjs/core';
+import { TransactionService } from './transaction.service';
+
+@Module({
+ imports: [DiscoveryModule],
+ providers: [TransactionService],
+})
+export class TransactionModule {}
diff --git a/backEnd/api/src/common/transaction/transaction.service.ts b/backEnd/api/src/common/transaction/transaction.service.ts
new file mode 100644
index 0000000..1f40328
--- /dev/null
+++ b/backEnd/api/src/common/transaction/transaction.service.ts
@@ -0,0 +1,102 @@
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
+import {
+ ORM,
+ queryRunnerLocalStorage,
+ sessionLocalStorage,
+ TRANSACTIONAL_KEY,
+} from './transaction.decorator';
+import { DataSource } from 'typeorm';
+import { Connection } from 'mongoose';
+import { InjectConnection } from '@nestjs/mongoose';
+import { TransactionRollback } from '../exception/exception';
+import { ClientSession } from 'mongoose';
+
+@Injectable()
+export class TransactionService implements OnModuleInit {
+ constructor(
+ private readonly discoveryService: DiscoveryService,
+ private readonly metadataScanner: MetadataScanner,
+ private readonly reflector: Reflector,
+ private readonly dataSource: DataSource,
+ @InjectConnection() private readonly connection: Connection,
+ ) {}
+
+ onModuleInit(): any {
+ const providers = this.discoveryService.getProviders();
+
+ const instances = providers
+ .filter((v) => v.isDependencyTreeStatic())
+ .filter(({ metatype, instance }) => {
+ return !(!metatype || !instance);
+ })
+ .map(({ instance }) => instance);
+
+ instances.map((instance) => {
+ const names = this.metadataScanner.getAllMethodNames(
+ Object.getPrototypeOf(instance),
+ );
+ for (const name of names) {
+ const originalMethod = instance[name];
+ const metadata = this.reflector.get(
+ TRANSACTIONAL_KEY,
+ originalMethod,
+ );
+ switch (metadata) {
+ case 'typeorm':
+ instance[name] = this.typeormTransaction(originalMethod, instance);
+ return;
+ case 'mongoose':
+ instance[name] = this.mongooseTransaction(originalMethod, instance);
+ }
+ }
+ });
+ }
+
+ typeormTransaction(originalMethod, instance) {
+ const dataSource = this.dataSource;
+ return async function (...args: any[]) {
+ const qr = await dataSource.createQueryRunner();
+
+ await queryRunnerLocalStorage.run({ qr }, async function () {
+ try {
+ await qr.startTransaction();
+ const result = await originalMethod.apply(instance, args);
+ await qr.commitTransaction();
+ return result;
+ } catch (e) {
+ await qr.rollbackTransaction();
+ this.logger.error(e);
+ throw new TransactionRollback();
+ } finally {
+ await qr.release();
+ }
+ });
+ };
+ }
+
+ mongooseTransaction(originalMethod, instance) {
+ const connection = this.connection;
+ return async function (...args: any[]) {
+ const session: ClientSession = await connection.startSession();
+
+ const result = await sessionLocalStorage.run(
+ { session },
+ async function () {
+ try {
+ await session.startTransaction();
+ const result = await originalMethod.apply(instance, args);
+ await session.commitTransaction();
+ return result;
+ } catch (e) {
+ await session.abortTransaction();
+ throw new TransactionRollback();
+ } finally {
+ await session.endSession();
+ }
+ },
+ );
+ return result;
+ };
+ }
+}
diff --git a/backEnd/api/src/users/users.service.ts b/backEnd/api/src/users/users.service.ts
index aec128a..1478318 100644
--- a/backEnd/api/src/users/users.service.ts
+++ b/backEnd/api/src/users/users.service.ts
@@ -3,7 +3,10 @@ import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from './entity/user.entity';
import { UserDto } from './dto/user.dto';
-import { TransactionRollback } from '../common/exception/exception';
+import {
+ getLocalStorageRepository,
+ Transactional,
+} from '../common/transaction/transaction.decorator';
type OAUTH = 'github' | 'google';
@Injectable()
export class UsersService {
@@ -11,26 +14,14 @@ export class UsersService {
@InjectRepository(UserEntity)
private usersRepository: Repository,
) {}
-
+ @Transactional('typeorm')
async addUser(userDTO: UserDto, oauth: OAUTH) {
const user = new UserEntity();
user.name = userDTO.name;
user.authServiceID = userDTO.authServiceID;
user.oauth = oauth;
-
- const qr = this.getQueryRunner();
- try {
- await qr.connect();
- await qr.startTransaction();
- await qr.manager.save(user);
- await qr.commitTransaction();
- } catch (e) {
- console.log(e);
- await qr.rollbackTransaction();
- throw new TransactionRollback();
- } finally {
- await qr.release();
- }
+ const repository = getLocalStorageRepository(UserEntity);
+ await repository.save(user);
}
async findUser(userDTO: UserDto): Promise {
@@ -41,9 +32,4 @@ export class UsersService {
});
return find;
}
-
- getQueryRunner() {
- const dataSource = this.usersRepository.manager.connection;
- return dataSource.createQueryRunner();
- }
}
diff --git a/backEnd/center/src/common/utils.ts b/backEnd/center/src/common/utils.ts
index 601ca26..5caad44 100644
--- a/backEnd/center/src/common/utils.ts
+++ b/backEnd/center/src/common/utils.ts
@@ -20,8 +20,8 @@ export const ERRORS = {
};
export const EVENT = {
- REGISTER: 'register',
- SIGNALING: 'signaling',
+ SIGNALING: 'signalingCpu',
+ CHAT: 'chatCpu',
};
export const USE_FULL = 100;
diff --git a/backEnd/center/src/connections/connections.controller.ts b/backEnd/center/src/connections/connections.controller.ts
index 9c36d07..0c3f550 100644
--- a/backEnd/center/src/connections/connections.controller.ts
+++ b/backEnd/center/src/connections/connections.controller.ts
@@ -1,21 +1,37 @@
-import { Controller, Post, Body } from '@nestjs/common';
+import { Controller, Post, Body, Get } from '@nestjs/common';
import { JoinRoomDto } from './dto/join-room.dto';
import { EventsService } from 'src/events/events.service';
import { ResponseDto } from 'src/common/dto/common-response.dto';
+import { EventsChatService } from 'src/events/events-chat.service';
-@Controller('connections')
+@Controller()
export class ConnectionsController {
- constructor(private readonly eventService: EventsService) {}
+ constructor(
+ private readonly eventService: EventsService,
+ private readonly eventChatService: EventsChatService,
+ ) {}
- @Post('join')
- create(@Body() data: JoinRoomDto): ResponseDto {
+ @Post('join/signalling')
+ findSignalingServer(@Body() data: JoinRoomDto): ResponseDto {
const response: ResponseDto = this.eventService.findServer(data);
return response;
}
- @Post('leave')
- leave(@Body() data: JoinRoomDto): ResponseDto {
+ @Post('leave/signalling')
+ leaveSignaling(@Body() data: JoinRoomDto): ResponseDto {
const response: ResponseDto = this.eventService.leaveRoom(data);
return response;
}
+
+ @Post('join/chatting')
+ findChattingServer(@Body() data: JoinRoomDto): ResponseDto {
+ const response: ResponseDto = this.eventChatService.findServer(data);
+ return response;
+ }
+
+ @Post('leave/chatting')
+ leaveChatting(@Body() data: JoinRoomDto): ResponseDto {
+ const response: ResponseDto = this.eventChatService.leaveRoom(data);
+ return response;
+ }
}
diff --git a/backEnd/center/src/connections/connections.module.ts b/backEnd/center/src/connections/connections.module.ts
index 00b3a70..5a8f81a 100644
--- a/backEnd/center/src/connections/connections.module.ts
+++ b/backEnd/center/src/connections/connections.module.ts
@@ -3,10 +3,11 @@ import { ConnectionsService } from './connections.service';
import { ConnectionsController } from './connections.controller';
import { EventsService } from 'src/events/events.service';
import { EventsModule } from 'src/events/events.module';
+import { EventsChatService } from 'src/events/events-chat.service';
@Module({
imports: [EventsModule],
controllers: [ConnectionsController],
- providers: [ConnectionsService, EventsService],
+ providers: [ConnectionsService, EventsService, EventsChatService],
})
export class ConnectionsModule {}
diff --git a/backEnd/center/src/events/events-chat.service.ts b/backEnd/center/src/events/events-chat.service.ts
new file mode 100644
index 0000000..4f8da35
--- /dev/null
+++ b/backEnd/center/src/events/events-chat.service.ts
@@ -0,0 +1,110 @@
+import { ConfigService, ConfigModule } from '@nestjs/config';
+import { InjectRedis } from '@liaoliaots/nestjs-redis';
+import { HttpStatus, Injectable, OnModuleInit } from '@nestjs/common';
+import Redis from 'ioredis';
+import { JoinRoomDto } from 'src/connections/dto/join-room.dto';
+import {
+ URLNotFoundException,
+ ValidateDtoException,
+} from 'src/common/exception/exception';
+import { ERRORS, EVENT, USE_FULL } from 'src/common/utils';
+import { ResponseDto } from 'src/common/dto/common-response.dto';
+
+@Injectable()
+export class EventsChatService implements OnModuleInit {
+ private returnUrl: string;
+ private serverToCpus: Map = new Map();
+ private roomToUrl: Map = new Map();
+
+ constructor(
+ @InjectRedis() private readonly client: Redis,
+ private readonly configService: ConfigService,
+ ) {}
+
+ onModuleInit() {
+ this.subscribe();
+ }
+
+ private subscribe() {
+ this.client.subscribe(EVENT.CHAT);
+
+ this.client.on('message', async (channel, message) => {
+ const data = JSON.parse(message);
+
+ if (channel === EVENT.CHAT) {
+ const { url, usages } = data;
+ this.validateUrl(url);
+ this.validateUsages(usages);
+ this.handleChatting(url, usages);
+ }
+ });
+ }
+
+ private handleChatting(url: string, usages: number) {
+ const nextServer = this.returnUrl;
+ const minUsages = this.serverToCpus.get(nextServer) || USE_FULL;
+
+ if (usages < minUsages) {
+ this.returnUrl = url;
+ }
+
+ this.serverToCpus.set(url, usages);
+ }
+
+ findServer(data: JoinRoomDto): ResponseDto {
+ const { roomName } = data;
+ this.validateRoom(roomName);
+
+ const isServer = this.roomToUrl.get(roomName);
+
+ if (isServer) {
+ const response = this.createResponse(HttpStatus.OK, { url: isServer });
+ return response;
+ }
+
+ const server =
+ this.returnUrl || this.configService.get('CHAT_SOCKET_URL');
+
+ if (!server) {
+ throw new URLNotFoundException(ERRORS.URL_NOT_FOUND.message);
+ }
+
+ this.roomToUrl.set(roomName, server);
+ const response = this.createResponse(HttpStatus.OK, { url: server });
+ return response;
+ }
+
+ leaveRoom(data: JoinRoomDto): ResponseDto {
+ const { roomName } = data;
+ this.validateRoom(roomName);
+ this.roomToUrl.delete(roomName);
+ const response = this.createResponse(HttpStatus.OK, { room: roomName });
+ return response;
+ }
+
+ validateRoom(room: string) {
+ if (!room) {
+ throw new ValidateDtoException(ERRORS.ROOM_EMPTY.message);
+ }
+ }
+
+ validateUrl(url: string) {
+ if (!url) {
+ throw new ValidateDtoException(ERRORS.URL_EMPTY.message);
+ }
+ }
+
+ validateUsages(usages: number) {
+ if (typeof usages !== 'number' || usages <= 0) {
+ throw new ValidateDtoException(ERRORS.USAGES_INVALID.message);
+ }
+ }
+
+ createResponse(statusCode: number, result: object): ResponseDto {
+ const response = new ResponseDto();
+ response.statusCode = statusCode;
+ response.result = result;
+ response.timestamp = new Date().toISOString();
+ return response;
+ }
+}
diff --git a/backEnd/center/src/events/events.service.ts b/backEnd/center/src/events/events.service.ts
index fee5c94..122dbcd 100644
--- a/backEnd/center/src/events/events.service.ts
+++ b/backEnd/center/src/events/events.service.ts
@@ -8,6 +8,7 @@ import {
} from 'src/common/exception/exception';
import { ERRORS, EVENT, USE_FULL } from 'src/common/utils';
import { ResponseDto } from 'src/common/dto/common-response.dto';
+import { ConfigService } from '@nestjs/config';
@Injectable()
export class EventsService implements OnModuleInit {
@@ -15,25 +16,21 @@ export class EventsService implements OnModuleInit {
private serverToCpus: Map = new Map();
private roomToUrl: Map = new Map();
- constructor(@InjectRedis() private readonly client: Redis) {}
+ constructor(
+ @InjectRedis() private readonly client: Redis,
+ private readonly configService: ConfigService,
+ ) {}
onModuleInit() {
this.subscribe();
}
private subscribe() {
- this.client.subscribe(EVENT.REGISTER);
this.client.subscribe(EVENT.SIGNALING);
this.client.on('message', async (channel, message) => {
const data = JSON.parse(message);
- if (channel === EVENT.REGISTER) {
- const { url } = data;
- this.validateUrl(url);
- this.handleRegister(url);
- }
-
if (channel === EVENT.SIGNALING) {
const { url, usages } = data;
this.validateUrl(url);
@@ -43,15 +40,6 @@ export class EventsService implements OnModuleInit {
});
}
- private handleRegister(url: string) {
- this.serverToCpus.set(url, 0);
-
- const nextServer = this.returnUrl;
- if (!nextServer) {
- this.returnUrl = url;
- }
- }
-
private handleSignaling(url: string, usages: number) {
const nextServer = this.returnUrl;
const minUsages = this.serverToCpus.get(nextServer) || USE_FULL;
@@ -74,7 +62,8 @@ export class EventsService implements OnModuleInit {
return response;
}
- const server = this.returnUrl;
+ const server =
+ this.returnUrl || this.configService.get('SIGNAL_SOCKET_URL');
if (!server) {
throw new URLNotFoundException(ERRORS.URL_NOT_FOUND.message);
diff --git a/backEnd/chat/package-lock.json b/backEnd/chat/package-lock.json
index 74a90b4..6faa398 100644
--- a/backEnd/chat/package-lock.json
+++ b/backEnd/chat/package-lock.json
@@ -24,11 +24,13 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
+ "cron": "^3.1.6",
"dotenv": "^16.3.1",
"express-session": "^1.17.3",
"kafkajs": "^2.2.4",
"mongoose": "^8.0.2",
"nest-winston": "^1.9.4",
+ "node-cron": "^3.0.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"socket.io": "^4.7.2",
@@ -2266,6 +2268,11 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
+ "node_modules/@types/luxon": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.7.tgz",
+ "integrity": "sha512-gKc9P2d4g5uYwmy4s/MO/yOVPmvHyvzka1YH6i5dM03UrFofHSmgc0D0ymbDRStFWHusk6cwwF6nhLm/ckBbbQ=="
+ },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -3893,6 +3900,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "node_modules/cron": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz",
+ "integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==",
+ "dependencies": {
+ "@types/luxon": "~3.3.0",
+ "luxon": "~3.4.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -6778,6 +6794,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/luxon": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
+ "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/macos-release": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz",
@@ -7219,6 +7243,25 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
+ "node_modules/node-cron": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+ "dependencies": {
+ "uuid": "8.3.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/node-cron/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
diff --git a/backEnd/chat/package.json b/backEnd/chat/package.json
index 91fd95a..5fbebd4 100644
--- a/backEnd/chat/package.json
+++ b/backEnd/chat/package.json
@@ -35,11 +35,13 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
+ "cron": "^3.1.6",
"dotenv": "^16.3.1",
"express-session": "^1.17.3",
"kafkajs": "^2.2.4",
"mongoose": "^8.0.2",
"nest-winston": "^1.9.4",
+ "node-cron": "^3.0.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"socket.io": "^4.7.2",
diff --git a/backEnd/chat/src/app.module.ts b/backEnd/chat/src/app.module.ts
index 9d5a8ad..bffa211 100644
--- a/backEnd/chat/src/app.module.ts
+++ b/backEnd/chat/src/app.module.ts
@@ -5,6 +5,7 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChatModule } from './chat/chat.module';
import { MongooseModule } from '@nestjs/mongoose';
+import { EventsModule } from './events/events.module';
@Module({
imports: [
@@ -31,6 +32,7 @@ import { MongooseModule } from '@nestjs/mongoose';
inject: [ConfigService],
}),
ChatModule,
+ EventsModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/backEnd/chat/src/events/events.module.ts b/backEnd/chat/src/events/events.module.ts
new file mode 100644
index 0000000..7715472
--- /dev/null
+++ b/backEnd/chat/src/events/events.module.ts
@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common';
+import { EventsService } from './events.service';
+
+@Module({
+ providers: [EventsService],
+ exports: [EventsService],
+})
+export class EventsModule {}
diff --git a/backEnd/chat/src/events/events.service.ts b/backEnd/chat/src/events/events.service.ts
new file mode 100644
index 0000000..6847db4
--- /dev/null
+++ b/backEnd/chat/src/events/events.service.ts
@@ -0,0 +1,46 @@
+import { InjectRedis } from '@liaoliaots/nestjs-redis';
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import Redis from 'ioredis';
+import * as cron from 'node-cron';
+import { Worker } from 'worker_threads';
+import * as path from 'path';
+
+@Injectable()
+export class EventsService implements OnModuleInit {
+ constructor(
+ @InjectRedis() private readonly client: Redis,
+ private readonly configService: ConfigService,
+ ) {}
+
+ private cpuUsageQueue: number[] = [];
+ private cpuWorker: Worker;
+ private usages: number;
+
+ onModuleInit() {
+ this.initCpuWorker();
+ this.scheduling();
+ }
+
+ private initCpuWorker() {
+ this.cpuWorker = new Worker(path.join(__dirname, 'worker/cpu.worker.js'));
+ this.cpuWorker.on('message', (cpuUsage: number) => {
+ if (this.cpuUsageQueue.length >= 20) {
+ this.cpuUsageQueue.shift();
+ }
+ this.cpuUsageQueue.push(cpuUsage);
+ const sum = this.cpuUsageQueue.reduce((acc, val) => acc + val, 0);
+ this.usages = sum / this.cpuUsageQueue.length;
+ });
+ }
+
+ private scheduling() {
+ cron.schedule('*/5 * * * *', () => {
+ const message = {
+ url: this.configService.get('SOCKET_URL'),
+ usages: this.usages,
+ };
+ this.client.publish('chatCpu', JSON.stringify(message));
+ });
+ }
+}
diff --git a/backEnd/chat/src/events/worker/cpu.worker.ts b/backEnd/chat/src/events/worker/cpu.worker.ts
new file mode 100644
index 0000000..ceba69c
--- /dev/null
+++ b/backEnd/chat/src/events/worker/cpu.worker.ts
@@ -0,0 +1,20 @@
+import { parentPort } from 'worker_threads';
+import * as os from 'os';
+
+function calculateCpuUsage() {
+ const cpus = os.cpus();
+ let totalIdleTime = 0;
+ let totalWorkTime = 0;
+
+ for (const cpu of cpus) {
+ for (const type in cpu.times) {
+ totalWorkTime += cpu.times[type];
+ }
+ totalIdleTime += cpu.times.idle;
+ }
+
+ const totalUsage = 100 - (100 * totalIdleTime) / totalWorkTime;
+ parentPort?.postMessage(totalUsage);
+}
+
+setInterval(calculateCpuUsage, 5000);
diff --git a/backEnd/running/src/codes/codes.service.ts b/backEnd/running/src/codes/codes.service.ts
index b922f93..b981028 100644
--- a/backEnd/running/src/codes/codes.service.ts
+++ b/backEnd/running/src/codes/codes.service.ts
@@ -40,8 +40,8 @@ export class CodesService {
try {
fs.writeFileSync(filePath, code);
const { stdout, stderr } = await this.runCommand(filePaths, language);
+ this.logger.debug(`${stdout}, ${stderr}`);
if (stderr) {
- console.log(stderr);
throw new RunningException(stderr.trim());
}
return this.getOutput(stdout);
@@ -57,11 +57,23 @@ export class CodesService {
}
}
- runCommand(
+ async runCommand(
filePaths: string[],
language: supportLang,
): Promise {
- const command = languageCommand(language, filePaths);
+ const commands = languageCommand(language, filePaths);
+ this.logger.debug(JSON.stringify(commands));
+
+ let command;
+ if (commands.length > 1) {
+ const { stdout, stderr } = await this.compile(commands[0]);
+ if (stderr) {
+ return { stdout, stderr };
+ }
+ command = commands[1];
+ } else {
+ command = commands[0];
+ }
return new Promise((resolve) => {
const commandParts = command.split(' ');
const stdout = [];
@@ -98,6 +110,29 @@ export class CodesService {
}
});
}
+ compile(command: string): Promise {
+ const commandParts = command.split(' ');
+ return new Promise((resolve) => {
+ const stdout = [];
+ const stderr = [];
+ const childProcess = spawn(commandParts[0], commandParts.slice(1));
+
+ childProcess.stdout.on('data', (data) => {
+ stdout.push(data);
+ });
+
+ childProcess.stderr.on('data', (data) => {
+ stderr.push(data);
+ });
+
+ childProcess.on('close', (code, signal) => {
+ this.logger.log(`complied done with code ${code}, ${signal}`);
+ const out = Buffer.concat(stdout).toString();
+ const err = Buffer.concat(stderr).toString();
+ resolve({ stdout: out, stderr: err });
+ });
+ });
+ }
getOutput(stdout: string): ResponseCodeDto {
return { output: stdout.trim() };
diff --git a/backEnd/running/src/common/supportLang.ts b/backEnd/running/src/common/supportLang.ts
index 048386c..c079498 100644
--- a/backEnd/running/src/common/supportLang.ts
+++ b/backEnd/running/src/common/supportLang.ts
@@ -19,21 +19,24 @@ export const distExtName = {
kotlin: '.jar',
};
-export function languageCommand(language, filePaths) {
+export function languageCommand(language, filePaths): string[] {
const [filepath, compile_dist] = filePaths;
switch (language) {
case 'python':
- return `python3 ${filepath}`;
+ return [`python3 ${filepath}`];
case 'javascript':
- return `node ${filepath}`;
+ return [`node ${filepath}`];
case 'java':
- return `java ${filepath}`;
+ return [`java ${filepath}`];
case 'c':
- return `gcc -o ${compile_dist} ${filepath} && ${compile_dist}`;
+ return [`gcc -o ${compile_dist} ${filepath}`, compile_dist];
case 'swift':
- return `swiftc -o ${compile_dist} ${filepath} && ${compile_dist}`;
+ return [`swiftc -o ${compile_dist} ${filepath}`, compile_dist];
case 'kotlin':
- return `kotlinc ${filepath} -include-runtime -d ${compile_dist} && java -jar ${compile_dist}`;
+ return [
+ `kotlinc ${filepath} -include-runtime -d ${compile_dist}`,
+ `java -jar ${compile_dist}`,
+ ];
}
}
export const needCompile = ['c', 'swift', 'kotlin'];
diff --git a/backEnd/signaling/src/events/events.service.ts b/backEnd/signaling/src/events/events.service.ts
index 7d0b47a..e128c10 100644
--- a/backEnd/signaling/src/events/events.service.ts
+++ b/backEnd/signaling/src/events/events.service.ts
@@ -19,7 +19,7 @@ export class EventsService implements OnModuleInit {
onModuleInit() {
this.initCpuWorker();
- this.publishSocketInfo();
+ this.scheduling();
}
private initCpuWorker() {
@@ -34,23 +34,13 @@ export class EventsService implements OnModuleInit {
});
}
- private publishSocketInfo() {
- const socketUrl = this.configService.get('SOCKET_URL');
-
- const message = {
- url: socketUrl,
- };
- this.client.publish('register', JSON.stringify(message));
- this.scheduling();
- }
-
private scheduling() {
cron.schedule('*/5 * * * *', () => {
const message = {
url: this.configService.get('SOCKET_URL'),
usages: this.usages,
};
- this.client.publish('signaling', JSON.stringify(message));
+ this.client.publish('signalingCpu', JSON.stringify(message));
});
}
}
diff --git a/frontEnd/index.html b/frontEnd/index.html
index 63cd6f8..1e1eac3 100644
--- a/frontEnd/index.html
+++ b/frontEnd/index.html
@@ -3,12 +3,13 @@
+
AlgoITNi
-
+
diff --git a/frontEnd/public/main.webp b/frontEnd/public/main.webp
new file mode 100644
index 0000000..8b1723e
Binary files /dev/null and b/frontEnd/public/main.webp differ
diff --git a/frontEnd/public/preview.png b/frontEnd/public/preview.png
new file mode 100644
index 0000000..232cef4
Binary files /dev/null and b/frontEnd/public/preview.png differ
diff --git a/frontEnd/src/components/common/RouterSpinner.tsx b/frontEnd/src/components/common/RouterSpinner.tsx
new file mode 100644
index 0000000..57973d9
--- /dev/null
+++ b/frontEnd/src/components/common/RouterSpinner.tsx
@@ -0,0 +1,18 @@
+import Spinner from './Spinner';
+
+export default function RouterSpinner() {
+ return (
+
+
+
+
+
+
+ Loading...
+
+
+ );
+}
diff --git a/frontEnd/src/components/room/ChattingSection.tsx b/frontEnd/src/components/room/ChattingSection.tsx
index 916d43a..348f0a6 100644
--- a/frontEnd/src/components/room/ChattingSection.tsx
+++ b/frontEnd/src/components/room/ChattingSection.tsx
@@ -26,19 +26,18 @@ function ChattingSection() {
const { ref: messageAreaRef, scrollRatio, handleScroll, moveToBottom } = useScroll();
const { isViewingLastMessage, isRecievedMessage, setIsRecievedMessage } = useLastMessageViewingState(scrollRatio);
- const handleRecieveMessage = (recievedMessage: string) => {
- const newMessage: MessageData | { using: boolean } = JSON.parse(recievedMessage);
- const remoteUsingAi = 'using' in newMessage;
+ const handleRecieveMessage = (recievedMessage: MessageData | { using: boolean }) => {
+ const remoteUsingAi = 'using' in recievedMessage;
if (remoteUsingAi) {
- setPostingAi(newMessage.using);
+ setPostingAi(recievedMessage.using);
return;
}
// 새로운 메시지가 일반적인 채팅 메시지인 경우
- if (newMessage.ai) setPostingAi(false); // AI의 메시지인 경우
+ if (recievedMessage.ai) setPostingAi(false); // AI의 메시지인 경우
- setAllMessage((prev) => [...prev, newMessage]);
+ setAllMessage((prev) => [...prev, recievedMessage]);
};
const handleChattingSocketError = (errorMessage: ErrorResponse) => {
diff --git a/frontEnd/src/components/room/modal/LoginModal.tsx b/frontEnd/src/components/room/modal/LoginModal.tsx
index e70813d..43c92ba 100644
--- a/frontEnd/src/components/room/modal/LoginModal.tsx
+++ b/frontEnd/src/components/room/modal/LoginModal.tsx
@@ -23,7 +23,7 @@ function LoginButtonWrapper({ handleClick, className, type, children }: LoginBut
);
return (
-
+
{children}
);
diff --git a/frontEnd/src/main.tsx b/frontEnd/src/main.tsx
index 0c101cb..df40399 100644
--- a/frontEnd/src/main.tsx
+++ b/frontEnd/src/main.tsx
@@ -1,14 +1,15 @@
+import { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import '@styles/index.css';
import Home from '@pages/Home.tsx';
-import Room from '@pages/Room.tsx';
-import Modals from './components/modal/Modals';
import reactQueryClient from './configs/reactQueryClient';
-import { CRDTProvider } from './contexts/crdt';
+import RouterSpinner from './components/common/RouterSpinner';
+
+const RoomPage = lazy(() => import('./pages/Room'));
const router = createBrowserRouter([
{
@@ -17,17 +18,18 @@ const router = createBrowserRouter([
},
{
path: '/:roomId',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
]);
function Main() {
return (
-
-
-
-
+
);
}
diff --git a/frontEnd/src/pages/Home.tsx b/frontEnd/src/pages/Home.tsx
index 162ce3c..5637eb1 100644
--- a/frontEnd/src/pages/Home.tsx
+++ b/frontEnd/src/pages/Home.tsx
@@ -60,7 +60,10 @@ export default function Home() {
-
+
);
diff --git a/frontEnd/src/pages/Room.tsx b/frontEnd/src/pages/Room.tsx
index deaef1e..489a9c5 100644
--- a/frontEnd/src/pages/Room.tsx
+++ b/frontEnd/src/pages/Room.tsx
@@ -8,6 +8,8 @@ import EditorSection from '@/components/room/EditorSection';
import ChattingSection from '@/components/room/ChattingSection';
import ControllSection from '@/components/room/ControllSection';
import useRoomConfigData from '@/stores/useRoomConfigData';
+import Modals from '@/components/modal/Modals';
+import { CRDTProvider } from '@/contexts/crdt';
const defaultCode = localStorage.getItem('code');
const defaultNickName = localStorage.getItem('nickName');
@@ -22,24 +24,27 @@ export default function Room() {
if (!isConnectionDone && !hasLogin) return ;
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
);
}