diff --git a/README.md b/README.md
index 43c706c..f5f559b 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,18 @@ slack의 서버, 채널 기능과 zoom의 인스턴스 화상채팅 기능을
|------|---|---|------|
|
|
|
|
|
+
+## 주요 기능
+### WebRTC를 이용한 실시간 화상, 음성채팅 기능
+
+
+### WebSpeech API를 이용한 Speech-To-Text 기능
+
+
+### Slack과 유사한 방식의 텍스트 채팅 서버, 채널 관리
+
+
+
## 기술 스택
| division | stack |
| --------------- | --------------------------------- |
@@ -24,13 +36,12 @@ slack의 서버, 채널 기능과 zoom의 인스턴스 화상채팅 기능을
[기술스택 상세]((https://github.com/boostcampwm-2021/web07-boostCam/wiki/Tech-Stack))
-## 기획서
-[기획서 링크](https://docs.google.com/presentation/d/1CMu3LHJmwsUydwi0grAzuaGcXTTmatm5mvMyuJue2Rs/edit?usp=sharing)
## 위키
[위키](https://github.com/boostcampwm-2021/web07-boostCam/wiki)에 가시면 아래 내용들을 확인할 수 있습니다
+ - 기획서
- 목업
- 그라운드 룰
- - 데일리 스크럼
+ - 데일리 스크럼 기록
- 회의록
- 백로그
diff --git a/backend/.env.timezone b/backend/.env.timezone
new file mode 100644
index 0000000..02fdee7
--- /dev/null
+++ b/backend/.env.timezone
@@ -0,0 +1 @@
+TZ=Asia/Seoul
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 75542e5..7eb01f2 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -14,6 +14,7 @@
"@nestjs/core": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/platform-socket.io": "^8.1.2",
+ "@nestjs/swagger": "^5.1.5",
"@nestjs/typeorm": "^8.0.2",
"@nestjs/websockets": "^8.1.2",
"aws-sdk": "^2.1033.0",
@@ -27,6 +28,7 @@
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"socket.io": "^4.3.1",
+ "swagger-ui-express": "^4.1.6",
"typeorm": "^0.2.40"
},
"devDependencies": {
@@ -1582,6 +1584,17 @@
}
}
},
+ "node_modules/@nestjs/mapped-types": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz",
+ "integrity": "sha512-26AW5jHadLXtvHs+M+Agd9KZ92dDlBrmD0rORlBlvn2KvsWs4JRaKl2mUsrW7YsdZeAu3Hc4ukqyYyDdyCmMWQ==",
+ "peerDependencies": {
+ "@nestjs/common": "^7.0.8 || ^8.0.0",
+ "class-transformer": "^0.2.0 || ^0.3.0 || ^0.4.0",
+ "class-validator": "^0.11.1 || ^0.12.0 || ^0.13.0",
+ "reflect-metadata": "^0.1.12"
+ }
+ },
"node_modules/@nestjs/platform-express": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.1.2.tgz",
@@ -1636,6 +1649,31 @@
"typescript": "^3.4.5 || ^4.3.5"
}
},
+ "node_modules/@nestjs/swagger": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-5.1.5.tgz",
+ "integrity": "sha512-jvsgciVEFcYVVEuLdNAmgJDpFSQzIoukg+Ovz1vJ97OeCprx0sVhmocn13nCl/dEwIkwoM3eby9OiaccX0A+3A==",
+ "dependencies": {
+ "@nestjs/mapped-types": "1.0.0",
+ "lodash": "4.17.21",
+ "path-to-regexp": "3.2.0"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^8.0.0",
+ "@nestjs/core": "^8.0.0",
+ "fastify-swagger": "*",
+ "reflect-metadata": "^0.1.12",
+ "swagger-ui-express": "*"
+ },
+ "peerDependenciesMeta": {
+ "fastify-swagger": {
+ "optional": true
+ },
+ "swagger-ui-express": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@nestjs/testing": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.1.2.tgz",
@@ -3306,6 +3344,22 @@
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
"dev": true
},
+ "node_modules/class-transformer": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz",
+ "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==",
+ "peer": true
+ },
+ "node_modules/class-validator": {
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz",
+ "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==",
+ "peer": true,
+ "dependencies": {
+ "libphonenumber-js": "^1.9.43",
+ "validator": "^13.7.0"
+ }
+ },
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -6687,6 +6741,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.9.43",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.43.tgz",
+ "integrity": "sha512-tNB87ZutAiAkl3DE/Bo0Mxqn/XZbNxhPg4v9bYBwQQW4dlhBGqXl1vtmPxeDWbrijzwOA9vRjOOFm5V9SK/W3w==",
+ "peer": true
+ },
"node_modules/lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -6716,8 +6776,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.get": {
"version": "4.4.2",
@@ -8651,6 +8710,25 @@
"node": ">=8"
}
},
+ "node_modules/swagger-ui-dist": {
+ "version": "3.52.5",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
+ "integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
+ "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==",
+ "dependencies": {
+ "swagger-ui-dist": "^3.18.1"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0"
+ }
+ },
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@@ -9498,6 +9576,15 @@
"node": ">=10.12.0"
}
},
+ "node_modules/validator": {
+ "version": "13.7.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
+ "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -11144,6 +11231,12 @@
"uuid": "8.3.2"
}
},
+ "@nestjs/mapped-types": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz",
+ "integrity": "sha512-26AW5jHadLXtvHs+M+Agd9KZ92dDlBrmD0rORlBlvn2KvsWs4JRaKl2mUsrW7YsdZeAu3Hc4ukqyYyDdyCmMWQ==",
+ "requires": {}
+ },
"@nestjs/platform-express": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.1.2.tgz",
@@ -11178,6 +11271,16 @@
"pluralize": "8.0.0"
}
},
+ "@nestjs/swagger": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-5.1.5.tgz",
+ "integrity": "sha512-jvsgciVEFcYVVEuLdNAmgJDpFSQzIoukg+Ovz1vJ97OeCprx0sVhmocn13nCl/dEwIkwoM3eby9OiaccX0A+3A==",
+ "requires": {
+ "@nestjs/mapped-types": "1.0.0",
+ "lodash": "4.17.21",
+ "path-to-regexp": "3.2.0"
+ }
+ },
"@nestjs/testing": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.1.2.tgz",
@@ -12503,6 +12606,22 @@
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
"dev": true
},
+ "class-transformer": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz",
+ "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==",
+ "peer": true
+ },
+ "class-validator": {
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz",
+ "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==",
+ "peer": true,
+ "requires": {
+ "libphonenumber-js": "^1.9.43",
+ "validator": "^13.7.0"
+ }
+ },
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -15065,6 +15184,12 @@
"type-check": "~0.4.0"
}
},
+ "libphonenumber-js": {
+ "version": "1.9.43",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.43.tgz",
+ "integrity": "sha512-tNB87ZutAiAkl3DE/Bo0Mxqn/XZbNxhPg4v9bYBwQQW4dlhBGqXl1vtmPxeDWbrijzwOA9vRjOOFm5V9SK/W3w==",
+ "peer": true
+ },
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -15088,8 +15213,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.get": {
"version": "4.4.2",
@@ -16569,6 +16693,19 @@
"supports-color": "^7.0.0"
}
},
+ "swagger-ui-dist": {
+ "version": "3.52.5",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
+ "integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
+ },
+ "swagger-ui-express": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
+ "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==",
+ "requires": {
+ "swagger-ui-dist": "^3.18.1"
+ }
+ },
"symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@@ -17127,6 +17264,12 @@
"source-map": "^0.7.3"
}
},
+ "validator": {
+ "version": "13.7.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
+ "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
+ "peer": true
+ },
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/backend/package.json b/backend/package.json
index e5c15f9..83bb4d5 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -26,6 +26,7 @@
"@nestjs/core": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/platform-socket.io": "^8.1.2",
+ "@nestjs/swagger": "^5.1.5",
"@nestjs/typeorm": "^8.0.2",
"@nestjs/websockets": "^8.1.2",
"aws-sdk": "^2.1033.0",
@@ -39,6 +40,7 @@
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"socket.io": "^4.3.1",
+ "swagger-ui-express": "^4.1.6",
"typeorm": "^0.2.40"
},
"devDependencies": {
diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts
deleted file mode 100644
index d22f389..0000000
--- a/backend/src/app.controller.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Test, TestingModule } from '@nestjs/testing';
-import { AppController } from './app.controller';
-import { AppService } from './app.service';
-
-describe('AppController', () => {
- let appController: AppController;
-
- beforeEach(async () => {
- const app: TestingModule = await Test.createTestingModule({
- controllers: [AppController],
- providers: [AppService],
- }).compile();
-
- appController = app.get(AppController);
- });
-
- describe('root', () => {
- it('should return "Hello World!"', () => {
- expect(appController.getHello()).toBe('Hello World!');
- });
- });
-});
diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts
deleted file mode 100644
index cce879e..0000000
--- a/backend/src/app.controller.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Controller, Get } from '@nestjs/common';
-import { AppService } from './app.service';
-
-@Controller()
-export class AppController {
- constructor(private readonly appService: AppService) {}
-
- @Get()
- getHello(): string {
- return this.appService.getHello();
- }
-}
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index 5a0d9b8..c21ae52 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import ormConfig from './config/ormconfig';
-import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CamModule } from './cam/cam.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -23,7 +22,7 @@ import githubConfig from './config/github.config';
imports: [
ConfigModule.forRoot({
load: [githubConfig],
- envFilePath: ['.env', '.env.github', '.env.redis'],
+ envFilePath: ['.env', '.env.github', '.env.redis', '.env.timezone'],
isGlobal: true,
}),
TypeOrmModule.forRoot(ormConfig()),
@@ -39,7 +38,6 @@ import githubConfig from './config/github.config';
UserChannelModule,
ImageModule,
],
- controllers: [AppController],
providers: [AppService, MessageGateway],
})
export class AppModule {}
diff --git a/backend/src/cam/cam-inner.service.ts b/backend/src/cam/cam-inner.service.ts
index 3e53153..d0fbfc2 100644
--- a/backend/src/cam/cam-inner.service.ts
+++ b/backend/src/cam/cam-inner.service.ts
@@ -1,6 +1,5 @@
-import { forwardRef, Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Status, CamMap } from '../types/cam';
-import { CamService } from './cam.service';
type RoomId = string;
type SocketId = string;
@@ -17,10 +16,7 @@ export class CamInnerService {
private map: Map>;
private sharedScreen: Map;
- constructor(
- @Inject(forwardRef(() => CamService))
- private readonly camService: CamService,
- ) {
+ constructor() {
this.map = new Map();
this.sharedScreen = new Map();
}
@@ -55,12 +51,7 @@ export class CamInnerService {
exitRoom(roomId: string, userId: string) {
if (!this.map.get(roomId)) return false;
const room = this.map.get(roomId).filter((user) => user.userId !== userId);
- if (room.length == 0) {
- this.map.delete(roomId);
- this.camService.deleteCam(roomId);
- } else {
- this.map.set(roomId, room);
- }
+ this.map.set(roomId, room);
}
updateStatus(roomId: string, userId: string, status: Status) {
@@ -108,4 +99,8 @@ export class CamInnerService {
const room = this.map.get(roomId);
return room && room.length < MAX_PEOPLE;
}
+
+ deleteRoom(roomId: RoomId) {
+ this.map.delete(roomId);
+ }
}
diff --git a/backend/src/cam/cam.controller.ts b/backend/src/cam/cam.controller.ts
index 6b954d5..ed5d824 100644
--- a/backend/src/cam/cam.controller.ts
+++ b/backend/src/cam/cam.controller.ts
@@ -1,17 +1,34 @@
-import { Controller, Post, Body, Get, Param } from '@nestjs/common';
+import {
+ Controller,
+ Post,
+ Body,
+ Get,
+ Param,
+ Session,
+ Delete,
+ ForbiddenException,
+ UseGuards,
+ ParseIntPipe,
+} from '@nestjs/common';
import ResponseEntity from '../common/response-entity';
-import { CreateCamDto } from './cam.dto';
+import { LoginGuard } from '../login/login.guard';
+import { ExpressSession } from '../types/session';
+import { RequestCamDto } from './cam.dto';
import { Cam } from './cam.entity';
import { CamService } from './cam.service';
-@Controller('api/cam')
+@Controller('/api/cam')
export class CamController {
constructor(private camService: CamService) {}
- @Post() async createCam(
- @Body() cam: CreateCamDto,
+ @UseGuards(LoginGuard)
+ @Post()
+ async createCam(
+ @Body() cam: RequestCamDto,
+ @Session() session: ExpressSession,
): Promise> {
+ cam.userId = session.user?.id;
const savedCam = await this.camService.createCam(cam);
return ResponseEntity.created(savedCam.id);
@@ -24,4 +41,21 @@ export class CamController {
return ResponseEntity.ok(cam);
}
+
+ @UseGuards(LoginGuard)
+ @Delete('/:id') async deleteCam(
+ @Param('id', new ParseIntPipe()) id: number,
+ @Session() session: ExpressSession,
+ ): Promise> {
+ const cam = await this.camService.findOneById(id);
+ const isOwner = session.user?.id == cam.ownerId;
+
+ if (!isOwner) {
+ throw new ForbiddenException();
+ }
+
+ await this.camService.deleteCam(id);
+
+ return ResponseEntity.noContent();
+ }
}
diff --git a/backend/src/cam/cam.dto.ts b/backend/src/cam/cam.dto.ts
index a12c848..e7ae283 100644
--- a/backend/src/cam/cam.dto.ts
+++ b/backend/src/cam/cam.dto.ts
@@ -1,19 +1,22 @@
import { Cam } from './cam.entity';
-export type CreateCamDto = {
+export type RequestCamDto = {
name: string;
serverId: number;
+ userId: number | null;
};
export class ResponseCamDto {
+ id: number;
name: string;
url: string;
- constructor(name: string, url: string) {
+ constructor(id: number, name: string, url: string) {
+ this.id = id;
this.name = name;
this.url = url;
}
static fromEntry(cam: Cam) {
- return new ResponseCamDto(cam.name, cam.url);
+ return new ResponseCamDto(cam.id, cam.name, cam.url);
}
}
diff --git a/backend/src/cam/cam.entity.ts b/backend/src/cam/cam.entity.ts
index 96fcc99..8ca6ba0 100644
--- a/backend/src/cam/cam.entity.ts
+++ b/backend/src/cam/cam.entity.ts
@@ -7,10 +7,11 @@ import {
} from 'typeorm';
import { Server } from '../server/server.entity';
+import { User } from '../user/user.entity';
@Entity()
export class Cam {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@Column()
@@ -19,9 +20,15 @@ export class Cam {
@Column()
url: string;
- @ManyToOne(() => Server)
+ @ManyToOne(() => Server, { onDelete: 'CASCADE' })
server: Server;
+ @ManyToOne(() => User, { onDelete: 'CASCADE' })
+ owner: User;
+
@RelationId((cam: Cam) => cam.server)
serverId: number;
+
+ @RelationId((cam: Cam) => cam.owner)
+ ownerId: number;
}
diff --git a/backend/src/cam/cam.gateway.ts b/backend/src/cam/cam.gateway.ts
index 49221b9..e5fda9a 100644
--- a/backend/src/cam/cam.gateway.ts
+++ b/backend/src/cam/cam.gateway.ts
@@ -8,7 +8,7 @@ import { Socket, Server } from 'socket.io';
import { Status, MessageInfo } from '../types/cam';
import { CamInnerService } from './cam-inner.service';
-@WebSocketGateway()
+@WebSocketGateway({ namespace: '/cam' })
export class CamGateway {
@WebSocketServer() server: Server;
constructor(private camInnerService: CamInnerService) {}
@@ -117,14 +117,14 @@ export class CamGateway {
});
}
- @SubscribeMessage('sendMessage')
+ @SubscribeMessage('sendCamMessage')
handleSendMessage(client: Socket, payload: MessageInfo): void {
if (!client.data.roomId || !client.data.userId) return;
const { roomId } = client.data;
const nicknameInfo = this.camInnerService.getRoomNicknameList(roomId);
client.broadcast
.to(roomId)
- .emit('receiveMessage', { payload, nicknameInfo });
+ .emit('receiveCamMessage', { payload, nicknameInfo });
}
@SubscribeMessage('changeNickname')
diff --git a/backend/src/cam/cam.module.ts b/backend/src/cam/cam.module.ts
index 2e797da..d594be8 100644
--- a/backend/src/cam/cam.module.ts
+++ b/backend/src/cam/cam.module.ts
@@ -9,10 +9,19 @@ import { Cam } from './cam.entity';
import { ServerRepository } from '../server/server.repository';
import { CamRepository } from './cam.repository';
import { Server } from '../server/server.entity';
+import { UserRepository } from '../user/user.repository';
+import { User } from '../user/user.entity';
@Module({
imports: [
- TypeOrmModule.forFeature([Cam, Server, CamRepository, ServerRepository]),
+ TypeOrmModule.forFeature([
+ Cam,
+ Server,
+ CamRepository,
+ ServerRepository,
+ User,
+ UserRepository,
+ ]),
],
providers: [CamGateway, CamInnerService, CamService],
controllers: [CamController],
diff --git a/backend/src/cam/cam.service.ts b/backend/src/cam/cam.service.ts
index 1439e3b..8a36d86 100644
--- a/backend/src/cam/cam.service.ts
+++ b/backend/src/cam/cam.service.ts
@@ -1,25 +1,24 @@
import {
BadRequestException,
ForbiddenException,
- forwardRef,
- Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { v4 } from 'uuid';
import { ServerRepository } from '../server/server.repository';
-import { CreateCamDto, ResponseCamDto } from './cam.dto';
+import { RequestCamDto, ResponseCamDto } from './cam.dto';
import { Cam } from './cam.entity';
import { CamRepository } from './cam.repository';
import { CamInnerService } from './cam-inner.service';
+import { UserRepository } from '../user/user.repository';
@Injectable()
export class CamService {
constructor(
private camRepository: CamRepository,
private serverRepository: ServerRepository,
- @Inject(forwardRef(() => CamInnerService))
+ private userRepository: UserRepository,
private readonly camInnerService: CamInnerService,
) {
this.camRepository.clear();
@@ -41,7 +40,17 @@ export class CamService {
return cam;
}
- async createCam(cam: CreateCamDto): Promise {
+ async findOneById(id: number): Promise {
+ const cam = await this.camRepository.findOne({ id: id });
+
+ if (!cam) {
+ throw new NotFoundException();
+ }
+
+ return cam;
+ }
+
+ async createCam(cam: RequestCamDto): Promise {
const camEntity = this.camRepository.create();
const server = await this.serverRepository.findOne({
id: cam.serverId,
@@ -51,9 +60,16 @@ export class CamService {
throw new BadRequestException();
}
+ const user = await this.userRepository.findOne({ id: cam?.userId });
+
+ if (!cam.userId || !user) {
+ throw new ForbiddenException();
+ }
+
camEntity.name = cam.name;
camEntity.server = server;
camEntity.url = v4();
+ camEntity.owner = user;
const savedCam = await this.camRepository.save(camEntity);
this.camInnerService.createRoom(camEntity.url);
@@ -61,8 +77,16 @@ export class CamService {
return savedCam;
}
- async deleteCam(url: string): Promise {
- await this.camRepository.delete({ url: url });
+ async deleteCam(id: number): Promise {
+ const camEntity = await this.camRepository.findOne({ id: id });
+
+ if (!camEntity) {
+ throw new BadRequestException();
+ }
+
+ this.camInnerService.deleteRoom(camEntity.url);
+
+ await this.camRepository.delete({ id: camEntity.id });
}
async getCamList(serverId: number): Promise {
diff --git a/backend/src/channel/channe.dto.ts b/backend/src/channel/channe.dto.ts
deleted file mode 100644
index 80134d7..0000000
--- a/backend/src/channel/channe.dto.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type ChannelFormDto = {
- name: string;
- description: string;
- serverId: number;
-};
diff --git a/backend/src/channel/channel-form.dto.ts b/backend/src/channel/channel-form.dto.ts
new file mode 100644
index 0000000..d343e92
--- /dev/null
+++ b/backend/src/channel/channel-form.dto.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class ChannelFormDto {
+ @ApiProperty()
+ name: string;
+
+ @ApiProperty()
+ description: string;
+
+ @ApiProperty()
+ serverId: number;
+}
diff --git a/backend/src/channel/channel.controller.ts b/backend/src/channel/channel.controller.ts
index 25b2e9b..967cd46 100644
--- a/backend/src/channel/channel.controller.ts
+++ b/backend/src/channel/channel.controller.ts
@@ -8,17 +8,18 @@ import {
Patch,
Session,
UseGuards,
+ ParseIntPipe,
} from '@nestjs/common';
import { LoginGuard } from '../login/login.guard';
import { ExpressSession } from '../types/session';
import { ChannelService } from './channel.service';
import { Channel } from './channel.entity';
-import { ChannelFormDto } from './channe.dto';
+import { ChannelFormDto } from './channel-form.dto';
import { UserChannelService } from '../user-channel/user-channel.service';
import ResponseEntity from '../common/response-entity';
-@Controller('api/channel')
+@Controller('/api/channels')
@UseGuards(LoginGuard)
export class ChannelController {
constructor(
@@ -28,36 +29,49 @@ export class ChannelController {
this.channelService = channelService;
this.userChannelService = userChannelService;
}
- @Get() async findAll(): Promise> {
- const channelList = await this.channelService.findAll();
- return ResponseEntity.ok(channelList);
- }
- @Get(':id') async findOne(
- @Param('id') id: number,
+ @Get(':channelId') async findOne(
+ @Param('channelId', new ParseIntPipe()) id: number,
): Promise> {
- const foundServer = await this.channelService.findOne(id);
- return ResponseEntity.ok(foundServer);
+ const foundChannel = await this.channelService.findOne(id);
+ return ResponseEntity.ok(foundChannel);
+ }
+
+ @Get(':channelId/auth') async checkAuthority(
+ @Param('channelId', new ParseIntPipe()) id: number,
+ @Session() session: ExpressSession,
+ ): Promise> {
+ const foundChannel = await this.channelService.findOne(id);
+ return ResponseEntity.ok(foundChannel.ownerId === session.user.id);
}
+
@Post() async saveChannel(
@Body() channel: ChannelFormDto,
@Session() session: ExpressSession,
): Promise> {
- const savedChannel = await this.channelService.createChannel(channel);
+ const savedChannel = await this.channelService.createChannel(
+ channel,
+ session.user.id,
+ );
await this.userChannelService.addNewChannel(savedChannel, session.user.id);
return ResponseEntity.ok(savedChannel);
}
- @Patch(':id') async updateUser(
- @Param('id') id: number,
+ @Patch(':channelId') async updateChannel(
+ @Param('channelId', new ParseIntPipe()) id: number,
@Body() channel: ChannelFormDto,
+ @Session() session: ExpressSession,
): Promise> {
- const changedChannel = await this.channelService.updateChannel(id, channel);
+ const changedChannel = await this.channelService.updateChannel(
+ id,
+ channel,
+ session.user.id,
+ );
return ResponseEntity.ok(changedChannel);
}
- @Delete(':id') async deleteChannel(
- @Param('id') id: number,
- ): Promise> {
+ @Delete(':channelId') async deleteChannel(
+ @Param('channelId', new ParseIntPipe()) id: number,
+ ): Promise> {
await this.channelService.deleteChannel(id);
- return ResponseEntity.ok(id);
+ return ResponseEntity.noContent();
}
}
diff --git a/backend/src/channel/channel.entity.ts b/backend/src/channel/channel.entity.ts
index 489ffc4..eece1b8 100644
--- a/backend/src/channel/channel.entity.ts
+++ b/backend/src/channel/channel.entity.ts
@@ -1,9 +1,18 @@
import { Server } from '../server/server.entity';
-import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
+import {
+ Entity,
+ Column,
+ PrimaryGeneratedColumn,
+ ManyToOne,
+ RelationId,
+ OneToMany,
+} from 'typeorm';
+import { User } from '../user/user.entity';
+import { UserChannel } from '../user-channel/user-channel.entity';
@Entity()
export class Channel {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@Column()
@@ -12,6 +21,32 @@ export class Channel {
@Column()
description: string;
- @ManyToOne(() => Server)
+ @ManyToOne(() => Server, { onDelete: 'CASCADE' })
server: Server;
+
+ @ManyToOne(() => User)
+ owner: User;
+
+ @OneToMany(() => UserChannel, (userChannels) => userChannels.channel)
+ userChannels: UserChannel[];
+
+ @RelationId((channel: Channel) => channel.owner)
+ ownerId: number;
+
+ @RelationId((channel: Channel) => channel.server)
+ serverId: number;
+
+ static newInstance(
+ name: string,
+ description: string,
+ server: Server,
+ owner: User,
+ ) {
+ const newChannel = new Channel();
+ newChannel.name = name;
+ newChannel.description = description;
+ newChannel.server = server;
+ newChannel.owner = owner;
+ return newChannel;
+ }
}
diff --git a/backend/src/channel/channel.module.ts b/backend/src/channel/channel.module.ts
index 0998999..e16672c 100644
--- a/backend/src/channel/channel.module.ts
+++ b/backend/src/channel/channel.module.ts
@@ -9,12 +9,14 @@ import { UserChannelService } from '../user-channel/user-channel.service';
import { UserChannelRepository } from '../user-channel/user-channel.repository';
import { UserRepository } from '../user/user.repository';
import { ChannelRepository } from './channel.repository';
+import { User } from '../user/user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Channel,
Server,
+ User,
UserChannelRepository,
UserRepository,
ChannelRepository,
diff --git a/backend/src/channel/channel.repository.ts b/backend/src/channel/channel.repository.ts
index 1745651..1da1c87 100644
--- a/backend/src/channel/channel.repository.ts
+++ b/backend/src/channel/channel.repository.ts
@@ -6,4 +6,28 @@ export class ChannelRepository extends Repository {
getAllList() {
return this.createQueryBuilder('channel').getMany();
}
+
+ getChannelListByServerId(serverId: number) {
+ return this.createQueryBuilder('channel')
+ .where('channel.serverId = :serverId', { serverId })
+ .getMany();
+ }
+
+ getJoinedChannelList(userId: number, serverId: number) {
+ return this.createQueryBuilder('channel')
+ .leftJoin('channel.userChannels', 'user_channel')
+ .where('channel.serverId = :serverId', { serverId })
+ .andWhere('user_channel.userId = :userId', { userId })
+ .getMany();
+ }
+
+ getNotJoinedChannelList(userId: number, serverId: number) {
+ return this.createQueryBuilder('channel')
+ .where('channel.serverId = :serverId', { serverId })
+ .andWhere(
+ 'channel.id NOT IN (select uc.channelId from user_channel uc where uc.userId = :userId)',
+ { userId },
+ )
+ .getMany();
+ }
}
diff --git a/backend/src/channel/channel.service.ts b/backend/src/channel/channel.service.ts
index d440acb..231b41d 100644
--- a/backend/src/channel/channel.service.ts
+++ b/backend/src/channel/channel.service.ts
@@ -1,39 +1,45 @@
-import { BadRequestException, Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/index';
-import { ChannelFormDto } from './channe.dto';
+import { ChannelFormDto } from './channel-form.dto';
import { Channel } from './channel.entity';
import { Server } from '../server/server.entity';
import { ChannelRepository } from './channel.repository';
+import { UserRepository } from '../user/user.repository';
+import { User } from '../user/user.entity';
@Injectable()
export class ChannelService {
- /** * 생성자 */ constructor(
+ constructor(
@InjectRepository(Channel) private channelRepository: ChannelRepository,
+ @InjectRepository(User) private userRepository: UserRepository,
@InjectRepository(Server) private serverRepository: Repository,
- ) {
- this.channelRepository = channelRepository;
- this.serverRepository = serverRepository;
- }
- findAll(): Promise {
- return this.channelRepository.find();
- }
- findOne(id: number): Promise {
- return this.channelRepository.findOne(
+ ) {}
+ async findOne(id: number): Promise {
+ const response = await this.channelRepository.findOne(
{ id: id },
- { relations: ['server'] },
+ { relations: ['server', 'owner'] },
);
+ if (!response) throw new NotFoundException('서버가 존재하지 않습니다!');
+ return response;
}
- async createChannel(channel: ChannelFormDto): Promise {
- const channelEntity = await this.createChannelEntity(channel);
+ async createChannel(
+ channel: ChannelFormDto,
+ userId: number,
+ ): Promise {
+ const channelEntity = await this.createChannelEntity(channel, userId);
const savedChannel = await this.channelRepository.save(channelEntity);
return savedChannel;
}
- async updateChannel(id: number, channel: ChannelFormDto): Promise {
- const channelEntity = await this.createChannelEntity(channel);
+ async updateChannel(
+ id: number,
+ channel: ChannelFormDto,
+ userId: number,
+ ): Promise {
+ const channelEntity = await this.createChannelEntity(channel, userId);
await this.channelRepository.update(id, channelEntity);
return channelEntity;
}
@@ -42,17 +48,22 @@ export class ChannelService {
await this.channelRepository.delete({ id: id });
}
- async createChannelEntity(channel: ChannelFormDto): Promise {
- const channelEntity = this.channelRepository.create();
+ async createChannelEntity(
+ channel: ChannelFormDto,
+ userId: number,
+ ): Promise {
+ const { name, description, serverId } = channel;
const server = await this.serverRepository.findOne({
- id: channel.serverId,
+ id: serverId,
+ });
+ const user = await this.userRepository.findOne({
+ id: userId,
});
- if (!server) throw new BadRequestException();
+ if (!server) throw new NotFoundException('서버가 존재하지 않습니다!');
+ if (!user) throw new NotFoundException('사용자가 존재하지 않습니다!');
- channelEntity.name = channel.name;
- channelEntity.description = channel.description;
- channelEntity.server = server;
- return channelEntity;
+ const newChannel = Channel.newInstance(name, description, server, user);
+ return newChannel;
}
}
diff --git a/backend/src/channel/dto/channel-response.dto.ts b/backend/src/channel/dto/channel-response.dto.ts
new file mode 100644
index 0000000..0611718
--- /dev/null
+++ b/backend/src/channel/dto/channel-response.dto.ts
@@ -0,0 +1,35 @@
+import { Channel } from '../channel.entity';
+
+class ChannelResponseDto {
+ id: number;
+ name: string;
+ description: string;
+ ownerId: number;
+ serverId: number;
+
+ constructor(
+ id: number,
+ name: string,
+ description: string,
+ ownerId: number,
+ serverId: number,
+ ) {
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.ownerId = ownerId;
+ this.serverId = serverId;
+ }
+
+ static fromEntity(channel: Channel) {
+ return new ChannelResponseDto(
+ channel.id,
+ channel.name,
+ channel.description,
+ channel.ownerId,
+ channel.serverId,
+ );
+ }
+}
+
+export default ChannelResponseDto;
diff --git a/backend/src/comment/comment.controller.ts b/backend/src/comment/comment.controller.ts
index c8b5468..dcb1c39 100644
--- a/backend/src/comment/comment.controller.ts
+++ b/backend/src/comment/comment.controller.ts
@@ -1,32 +1,29 @@
-import { Body, Controller, Get, Post, Query, Session } from '@nestjs/common';
+import {
+ Controller,
+ Get,
+ ParseIntPipe,
+ Query,
+ UseGuards,
+} from '@nestjs/common';
+import { ApiExtraModels, ApiOkResponse } from '@nestjs/swagger';
import ResponseEntity from '../common/response-entity';
-import { ExpressSession } from '../types/session';
+import { LoginGuard } from '../login/login.guard';
import { CommentDto } from './comment.dto';
+import { commentDtoSchema } from './comment.schema';
import { CommentService } from './comment.service';
@Controller('/api/comments')
+@ApiExtraModels(ResponseEntity)
+@ApiExtraModels(CommentDto)
+@UseGuards(LoginGuard)
export class CommentController {
constructor(private commentService: CommentService) {}
- @Post()
- async sendComment(
- @Session() session: ExpressSession,
- @Body('channelId') channelId: number,
- @Body('messageId') messageId: number,
- @Body('contents') contents: string,
- ): Promise> {
- const sender = session.user;
- const newComment = await this.commentService.sendComment(
- sender.id,
- channelId,
- messageId,
- contents,
- );
- return ResponseEntity.ok(newComment);
- }
-
+ @ApiOkResponse(commentDtoSchema)
@Get()
- async findCommentsByMessageId(@Query('messageId') messageId: number) {
+ async findCommentsByMessageId(
+ @Query('messageId', new ParseIntPipe()) messageId: number,
+ ) {
const comments = await this.commentService.findCommentsByMessageId(
messageId,
);
diff --git a/backend/src/comment/comment.dto.ts b/backend/src/comment/comment.dto.ts
index 217db72..42a897d 100644
--- a/backend/src/comment/comment.dto.ts
+++ b/backend/src/comment/comment.dto.ts
@@ -1,11 +1,21 @@
+import { ApiProperty } from '@nestjs/swagger';
import { UserDto } from '../user/user.dto';
import { Comment } from './comment.entity';
export class CommentDto {
+ @ApiProperty()
id: number;
+
+ @ApiProperty()
contents: string;
+
+ @ApiProperty()
createdAt: Date;
+
+ @ApiProperty()
sender: UserDto;
+
+ @ApiProperty()
messageId: number;
static newInstance(
diff --git a/backend/src/comment/comment.entity.ts b/backend/src/comment/comment.entity.ts
index 149b8f5..ccf1e9e 100644
--- a/backend/src/comment/comment.entity.ts
+++ b/backend/src/comment/comment.entity.ts
@@ -11,7 +11,7 @@ import { User } from '../user/user.entity';
@Entity()
export class Comment {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@Column()
@@ -26,7 +26,7 @@ export class Comment {
@RelationId((comment: Comment) => comment.sender)
senderId: number;
- @ManyToOne(() => Message)
+ @ManyToOne(() => Message, { onDelete: 'CASCADE' })
message: Message;
@RelationId((comment: Comment) => comment.message)
diff --git a/backend/src/comment/comment.repository.ts b/backend/src/comment/comment.repository.ts
index 2ced5ee..d1c6704 100644
--- a/backend/src/comment/comment.repository.ts
+++ b/backend/src/comment/comment.repository.ts
@@ -7,6 +7,7 @@ export class CommentRepository extends Repository {
return this.createQueryBuilder('comment')
.innerJoinAndSelect('comment.sender', 'user')
.where('comment.messageId = :messageId', { messageId })
+ .orderBy('comment.id')
.getMany();
}
}
diff --git a/backend/src/comment/comment.schema.ts b/backend/src/comment/comment.schema.ts
new file mode 100644
index 0000000..d3eb163
--- /dev/null
+++ b/backend/src/comment/comment.schema.ts
@@ -0,0 +1,20 @@
+import { getSchemaPath } from '@nestjs/swagger';
+
+import ResponseEntity from '../common/response-entity';
+import { CommentDto } from './comment.dto';
+
+export const commentDtoSchema = {
+ schema: {
+ allOf: [
+ { $ref: getSchemaPath(ResponseEntity) },
+ {
+ properties: {
+ data: {
+ type: 'array',
+ items: { $ref: getSchemaPath(CommentDto) },
+ },
+ },
+ },
+ ],
+ },
+};
diff --git a/backend/src/common/response-entity.ts b/backend/src/common/response-entity.ts
index 963c646..3a896e5 100644
--- a/backend/src/common/response-entity.ts
+++ b/backend/src/common/response-entity.ts
@@ -1,5 +1,9 @@
+import { ApiProperty } from '@nestjs/swagger';
+
class ResponseEntity {
+ @ApiProperty()
statusCode: number;
+ @ApiProperty()
message: string;
data: T;
constructor(statusCode: number, message: string, data: T) {
diff --git a/backend/src/emoticon/emoticon.entity.ts b/backend/src/emoticon/emoticon.entity.ts
index bd30550..78f7b27 100644
--- a/backend/src/emoticon/emoticon.entity.ts
+++ b/backend/src/emoticon/emoticon.entity.ts
@@ -2,7 +2,7 @@ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Emoticon {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@Column()
diff --git a/backend/src/login/login.controller.ts b/backend/src/login/login.controller.ts
index 0f3a5b6..026b67a 100644
--- a/backend/src/login/login.controller.ts
+++ b/backend/src/login/login.controller.ts
@@ -1,20 +1,25 @@
import { Controller, Get, Query, Session } from '@nestjs/common';
+import { ApiOkResponse } from '@nestjs/swagger';
+import ResponseEntity from '../common/response-entity';
import { ExpressSession } from '../types/session';
+import { UserDto } from '../user/user.dto';
import { LoginService } from './login.service';
@Controller('/api/login')
export class LoginController {
constructor(private loginService: LoginService) {}
+
+ @ApiOkResponse({ type: UserDto })
@Get('/github')
async githubLogin(
@Session()
session: ExpressSession,
@Query('code') code: string,
- ) {
+ ): Promise> {
const user = await this.loginService.githubLogin(code);
session.user = user;
session.save();
- return user;
+ return ResponseEntity.noContent();
}
}
diff --git a/backend/src/main.ts b/backend/src/main.ts
index cf83543..a904c70 100644
--- a/backend/src/main.ts
+++ b/backend/src/main.ts
@@ -1,6 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { ExpressPeerServer } from 'peer';
import { ConfigService } from '@nestjs/config';
+import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { createSessionMiddleware } from './session';
@@ -13,6 +14,13 @@ async function bootstrap() {
const configService = app.get(ConfigService);
const session = createSessionMiddleware(configService);
+ const config = new DocumentBuilder()
+ .setTitle('boostCam API')
+ .setDescription('boostCam API description')
+ .build();
+ const document = SwaggerModule.createDocument(app, config);
+ SwaggerModule.setup('api', app, document);
+
app.use(session);
app.useWebSocketAdapter(new MessageSessionAdapter(app, session));
app.use('/peerjs', peerServer);
diff --git a/backend/src/message.adapter.ts b/backend/src/message.adapter.ts
index 9867e39..6620310 100644
--- a/backend/src/message.adapter.ts
+++ b/backend/src/message.adapter.ts
@@ -3,19 +3,15 @@ import { Server } from 'socket.io';
import { INestApplication } from '@nestjs/common';
export class MessageSessionAdapter extends IoAdapter {
- private app: INestApplication;
private sessionMiddleware;
constructor(app: INestApplication, sessionMiddleware) {
super(app);
- this.app = app;
this.sessionMiddleware = sessionMiddleware;
}
createIOServer(port: number, options?: any): any {
const server: Server = super.createIOServer(port, options);
-
- this.app.use(this.sessionMiddleware);
server.use((socket, next) => {
this.sessionMiddleware(socket.request, {}, next);
});
diff --git a/backend/src/message.gateway.ts b/backend/src/message.gateway.ts
index a7dea01..e278226 100644
--- a/backend/src/message.gateway.ts
+++ b/backend/src/message.gateway.ts
@@ -4,6 +4,7 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
+import { CommentService } from './comment/comment.service';
import { MessageDto } from './message/message.dto';
import { MessageService } from './message/message.service';
import { ExpressSession } from './types/session';
@@ -15,7 +16,7 @@ declare module 'http' {
}
}
-@WebSocketGateway({ namespace: '/message' })
+@WebSocketGateway()
export class MessageGateway {
@WebSocketServer()
private server: Server;
@@ -23,6 +24,7 @@ export class MessageGateway {
constructor(
private userChannelService: UserChannelService,
private messageService: MessageService,
+ private commentService: CommentService,
) {}
@SubscribeMessage('joinChannels')
@@ -66,6 +68,25 @@ export class MessageGateway {
this.emitMessage(channelId, newMessage);
}
+ @SubscribeMessage('sendComment')
+ async handleSendComment(
+ client: Socket,
+ payload: { channelId: number; messageId: number; contents: string },
+ ) {
+ if (!this.checkLoginSession(client)) {
+ return;
+ }
+ const { channelId, messageId, contents } = payload;
+ const sender = client.request.session.user;
+ const newComment = await this.commentService.sendComment(
+ sender.id,
+ channelId,
+ messageId,
+ contents,
+ );
+ this.server.to(`${channelId}`).emit('receiveComment', newComment);
+ }
+
private checkLoginSession(client: Socket): boolean {
const user = client.request.session.user;
return !!user;
diff --git a/backend/src/message/message.controller.ts b/backend/src/message/message.controller.ts
index 1e7d24b..8433d1c 100644
--- a/backend/src/message/message.controller.ts
+++ b/backend/src/message/message.controller.ts
@@ -1,59 +1,32 @@
import {
- Body,
Controller,
Get,
- Post,
+ ParseIntPipe,
Query,
Session,
UseGuards,
} from '@nestjs/common';
+import { ApiExtraModels, ApiOkResponse } from '@nestjs/swagger';
import ResponseEntity from '../common/response-entity';
import { LoginGuard } from '../login/login.guard';
import { ExpressSession } from '../types/session';
import { MessageDto } from './message.dto';
+import { MessageDtoSchema } from './message.scheme';
import { MessageService } from './message.service';
@Controller('/api/messages')
@UseGuards(LoginGuard)
+@ApiExtraModels(ResponseEntity)
+@ApiExtraModels(MessageDto)
export class MessageController {
constructor(private messageService: MessageService) {}
- @Post()
- async sendMessage(
- @Session() session: ExpressSession,
- @Body('channelId') channelId: number,
- @Body('contents') contents: string,
- ): Promise> {
- const sender = session.user;
- const newMessage = await this.messageService.sendMessage(
- sender.id,
- channelId,
- contents,
- );
-
- return ResponseEntity.ok(newMessage);
- }
-
- @Get('/getone')
- async findMessageByMessageId(
- @Session() session: ExpressSession,
- @Query('messageId') messageId: number,
- @Query('channelId') channelId: number,
- ) {
- const sender = session.user;
- const selectedMessage = await this.messageService.findMessagesByMessageId(
- sender.id,
- channelId,
- messageId,
- );
- return ResponseEntity.ok(selectedMessage);
- }
-
+ @ApiOkResponse(MessageDtoSchema)
@Get()
async findMessagesByChannelId(
@Session() session: ExpressSession,
- @Query('channelId') channelId: number,
- ) {
+ @Query('channelId', new ParseIntPipe()) channelId: number,
+ ): Promise> {
const sender = session.user;
const channelMessages = await this.messageService.findMessagesByChannelId(
sender.id,
diff --git a/backend/src/message/message.dto.ts b/backend/src/message/message.dto.ts
index 29d4df3..dfdb9d1 100644
--- a/backend/src/message/message.dto.ts
+++ b/backend/src/message/message.dto.ts
@@ -1,11 +1,17 @@
+import { ApiProperty } from '@nestjs/swagger';
import { UserDto } from '../user/user.dto';
import { Message } from './message.entity';
export class MessageDto {
+ @ApiProperty()
id: number;
+ @ApiProperty()
contents: string;
+ @ApiProperty()
channelId: number;
+ @ApiProperty()
createdAt: Date;
+ @ApiProperty()
sender: UserDto;
static newInstance(
diff --git a/backend/src/message/message.entity.ts b/backend/src/message/message.entity.ts
index e73a7ec..19941f7 100644
--- a/backend/src/message/message.entity.ts
+++ b/backend/src/message/message.entity.ts
@@ -11,13 +11,13 @@ import {
@Entity()
export class Message {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@Column()
contents: string;
- @ManyToOne(() => Channel)
+ @ManyToOne(() => Channel, { onDelete: 'CASCADE' })
channel: Channel;
@RelationId((message: Message) => message.channel)
diff --git a/backend/src/message/message.repository.ts b/backend/src/message/message.repository.ts
index 492fd8c..5908f47 100644
--- a/backend/src/message/message.repository.ts
+++ b/backend/src/message/message.repository.ts
@@ -7,6 +7,7 @@ export class MessageRepository extends Repository {
return this.createQueryBuilder('message')
.innerJoinAndSelect('message.sender', 'user')
.where('message.channelId = :channelId', { channelId })
+ .orderBy('message.id')
.getMany();
}
}
diff --git a/backend/src/message/message.scheme.ts b/backend/src/message/message.scheme.ts
new file mode 100644
index 0000000..31356d0
--- /dev/null
+++ b/backend/src/message/message.scheme.ts
@@ -0,0 +1,20 @@
+import { getSchemaPath } from '@nestjs/swagger';
+
+import ResponseEntity from '../common/response-entity';
+import { MessageDto } from './message.dto';
+
+export const MessageDtoSchema = {
+ schema: {
+ allOf: [
+ { $ref: getSchemaPath(ResponseEntity) },
+ {
+ properties: {
+ data: {
+ type: 'array',
+ items: { $ref: getSchemaPath(MessageDto) },
+ },
+ },
+ },
+ ],
+ },
+};
diff --git a/backend/src/message/message.service.ts b/backend/src/message/message.service.ts
index 5a5f1f7..4f46c25 100644
--- a/backend/src/message/message.service.ts
+++ b/backend/src/message/message.service.ts
@@ -38,14 +38,4 @@ export class MessageService {
const messages = await this.messageRepository.findByChannelId(channelId);
return messages.map(MessageDto.fromEntity);
}
-
- async findMessagesByMessageId(
- senderId: number,
- channelId: number,
- messageId: number,
- ) {
- await this.userServerService.checkUserChannelAccess(senderId, channelId);
- const message = await this.messageRepository.findOne(messageId);
- return MessageDto.fromEntity(message);
- }
}
diff --git a/backend/src/server/dto/request-server.dto.ts b/backend/src/server/dto/request-server.dto.ts
index 37256c8..6c24b60 100644
--- a/backend/src/server/dto/request-server.dto.ts
+++ b/backend/src/server/dto/request-server.dto.ts
@@ -1,7 +1,11 @@
+import { ApiProperty } from '@nestjs/swagger';
import { Server } from '../server.entity';
class RequestServerDto {
+ @ApiProperty()
name: string;
+
+ @ApiProperty()
description: string;
constructor(name: string, description: string) {
diff --git a/backend/src/server/dto/response-server-users.dto.ts b/backend/src/server/dto/response-server-users.dto.ts
index ce32672..d143817 100644
--- a/backend/src/server/dto/response-server-users.dto.ts
+++ b/backend/src/server/dto/response-server-users.dto.ts
@@ -1,21 +1,25 @@
+import { ApiProperty } from '@nestjs/swagger';
import { Server } from '../../server/server.entity';
-
-type UserInfo = {
- nickname: string;
- profile: string;
-};
+import { UserDto } from '../../user/user.dto';
class ServerWithUsersDto {
+ @ApiProperty()
description: string;
+
+ @ApiProperty()
name: string;
+
+ @ApiProperty()
imgUrl: string;
- users: UserInfo[];
+
+ @ApiProperty()
+ users: UserDto[];
constructor(
description: string,
name: string,
imgUrl: string,
- users: UserInfo[],
+ users: UserDto[],
) {
this.description = description;
this.name = name;
@@ -28,12 +32,13 @@ class ServerWithUsersDto {
server.description,
server.name,
server.imgUrl,
- server.userServer.map((userServer) => {
- return {
- nickname: userServer.user.nickname,
- profile: userServer.user.profile,
- };
- }),
+ server.userServer.map((userServer) =>
+ UserDto.newInstance(
+ userServer.user.id,
+ userServer.user.nickname,
+ userServer.user.profile,
+ ),
+ ),
);
}
}
diff --git a/backend/src/server/server.controller.ts b/backend/src/server/server.controller.ts
index d420e3a..87820e4 100644
--- a/backend/src/server/server.controller.ts
+++ b/backend/src/server/server.controller.ts
@@ -8,11 +8,11 @@ import {
Patch,
UseGuards,
Session,
- HttpException,
UseInterceptors,
UploadedFile,
HttpCode,
HttpStatus,
+ ParseIntPipe,
} from '@nestjs/common';
import { ServerService } from './server.service';
@@ -25,8 +25,19 @@ import { ImageService } from '../image/image.service';
import ServerWithUsersDto from './dto/response-server-users.dto';
import { CamService } from '../cam/cam.service';
import { ResponseCamDto } from '../cam/cam.dto';
+import { ApiExtraModels, ApiOkResponse } from '@nestjs/swagger';
+import { UserDto } from '../user/user.dto';
+import {
+ serverWithUserDtoSchema,
+ serverCodeSchema,
+ emptyResponseSchema,
+} from './server.schema';
+@UseGuards(LoginGuard)
@Controller('/api/servers')
+@ApiExtraModels(ResponseEntity)
+@ApiExtraModels(ServerWithUsersDto)
+@ApiExtraModels(UserDto)
export class ServerController {
constructor(
private serverService: ServerService,
@@ -34,36 +45,44 @@ export class ServerController {
private camService: CamService,
) {}
- @Get('/:id/users') async findOneWithUsers(
- @Param('id') id: number,
+ @ApiOkResponse(serverWithUserDtoSchema)
+ @Get('/:id/users')
+ async findOneWithUsers(
+ @Param('id', new ParseIntPipe()) id: number,
): Promise> {
const serverWithUsers = await this.serverService.findOneWithUsers(id);
return ResponseEntity.ok(serverWithUsers);
}
@Get('/:id/cam') async findCams(
- @Param('id') id: number,
+ @Param('id', new ParseIntPipe()) id: number,
): Promise> {
const cam = await this.camService.getCamList(id);
return ResponseEntity.ok(cam);
}
- @Get('/:id/code') async findCode(
- @Param('id') id: number,
+ @ApiOkResponse(serverCodeSchema)
+ @Get('/:id/code')
+ async findCode(
+ @Param('id', new ParseIntPipe()) id: number,
): Promise> {
const code = await this.serverService.findCode(id);
return ResponseEntity.ok(code);
}
- @Patch('/:id/code') async refreshCode(
- @Param('id') id: number,
+ @ApiOkResponse(serverCodeSchema)
+ @Patch('/:id/code')
+ async refreshCode(
+ @Session()
+ session: ExpressSession,
+ @Param('id', new ParseIntPipe()) id: number,
): Promise> {
- const code = await this.serverService.refreshCode(id);
+ const user = session.user;
+ const code = await this.serverService.refreshCode(id, user);
return ResponseEntity.ok(code);
}
@Post()
- @UseGuards(LoginGuard)
@UseInterceptors(FileInterceptor('icon'))
async createServer(
@Session()
@@ -71,82 +90,64 @@ export class ServerController {
@Body() requestServerDto: RequestServerDto,
@UploadedFile() icon: Express.Multer.File,
): Promise> {
- try {
- requestServerDto = new RequestServerDto(
- requestServerDto.name,
- requestServerDto.description,
- );
- let imgUrl: string;
+ requestServerDto = new RequestServerDto(
+ requestServerDto.name,
+ requestServerDto.description,
+ );
+ let imgUrl: string;
- if (icon && icon.mimetype.substring(0, 5) === 'image') {
- const uploadedFile = await this.imageService.uploadFile(icon);
- imgUrl = uploadedFile.Location;
- }
- const user = session.user;
- const newServer = await this.serverService.create(
- user,
- requestServerDto,
- imgUrl,
- );
- return ResponseEntity.created(newServer.id);
- } catch (error) {
- if (error instanceof HttpException) {
- throw error;
- }
- throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
+ if (icon && icon.mimetype.substring(0, 5) === 'image') {
+ const uploadedFile = await this.imageService.uploadFile(icon);
+ imgUrl = uploadedFile.Location;
}
+ const user = session.user;
+ const newServerId = await this.serverService.create(
+ user,
+ requestServerDto,
+ imgUrl,
+ );
+
+ return ResponseEntity.created(newServerId);
}
+ @ApiOkResponse(serverCodeSchema)
@Patch('/:id')
@HttpCode(HttpStatus.NO_CONTENT)
@UseInterceptors(FileInterceptor('icon'))
async updateServer(
@Session()
session: ExpressSession,
- @Param('id') id: number,
+ @Param('id', new ParseIntPipe()) id: number,
@Body() requestServerDto: RequestServerDto,
@UploadedFile() icon: Express.Multer.File,
): Promise> {
- try {
- requestServerDto = new RequestServerDto(
- requestServerDto.name,
- requestServerDto.description,
- );
- let imgUrl: string;
-
- if (icon && icon.mimetype.substring(0, 5) === 'image') {
- const uploadedFile = await this.imageService.uploadFile(icon);
- imgUrl = uploadedFile.Location;
- }
- const user = session.user;
+ requestServerDto = new RequestServerDto(
+ requestServerDto.name,
+ requestServerDto.description,
+ );
+ let imgUrl: string;
- await this.serverService.updateServer(id, requestServerDto, user, imgUrl);
-
- return ResponseEntity.noContent();
- } catch (error) {
- if (error instanceof HttpException) {
- throw error;
- }
- throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
+ if (icon && icon.mimetype.substring(0, 5) === 'image') {
+ const uploadedFile = await this.imageService.uploadFile(icon);
+ imgUrl = uploadedFile.Location;
}
+ const user = session.user;
+ await this.serverService.updateServer(id, requestServerDto, user, imgUrl);
+
+ return ResponseEntity.noContent();
}
+ @ApiOkResponse(emptyResponseSchema)
@Delete('/:id')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteServer(
@Session()
session: ExpressSession,
- @Param('id') id: number,
+ @Param('id', new ParseIntPipe()) id: number,
): Promise> {
- try {
- const user = session.user;
- await this.serverService.deleteServer(id, user);
- return ResponseEntity.noContent();
- } catch (error) {
- if (error instanceof HttpException) {
- throw error;
- }
- throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
- }
+ const user = session.user;
+ await this.serverService.deleteServer(id, user);
+
+ return ResponseEntity.noContent();
}
}
diff --git a/backend/src/server/server.entity.ts b/backend/src/server/server.entity.ts
index 472026c..b999f20 100644
--- a/backend/src/server/server.entity.ts
+++ b/backend/src/server/server.entity.ts
@@ -6,12 +6,13 @@ import {
ManyToOne,
JoinColumn,
OneToMany,
+ RelationId,
} from 'typeorm';
import { User } from '../user/user.entity';
@Entity()
export class Server {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@Column()
@@ -30,6 +31,9 @@ export class Server {
@JoinColumn({ referencedColumnName: 'id' })
owner: User;
+ @RelationId((server: Server) => server.owner)
+ ownerId: number;
+
@OneToMany(() => UserServer, (userServer) => userServer.server)
userServer: UserServer[];
diff --git a/backend/src/server/server.schema.ts b/backend/src/server/server.schema.ts
new file mode 100644
index 0000000..7b1d0ae
--- /dev/null
+++ b/backend/src/server/server.schema.ts
@@ -0,0 +1,60 @@
+import { getSchemaPath } from '@nestjs/swagger';
+
+import ResponseEntity from '../common/response-entity';
+import { UserDto } from '../user/user.dto';
+import ServerWithUsersDto from './dto/response-server-users.dto';
+
+export const serverWithUserDtoSchema = {
+ schema: {
+ allOf: [
+ { $ref: getSchemaPath(ResponseEntity) },
+ {
+ properties: {
+ data: {
+ allOf: [
+ { $ref: getSchemaPath(ServerWithUsersDto) },
+ {
+ properties: {
+ users: {
+ type: 'array',
+ items: { $ref: getSchemaPath(UserDto) },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+};
+
+export const serverCodeSchema = {
+ schema: {
+ allOf: [
+ { $ref: getSchemaPath(ResponseEntity) },
+ {
+ properties: {
+ data: {
+ type: 'string',
+ },
+ },
+ },
+ ],
+ },
+};
+
+export const emptyResponseSchema = {
+ schema: {
+ allOf: [
+ { $ref: getSchemaPath(ResponseEntity) },
+ {
+ properties: {
+ data: {
+ type: null,
+ },
+ },
+ },
+ ],
+ },
+};
diff --git a/backend/src/server/server.service.spec.ts b/backend/src/server/server.service.spec.ts
index e99a895..9b0ef19 100644
--- a/backend/src/server/server.service.spec.ts
+++ b/backend/src/server/server.service.spec.ts
@@ -47,6 +47,8 @@ describe('ServerService', () => {
const existsServerId = 1;
const userId = 1;
+ const newServerId = 2;
+ const newUserServerId = 1;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -126,26 +128,40 @@ describe('ServerService', () => {
describe('refreshCode()', () => {
it('코드 재생성에 성공할 경우', async () => {
- serverRepository.findOne.mockResolvedValue(existsServer);
+ serverRepository.findOneWithOwner.mockResolvedValue(existsServer);
const originCode = existsServer.code;
- const code = await serverService.refreshCode(existsServerId);
+ const code = await serverService.refreshCode(existsServerId, user);
expect(code).not.toBe(originCode);
});
it('서버가 존재하지 않을 경우', async () => {
const nonExistsId = 0;
- serverRepository.findOne.mockResolvedValue(undefined);
+ serverRepository.findOneWithOwner.mockResolvedValue(undefined);
try {
- await serverService.refreshCode(nonExistsId);
+ await serverService.refreshCode(nonExistsId, user);
} catch (error) {
expect(error.response.message).toBe('존재하지 않는 서버입니다.');
expect(error.response.error).toBe('Bad Request');
expect(error.response.statusCode).toBe(HttpStatus.BAD_REQUEST);
}
});
+
+ it('권한이 없을 경우', async () => {
+ const userNotOwner = new User();
+ userNotOwner.id = 0;
+ serverRepository.findOneWithOwner.mockResolvedValue(existsServer);
+
+ try {
+ await serverService.refreshCode(existsServerId, userNotOwner);
+ } catch (error) {
+ expect(error.response.message).toBe('권한이 없습니다.');
+ expect(error.response.error).toBe('Forbidden');
+ expect(error.response.statusCode).toBe(HttpStatus.FORBIDDEN);
+ }
+ });
});
describe('create()', () => {
@@ -153,17 +169,15 @@ describe('ServerService', () => {
serverRepository.save.mockResolvedValue(newServer);
serverRepository.findOne.mockResolvedValue(newServer);
userServerRepository.findByUserIdAndServerId.mockResolvedValue(undefined);
- userServerRepository.save.mockResolvedValue(newUserServer);
+ userServerRepository.save.mockResolvedValue(newUserServerId);
- const createdServer = await serverService.create(
+ const createdServerId = await serverService.create(
user,
requestServerDto,
'',
);
- expect(createdServer.name).toBe(requestServerDto.name);
- expect(createdServer.description).toBe(requestServerDto.description);
- expect(createdServer.owner.id).toBe(user.id);
+ expect(createdServerId).toBe(newServerId);
});
});
@@ -267,6 +281,7 @@ describe('ServerService', () => {
user.id = userId;
requestServerDto = new RequestServerDto(serverName, serverDescription);
newServer = new Server();
+ newServer.id = newServerId;
newServer.description = serverDescription;
newServer.name = serverName;
newServer.owner = user;
@@ -280,5 +295,8 @@ describe('ServerService', () => {
existsServer.owner = user;
existsServer.userServer = [existsUserServer];
existsServer.code = v4();
+
+ newUserServer = new UserServer();
+ newUserServer.id = newUserServerId;
};
});
diff --git a/backend/src/server/server.service.ts b/backend/src/server/server.service.ts
index 515717a..3c1a4fd 100644
--- a/backend/src/server/server.service.ts
+++ b/backend/src/server/server.service.ts
@@ -24,9 +24,6 @@ export class ServerService {
@InjectRepository(ServerRepository)
private serverRepository: ServerRepository,
) {}
- findAll(): Promise {
- return this.serverRepository.find({ relations: ['owner'] });
- }
findOne(id: number): Promise {
return this.serverRepository.findOne({ id: id });
@@ -52,12 +49,15 @@ export class ServerService {
return server.code;
}
- async refreshCode(id: number): Promise {
- const server = await this.serverRepository.findOne(id);
+ async refreshCode(id: number, user: User): Promise {
+ const server = await this.serverRepository.findOneWithOwner(id);
if (!server) {
throw new BadRequestException('존재하지 않는 서버입니다.');
}
+ if (server.owner.id !== user.id) {
+ throw new ForbiddenException('권한이 없습니다.');
+ }
server.code = v4();
this.serverRepository.save(server);
@@ -68,7 +68,7 @@ export class ServerService {
user: User,
requestServerDto: RequestServerDto,
imgUrl: string | undefined,
- ): Promise {
+ ): Promise {
const server = requestServerDto.toServerEntity();
server.owner = user;
server.imgUrl = imgUrl || '';
@@ -77,7 +77,7 @@ export class ServerService {
const createdServer = await this.serverRepository.save(server);
this.userServerService.create(user, createdServer.code);
- return createdServer;
+ return createdServer.id;
}
async updateServer(
diff --git a/backend/src/user-channel/user-channel.controller.ts b/backend/src/user-channel/user-channel.controller.ts
index e044400..5d221c3 100644
--- a/backend/src/user-channel/user-channel.controller.ts
+++ b/backend/src/user-channel/user-channel.controller.ts
@@ -7,6 +7,10 @@ import {
UseGuards,
Post,
Body,
+ HttpStatus,
+ HttpException,
+ Query,
+ ParseIntPipe,
} from '@nestjs/common';
import ResponseEntity from '../common/response-entity';
import { LoginGuard } from '../login/login.guard';
@@ -14,37 +18,33 @@ import { ExpressSession } from '../types/session';
import { UserChannelService } from './user-channel.service';
import { ChannelService } from '../channel/channel.service';
import { UserChannel } from './user-channel.entity';
-import { Channel } from '../channel/channel.entity';
+import { User } from '../user/user.entity';
+import ChannelResponseDto from '../channel/dto/channel-response.dto';
-@Controller('/api/user/servers')
+@Controller('/api/user/servers/:serverId')
@UseGuards(LoginGuard)
export class UserChannelController {
constructor(
private userChannelService: UserChannelService,
private channelService: ChannelService,
- ) {
- this.userChannelService = userChannelService;
- this.channelService = channelService;
- }
+ ) {}
- @Get('/:id/channels/joined/')
+ @Get('/channels/joined/')
async getJoinedChannelList(
- @Param('id') serverId: number,
+ @Param('serverId', new ParseIntPipe()) serverId: number,
@Session() session: ExpressSession,
) {
- const response = await this.userChannelService.getJoinedChannelListByUserId(
- serverId,
- session.user.id,
- );
- const joinedChannelList = response.map(
- (userChannel) => userChannel.channel,
- );
- return ResponseEntity.ok(joinedChannelList);
+ const joinedChannelList =
+ await this.userChannelService.getJoinedChannelListByUserId(
+ serverId,
+ session.user.id,
+ );
+ return ResponseEntity.ok(joinedChannelList);
}
- @Get('/:id/channels/notjoined/')
+ @Get('/channels/notjoined/')
async getNotJoinedChannelList(
- @Param('id') serverId: number,
+ @Param('serverId', new ParseIntPipe()) serverId: number,
@Session() session: ExpressSession,
) {
const response =
@@ -52,16 +52,26 @@ export class UserChannelController {
serverId,
session.user.id,
);
- const notJoinedChannelList = response.map(
- (userChannel) => userChannel.channel,
- );
- return ResponseEntity.ok(notJoinedChannelList);
+ return ResponseEntity.ok(response);
}
- @Post()
+ @Get('/channels/users')
+ async getJoinedUserList(
+ @Param('serverId', new ParseIntPipe()) serverId: number,
+ @Query('channelId', new ParseIntPipe()) channelId: number,
+ ) {
+ const response =
+ await this.userChannelService.findJoinedUserListByChannelId(
+ serverId,
+ channelId,
+ );
+ return ResponseEntity.ok(response);
+ }
+
+ @Post('/channels')
async joinNewChannel(
@Body('channelId') channelId: number,
- @Body('serverId') serverId: number,
+ @Param('serverId', new ParseIntPipe()) serverId: number,
@Session() session: ExpressSession,
) {
const selectedChannel = await this.channelService.findOne(channelId);
@@ -72,8 +82,22 @@ export class UserChannelController {
return ResponseEntity.ok(savedChannel);
}
- @Delete('/:id')
- delete(@Param('id') id: number) {
- return this.userChannelService.deleteById(id);
+ @Delete('/channels/:channelId')
+ async delete(
+ @Param('channelId', new ParseIntPipe()) channeld: number,
+ @Session() session: ExpressSession,
+ ) {
+ try {
+ this.userChannelService.deleteByUserIdAndChannelId(
+ session.user.id,
+ channeld,
+ );
+ return ResponseEntity.noContent();
+ } catch (error) {
+ if (error instanceof HttpException) {
+ throw error;
+ }
+ throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
+ }
}
}
diff --git a/backend/src/user-channel/user-channel.entity.ts b/backend/src/user-channel/user-channel.entity.ts
index febba17..5d68c55 100644
--- a/backend/src/user-channel/user-channel.entity.ts
+++ b/backend/src/user-channel/user-channel.entity.ts
@@ -5,7 +5,7 @@ import { Channel } from '../channel/channel.entity';
@Entity()
export class UserChannel {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
diff --git a/backend/src/user-channel/user-channel.repository.ts b/backend/src/user-channel/user-channel.repository.ts
index 740cab5..794f330 100644
--- a/backend/src/user-channel/user-channel.repository.ts
+++ b/backend/src/user-channel/user-channel.repository.ts
@@ -10,27 +10,26 @@ export class UserChannelRepository extends Repository {
.getMany();
}
- getJoinedChannelListByUserId(userId: number, serverId: number) {
+ getUserChannelListByUserId(userId: number) {
return this.createQueryBuilder('user_channel')
- .leftJoinAndSelect('user_channel.channel', 'channel')
.where('user_channel.user = :userId', { userId: userId })
- .andWhere('user_channel.server = :serverId', { serverId: serverId })
.getMany();
}
- getNotJoinedChannelListByUserId(userId: number, serverId: number) {
+ getJoinedUserListByChannelId(serverId: number, channelId: number) {
return this.createQueryBuilder('user_channel')
- .leftJoinAndSelect('user_channel.channel', 'channel')
- .where('user_channel.channel IN (:id)', {
- id: this.getJoinedChannelListByUserId(userId, serverId),
- })
+ .innerJoinAndSelect('user_channel.user', 'user')
+ .where('user_channel.channelId = :channelId', { channelId: channelId })
+ .andWhere('user_channel.server = :serverId', { serverId: serverId })
.getMany();
}
- deleteByUserIdAndChannelId(userId: number, channelId: number) {
+ getUserChannelByUserIdAndChannelId(userId: number, channelId: number) {
return this.createQueryBuilder('user_channel')
.where('user_channel.user = :userId', { userId: userId })
- .andWhere('user_channel.channelId = :channelId', { channelId: channelId })
- .delete();
+ .andWhere('user_channel.channelId = :channelId', {
+ channelId: channelId,
+ })
+ .getOne();
}
}
diff --git a/backend/src/user-channel/user-channel.service.ts b/backend/src/user-channel/user-channel.service.ts
index e944906..7f23ff7 100644
--- a/backend/src/user-channel/user-channel.service.ts
+++ b/backend/src/user-channel/user-channel.service.ts
@@ -1,15 +1,19 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { DeleteQueryBuilder, DeleteResult } from 'typeorm';
+import { DeleteResult } from 'typeorm';
+import { ChannelRepository } from '../channel/channel.repository';
import { UserChannelRepository } from './user-channel.repository';
import { UserChannel } from './user-channel.entity';
import { Channel } from '../channel/channel.entity';
import { UserRepository } from '../user/user.repository';
+import ChannelResponseDto from '../channel/dto/channel-response.dto';
@Injectable()
export class UserChannelService {
constructor(
+ @InjectRepository(ChannelRepository)
+ private channelRepository: ChannelRepository,
@InjectRepository(UserChannelRepository)
private userChannelRepository: UserChannelRepository,
@InjectRepository(UserRepository) private userRepository: UserRepository,
@@ -27,56 +31,56 @@ export class UserChannelService {
return await this.userChannelRepository.save(userChannel);
}
- deleteById(id: number): Promise {
- return this.userChannelRepository.delete(id);
+ deleteByChannelId(channelId: number): Promise {
+ return this.userChannelRepository.delete({ channelId });
}
- getJoinedChannelListByUserId(
+ async getJoinedChannelListByUserId(
serverId: number,
userId: number,
- ): Promise {
- return this.userChannelRepository.getJoinedChannelListByUserId(
+ ): Promise {
+ const joinedChannelList = await this.channelRepository.getJoinedChannelList(
userId,
serverId,
);
+
+ return joinedChannelList.map(ChannelResponseDto.fromEntity);
}
async getNotJoinedChannelListByUserId(
serverId: number,
userId: number,
- ): Promise {
- const allList = await this.userChannelRepository.getAllList(serverId);
- const joinedList =
- await this.userChannelRepository.getJoinedChannelListByUserId(
- userId,
- serverId,
- );
- const joinedChannelList = joinedList.map(
- (userChannel) => userChannel.channel.id,
- );
+ ): Promise {
+ const notJoinedChannelList =
+ await this.channelRepository.getNotJoinedChannelList(userId, serverId);
- const notJoinedList = allList.filter(
- (userChannel) => !joinedChannelList.includes(userChannel.channel.id),
- );
-
- return notJoinedList;
+ return notJoinedChannelList.map(ChannelResponseDto.fromEntity);
}
- deleteByUserIdAndChannelId(
- userId: number,
- serverId: number,
- ): DeleteQueryBuilder {
- return this.userChannelRepository.deleteByUserIdAndChannelId(
- userId,
- serverId,
- );
+ async deleteByUserIdAndChannelId(userId: number, channelId: number) {
+ const res =
+ await this.userChannelRepository.getUserChannelByUserIdAndChannelId(
+ userId,
+ channelId,
+ );
+ this.userChannelRepository.delete({ id: res.id });
}
async findChannelsByUserId(userId: number) {
- const userChannels = await this.userChannelRepository
- .createQueryBuilder('user_channel')
- .where('user_channel.user = :userId', { userId: userId })
- .getMany();
+ const userChannels =
+ await this.userChannelRepository.getUserChannelListByUserId(userId);
return userChannels.map((uc) => uc.channelId.toString());
}
+
+ async findJoinedUserListByChannelId(serverId: number, channelId: number) {
+ const joinedUserList =
+ await this.userChannelRepository.getJoinedUserListByChannelId(
+ serverId,
+ channelId,
+ );
+ if (!joinedUserList)
+ throw new NotFoundException('채널에 사용자가 존재하지 않습니다!');
+ const userList = joinedUserList.map((data) => data.user);
+ return userList;
+ }
}
diff --git a/backend/src/user-server/user-server.controller.ts b/backend/src/user-server/user-server.controller.ts
index b748ed5..c8d4aa5 100644
--- a/backend/src/user-server/user-server.controller.ts
+++ b/backend/src/user-server/user-server.controller.ts
@@ -6,36 +6,42 @@ import {
Param,
Session,
UseGuards,
- HttpException,
HttpCode,
HttpStatus,
+ Get,
+ ParseIntPipe,
} from '@nestjs/common';
import { LoginGuard } from '../login/login.guard';
import { ExpressSession } from '../types/session';
import { UserServerService } from './user-server.service';
import ResponseEntity from '../common/response-entity';
+import UserServerListDto from './dto/user-server-list.dto';
-@Controller('/api/users/servers')
+@Controller('/api/user/servers')
@UseGuards(LoginGuard)
export class UserServerController {
constructor(private userServerService: UserServerService) {}
+ @Get()
+ async getServersByUserId(
+ @Session()
+ session: ExpressSession,
+ ): Promise> {
+ const userId = session.user.id;
+ const data = await this.userServerService.getServerListByUserId(userId);
+
+ return ResponseEntity.ok(data);
+ }
+
@Post()
async createUserServer(
@Session()
session: ExpressSession,
@Body() { code },
) {
- try {
- const user = session.user;
- const newUserServer = await this.userServerService.create(user, code);
- return ResponseEntity.created(newUserServer.id);
- } catch (error) {
- if (error instanceof HttpException) {
- throw error;
- }
- throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
- }
+ const user = session.user;
+ const newUserServerId = await this.userServerService.create(user, code);
+ return ResponseEntity.created(newUserServerId);
}
@Delete('/:id')
@@ -43,17 +49,10 @@ export class UserServerController {
async delete(
@Session()
session: ExpressSession,
- @Param('id') id: number,
+ @Param('id', new ParseIntPipe()) id: number,
) {
- try {
- const userId = session.user.id;
- await this.userServerService.deleteById(id, userId);
- return ResponseEntity.noContent();
- } catch (error) {
- if (error instanceof HttpException) {
- throw error;
- }
- throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
- }
+ const userId = session.user.id;
+ await this.userServerService.deleteById(id, userId);
+ return ResponseEntity.noContent();
}
}
diff --git a/backend/src/user-server/user-server.entity.ts b/backend/src/user-server/user-server.entity.ts
index db59aff..41d553c 100644
--- a/backend/src/user-server/user-server.entity.ts
+++ b/backend/src/user-server/user-server.entity.ts
@@ -4,7 +4,7 @@ import { Server } from '../server/server.entity';
@Entity()
export class UserServer {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
diff --git a/backend/src/user-server/user-server.service.spec.ts b/backend/src/user-server/user-server.service.spec.ts
index 1533a0a..7d8e4de 100644
--- a/backend/src/user-server/user-server.service.spec.ts
+++ b/backend/src/user-server/user-server.service.spec.ts
@@ -42,6 +42,7 @@ describe('UserServerService', () => {
const userId = 1;
const serverId = 1;
const existUserServerId = 1;
+ const userServerId = 2;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -75,10 +76,9 @@ describe('UserServerService', () => {
serverRepository.findOne.mockResolvedValue(server);
userServerRepository.findByUserIdAndServerId.mockResolvedValue(undefined);
- const newUserServer = await service.create(user, server.code);
+ const newUserServerId = await service.create(user, server.code);
- expect(newUserServer.user).toBe(user);
- expect(newUserServer.server).toBe(server);
+ expect(newUserServerId).toBe(userServerId);
});
it('서버가 존재하지 않는 경우', async () => {
@@ -186,6 +186,7 @@ describe('UserServerService', () => {
server.code = v4();
userServer = new UserServer();
+ userServer.id = userServerId;
userServer.user = user;
userServer.server = server;
diff --git a/backend/src/user-server/user-server.service.ts b/backend/src/user-server/user-server.service.ts
index ce188e2..aeea010 100644
--- a/backend/src/user-server/user-server.service.ts
+++ b/backend/src/user-server/user-server.service.ts
@@ -22,7 +22,7 @@ export class UserServerService {
private userServerRepository: UserServerRepository,
) {}
- async create(user: User, code: string): Promise {
+ async create(user: User, code: string): Promise {
const newUserServer = new UserServer();
newUserServer.user = user;
newUserServer.server = await this.serverService.findByCode(code);
@@ -38,7 +38,8 @@ export class UserServerService {
throw new BadRequestException('이미 등록된 서버입니다.');
}
- return this.userServerRepository.save(newUserServer);
+ const newUserServerId = await this.userServerRepository.save(newUserServer);
+ return newUserServerId.id;
}
async deleteById(id: number, userId: number): Promise {
diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts
index 10e370e..a77ec61 100644
--- a/backend/src/user/user.controller.ts
+++ b/backend/src/user/user.controller.ts
@@ -1,28 +1,14 @@
import { Controller, Get, UseGuards, Session } from '@nestjs/common';
-import { UserServerService } from '../user-server/user-server.service';
+import ResponseEntity from '../common/response-entity';
import { LoginGuard } from '../login/login.guard';
import { ExpressSession } from '../types/session';
-import ResponseEntity from '../common/response-entity';
-import UserServerListDto from '../user-server/dto/user-server-list.dto';
+import { UserDto } from './user.dto';
@Controller('/api/user')
@UseGuards(LoginGuard)
export class UserController {
- constructor(private userServerService: UserServerService) {}
-
- @Get('/servers')
- async getServersByUserId(
- @Session()
- session: ExpressSession,
- ): Promise> {
- const userId = session.user.id;
- const data = await this.userServerService.getServerListByUserId(userId);
-
- return ResponseEntity.ok(data);
- }
-
@Get()
getUser(@Session() session: ExpressSession) {
- return session.user;
+ return ResponseEntity.ok(UserDto.fromEntity(session.user));
}
}
diff --git a/backend/src/user/user.dto.ts b/backend/src/user/user.dto.ts
index 8c07a60..20eb460 100644
--- a/backend/src/user/user.dto.ts
+++ b/backend/src/user/user.dto.ts
@@ -1,8 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
import { User } from './user.entity';
export class UserDto {
+ @ApiProperty()
id: number;
+ @ApiProperty()
nickname: string;
+ @ApiProperty()
profile: string;
static newInstance(id: number, nickname: string, profile: string) {
diff --git a/backend/src/user/user.entity.ts b/backend/src/user/user.entity.ts
index e9149b1..9887e61 100644
--- a/backend/src/user/user.entity.ts
+++ b/backend/src/user/user.entity.ts
@@ -2,10 +2,10 @@ import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
- @PrimaryGeneratedColumn({ type: 'bigint' })
+ @PrimaryGeneratedColumn()
id: number;
- @Column({ type: 'bigint' })
+ @Column()
githubId: number;
@Column()
diff --git a/frontend/public/index.html b/frontend/public/index.html
index aa069f2..11b1d98 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -5,10 +5,7 @@
-
+
- React App
+ boostCam
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 9d6662f..a8941eb 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,22 +1,25 @@
-import React from 'react';
+import React, { Suspense } from 'react';
import { RecoilRoot } from 'recoil';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
-import LoginMain from './components/LoginPage/LoginMain';
-import Cam from './components/Cam/Cam';
-import BoostCamMain from './components/Main/BoostCamMain';
-import LoginCallback from './components/LoginPage/LoginCallback';
+import LoginMain from './components/Login/LoginMain';
+import CamMain from './components/Cam/CamMain';
+import LoginCallback from './components/Login/LoginCallback';
+import Main from './components/Main/Main';
+import Loading from './components/core/Loading';
function App(): JSX.Element {
return (
-
- } />
- } />
- } />
- } />
-
+ }>
+
+ } />
+ } />
+ } />
+ } />
+
+
);
diff --git a/frontend/src/assets/hmm.gif b/frontend/src/assets/hmm.gif
new file mode 100644
index 0000000..8d31a4c
Binary files /dev/null and b/frontend/src/assets/hmm.gif differ
diff --git a/frontend/src/assets/loading.png b/frontend/src/assets/loading.png
new file mode 100644
index 0000000..7373e63
Binary files /dev/null and b/frontend/src/assets/loading.png differ
diff --git a/frontend/src/assets/loading.svg b/frontend/src/assets/loading.svg
new file mode 100644
index 0000000..1e9d52a
--- /dev/null
+++ b/frontend/src/assets/loading.svg
@@ -0,0 +1,55 @@
+
+
+
diff --git a/frontend/src/atoms/socket.ts b/frontend/src/atoms/socket.ts
deleted file mode 100644
index 8a6fa87..0000000
--- a/frontend/src/atoms/socket.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { atom } from 'recoil';
-import { io } from 'socket.io-client';
-
-const socketState = atom({
- key: 'socket',
- default: io(),
-});
-
-export default socketState;
diff --git a/frontend/src/atoms/user.ts b/frontend/src/atoms/user.ts
index 1796ff1..6c96a3a 100644
--- a/frontend/src/atoms/user.ts
+++ b/frontend/src/atoms/user.ts
@@ -1,9 +1,14 @@
-import { atom } from 'recoil';
-import User from '../types/user';
+import { selector } from 'recoil';
-const userState = atom({
+import { User } from '../types/user';
+import { fetchData } from '../utils/fetchMethods';
+
+const userState = selector({
key: 'user',
- default: null,
+ get: async () => {
+ const { data } = await fetchData('GET', '/api/user');
+ return data;
+ },
});
export default userState;
diff --git a/frontend/src/components/Cam/Cam.tsx b/frontend/src/components/Cam/CamMain.tsx
similarity index 73%
rename from frontend/src/components/Cam/Cam.tsx
rename to frontend/src/components/Cam/CamMain.tsx
index 48e42a4..f70dbf8 100644
--- a/frontend/src/components/Cam/Cam.tsx
+++ b/frontend/src/components/Cam/CamMain.tsx
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
+import { useRecoilValue } from 'recoil';
import ButtonBar from './Menu/ButtonBar';
import ChattingTab from './Menu/ChattingTab';
@@ -15,50 +16,33 @@ import CamNotFoundPage from './Page/CamNotFoundPage';
import CamLoadingPage from './Page/CamLoadingPage';
import CamNotAvailablePage from './Page/CamNotAvailablePage';
import CamErrorPage from './Page/CamErrorPage';
+import userState from '../../atoms/user';
+import { fetchData } from '../../utils/fetchMethods';
+import { flex } from '../../utils/styledComponentFunc';
const Container = styled.div`
+ background-color: black;
width: 100vw;
height: 100vh;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
- overflow-x: hidden;
- overflow-y: hidden;
- background-color: black;
-`;
-
-const UpperTab = styled.div`
- margin-top: 5px;
- width: 98vw;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
+ ${flex('row', 'space-between')};
position: relative;
`;
-function Cam(): JSX.Element {
+function CamMain(): JSX.Element {
+ const user = useRecoilValue(userState);
const [userInfo, setUserInfo] = useState({ roomId: null, nickname: null });
const [statusCode, setStatusCode] = useState(0);
const camRef = useRef(null);
const checkRoomExist = async (roomId: string) => {
- const response = await fetch(`/api/cam/${roomId}`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
-
- const json = await response.json();
-
- setStatusCode(json.statusCode);
+ const { statusCode: newStatusCode } = await fetchData('GET', `/api/cam/${roomId}`);
+ setStatusCode(newStatusCode);
};
useEffect(() => {
const roomId = new URLSearchParams(new URL(window.location.href).search).get('roomid');
+
if (roomId) {
checkRoomExist(roomId);
}
@@ -66,6 +50,13 @@ function Cam(): JSX.Element {
setUserInfo((prev) => ({ ...prev, roomId }));
}, []);
+ useEffect(() => {
+ if (!user) {
+ return;
+ }
+ setUserInfo((prev) => ({ ...prev, nickname: user.nickname }));
+ }, [user]);
+
switch (statusCode) {
case 0:
return ;
@@ -83,11 +74,9 @@ function Cam(): JSX.Element {
-
-
-
-
-
+
+
+
@@ -100,4 +89,4 @@ function Cam(): JSX.Element {
}
}
-export default Cam;
+export default CamMain;
diff --git a/frontend/src/components/Cam/CamStore.tsx b/frontend/src/components/Cam/CamStore.tsx
index 5b5261f..6145fc8 100644
--- a/frontend/src/components/Cam/CamStore.tsx
+++ b/frontend/src/components/Cam/CamStore.tsx
@@ -1,5 +1,5 @@
-import React, { createContext } from 'react';
-import { io } from 'socket.io-client';
+import React, { createContext, useEffect, useRef } from 'react';
+import { io, Socket } from 'socket.io-client';
import useUserMedia from '../../hooks/useUserMedia';
import { UserInfo } from '../../types/cam';
@@ -11,21 +11,43 @@ type CamStoreProps = {
};
export const CamStoreContext = createContext(null);
-const socket = io();
function CamStore(props: CamStoreProps): JSX.Element {
const { children, userInfo, setUserInfo } = props;
const currentURL = new URL(window.location.href);
const roomId = currentURL.searchParams.get('roomid');
+
+ const socketRef = useRef();
+
+ if (!socketRef.current) {
+ const socket = io('/cam', {
+ withCredentials: true,
+ forceNew: true,
+ transports: ['polling'],
+ });
+
+ socketRef.current = socket;
+ }
+
const { localStatus, localStream, setLocalStatus, screenList } = useUserMedia({
- socket,
+ socket: socketRef.current,
roomId,
userInfo,
});
+ useEffect(() => {
+ const socketDisconnect = () => {
+ socketRef?.current?.disconnect();
+ };
+ window.addEventListener('popstate', socketDisconnect);
+ return () => {
+ window.removeEventListener('popstate', socketDisconnect);
+ };
+ }, []);
+
return (
{children}
diff --git a/frontend/src/components/Cam/Menu/ButtonBar.tsx b/frontend/src/components/Cam/Menu/ButtonBar.tsx
index 494d945..23d1298 100644
--- a/frontend/src/components/Cam/Menu/ButtonBar.tsx
+++ b/frontend/src/components/Cam/Menu/ButtonBar.tsx
@@ -1,13 +1,14 @@
import React, { RefObject, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
-import { ButtonBarIcons } from '../../../utils/SvgIcons';
+import { ButtonBarIcons } from '../../../utils/svgIcons';
import { CamStoreContext } from '../CamStore';
import type { Status } from '../../../types/cam';
import { ToggleStoreContext } from '../ToggleStore';
import { STTStoreContext } from '../STT/STTStore';
import { SharedScreenStoreContext } from '../SharedScreen/SharedScreenStore';
import NicknameModal from './NicknameModal';
+import { flex } from '../../../utils/styledComponentFunc';
const {
MicIcon,
@@ -24,34 +25,27 @@ const {
} = ButtonBarIcons;
const Container = styled.div<{ isMouseOnCamPage: boolean }>`
- width: 98vw;
+ width: 100%;
height: 8vh;
- margin-top: 5px;
-
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
+ box-sizing: border-box;
+ padding: 10px;
+ ${flex('row', 'space-between', 'center')};
border-radius: 10px;
transition: bottom 0.5s ease;
position: absolute;
bottom: ${(props) => (props.isMouseOnCamPage ? '0' : '-8vh')};
+ background-color: black;
`;
const ButtonContainer = styled.div`
- display: flex;
- flex-direction: row;
- align-items: center;
+ ${flex('row', 'initial', 'center')};
`;
const Button = styled.div<{ color?: string }>`
width: 9vw;
height: 7vh;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
+ ${flex('column', 'flex-start', 'center')};
color: ${(props) => (props.color ? props.color : '#bbbbbb')};
border-radius: 10px;
@@ -77,7 +71,7 @@ function ButtonBar(props: ButtonBarProps): JSX.Element {
const [isMouseOnCamPage, setMouseOnCamPage] = useState(true);
const [isActiveNicknameModal, setIsActiveNicknameModal] = useState(false);
- const { localStream, setLocalStatus, localStatus, setUserInfo } = useContext(CamStoreContext);
+ const { localStream, setLocalStatus, localStatus, setUserInfo, socket } = useContext(CamStoreContext);
const { handleChattingTabActive } = useContext(ToggleStoreContext);
const { toggleSTTActive, isSTTActive } = useContext(STTStoreContext);
@@ -110,6 +104,7 @@ function ButtonBar(props: ButtonBarProps): JSX.Element {
};
const handleExit = () => {
+ socket.disconnect();
window.history.back();
};
diff --git a/frontend/src/components/Cam/Menu/ChattingTab.tsx b/frontend/src/components/Cam/Menu/ChattingTab.tsx
index edcb776..2f8b9f5 100644
--- a/frontend/src/components/Cam/Menu/ChattingTab.tsx
+++ b/frontend/src/components/Cam/Menu/ChattingTab.tsx
@@ -4,20 +4,20 @@ import styled from 'styled-components';
import STTScreen from '../STT/STTScreen';
import { ToggleStoreContext } from '../ToggleStore';
import { CamStoreContext } from '../CamStore';
+import getCurrentDate from '../../../utils/getCurrentDate';
+import { CamMessageInfo, CamRoomInfo } from '../../../types/cam';
+import { customScroll, flex } from '../../../utils/styledComponentFunc';
const Container = styled.div<{ isActive: boolean; isMouseOnCamPage: boolean }>`
height: 90vh;
background-color: #ffffff;
- display: flex;
+
transition: right 0.5s, opacity 0.5s;
position: absolute;
width: 27vw;
right: ${(props) => (props.isActive ? '0' : '-30vw')};
opacity: ${(props) => (props.isActive ? '1' : '0')};
-
- flex-direction: column;
- justify-content: space-around;
- align-items: center;
+ ${flex('column', 'space-around', 'center')};
`;
const ChatLogs = styled.div`
@@ -26,38 +26,20 @@ const ChatLogs = styled.div`
background-color: #ffffff;
overflow-y: auto;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
-
- &::-webkit-scrollbar {
- width: 10px;
- }
- &::-webkit-scrollbar-thumb {
- background-color: #999999;
- border-radius: 10px;
- }
- &::-webkit-scrollbar-track {
- background-color: #cccccc;
- border-radius: 10px;
- }
+ ${flex('column', 'flex-start', 'center')};
+ ${customScroll()};
`;
const ChatContainer = styled.div<{ isMe: boolean }>`
width: 90%;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
+ ${flex('column', 'flex-start')};
align-items: ${(props) => (props.isMe ? 'flex-end' : 'flex-start')};
`;
const ChatTop = styled.div<{ isMe: boolean }>`
width: 100%;
- display: flex;
- flex-direction: row;
+ ${flex('row', 'initial', 'center')};
justify-content: ${(props) => (props.isMe ? 'end' : 'start')};
- align-items: center;
margin-top: 5px;
`;
@@ -94,21 +76,7 @@ const ChatTextarea = styled.textarea`
font-size: 16px;
padding: 10px 8px;
box-sizing: border-box;
-
- &::-webkit-scrollbar {
- width: 10px;
- padding: 0px 8px;
- }
- &::-webkit-scrollbar-thumb {
- background-color: #999999;
- border-radius: 10px;
- padding: 0px 8px;
- }
- &::-webkit-scrollbar-track {
- background-color: #cccccc;
- border-radius: 10px;
- padding: 0px 8px;
- }
+ ${customScroll()};
`;
const TextContainer = styled.div`
@@ -119,42 +87,11 @@ const TextContainer = styled.div`
width: -webkit-fill-available;
`;
-type CurrentDate = {
- year: number;
- month: number;
- date: number;
- hour: number;
- minutes: number;
-};
-
-type MessageInfo = {
- msg: string;
- room: string | null;
- user: string;
- date: CurrentDate;
-};
-
-type RoomInfo = {
- socketId: string;
- userNickname: string;
-};
-
-const getCurrentDate = (): CurrentDate => {
- const today: Date = new Date();
- return {
- year: today.getFullYear(),
- month: today.getMonth() + 1,
- date: today.getDate(),
- hour: today.getHours(),
- minutes: today.getMinutes(),
- };
-};
-
function ChattingTab(): JSX.Element {
const { isChattingTabActive, isMouseOnCamPage } = useContext(ToggleStoreContext);
const { userInfo, setLocalStatus, socket } = useContext(CamStoreContext);
- const [chatLogs, setChatLogs] = useState([]);
- const [nicknameList, setNicknameList] = useState([
+ const [chatLogs, setChatLogs] = useState([]);
+ const [nicknameList, setNicknameList] = useState([
{
socketId: socket.id,
userNickname: userInfo.nickname,
@@ -164,10 +101,11 @@ function ChattingTab(): JSX.Element {
const chatLogsRef = useRef(null);
const sendMessage = (msg: string) => {
- const currentDate = getCurrentDate();
- const msgInfo: MessageInfo = { msg, room, user: socket.id, date: currentDate };
+ const today = new Date();
+ const currentDate = getCurrentDate(today);
+ const msgInfo: CamMessageInfo = { msg, room, user: socket.id, date: currentDate };
- socket.emit('sendMessage', msgInfo);
+ socket.emit('sendCamMessage', msgInfo);
setChatLogs((logs) => [...logs, msgInfo]);
};
@@ -186,11 +124,14 @@ function ChattingTab(): JSX.Element {
};
useEffect(() => {
- socket.on('receiveMessage', ({ payload, nicknameInfo }: { payload: MessageInfo; nicknameInfo: RoomInfo[] }) => {
- setChatLogs((logs) => [...logs, payload]);
- setNicknameList(nicknameInfo);
- });
- socket.on('getNicknameList', (nicknameInfo: RoomInfo[]) => {
+ socket.on(
+ 'receiveCamMessage',
+ ({ payload, nicknameInfo }: { payload: CamMessageInfo; nicknameInfo: CamRoomInfo[] }) => {
+ setChatLogs((logs) => [...logs, payload]);
+ setNicknameList(nicknameInfo);
+ },
+ );
+ socket.on('getNicknameList', (nicknameInfo: CamRoomInfo[]) => {
setNicknameList(nicknameInfo);
});
}, []);
@@ -201,7 +142,7 @@ function ChattingTab(): JSX.Element {
}
});
- const currentChatLogs = chatLogs.map((data: MessageInfo, index: number): JSX.Element => {
+ const currentChatLogs = chatLogs.map((data: CamMessageInfo, index: number): JSX.Element => {
const { msg, date, user } = data;
const time = `${date.hour}:${date.minutes < 10 ? `0${date.minutes}` : date.minutes}`;
const isMe = user === socket.id;
diff --git a/frontend/src/components/Cam/Menu/NicknameModal.tsx b/frontend/src/components/Cam/Menu/NicknameModal.tsx
index 1b547c1..598e4f3 100644
--- a/frontend/src/components/Cam/Menu/NicknameModal.tsx
+++ b/frontend/src/components/Cam/Menu/NicknameModal.tsx
@@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import styled from 'styled-components';
import { UserInfo } from '../../../types/cam';
+import { flex } from '../../../utils/styledComponentFunc';
import { CamStoreContext } from '../CamStore';
const Container = styled.div`
@@ -10,10 +11,7 @@ const Container = styled.div`
height: 100vh;
left: 0px;
right: 0px;
-
- display: flex;
- justify-content: space-around;
- align-items: center;
+ ${flex('column', 'space-around', 'center')}
z-index: 2;
`;
@@ -30,10 +28,7 @@ const ModalBox = styled.div`
width: 30%;
height: 20%;
background-color: white;
- display: flex;
- flex-direction: column;
- justify-content: space-around;
- align-items: center;
+ ${flex('column', 'space-around', 'center')}
padding: 20px;
border-radius: 20px;
box-shadow: 0px 5px 22px -2px #000000;
@@ -42,10 +37,7 @@ const ModalBox = styled.div`
const Form = styled.form`
border-radius: 20px;
-
- display: flex;
- justify-content: space-around;
- align-items: center;
+ ${flex('row', 'space-around', 'center')}
`;
const Input = styled.input`
diff --git a/frontend/src/components/Cam/Menu/UserListTab.tsx b/frontend/src/components/Cam/Menu/UserListTab.tsx
index bf29742..95e5108 100644
--- a/frontend/src/components/Cam/Menu/UserListTab.tsx
+++ b/frontend/src/components/Cam/Menu/UserListTab.tsx
@@ -7,6 +7,7 @@ import LocalUserScreen from '../Screen/LocalUserScreen';
import Draggable from '../../core/Draggable';
import type { Screen } from '../../../types/cam';
import { ToggleStoreContext } from '../ToggleStore';
+import { customScroll } from '../../../utils/styledComponentFunc';
const Container = styled.div<{ isActive: boolean }>`
position: absolute;
@@ -17,17 +18,7 @@ const Container = styled.div<{ isActive: boolean }>`
display: ${(props) => (props.isActive ? 'block' : 'none')};
overflow-y: auto;
- &::-webkit-scrollbar {
- width: 10px;
- }
- &::-webkit-scrollbar-thumb {
- background-color: #999999;
- border-radius: 10px;
- }
- &::-webkit-scrollbar-track {
- background-color: #cccccc;
- border-radius: 10px;
- }
+ ${customScroll()};
`;
function UserListTab(): JSX.Element {
@@ -43,9 +34,9 @@ function UserListTab(): JSX.Element {
isActive={isUserListTabActive}
>
-
+
{screenList.map((screen: Screen) => (
-
+
))}
diff --git a/frontend/src/components/Cam/Page/CamDefaultPage.tsx b/frontend/src/components/Cam/Page/CamDefaultPage.tsx
index e95603a..2e0d188 100644
--- a/frontend/src/components/Cam/Page/CamDefaultPage.tsx
+++ b/frontend/src/components/Cam/Page/CamDefaultPage.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import styled from 'styled-components';
+import { flex } from '../../../utils/styledComponentFunc';
type CamDefaultPageProps = {
backgroundSrc: string;
@@ -12,10 +13,7 @@ const Container = styled.div`
height: 100vh;
left: 0px;
right: 0px;
-
- display: flex;
- justify-content: center;
- align-items: center;
+ ${flex('row', 'center', 'center')}
background-color: white;
`;
diff --git a/frontend/src/components/Cam/Page/CamErrorPage.tsx b/frontend/src/components/Cam/Page/CamErrorPage.tsx
index 60dbc89..50f96ea 100644
--- a/frontend/src/components/Cam/Page/CamErrorPage.tsx
+++ b/frontend/src/components/Cam/Page/CamErrorPage.tsx
@@ -1,14 +1,13 @@
import React from 'react';
import styled from 'styled-components';
+import { flex } from '../../../utils/styledComponentFunc';
import CamDefaultPage from './CamDefaultPage';
const Title = styled.div`
background-color: rgba(0, 0, 0, 0.7);
width: 100%;
height: 20%;
- display: flex;
- justify-content: center;
- align-items: center;
+ ${flex('row', 'center', 'center')};
color: white;
font-size: 44px;
`;
diff --git a/frontend/src/components/Cam/Page/CamLoadingPage.tsx b/frontend/src/components/Cam/Page/CamLoadingPage.tsx
index 820e554..b835ef9 100644
--- a/frontend/src/components/Cam/Page/CamLoadingPage.tsx
+++ b/frontend/src/components/Cam/Page/CamLoadingPage.tsx
@@ -1,14 +1,13 @@
import React from 'react';
import styled from 'styled-components';
+import { flex } from '../../../utils/styledComponentFunc';
import CamDefaultPage from './CamDefaultPage';
const Title = styled.div`
background-color: rgba(0, 0, 0, 0.7);
width: 100%;
height: 20%;
- display: flex;
- justify-content: center;
- align-items: center;
+ ${flex('row', 'center', 'center')};
color: white;
font-size: 44px;
`;
diff --git a/frontend/src/components/Cam/Page/CamNickNameInputPage.tsx b/frontend/src/components/Cam/Page/CamNickNameInputPage.tsx
index 6c2725f..35336e4 100644
--- a/frontend/src/components/Cam/Page/CamNickNameInputPage.tsx
+++ b/frontend/src/components/Cam/Page/CamNickNameInputPage.tsx
@@ -1,15 +1,14 @@
import React from 'react';
import styled from 'styled-components';
import { UserInfo } from '../../../types/cam';
+import { flex } from '../../../utils/styledComponentFunc';
import CamDefaultPage from './CamDefaultPage';
const Form = styled.form`
background-color: rgba(0, 0, 0, 0.7);
width: 100%;
height: 20%;
- display: flex;
- justify-content: center;
- align-items: center;
+ ${flex('row', 'center', 'center')};
color: white;
font-size: 44px;
`;
diff --git a/frontend/src/components/Cam/Page/CamNotAvailablePage.tsx b/frontend/src/components/Cam/Page/CamNotAvailablePage.tsx
index 21d919a..debadda 100644
--- a/frontend/src/components/Cam/Page/CamNotAvailablePage.tsx
+++ b/frontend/src/components/Cam/Page/CamNotAvailablePage.tsx
@@ -1,14 +1,13 @@
import React from 'react';
import styled from 'styled-components';
+import { flex } from '../../../utils/styledComponentFunc';
import CamDefaultPage from './CamDefaultPage';
const Title = styled.div`
background-color: rgba(0, 0, 0, 0.7);
width: 100%;
height: 20%;
- display: flex;
- justify-content: center;
- align-items: center;
+ ${flex('row', 'center', 'center')};
color: white;
font-size: 44px;
`;
diff --git a/frontend/src/components/Cam/Page/CamNotFoundPage.tsx b/frontend/src/components/Cam/Page/CamNotFoundPage.tsx
index 4632cc0..6652178 100644
--- a/frontend/src/components/Cam/Page/CamNotFoundPage.tsx
+++ b/frontend/src/components/Cam/Page/CamNotFoundPage.tsx
@@ -1,14 +1,13 @@
import React from 'react';
import styled from 'styled-components';
+import { flex } from '../../../utils/styledComponentFunc';
import CamDefaultPage from './CamDefaultPage';
const Title = styled.div`
background-color: rgba(0, 0, 0, 0.7);
width: 100%;
height: 20%;
- display: flex;
- justify-content: center;
- align-items: center;
+ ${flex('row', 'center', 'center')};
color: white;
font-size: 44px;
`;
diff --git a/frontend/src/components/Cam/STT/STTScreen.tsx b/frontend/src/components/Cam/STT/STTScreen.tsx
index 5821ea6..30b6940 100644
--- a/frontend/src/components/Cam/STT/STTScreen.tsx
+++ b/frontend/src/components/Cam/STT/STTScreen.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useContext } from 'react';
import styled from 'styled-components';
import { Status } from '../../../types/cam';
+import { flex } from '../../../utils/styledComponentFunc';
import { STTStoreContext } from './STTStore';
const Container = styled.div`
@@ -11,9 +12,7 @@ const Container = styled.div`
height: 80px;
border-bottom: 2px solid #999999;
margin-top: 5px;
-
- display: flex;
- flex-direction: column;
+ ${flex('column')}
`;
const Title = styled.div`
diff --git a/frontend/src/components/Cam/Screen/DefaultScreen.tsx b/frontend/src/components/Cam/Screen/DefaultScreen.tsx
index 089ea4f..7396c58 100644
--- a/frontend/src/components/Cam/Screen/DefaultScreen.tsx
+++ b/frontend/src/components/Cam/Screen/DefaultScreen.tsx
@@ -1,10 +1,10 @@
import styled from 'styled-components';
+import { flex } from '../../../utils/styledComponentFunc';
const Container = styled.div`
- width: 100%;
+ width: 90%;
max-height: 100%;
- display: flex;
- justify-content: center;
+ ${flex('row', 'center')};
`;
const DefaultImg = styled.img`
diff --git a/frontend/src/components/Cam/Screen/LocalUserScreen.tsx b/frontend/src/components/Cam/Screen/LocalUserScreen.tsx
index eed3e9c..8839933 100644
--- a/frontend/src/components/Cam/Screen/LocalUserScreen.tsx
+++ b/frontend/src/components/Cam/Screen/LocalUserScreen.tsx
@@ -4,23 +4,28 @@ import styled from 'styled-components';
import DefaultScreen from './DefaultScreen';
import { CamStoreContext } from '../CamStore';
import StreamStatusIndicator from './StreamStatusIndicator';
+import { flex } from '../../../utils/styledComponentFunc';
-const Container = styled.div`
+const Container = styled.div<{ numOfScreen: number }>`
position: relative;
- width: 100%;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 90%;
+ width: calc(100% / ${(props) => Math.ceil(props.numOfScreen ** 0.5)});
+ height: calc(100% / ${(props) => Math.floor((props.numOfScreen + 1) ** 0.5)});
+ ${flex('column', 'center', 'center')}
+ aspect-ratio: 16/9;
+ overflow: hidden;
`;
const Video = styled.video`
max-height: 100%;
- width: 100%;
+ width: 90%;
`;
-function LocalUserScreen(): JSX.Element {
+type LocalUserScreenProps = {
+ numOfScreen: number;
+};
+
+function LocalUserScreen(props: LocalUserScreenProps): JSX.Element {
+ const { numOfScreen } = props;
const videoRef = useRef(null);
const { localStream, localStatus, userInfo } = useContext(CamStoreContext);
@@ -34,7 +39,7 @@ function LocalUserScreen(): JSX.Element {
});
return (
-
+
{localStatus.stream && localStatus.video ? (