From 649ef6ca7f11eda398618d645999c258eac405c9 Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 02:46:56 +0900 Subject: [PATCH 01/15] =?UTF-8?q?Fix:=20Socket=20Disconnect=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=20=EB=B0=A9=20=EB=82=98=EA=B0=80=EA=B2=8C=EB=81=94=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/socket/src/gateway/room.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/socket/src/gateway/room.service.ts b/server/socket/src/gateway/room.service.ts index b05e7366..5a45395b 100644 --- a/server/socket/src/gateway/room.service.ts +++ b/server/socket/src/gateway/room.service.ts @@ -137,7 +137,7 @@ export class RoomService { } } this.saveRoomByUUID(uuid, findRoom); - + await this.leaveRoomRequestToApiServer(uuid); this.emitEventForUserList(server, uuid); } }); From 7e1a8ef652838dc628d6df2a2454a6d49f410b0e Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 02:47:08 +0900 Subject: [PATCH 02/15] =?UTF-8?q?Fix:=20Error=20Msg=20=EC=95=88=EB=8D=98?= =?UTF-8?q?=EC=A0=B8=EC=A7=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/socket/src/gateway/room.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/socket/src/gateway/room.service.ts b/server/socket/src/gateway/room.service.ts index 5a45395b..d0acfc03 100644 --- a/server/socket/src/gateway/room.service.ts +++ b/server/socket/src/gateway/room.service.ts @@ -113,9 +113,9 @@ export class RoomService { disconnectClient(client: Socket, server: Server) { Redis.get(client.id, (err, uuid) => { - if (err || !uuid) return; + if (err || !uuid) return this.emitEventForError({ client, server }, Exception.clientNotFound); Redis.get(uuid, async (err, data) => { - if (err || !data) return; + if (err || !data) return this.emitEventForError({ client, server }, Exception.roomNotFound); const findRoom = JSON.parse(data); const findOwnerNickname = findRoom['userList'][findRoom.owner].nickname; const findMyNickname = findRoom['userList'][client.id].nickname; From 47623efd4bb4bb2651e04df82e2bb6df8846708e Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 03:03:08 +0900 Subject: [PATCH 03/15] =?UTF-8?q?Chore:=20Room=20Service=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/socket/src/gateway/room.service.ts | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/server/socket/src/gateway/room.service.ts b/server/socket/src/gateway/room.service.ts index d0acfc03..40a2b565 100644 --- a/server/socket/src/gateway/room.service.ts +++ b/server/socket/src/gateway/room.service.ts @@ -169,6 +169,11 @@ export class RoomService { Redis.multi().set(uuid, JSON.stringify(findRoom)).set(client.id, uuid).exec(); } + /** + * 소켓으로 유저 리스트 이벤트를 해당 방에 접속중인 클라이언트에게 보냅니다. + * @param server 보낼 주체(서버) + * @param uuid 이벤트를 보낼 대상이 접속중인 방의 고유 uuid + */ emitEventForUserList(server: Server, uuid: string) { Redis.get(uuid, (err, data) => { if (err || !data) return this.emitEventForError({ client: uuid, server }, Exception.roomNotFound); @@ -176,19 +181,42 @@ export class RoomService { }); } + /** + * 소켓으로 빈 유저 리스트 이벤트를 해당 방에 접속중인 클라이언트에게 보냅니다. + * @param server 보낼 주체(서버) + * @param uuid 이벤트를 보낼 대상이 접속중인 방의 고유 uuid + */ emitEventForEmptyUserList(server: Server, uuid: string) { server.to(uuid).emit(RoomEvent.UserList, {}); } + /** + * 소켓으로 검증 이벤트를 해당 클라이언트에게 보냅니다. + * @param client 소켓으로 검증 이벤트를 받을 클라이언트 + * @param server 소켓으로 검증 이벤트를 보낼 서버 + * @param isVerify 검증의 유무 + */ emitEventForVerify({ client, server }, isVerify: boolean) { server.to(client.id).emit(RoomEvent.IsVerify, isVerify); } + /** + * 소켓으로 에러 이벤트를 해당 클라이언트에게 보냅니다. + * @param client 소켓으로 에러 이벤트를 받을 클라이언트 + * @param server 소켓으로 에러 이벤트를 보낼 서버 + * @param message 에러 메시지 + */ emitEventForError({ client, server }, message) { server.to(client.id).emit(RoomEvent.Error, message); } // Same := registerRoom() + /** + * Redis 에 uuid:roomData 구조로 저장합니다. + * registerRoom() 개념과 같습니다. + * @param uuid key + * @param roomData value + */ saveRoomByUUID(uuid: string, roomData: any): void { Redis.set(uuid, JSON.stringify(roomData)); } From d82bfb9e9e84ff7427f1d1d7229531198c2fb3ca Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 03:03:45 +0900 Subject: [PATCH 04/15] =?UTF-8?q?Refactor:=20=EB=B0=98=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=9E=91=EC=97=85=20=EB=A6=AC=ED=8E=99=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isOwner 함수로 바꾸기 --- server/socket/src/gateway/room.service.ts | 25 ++++++++++------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/server/socket/src/gateway/room.service.ts b/server/socket/src/gateway/room.service.ts index 40a2b565..01704809 100644 --- a/server/socket/src/gateway/room.service.ts +++ b/server/socket/src/gateway/room.service.ts @@ -27,16 +27,13 @@ export class RoomService { Redis.get(uuid, (err, data) => { if (err || !data) return this.emitEventForError({ client, server }, Exception.roomNotFound); const findRoom = JSON.parse(data); - const findOwnerNickname = findRoom['userList'][findRoom.owner].nickname; - const findMyNickname = findRoom['userList'][client.id].nickname; - if (findOwnerNickname === findMyNickname) { + if (this.isOwner(findRoom, client)) { for (const userInfo of Object.entries(findRoom.userList)) { const socketId: string = userInfo[0]; this.deRegisterUserBySocketID(socketId); } this.deRegisterRoom(uuid); - this.emitEventForEmptyUserList(server, uuid); - return; + return this.emitEventForEmptyUserList(server, uuid); } delete findRoom['userList'][client.id]; Redis.multi().set(uuid, JSON.stringify(findRoom)).del(client.id).exec(); @@ -48,9 +45,7 @@ export class RoomService { Redis.get(uuid, (err, data) => { if (err || !data) return this.emitEventForError({ client, server }, Exception.roomNotFound); const findRoom = JSON.parse(data); - const findOwnerNickname = findRoom['userList'][findRoom.owner].nickname; - const findMyNickname = findRoom['userList'][client.id].nickname; - if (findOwnerNickname !== findMyNickname) { + if (!this.isOwner(findRoom, client)) { return this.emitEventForError({ client, server }, Exception.clientUnauthorized); } for (const userInfo of Object.entries(findRoom.userList)) { @@ -71,9 +66,7 @@ export class RoomService { Redis.get(uuid, (err, data) => { if (err || !data) return this.emitEventForError({ client, server }, Exception.roomNotFound); const findRoom = JSON.parse(data); - const findOwnerNickname = findRoom['userList'][findRoom.owner].nickname; - const findMyNickname = findRoom['userList'][client.id].nickname; - if (findOwnerNickname === findMyNickname) { + if (this.isOwner(findRoom, client)) { for (const userInfo of Object.entries(findRoom.userList)) { const socketId: string = userInfo[0]; this.deRegisterUserBySocketID(socketId); @@ -111,15 +104,20 @@ export class RoomService { }); } + isOwner(findRoom: any, client: Socket): boolean { + const findOwnerNickname = findRoom['userList'][findRoom.owner].nickname; + const findMyNickname = findRoom['userList'][client.id].nickname; + return findOwnerNickname === findMyNickname; + } + disconnectClient(client: Socket, server: Server) { Redis.get(client.id, (err, uuid) => { if (err || !uuid) return this.emitEventForError({ client, server }, Exception.clientNotFound); Redis.get(uuid, async (err, data) => { if (err || !data) return this.emitEventForError({ client, server }, Exception.roomNotFound); const findRoom = JSON.parse(data); - const findOwnerNickname = findRoom['userList'][findRoom.owner].nickname; const findMyNickname = findRoom['userList'][client.id].nickname; - if (findMyNickname === findOwnerNickname) { + if (this.isOwner(findRoom, client)) { for (const userInfo of Object.entries(findRoom.userList)) { const socketId: string = userInfo[0]; this.deRegisterUserBySocketID(socketId); @@ -210,7 +208,6 @@ export class RoomService { server.to(client.id).emit(RoomEvent.Error, message); } - // Same := registerRoom() /** * Redis 에 uuid:roomData 구조로 저장합니다. * registerRoom() 개념과 같습니다. From 00163321550b8fd447c5dfc1c6cbdb2f919b80cc Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 03:27:15 +0900 Subject: [PATCH 05/15] =?UTF-8?q?Fix:=20Socket=20Disconnect=20=ED=87=B4?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/socket/package.json | 2 +- server/socket/src/gateway/room.service.ts | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/server/socket/package.json b/server/socket/package.json index 1f92c39a..dee7699c 100644 --- a/server/socket/package.json +++ b/server/socket/package.json @@ -12,7 +12,7 @@ "start": "nest start", "start:dev": "set NODE_ENV=dev&&nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "set NODE_ENV=prod&&nest start", + "start:prod": "set NODE_ENV=production&&nest start", "start:3000": "cross-env NODE_PORT=3000 nest start", "start:3001": "cross-env NODE_PORT=3001 nest start", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", diff --git a/server/socket/src/gateway/room.service.ts b/server/socket/src/gateway/room.service.ts index 01704809..3483ae7e 100644 --- a/server/socket/src/gateway/room.service.ts +++ b/server/socket/src/gateway/room.service.ts @@ -219,18 +219,16 @@ export class RoomService { } async leaveRoomRequestToApiServer(uuid): Promise { - await axios.post(`${baseURL}/api/room/socket/leave/${uuid}`, { - headers: { - 'socket-secret-key': process.env.SOCKET_SECRET_KEY ?? '', - }, - }); + const headers = { + 'socket-secret-key': process.env.SOCKET_SECRET_KEY ?? '', + }; + const response = await axios.post(`${baseURL}/api/room/socket/leave/${uuid}`, undefined, { headers }); } async deleteRoomRequestToApiServer(uuid): Promise { - await axios.delete(`${baseURL}/api/room/socket/${uuid}`, { - headers: { - 'socket-secret-key': process.env.SOCKET_SECRET_KEY ?? '', - }, - }); + const headers = { + 'socket-secret-key': process.env.SOCKET_SECRET_KEY ?? '', + }; + await axios.delete(`${baseURL}/api/room/socket/${uuid}`, { headers }); } } From 6044c6f883a3a03030b32d44517062d272c898ac Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 03:28:02 +0900 Subject: [PATCH 06/15] Refactor: CI --- server/socket/src/gateway/room.service.ts | 2 +- server/socket/test/app.e2e-spec.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/server/socket/src/gateway/room.service.ts b/server/socket/src/gateway/room.service.ts index 3483ae7e..23a202f4 100644 --- a/server/socket/src/gateway/room.service.ts +++ b/server/socket/src/gateway/room.service.ts @@ -222,7 +222,7 @@ export class RoomService { const headers = { 'socket-secret-key': process.env.SOCKET_SECRET_KEY ?? '', }; - const response = await axios.post(`${baseURL}/api/room/socket/leave/${uuid}`, undefined, { headers }); + await axios.post(`${baseURL}/api/room/socket/leave/${uuid}`, undefined, { headers }); } async deleteRoomRequestToApiServer(uuid): Promise { diff --git a/server/socket/test/app.e2e-spec.ts b/server/socket/test/app.e2e-spec.ts index 50cda623..1a013bef 100644 --- a/server/socket/test/app.e2e-spec.ts +++ b/server/socket/test/app.e2e-spec.ts @@ -16,9 +16,6 @@ describe('AppController (e2e)', () => { }); it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); }); }); From 40c317417d8d41ff2e862fe93e88bd993666209b Mon Sep 17 00:00:00 2001 From: Dev-Beom Date: Fri, 3 Dec 2021 03:40:59 +0900 Subject: [PATCH 07/15] =?UTF-8?q?Refactor:=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=EB=B3=80=EA=B2=BD,=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EB=AA=85=20=EB=B3=80=EA=B2=BD,=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/src/builder/{ => dev-field}/dev-field.builder.ts | 4 ++-- server/api/src/builder/{ => history}/history.builder.ts | 6 +++--- server/api/src/builder/index.ts | 6 +++--- server/api/src/builder/{ => visit}/visit.builder.ts | 4 ++-- server/api/src/domain/auth/service/auth.service.spec.ts | 2 +- server/api/src/domain/auth/service/auth.service.ts | 2 +- server/api/src/domain/base-time.entity.ts | 2 +- server/api/src/domain/history/history.entity.ts | 2 +- .../api/src/domain/history/repository/visit.repository.ts | 2 +- server/api/src/domain/history/service/history.service.ts | 5 +---- server/api/src/domain/history/visit.entity.ts | 2 +- server/api/src/domain/user/controller/user.controller.ts | 1 - server/api/src/domain/user/user.entity.ts | 2 +- server/api/src/transformer/index.ts | 3 +++ server/api/src/transformer/local-date-time.transformer.ts | 2 +- server/api/src/transformer/local-date.transformer..ts | 2 +- server/api/src/utils/index.ts | 2 ++ server/api/test/mock.object.ts | 2 +- server/socket/src/gateway/room.gateway.ts | 2 +- 19 files changed, 27 insertions(+), 26 deletions(-) rename server/api/src/builder/{ => dev-field}/dev-field.builder.ts (70%) rename server/api/src/builder/{ => history}/history.builder.ts (67%) rename server/api/src/builder/{ => visit}/visit.builder.ts (77%) create mode 100644 server/api/src/transformer/index.ts create mode 100644 server/api/src/utils/index.ts diff --git a/server/api/src/builder/dev-field.builder.ts b/server/api/src/builder/dev-field/dev-field.builder.ts similarity index 70% rename from server/api/src/builder/dev-field.builder.ts rename to server/api/src/builder/dev-field/dev-field.builder.ts index 43d84a4c..e68b6d30 100644 --- a/server/api/src/builder/dev-field.builder.ts +++ b/server/api/src/builder/dev-field/dev-field.builder.ts @@ -1,5 +1,5 @@ -import { BuilderCommon } from './builder'; -import { DevField } from '../domain/field/dev-field.entity'; +import { BuilderCommon } from '../builder'; +import { DevField } from '../../domain/field/dev-field.entity'; export class DevFieldBuilder extends BuilderCommon { constructor() { diff --git a/server/api/src/builder/history.builder.ts b/server/api/src/builder/history/history.builder.ts similarity index 67% rename from server/api/src/builder/history.builder.ts rename to server/api/src/builder/history/history.builder.ts index c6892b8d..8a2387c3 100644 --- a/server/api/src/builder/history.builder.ts +++ b/server/api/src/builder/history/history.builder.ts @@ -1,7 +1,7 @@ import { LocalDate } from 'js-joda'; -import { BuilderCommon } from './builder'; -import { User } from '../domain/user/user.entity'; -import { History } from '../domain/history/history.entity'; +import { BuilderCommon } from '../builder'; +import { User } from '../../domain/user/user.entity'; +import { History } from '../../domain/history/history.entity'; export class HistoryBuilder extends BuilderCommon { constructor() { diff --git a/server/api/src/builder/index.ts b/server/api/src/builder/index.ts index 3dd55a16..ee936dc3 100644 --- a/server/api/src/builder/index.ts +++ b/server/api/src/builder/index.ts @@ -1,8 +1,8 @@ export { RoomBuilder } from './room/room.builder'; export { UserBuilder } from './user/user.builder'; -export { HistoryBuilder } from './history.builder'; -export { VisitBuilder } from './visit.builder'; -export { DevFieldBuilder } from './dev-field.builder'; +export { HistoryBuilder } from './history/history.builder'; +export { VisitBuilder } from './visit/visit.builder'; +export { DevFieldBuilder } from './dev-field/dev-field.builder'; export { LoginRequestDtoBuilder } from './auth/login-request.dto.builder'; export { UserResponseDtoBuilder } from './user/user-response.dto.builder'; export { UserUpdateDtoBuilder } from './user/user-update.dto.builder'; diff --git a/server/api/src/builder/visit.builder.ts b/server/api/src/builder/visit/visit.builder.ts similarity index 77% rename from server/api/src/builder/visit.builder.ts rename to server/api/src/builder/visit/visit.builder.ts index 83aa2d9e..ffbe8dab 100644 --- a/server/api/src/builder/visit.builder.ts +++ b/server/api/src/builder/visit/visit.builder.ts @@ -1,6 +1,6 @@ import { LocalDate } from 'js-joda'; -import { BuilderCommon } from './builder'; -import { Visit } from '../domain/history/visit.entity'; +import { BuilderCommon } from '../builder'; +import { Visit } from '../../domain/history/visit.entity'; export class VisitBuilder extends BuilderCommon { constructor() { super(Visit); diff --git a/server/api/src/domain/auth/service/auth.service.spec.ts b/server/api/src/domain/auth/service/auth.service.spec.ts index 4f028ece..a186cae6 100644 --- a/server/api/src/domain/auth/service/auth.service.spec.ts +++ b/server/api/src/domain/auth/service/auth.service.spec.ts @@ -18,7 +18,7 @@ import { HistoryBuilder, LoginRequestDtoBuilder, UserBuilder, UserResponseDtoBui import { BadRequestException, HttpStatus, UnauthorizedException } from '@nestjs/common'; import { UserException } from '../../../exception'; import { LoginRequestDto } from '../dto/login-request.dto'; -import { Bcrypt } from '../../../utils/bcrypt.util'; +import { Bcrypt } from '../../../utils'; import { User } from '../../user/user.entity'; import { LocalDate } from 'js-joda'; import { DevField } from '../../field/dev-field.entity'; diff --git a/server/api/src/domain/auth/service/auth.service.ts b/server/api/src/domain/auth/service/auth.service.ts index 8e63c7da..fde2e7bd 100644 --- a/server/api/src/domain/auth/service/auth.service.ts +++ b/server/api/src/domain/auth/service/auth.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import { LocalDate } from 'js-joda'; -import { Bcrypt } from '../../../utils/bcrypt.util'; +import { Bcrypt } from '../../../utils'; import { UserBuilder, UserResponseDtoBuilder } from '../../../builder'; import { DevFieldException, UserException } from '../../../exception'; import { User } from '../../user/user.entity'; diff --git a/server/api/src/domain/base-time.entity.ts b/server/api/src/domain/base-time.entity.ts index a648756b..ee052bfc 100644 --- a/server/api/src/domain/base-time.entity.ts +++ b/server/api/src/domain/base-time.entity.ts @@ -1,5 +1,5 @@ import { CreateDateColumn, Generated, PrimaryColumn, UpdateDateColumn } from 'typeorm'; -import { BigintValueTransformer } from '../transformer/bigint-value.transformer'; +import { BigintValueTransformer } from '../transformer'; export abstract class BaseTimeEntity { @Generated('increment') diff --git a/server/api/src/domain/history/history.entity.ts b/server/api/src/domain/history/history.entity.ts index 50d7907a..bc90c1e6 100644 --- a/server/api/src/domain/history/history.entity.ts +++ b/server/api/src/domain/history/history.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { LocalDate } from 'js-joda'; -import { LocalDateTransformer } from '../../transformer/local-date.transformer.'; +import { LocalDateTransformer } from '../../transformer'; import { User } from '../user/user.entity'; @Entity() diff --git a/server/api/src/domain/history/repository/visit.repository.ts b/server/api/src/domain/history/repository/visit.repository.ts index e2d1f6b8..1aa3787a 100644 --- a/server/api/src/domain/history/repository/visit.repository.ts +++ b/server/api/src/domain/history/repository/visit.repository.ts @@ -12,6 +12,6 @@ export class VisitRepository extends Repository { async addVisitCount(count: number) { const visit: Visit = new VisitBuilder().setDate(LocalDate.now().minusDays(1)).setTotalVisit(count).build(); - this.save(visit); + await this.save(visit); } } diff --git a/server/api/src/domain/history/service/history.service.ts b/server/api/src/domain/history/service/history.service.ts index ba9f3416..04730703 100644 --- a/server/api/src/domain/history/service/history.service.ts +++ b/server/api/src/domain/history/service/history.service.ts @@ -34,10 +34,7 @@ export class HistoryService { const user: User = await this.userRepository.findUserByUserEmail(email); if (!user) throw UserException.userNotFound(); const year = LocalDate.now().year(); - const result: HistoryResponseDto = new HistoryResponseDto( - await this.historyRepository.getMonthHistoryByUser(user, year), - ); - return result; + return new HistoryResponseDto(await this.historyRepository.getMonthHistoryByUser(user, year)); } async getLastVisitCount() { diff --git a/server/api/src/domain/history/visit.entity.ts b/server/api/src/domain/history/visit.entity.ts index 501cb886..4e299e48 100644 --- a/server/api/src/domain/history/visit.entity.ts +++ b/server/api/src/domain/history/visit.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { LocalDate } from 'js-joda'; -import { LocalDateTransformer } from '../../transformer/local-date.transformer.'; +import { LocalDateTransformer } from '../../transformer'; @Entity() export class Visit { diff --git a/server/api/src/domain/user/controller/user.controller.ts b/server/api/src/domain/user/controller/user.controller.ts index 9f9d4f86..c8255574 100644 --- a/server/api/src/domain/user/controller/user.controller.ts +++ b/server/api/src/domain/user/controller/user.controller.ts @@ -4,7 +4,6 @@ import { Req, Delete, Get, - Param, Patch, Post, UploadedFile, diff --git a/server/api/src/domain/user/user.entity.ts b/server/api/src/domain/user/user.entity.ts index ced78b3a..427d19f3 100644 --- a/server/api/src/domain/user/user.entity.ts +++ b/server/api/src/domain/user/user.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { LocalDate } from 'js-joda'; -import { LocalDateTransformer } from '../../transformer/local-date.transformer.'; +import { LocalDateTransformer } from '../../transformer'; import { DevField } from '../field/dev-field.entity'; import { History } from '../history/history.entity'; diff --git a/server/api/src/transformer/index.ts b/server/api/src/transformer/index.ts new file mode 100644 index 00000000..08b3984e --- /dev/null +++ b/server/api/src/transformer/index.ts @@ -0,0 +1,3 @@ +export { BigintValueTransformer } from './bigint-value.transformer'; +export { LocalDateTransformer } from './local-date.transformer.'; +export { LocalDateTimeTransformer } from './local-date-time.transformer'; diff --git a/server/api/src/transformer/local-date-time.transformer.ts b/server/api/src/transformer/local-date-time.transformer.ts index 90b827a8..57736e8a 100644 --- a/server/api/src/transformer/local-date-time.transformer.ts +++ b/server/api/src/transformer/local-date-time.transformer.ts @@ -1,6 +1,6 @@ import { ValueTransformer } from 'typeorm'; import { LocalDateTime } from 'js-joda'; -import { DateTimeUtil } from '../utils/date-time.util'; +import { DateTimeUtil } from '../utils'; export class LocalDateTimeTransformer implements ValueTransformer { to(entityValue: LocalDateTime): Date | null { diff --git a/server/api/src/transformer/local-date.transformer..ts b/server/api/src/transformer/local-date.transformer..ts index aaea2e76..2667ab81 100644 --- a/server/api/src/transformer/local-date.transformer..ts +++ b/server/api/src/transformer/local-date.transformer..ts @@ -1,6 +1,6 @@ import { ValueTransformer } from 'typeorm'; import { LocalDate } from 'js-joda'; -import { DateTimeUtil } from '../utils/date-time.util'; +import { DateTimeUtil } from '../utils'; export class LocalDateTransformer implements ValueTransformer { to(entityValue: LocalDate): Date | null { diff --git a/server/api/src/utils/index.ts b/server/api/src/utils/index.ts new file mode 100644 index 00000000..209b87fd --- /dev/null +++ b/server/api/src/utils/index.ts @@ -0,0 +1,2 @@ +export { Bcrypt } from './bcrypt.util'; +export { DateTimeUtil } from './date-time.util'; diff --git a/server/api/test/mock.object.ts b/server/api/test/mock.object.ts index 28107a86..03fe3c61 100644 --- a/server/api/test/mock.object.ts +++ b/server/api/test/mock.object.ts @@ -1,4 +1,4 @@ -import { DevFieldBuilder } from '../src/builder/dev-field.builder'; +import { DevFieldBuilder } from '../src/builder/dev-field/dev-field.builder'; import { RoomBuilder, UserBuilder } from '../src/builder'; import { datatype, internet, lorem } from 'faker'; import { RoomType } from '../src/domain/room/room.entity'; diff --git a/server/socket/src/gateway/room.gateway.ts b/server/socket/src/gateway/room.gateway.ts index 1e98d49c..d44816ab 100644 --- a/server/socket/src/gateway/room.gateway.ts +++ b/server/socket/src/gateway/room.gateway.ts @@ -10,7 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { Logger } from '@nestjs/common'; import { IMessage, IRoomRequest } from './room.interface'; -import { LocalDateTime, ZoneId } from '@js-joda/core'; +import { LocalDateTime} from '@js-joda/core'; import { RoomEvent } from './room.event'; import { RoomService } from './room.service'; From 76c1e848a52e2a2ef74c0b84aada269a335641d6 Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Fri, 3 Dec 2021 15:17:57 +0900 Subject: [PATCH 08/15] =?UTF-8?q?Refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EC=9D=B4=ED=83=88?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B8=EC=9B=90=EA=B0=90=EC=86=8C=20api=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자가 새로고침/브라우저 종료를 통한 브라우저 이탈시 인원감소 api 요청을 삭제했습니다. - 이제 서버에서 disconnect 이벤트를 기반으로 수행하게 됩니다. --- client/src/pages/Campfire/Campfire.tsx | 2 -- client/src/pages/Tadak/Tadak.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/client/src/pages/Campfire/Campfire.tsx b/client/src/pages/Campfire/Campfire.tsx index 27cc96b9..5cfe6698 100644 --- a/client/src/pages/Campfire/Campfire.tsx +++ b/client/src/pages/Campfire/Campfire.tsx @@ -12,7 +12,6 @@ import BGMContextProvider from '@contexts/bgmContext'; import { useUser } from '@contexts/userContext'; import { RoomType } from '@utils/constant'; import { RoomInfoType } from '@src/types'; -import { postLeaveRoom } from '@src/apis'; interface LocationProps { pathname: string; @@ -35,7 +34,6 @@ const Campfire = ({ location }: RoomProps): JSX.Element => { useEffect(() => { if (!userInfo.login) { - postLeaveRoom(uuid); history.goBack(); return; } diff --git a/client/src/pages/Tadak/Tadak.tsx b/client/src/pages/Tadak/Tadak.tsx index 7e68795e..4a412f41 100644 --- a/client/src/pages/Tadak/Tadak.tsx +++ b/client/src/pages/Tadak/Tadak.tsx @@ -9,7 +9,6 @@ import VideoList from '@components/video/VideoList'; import Loader from '@components/common/Loader'; import { useUser } from '@contexts/userContext'; import { RoomInfoType } from '@src/types'; -import { postLeaveRoom } from '@src/apis'; interface LocationProps { pathname: string; @@ -31,7 +30,6 @@ const Tadak = ({ location }: TadakProps): JSX.Element => { useEffect(() => { if (!userInfo.login) { - postLeaveRoom(uuid); history.goBack(); return; } From 34d88c1ecc4da206f6578d2058736a88c41c3fb7 Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Fri, 3 Dec 2021 15:27:33 +0900 Subject: [PATCH 09/15] =?UTF-8?q?Design:=20=EB=A9=94=EC=9D=B8=20=EB=A3=B8?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=B5=9C=EC=86=8C=EB=84=88=EB=B9=84=20?= =?UTF-8?q?=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 룸카드와 룸카드의 개발필드 부분에 최소너비를 부여해 찌그러지지 않도록 디자인 개선 --- client/src/components/room/RoomCard/style.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/room/RoomCard/style.ts b/client/src/components/room/RoomCard/style.ts index e3e52506..d487d24c 100644 --- a/client/src/components/room/RoomCard/style.ts +++ b/client/src/components/room/RoomCard/style.ts @@ -7,6 +7,7 @@ export const RoomCardWrapper = styled.div` flex-direction: column; background-color: ${({ theme }) => theme.colors.white}; padding: ${({ theme }) => theme.paddings.base}; + min-width: 15rem; width: 100%; height: ${ROOM_CARD.height}rem; border-radius: ${({ theme }) => theme.borderRadius.base}; @@ -77,6 +78,7 @@ export const RoomCardBottom = styled.div` `; export const RoomFieldType = styled.div<{ bgColor: DevFieldType }>` + min-width: 5.1rem; width: 6rem; height: 2rem; ${({ theme, bgColor }) => css` From 443584981005246a6b5254f7508d3df3ccd76831 Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Sat, 4 Dec 2021 17:43:21 +0900 Subject: [PATCH 10/15] =?UTF-8?q?Fix:=20=ED=99=94=EB=A9=B4=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=EC=8B=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EA=B0=80=20?= =?UTF-8?q?=EA=B9=9C=EB=B9=A1=EC=9D=B4=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isLoading 상태를 도입하여, 초기에만 로더가 돌아가도록 수정 --- client/src/pages/Tadak/Tadak.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/src/pages/Tadak/Tadak.tsx b/client/src/pages/Tadak/Tadak.tsx index 4a412f41..b8dc90f9 100644 --- a/client/src/pages/Tadak/Tadak.tsx +++ b/client/src/pages/Tadak/Tadak.tsx @@ -22,7 +22,8 @@ interface TadakProps { const Tadak = ({ location }: TadakProps): JSX.Element => { const { agoraAppId, agoraToken, uuid, owner, maxHeadcount } = location?.state; const [users, setUsers] = useState([]); - const [start, setStart] = useState(false); + const [start, setStart] = useState(false); + const [isLoading, setIsLoading] = useState(true); const client = useClient(); const { ready, tracks } = useMicrophoneAndCameraTracks(); const userInfo = useUser(); @@ -72,6 +73,7 @@ const Tadak = ({ location }: TadakProps): JSX.Element => { await tracks[1].setEnabled(false); await tracks[0].setEnabled(false); } + setIsLoading(false); setStart(true); }; @@ -83,14 +85,14 @@ const Tadak = ({ location }: TadakProps): JSX.Element => { return ( - {!start ? ( + {isLoading ? ( ) : ( <> - {tracks && } - {tracks && } + {start && tracks && } + {ready && tracks && } )} From b71dfdac0a7f6300eb2d23fcd459f0e670291b50 Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Sat, 4 Dec 2021 17:51:27 +0900 Subject: [PATCH 11/15] =?UTF-8?q?Fix:=20#302=20-=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=EB=A5=BC=20=EC=8B=9C=EB=8F=84=ED=95=A0=20?= =?UTF-8?q?=EB=95=8C=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 버그의 내용은 이슈에 담겨있습니다. - createScrenVideoTrack이 비디오/오디오 권한과는 달리 일회성 허가를 받지 않는 듯 합니다. - 그리하여 새로운 변수를 생성해서, 매번 실행할 때 마다 다른 스트림을 가지도록 변경했습니다. --- client/src/components/video/config.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/client/src/components/video/config.ts b/client/src/components/video/config.ts index 8a0ea086..b2e8a5c6 100644 --- a/client/src/components/video/config.ts +++ b/client/src/components/video/config.ts @@ -4,6 +4,9 @@ import { createMicrophoneAndCameraTracks, createMicrophoneAudioTrack, createScreenVideoTrack, + ILocalAudioTrack, + ILocalVideoTrack, + AgoraRTCError, } from 'agora-rtc-react'; const config: ClientConfig = { @@ -14,12 +17,19 @@ const config: ClientConfig = { const useClient = createClient(config); const useMicrophoneAndCameraTracks = createMicrophoneAndCameraTracks(); const useMicrophoneTrack = createMicrophoneAudioTrack(); -const useScreenVideoTrack = createScreenVideoTrack( - { - encoderConfig: '1080p_1', - optimizationMode: 'detail', - }, - 'disable', -); +const useScreenVideoTrack = (): { + ready: boolean; + tracks: ILocalVideoTrack | [ILocalVideoTrack, ILocalAudioTrack]; + error: AgoraRTCError | null; +} => { + const screenShare = createScreenVideoTrack( + { + encoderConfig: '1080p_1', + optimizationMode: 'detail', + }, + 'disable', + ); + return screenShare(); +}; export { useClient, useMicrophoneAndCameraTracks, useMicrophoneTrack, useScreenVideoTrack }; From 163e319e788f8f11af05c4f2c65aa254bc14ae6c Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Sat, 4 Dec 2021 17:56:40 +0900 Subject: [PATCH 12/15] =?UTF-8?q?Refactor:=20=EC=8A=A4=ED=81=AC=EB=A6=B0?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=EB=B2=84=ED=8A=BC=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=ED=95=A8=EC=88=98=20callback=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스크린공유버튼의 핸들러함수를 콜백처리하였습니다. - 컴포넌트가 리렌더링되어도 함수가 재생성되지 않습니다. --- client/src/components/video/VideoController/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/components/video/VideoController/index.tsx b/client/src/components/video/VideoController/index.tsx index dd66b40d..31ad3bf4 100644 --- a/client/src/components/video/VideoController/index.tsx +++ b/client/src/components/video/VideoController/index.tsx @@ -43,7 +43,9 @@ const VideoController = ({ tracks, setStart, uuid, ownerId }: VideoControllerPro } }; - const handleScreenShare = () => setScreenShare(!screenShare); + const handleScreenShare = useCallback(() => { + setScreenShare((prev) => !prev); + }, []); const leaveChannel = useCallback(async () => { if (ownerId === user.id) { From fb648bf229612aa2723b7b4028713b8d046e9b28 Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Sat, 4 Dec 2021 17:59:35 +0900 Subject: [PATCH 13/15] =?UTF-8?q?Fix:=20#303=20-=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존에 타닥타닥방에 isLoading 변수를 도입하여 화면깜빡임 문제를 해결했습니다. - 화면공유시 타닥타닥방의 start 상태를 변경하지 않습니다. - 트랙이 종료되었을때(화면공유를 끊었을 때) 트랙을 닫도록 합니다. - 그리고 최종적으로 screenshare 상태를 false로 바꿔 screenshare 컴포넌트를 비활성화합니다. - unmount시에는 트랙이 남아있을때만 퍼블리시 해제 및 트랙을 닫습니다. - 바뀐 useScreenVideoTrack 함수를 사용하므로 매번 공유시마다 새로운 스트림이 생성됩니다. --- client/src/components/video/Screenshare/index.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/src/components/video/Screenshare/index.tsx b/client/src/components/video/Screenshare/index.tsx index 30f47bfb..a45ad9de 100644 --- a/client/src/components/video/Screenshare/index.tsx +++ b/client/src/components/video/Screenshare/index.tsx @@ -22,24 +22,27 @@ const ScreenShare = ({ useEffect(() => { const pulishScreenShare = async () => { - setStart(false); await client.unpublish(preTracks[1]); await client.publish(tracks); - setStart(true); if (!Array.isArray(tracks)) { tracks.on('track-ended', async () => { - setScreenShare(false); await client.unpublish(tracks); + tracks.close(); if (trackState.video) { await client.publish(preTracks[1]); } + setScreenShare(false); }); } }; - if (ready) pulishScreenShare(); + if (ready && tracks) pulishScreenShare(); if (error) setScreenShare(false); + return () => { - client.unpublish(tracks); + if (!error && !Array.isArray(tracks)) { + client.unpublish(tracks); + tracks.close(); + } }; }, [setStart, setScreenShare, screenShare, client, preTracks, trackState, tracks, ready, error]); From df801c866f439d4abf9a6ef17b78b31c965ff6ca Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Sat, 4 Dec 2021 18:17:59 +0900 Subject: [PATCH 14/15] =?UTF-8?q?Fix:=20#303=20-=20=EC=B2=AB=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EA=B3=B5=EC=9C=A0=EC=8B=9C=20unmount=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=8A=B8=EB=9E=99=EB=8B=AB=ED=9E=98?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면공유시 간헐적으로 unmount에 달린 트랙닫힘로직이 수행되던 문제 - firstRenderRef 변수를 도입하여 트랙변화로 인한 첫 unmount시에, - 트랙닫힘로직을 수행하지 않도록 개선 - 두번째 unmount부터 화면닫힘로직이 수행되므로, - 화면공유 후 페이지 이탈시 기존의 트랙닫힘로직 실행 가능 --- client/src/components/video/Screenshare/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/components/video/Screenshare/index.tsx b/client/src/components/video/Screenshare/index.tsx index a45ad9de..a449cbe0 100644 --- a/client/src/components/video/Screenshare/index.tsx +++ b/client/src/components/video/Screenshare/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-react'; import { useClient, useScreenVideoTrack } from '@components/video/config'; @@ -19,6 +19,7 @@ const ScreenShare = ({ }: ScreenShareDivProps): JSX.Element => { const client = useClient(); const { ready, tracks, error } = useScreenVideoTrack(); + const firstRenderRef = useRef(true); useEffect(() => { const pulishScreenShare = async () => { @@ -39,6 +40,10 @@ const ScreenShare = ({ if (error) setScreenShare(false); return () => { + if (firstRenderRef.current) { + firstRenderRef.current = false; + return; + } if (!error && !Array.isArray(tracks)) { client.unpublish(tracks); tracks.close(); From b5d5711150b86b8c88d8aa11dcbc24d2ed771abe Mon Sep 17 00:00:00 2001 From: jiho-bae Date: Sat, 4 Dec 2021 18:53:18 +0900 Subject: [PATCH 15/15] =?UTF-8?q?Fix:=20=ED=99=94=EB=A9=B4=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=EB=90=9C=20=EB=B9=84=EB=94=94=EC=98=A4=EB=A5=BC=20?= =?UTF-8?q?=EB=88=84=EB=A5=BC=EC=8B=9C=EC=97=90=EB=A7=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존의 비디오를 구분하지 않던 방식에서 화면공유된 비디오를 누르면 전체화면으로 변경 --- client/src/components/video/VideoCard/index.tsx | 7 ++++--- client/src/components/video/config.ts | 3 ++- client/src/utils/constant.ts | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/components/video/VideoCard/index.tsx b/client/src/components/video/VideoCard/index.tsx index f6894ba6..fe1a1e12 100644 --- a/client/src/components/video/VideoCard/index.tsx +++ b/client/src/components/video/VideoCard/index.tsx @@ -7,7 +7,7 @@ import { AgoraVideoPlayer, } from 'agora-rtc-react'; import { VideoWrap, VolumeVisualizer } from './style'; -import { SPEAK } from '@utils/constant'; +import { SCREEN_SHARE_HEIGHT, SPEAK } from '@utils/constant'; import { useToast } from '@hooks/useToast'; import { TOAST_MESSAGE } from '@utils/constant'; @@ -69,8 +69,9 @@ const VideoCard = ({ videoTrack, audioTrack }: VideoCardProps): JSX.Element => { { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - videoTrack?.getMediaStreamTrack().readyState === 'live' && openFullscreen(); + if (videoTrack?.getCurrentFrameData()?.height === SCREEN_SHARE_HEIGHT) { + openFullscreen(); + } }} ref={videoRef}> {videoTrack && ( diff --git a/client/src/components/video/config.ts b/client/src/components/video/config.ts index b2e8a5c6..883aeec5 100644 --- a/client/src/components/video/config.ts +++ b/client/src/components/video/config.ts @@ -8,6 +8,7 @@ import { ILocalVideoTrack, AgoraRTCError, } from 'agora-rtc-react'; +import { SCREEN_SHARE_HEIGHT } from '@utils/constant'; const config: ClientConfig = { mode: 'rtc', @@ -24,7 +25,7 @@ const useScreenVideoTrack = (): { } => { const screenShare = createScreenVideoTrack( { - encoderConfig: '1080p_1', + encoderConfig: `${SCREEN_SHARE_HEIGHT}p_1`, optimizationMode: 'detail', }, 'disable', diff --git a/client/src/utils/constant.ts b/client/src/utils/constant.ts index 21fa7ca4..00e7c50b 100644 --- a/client/src/utils/constant.ts +++ b/client/src/utils/constant.ts @@ -34,6 +34,8 @@ export const INPUT = { export const TOAST_TIME = 2000; +export const SCREEN_SHARE_HEIGHT = 1080; + export const MODAL_NAME = { login: '로그인', join: '회원가입', @@ -86,7 +88,7 @@ export const TOAST_MESSAGE = { introduceHost: '호스트는 참가자를 강퇴할 수 있습니다.  강퇴 당한 사용자는 다시 이 방에 들어올 수 없으니 신중하게 사용해주세요!', narcissism: '누구나 자기 자신을 좋아합니다...!', - infoDoubleClick: '더블 클릭 하면 어떤 일이 일어날까요...?', + infoDoubleClick: '화면공유된 블록을 더블 클릭 하면 어떤 일이 일어날까요...?', introFireAnimation: '자세히 보시면 불꽃이 일렁입니다...!', introMoon: '자세히 보시면 달이 반짝입니다...!', introSky: '자세히 보시면 별이 반짝이고 별똥별이 떨어집니다...!',