-
Notifications
You must be signed in to change notification settings - Fork 4
[Feat] 스터디 세션 기능 서버측 기능 추가 #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
3dac707
feat: redis 설치 및 연결
twalla26 aa3c6b8
feat: room 모듈, 서비스, 게이트웨이 초기화
twalla26 10ecd44
refactor: join_room 이벤트를 room 모듈로 이동
twalla26 d0b8a91
feat: createRoom 구현
twalla26 803230d
chore: merge conflict 해결
twalla26 0b14b03
Merge pull request #114 from twalla26/feature/study-session
kcw2205 93f7a94
feat: `Redis Service` 추가
kcw2205 f187e89
feat: 스터디 세션 모델 추가
kcw2205 6427dd1
feat: 방 생성 DTO 추가
kcw2205 043ebae
feat: 스터디 세션 서비스 추가
kcw2205 e955a20
feat: 스터디 세션 레포지토리 추가
kcw2205 4e39b5a
feat: 스터디 세션 게이트웨이 작업 갱신
kcw2205 d86a74e
feat: 스터디 세션 모듈 업데이트
kcw2205 202312d
fix: `getValues` 함수 수정
kcw2205 d723def
fix: 스터디 세션 게이트웨이 버그 수정
kcw2205 048ea94
refactor: 스터디 세션 레포지토리 리팩토링
kcw2205 be3d83b
refactor: 스터디 세션 서비스 로그문 삭제
kcw2205 5971a4e
feat: 스터디 세션 퇴장 로직 구현
twalla26 b7b2ee6
refactor: 서비스 로직 리팩토링
twalla26 b9cae9d
Merge branch 'feature/study-session-suggestion' into feature/study-se…
twalla26 14d0b43
Merge pull request #118 from twalla26/feature/study-session
twalla26 f45d700
fix: `handleLeaveRoom` 로직 수정
kcw2205 e7818b9
fix: 스터디 세션 레포지 토리 수정
kcw2205 039126c
feat: 스터디 세션 최대 인원 설정 기능 추가
kcw2205 fe9039f
feat: 감정표현(reaction) 기능 추가
kcw2205 9edeb22
feat: 스터디 공개방 API 기능 추가
kcw2205 c251438
feat: 방 멤버 정보 반환 값 변경
kcw2205 236cef0
feat: 변경된 웹소켓 API 명세서에 따라 반환값 수정
kcw2205 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import "dotenv/config"; | ||
|
|
||
| export const redisConfig = { | ||
| host: process.env.REDIS_HOST, | ||
| port: parseInt(process.env.REDIS_PORT), | ||
| password: process.env.REDIS_PASSWORD, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { Module } from "@nestjs/common"; | ||
| import { RedisService } from "./redis.service"; | ||
|
|
||
| @Module({ | ||
| providers: [RedisService], | ||
| }) | ||
| export class RedisModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Test, TestingModule } from "@nestjs/testing"; | ||
| import { RedisService } from "./redis.service"; | ||
|
|
||
| describe("RedisService", () => { | ||
| let service: RedisService; | ||
|
|
||
| beforeEach(async () => { | ||
| const module: TestingModule = await Test.createTestingModule({ | ||
| providers: [RedisService], | ||
| }).compile(); | ||
|
|
||
| service = module.get<RedisService>(RedisService); | ||
| }); | ||
|
|
||
| it("should be defined", () => { | ||
| expect(service).toBeDefined(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { Injectable } from "@nestjs/common"; | ||
| import Redis from "ioredis"; | ||
| import { redisConfig } from "../config/redis.config"; | ||
| import { stringify } from "ts-jest"; | ||
|
|
||
| @Injectable() | ||
| export class RedisService { | ||
| private readonly client: Redis = new Redis({ | ||
| host: redisConfig.host, | ||
| port: redisConfig.port, | ||
| password: redisConfig.password, | ||
| }); | ||
|
|
||
| async set(key: string, value: any, ttl: number = 0) { | ||
| if (typeof value === "object") value = JSON.stringify(value); | ||
|
|
||
| await this.client.set(key, value, "KEEPTTL"); | ||
| await this.client.expire(key, ttl); | ||
| } | ||
|
|
||
| async get(key: string) { | ||
| return this.client.get(key); | ||
| } | ||
|
|
||
| async getTTL(key: string) { | ||
| return this.client.ttl(key); | ||
| } | ||
|
|
||
| async getKeys(query: string) { | ||
| const keys: string[] = []; | ||
| let cursor = "0"; | ||
|
|
||
| do { | ||
| const [nextCursor, matchedKeys] = await this.client.scan( | ||
| cursor, | ||
| "MATCH", | ||
| query, | ||
| "COUNT", | ||
| "100" | ||
| ); | ||
| cursor = nextCursor; | ||
| keys.push(...matchedKeys); | ||
| } while (cursor !== "0"); | ||
| return keys; | ||
| } | ||
|
|
||
| async getHashValueByField(key: string, field: string) { | ||
| return this.client.hget(key, field); | ||
| } | ||
|
|
||
| async setHashValueByField(key: string, field: string, value: any) { | ||
| if (typeof value !== "string") value = stringify(value); | ||
| return this.client.hset(key, field, value); | ||
| } | ||
|
|
||
| async delete(...keys: string[]) { | ||
| return this.client.del(...keys); | ||
| } | ||
|
|
||
| async getValues(query: string) { | ||
| const keys = await this.getKeys(query); | ||
| if (!keys.length) return null; | ||
| return this.client.mget(keys); | ||
| } | ||
|
|
||
| async getMap(query: string, valueType: "object" | "primitive" = "object") { | ||
| const keys = await this.getKeys(query); | ||
| const values = await this.getValues(query); | ||
| if (!values) return null; | ||
|
|
||
| return keys.reduce((acc, key, index) => { | ||
| acc[key] = | ||
| valueType === "object" | ||
| ? JSON.parse(values[index]) | ||
| : values[index]; | ||
| return acc; | ||
| }, {}); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export interface CreateRoomDto { | ||
| title: string; | ||
| status: "PUBLIC" | "PRIVATE"; | ||
| nickname: string; | ||
| socketId: string; | ||
| maxParticipants?: number; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Test, TestingModule } from "@nestjs/testing"; | ||
| import { RoomGateway } from "./room.gateway"; | ||
|
|
||
| describe("RoomGateway", () => { | ||
| let gateway: RoomGateway; | ||
|
|
||
| beforeEach(async () => { | ||
| const module: TestingModule = await Test.createTestingModule({ | ||
| providers: [RoomGateway], | ||
| }).compile(); | ||
|
|
||
| gateway = module.get<RoomGateway>(RoomGateway); | ||
| }); | ||
|
|
||
| it("should be defined", () => { | ||
| expect(gateway).toBeDefined(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import { | ||
| WebSocketGateway, | ||
| WebSocketServer, | ||
| OnGatewayConnection, | ||
| OnGatewayDisconnect, | ||
| SubscribeMessage, | ||
| } from "@nestjs/websockets"; | ||
| import { Server, Socket } from "socket.io"; | ||
| import { RoomService } from "./room.service"; | ||
|
|
||
| const EVENT_NAME = { | ||
| CREATE_ROOM: "create_room", | ||
| JOIN_ROOM: "join_room", | ||
| LEAVE_ROOM: "leave_room", | ||
| MASTER_CHANGED: "master_changed", | ||
| FINISH_ROOM: "finish_room", | ||
| REACTION: "reaction", | ||
|
|
||
| ROOM_CREATED: "room_created", | ||
| USER_EXIT: "user_exit", | ||
| ROOM_FULL: "room_full", | ||
| ALL_USERS: "all_users", | ||
| ROOM_FINISHED: "room_finished", | ||
| } as const; | ||
|
|
||
| /** | ||
| * 연결과 관련된 에러를 처리 | ||
| * | ||
| * 보다 연결과 관련된 코드가 존재 | ||
| */ | ||
| @WebSocketGateway({ | ||
| cors: { | ||
| origin: "*", // CORS 설정 | ||
| }, | ||
| }) | ||
| @WebSocketGateway() | ||
| export class RoomGateway implements OnGatewayConnection, OnGatewayDisconnect { | ||
| @WebSocketServer() | ||
| server: Server; | ||
|
|
||
| constructor(private readonly roomService: RoomService) {} | ||
|
|
||
| handleConnection(client: Socket) { | ||
| console.log(`Client connected in room: ${client.id}`); | ||
| } | ||
|
|
||
| async handleDisconnect(client: Socket) { | ||
| console.log(`Client disconnected in room: ${client.id}`); | ||
| await this.handleLeaveRoom(client); | ||
| } | ||
|
|
||
| @SubscribeMessage(EVENT_NAME.CREATE_ROOM) | ||
| async handleCreateRoom(client: Socket, data: any) { | ||
| const { title, nickname, status, maxParticipants } = data; // unknown 으로 받고, Dto와 Pipe로 검증받기 | ||
| try { | ||
| const roomId = await this.roomService.createRoom({ | ||
| title, | ||
| status, | ||
| socketId: client.id, | ||
| nickname, | ||
| maxParticipants, | ||
| }); | ||
| client.join(roomId); | ||
| this.server.to(roomId).emit(EVENT_NAME.ROOM_CREATED, { roomId }); | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| } | ||
|
|
||
| @SubscribeMessage(EVENT_NAME.JOIN_ROOM) | ||
| async handleJoinRoom(client: Socket, data: any) { | ||
| const { roomId, nickname } = data; | ||
|
|
||
| if (!(await this.roomService.checkAvailable(roomId))) { | ||
| // client joins full room | ||
| client.emit(EVENT_NAME.ROOM_FULL); | ||
| return; | ||
| } | ||
|
|
||
| await this.roomService.joinRoom(client.id, roomId, nickname); | ||
|
|
||
| client.join(roomId); | ||
|
|
||
| console.log(`[${data.roomId}]: ${client.id} enter`); | ||
|
|
||
| const usersInThisRoom = ( | ||
| await this.roomService.getMemberSocket(roomId) | ||
| ).filter((user) => user !== client.id); | ||
|
|
||
| client.emit(EVENT_NAME.ALL_USERS, usersInThisRoom); | ||
| } | ||
|
|
||
| @SubscribeMessage(EVENT_NAME.LEAVE_ROOM) | ||
| async handleLeaveRoom(client: Socket) { | ||
| const roomId = await this.roomService.getRoomId(client.id); | ||
|
|
||
| const isHost = await this.roomService.checkHost(client.id); | ||
| const leftCount = await this.roomService.leaveRoom(client.id); | ||
|
|
||
| if (!leftCount) { | ||
| await this.roomService.deleteRoom(roomId); | ||
| return; | ||
| } | ||
|
|
||
| if (isHost) { | ||
| const hostConnection = await this.roomService.delegateHost(roomId); | ||
| this.server.to(roomId).emit(EVENT_NAME.MASTER_CHANGED, { | ||
| masterSocketId: hostConnection.socketId, | ||
| masterNickname: hostConnection.nickname, | ||
| }); | ||
| } | ||
|
|
||
| this.server | ||
| .to(roomId) | ||
| .emit(EVENT_NAME.USER_EXIT, { socketId: client.id }); | ||
| } | ||
|
|
||
| @SubscribeMessage(EVENT_NAME.FINISH_ROOM) | ||
| async handleFinishRoom(client: Socket) { | ||
| const roomId = await this.roomService.finishRoom(client.id); | ||
| this.server.to(roomId).emit(EVENT_NAME.ROOM_FINISHED); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export type RoomStatus = "PUBLIC" | "PRIVATE"; | ||
|
|
||
| export interface Room { | ||
| title: string; | ||
| status: RoomStatus; | ||
| maxParticipants: number; | ||
| createdAt: number; | ||
| host: string; | ||
| } | ||
|
|
||
| export interface MemberConnection { | ||
| joinTime: number; | ||
| isHost: boolean; | ||
| nickname: string; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { Module } from "@nestjs/common"; | ||
| import { RoomService } from "./room.service"; | ||
| import { RoomGateway } from "./room.gateway"; | ||
| import { RedisService } from "../redis/redis.service"; | ||
| import { RoomRepository } from "./room.repository"; | ||
|
|
||
| @Module({ | ||
| providers: [RoomService, RoomGateway, RedisService, RoomRepository], | ||
| }) | ||
| export class RoomModule {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 코드는 join_room 이벤트 처리하는 코드인가요? 룸 아이디랑 사용자 닉네임을 받고, 방이 꽉차면 room_full을 보내고, 아니라면 join_room을 바로 하는거 같은데, 이 부분은 비즈니스 로직과 웹소켓 통신을 분리해도 괜찮지 않을까 생각했습니다!
바로 emit, join하는게 아니라 return { status: '상태' } 이런식으로 반환(비즈니스 로직)해서 이걸 받아서, 웹소켓 통신 처리 코드에서 상태 코드를 통해 웹소켓 통신을 처리할 수도 있을거 같아요! 이렇게 분리하면 테스트 할 때 비즈니스 로직에서는 소켓 없이도 테스트 가능해지지 않을까하는 생각이 듭니다.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반환하는 상대에 따라서 저는
emit말고ACK로 설정하는 것도 좋다고 생각합니다.테스트 관련 언급해주셔서 감사합니다. 결국엔 테스트 가능하게 코드를 짜야 검증이 쉽기 때문에 중요한 요소라고 생각이 되네요. 참고하겠습니다.