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 -#### 동료들과 함께 소통하며(화상, 음성, 채팅) 알고리즘 학습을 할 수 있는 플랫폼 - +
+ -##### [팀 노션 바로가기](!https://energetic-palm-634.notion.site/AlgoITNi-4d712d57a7be42bfb625d23d5eab5453?pvs=4) ---- -# 팀원소개 +

동료들과 함께 소통하며 알고리즘 학습을 할 수 있는 플랫폼

+
🗝️ KeyWords
+#WebRTC #Socket #CRDT +
+
+ + + + + + +
+ + + + + + +
+ + + + + +
+
+
-| J065 서위영 | J094 이동길 | J126 이희경 | J151 지승민 | -| :----------------------------------------------------------------------------: | :----------------------------------------------------------------------------: | :--------------------------------------------------------------------------: | :--------------------------------------------------------------------------: | -| | | | | -| **Front-End** | **Front-End** | **Back-End** | **Back-End** | -| [HBSPS](https://github.com/HBSPS) | [d0422](https://github.com/d0422) | [HKLeeeee](https://github.com/HKLeeeee) | [Gseungmin](https://github.com/Gseungmin) | +--- -# Tech -`#WebRTC` `#Socket` `#CRDT` +# 🔎서버 아키텍처 +![Untitled](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/019fa58f-c8c0-47f3-87a9-ea7cf36722d0) -| 분야 | 기술스택 | -|----|------| -| FE | React, Typescript, WebRTC, github action, socket.io | -| BE | NestJS, Typescript, Docker, github action, socket.io, mongoDB, MySQL, Redis | -# 주요 기능 소개 -### 메인화면 +# 🔎 주요 기능 + +### 🐱 화상회의 방 생성 버튼으로 새롭게 방을 만들거나 공유받은 방 코드로 이미 있는 방에 참여할 수 있습니다. +![EnterRoom](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/fea34177-cffe-4700-914c-a304d0302f51) + + 동료들과 화상회의를 하며 소통할 수 있습니다.
+WebRTC P2P로 통신합시다.
+ ![4명입장](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/323fc7d1-5b4e-455f-923d-5db5855a0146) -![main](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/f4b34b43-efbf-4ada-8806-6a7bb9cd17d5) -### 🐱 화상회의 - 동료들과 화상회의를 하며 소통할 수 있습니다. ### 😎 코드 공동 편집 - 코드 편집기를 통해 코드를 공동 편집할 수 있습니다.
+ 코드 편집기를 통해 코드를 작성할 수 있습니다.
+CRDT로 공동편집을 구현해 참여한 사람들과 함께 편집할 수 있습니다.
집단 지성을 발휘해보세요! + + ![crdt](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/42d71ab8-da12-4288-9a5d-47f9524cce96) + +### 📥 문제 보기 +문제의 링크를 입력해 문제를 보면서 풀이할 수 있습니다.
+크롤링을 통해서 입력한 링크를 가져옵니다.
+백준 사이트를 가장 잘 보여줍니다.
+![showProm2](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/c2174696-2465-47dd-b8ab-aa47c7e7abbc) + ### 🐍 코드 실행 -작성한 코드를 실행하고 실행 결과를 확인할 수 있습니다. +작성한 코드를 실행하고 실행 결과를 확인할 수 있습니다.
+소켓과 메세지 큐를 통해 코드 실행이 요청됩니다.
+지원 언어 : `Python` `Javascript` `Java` `C` `Swift` `Kotlin` + +![running](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/282bcc84-4045-49f6-a5f7-2e5995483e73) + ### 💬 채팅 -채팅을 통해서도 소통할 수 있습니다.
-음성 채팅이 어려운 상황에서나 참고할 자료를 보낼 때 활용할 수 있습니다. +채팅을 통해서도 소통할 수 있습니다. 음성 채팅이 어려운 상황에서나 참고할 자료를 보낼 때 활용할 수 있습니다.
+Pub/Sub을 활용해 다중 서버 환경에서도 채팅을 할 수 있습니다.
+채팅 중 **클로바X**에게 질문하고 답변 받을 수 있습니다. + +![chat](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/a8f14d69-863e-4c6f-8779-ee6422d2dcce) -# 우리가 일하는 방식 +# 🔎 기술적 도전 +### 프론트엔드의 기술적 도전 +- [코드 에디터에서의 공동편집을 위한 CRDT구현](https://energetic-palm-634.notion.site/4826739090cf431e829bd928fd46a297?v=09650c23000d477f828c92563f0c8368&pvs=4) + - 3번의 시도와 구현, 문제해결기와 더 나은 기능을 위해 라이브러리를 도입한 이야기 +- [홈화면 성능 최적화 도전하기](https://energetic-palm-634.notion.site/f7286ebaa50f484da0a88a37888f77dc?v=f46a3e1fd63e435c9b1f642d220888ac&pvs=40) + - 더 나은 UX를 위해 홈화면 초기 렌더링 성능 약 14% 개선 +- [수많은 모달을 관리하기 위한 공통 모달 만들기](https://energetic-palm-634.notion.site/23cca8a3b3b44fce9a9df4b0a7e70dcd?v=9c4c39359a0e445dbdc2b7cdb2d74c68&pvs=4) + - 중첩 모달과 많은 모달들을 쉽고 효율적으로 관리하기위한 모달 시스템 만들기 +### 백엔드의 기술적 도전 +- [다중 서버 환경에서 코드 실행 동시 요청 처리하기](https://energetic-palm-634.notion.site/bfeb2b52f3f34fe2af9bf93f254f8f5c?v=82acb687cdb74475986d223ac753bf05&pvs=4) + - 소켓, Message Queue, Pub/Sub을 도입하여 CPU 사용량 43%, Memory 사용량 21% 감소시킨 이야기 +- [다중 소켓 서버 트래픽 관리하기](https://energetic-palm-634.notion.site/d243a71d17f94018bd94a6b825fddfe4?v=803c0b95332343e1918ee10ff269e4f6&pvs=4) + - 중앙서버와 Pub/Sub 도입으로 트래픽 분산 & 확장에 자유로운 구조로 개선 +- [DB 부하 분산하기](https://energetic-palm-634.notion.site/8c129aa38b2f40c784b7641d8941571d?v=340d00941d4641f9bc47ee292d9d9cf5&pvs=4) + - Master DB CPU 사용룰 90% 감소 및 요청처리 95% 증가 +- [Nginx 캐싱으로 크롤링 속도 높이기](https://energetic-palm-634.notion.site/270f92cdadaa475aa3827b300c511172?v=d67c232d930549948bdd0ad4c306c14f&pvs=4) + - 429 Error 및 InMemory 용량부족 해결 과정 (16만건 처리에 걸리는 시간 64% 감소) +- [도커 이미지 최적화](https://energetic-palm-634.notion.site/f35c15bc99a842a18ce095fa6bf1c806?v=efbb8ec67beb43b89792200fc1f3c9a1&pvs=4) + - 도커 이미지 사이즈 85% 감소시킨 이야기 +- [서버에서 OAuth 처리하여 자원 보호하기](https://energetic-palm-634.notion.site/69f2e78273884a65b52c370debb83073?v=2b272ead31924af59732edbda24cef84&pvs=4) + - OAuth2.0을 도입하고 안전하게 자원을 관리하는 이야기 + +# 🔎 개발기 +개발하면서 공부한 내용들과 고민 과정, 이유, 해결 방법을 기록했습니다. + +[FE] +- [Web RTC를 이해해보자](https://energetic-palm-634.notion.site/Web-RTC-1e8d918a19be444da6b0656167df35a6?pvs=4) +- [S3, CloudFront로 OAC를 통해 프론트엔드 배포하기](https://energetic-palm-634.notion.site/FrontEnd-CICD-with-S3-Cloud-Front-64ac0d2dab194a04b14743d034deb1c5?pvs=4) +- [로컬 환경에서 쿠키 테스트하기](https://energetic-palm-634.notion.site/8f53abc52d6a4b72816fc4aa9c211de2?pvs=4) +- [채팅창에 쓰로틀링 적용하기](https://energetic-palm-634.notion.site/9e768460a8904a8e859ba13cab0f78c2?pvs=4) +- [쉘 스크립트로 디렉토리별 pre-commit 적용하기](https://energetic-palm-634.notion.site/pre-commit-a60bec2c72e440a2ad414a1ab4b18f29?pvs=4) + +[BE] +- [세션을 활용해 로그인 후 원래위치로 돌아가기](https://energetic-palm-634.notion.site/d2f6157bdcef40a6a72eacbb28acb798?pvs=4) +- [Transaction 관심사 분리하기](https://energetic-palm-634.notion.site/AsyncLocalStorage-Transaction-34f42523c0ec43f4b633eb7944c0b29d?pvs=4) +- [SSL Termination을 통해 안전하게 HTTP 통신하기](https://energetic-palm-634.notion.site/SSL-Termination-HTTP-70c76949740f4452a2899fa1e617628a?pvs=4) +- [Blue-Green으로 무중단 배포하기](https://energetic-palm-634.notion.site/57396ff1e3174251ba2c7487ab070a53?pvs=4) +- [Clove X 도입하기](https://www.notion.so/Clova-Studio-d990f41d3e814b708906e64fd4707a24?pvs=4) + + +[👉 더 많은 기술정리 보러가기](https://www.notion.so/f4562ec49e0245d2b6ef203588c031ea?v=fbfeb754b1a4471e8ffc174a45c64346&pvs=4) + + +# 🔎 팀 소개 + +| J065 서위영 | J094 이동길 | J126 이희경 | J151 지승민 | +|:------------------------------------------------------------------------------:| :----------------------------------------------------------------------------: | :--------------------------------------------------------------------------: | :--------------------------------------------------------------------------: | +| | | | | +| **Front-End** | **Front-End** | **Back-End** | **Back-End** | +| [@HBSPS](https://github.com/HBSPS) | [@d0422](https://github.com/d0422) | [@HKLeeeee](https://github.com/HKLeeeee) | [@Gseungmin](https://github.com/Gseungmin) | + + +## 우리가 일하는 방식 - [그라운드 룰](https://energetic-palm-634.notion.site/1f2cbea527e341c7ad1c8fd84ed5104d?pvs=4) - [깃 컨벤션](https://energetic-palm-634.notion.site/Git-Convention-8563596644404eb49148a940773d2be8?pvs=4) - [게더타운 규칙](https://energetic-palm-634.notion.site/b3b67313c1f748e7b58abf99466b000b?pvs=4) + +--- +😽 Team Notion diff --git a/backEnd/api/src/app.module.ts b/backEnd/api/src/app.module.ts index 5a917c8..dc64490 100644 --- a/backEnd/api/src/app.module.ts +++ b/backEnd/api/src/app.module.ts @@ -14,6 +14,7 @@ import { RedisModule } from './redis/redis.module'; import { AuthModule } from './auth/auth.module'; import { CrawlerModule } from './crawler/crawler.module'; import { CodesModule } from './codes/codes.module'; +import { TransactionModule } from './common/transaction/transaction.module'; @Module({ imports: [ @@ -69,6 +70,7 @@ import { CodesModule } from './codes/codes.module'; AuthModule, CrawlerModule, CodesModule, + TransactionModule, ], controllers: [AppController], providers: [AppService, TimeoutInterceptor, WinstonLogger], diff --git a/backEnd/api/src/codes/codes.controller.ts b/backEnd/api/src/codes/codes.controller.ts index b7be8a6..3bb7bf4 100644 --- a/backEnd/api/src/codes/codes.controller.ts +++ b/backEnd/api/src/codes/codes.controller.ts @@ -77,12 +77,8 @@ export class CodesController { async delete(@Req() req: Request, @Param('id') id: string) { const user: UserInfoDto = req.user as UserInfoDto; const userID = user.id; - try { - await this.codesService.delete(userID, id); - return { message: 'delete success' }; - } catch (e) { - this.logger.error(e); - throw new ResourceNotFound(); - } + const result = await this.codesService.delete(userID, id); + if (result.deletedCount <= 0) throw new ResourceNotFound(); + return { message: 'delete success' }; } } diff --git a/backEnd/api/src/codes/codes.service.ts b/backEnd/api/src/codes/codes.service.ts index 1d2f967..fab9ef3 100644 --- a/backEnd/api/src/codes/codes.service.ts +++ b/backEnd/api/src/codes/codes.service.ts @@ -3,7 +3,12 @@ import { InjectConnection, InjectModel } from '@nestjs/mongoose'; import { Code } from './schemas/code.schemas'; import { Connection, Model } from 'mongoose'; import { SaveCodeDto } from './dto/saveCode.dto'; -import { TransactionRollback } from '../common/exception/exception'; +import { + getSession, + Transactional, +} from '../common/transaction/transaction.decorator'; +import { ClientSession } from 'mongoose'; +import { DeleteResult } from 'mongodb'; @Injectable() export class CodesService { @@ -13,20 +18,13 @@ export class CodesService { @InjectConnection() private readonly connection: Connection, ) {} - async save(saveCodeDto: SaveCodeDto): Promise { - 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 ( +
+
+ + + main + +
+
+ +
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() {
- main + + + main +
); 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 ( -
-
-
- -
-
-
- - + +
+
+
+
-
- +
+
+ + +
+
+ +
+
+ +
-
- -
-
+ +
); }