Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3dac707
feat: redis 설치 및 연결
twalla26 Nov 12, 2024
aa3c6b8
feat: room 모듈, 서비스, 게이트웨이 초기화
twalla26 Nov 12, 2024
10ecd44
refactor: join_room 이벤트를 room 모듈로 이동
twalla26 Nov 12, 2024
d0b8a91
feat: createRoom 구현
twalla26 Nov 12, 2024
803230d
chore: merge conflict 해결
twalla26 Nov 12, 2024
0b14b03
Merge pull request #114 from twalla26/feature/study-session
kcw2205 Nov 12, 2024
93f7a94
feat: `Redis Service` 추가
kcw2205 Nov 13, 2024
f187e89
feat: 스터디 세션 모델 추가
kcw2205 Nov 13, 2024
6427dd1
feat: 방 생성 DTO 추가
kcw2205 Nov 13, 2024
043ebae
feat: 스터디 세션 서비스 추가
kcw2205 Nov 13, 2024
e955a20
feat: 스터디 세션 레포지토리 추가
kcw2205 Nov 13, 2024
4e39b5a
feat: 스터디 세션 게이트웨이 작업 갱신
kcw2205 Nov 13, 2024
d86a74e
feat: 스터디 세션 모듈 업데이트
kcw2205 Nov 13, 2024
202312d
fix: `getValues` 함수 수정
kcw2205 Nov 13, 2024
d723def
fix: 스터디 세션 게이트웨이 버그 수정
kcw2205 Nov 13, 2024
048ea94
refactor: 스터디 세션 레포지토리 리팩토링
kcw2205 Nov 13, 2024
be3d83b
refactor: 스터디 세션 서비스 로그문 삭제
kcw2205 Nov 13, 2024
5971a4e
feat: 스터디 세션 퇴장 로직 구현
twalla26 Nov 13, 2024
b7b2ee6
refactor: 서비스 로직 리팩토링
twalla26 Nov 14, 2024
b9cae9d
Merge branch 'feature/study-session-suggestion' into feature/study-se…
twalla26 Nov 14, 2024
14d0b43
Merge pull request #118 from twalla26/feature/study-session
twalla26 Nov 14, 2024
f45d700
fix: `handleLeaveRoom` 로직 수정
kcw2205 Nov 14, 2024
e7818b9
fix: 스터디 세션 레포지 토리 수정
kcw2205 Nov 14, 2024
039126c
feat: 스터디 세션 최대 인원 설정 기능 추가
kcw2205 Nov 14, 2024
fe9039f
feat: 감정표현(reaction) 기능 추가
kcw2205 Nov 14, 2024
9edeb22
feat: 스터디 공개방 API 기능 추가
kcw2205 Nov 14, 2024
c251438
feat: 방 멤버 정보 반환 값 변경
kcw2205 Nov 14, 2024
236cef0
feat: 변경된 웹소켓 API 명세서에 따라 반환값 수정
kcw2205 Nov 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.6",
"@nestjs/websockets": "^10.4.6",
"dotenv": "^16.4.5",
"ioredis": "^5.4.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1"
Expand Down
8 changes: 6 additions & 2 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { SocketModule } from "./socket/socket.module";
import { SocketModule } from "./signaling-server/socket.module";
import { RoomModule } from "./room/room.module";
import { RedisModule } from "./redis/redis.module";

import "dotenv/config";

@Module({
imports: [SocketModule],
imports: [SocketModule, RoomModule, RedisModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
7 changes: 7 additions & 0 deletions backend/src/config/redis.config.ts
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,
};
7 changes: 7 additions & 0 deletions backend/src/redis/redis.module.ts
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 {}
18 changes: 18 additions & 0 deletions backend/src/redis/redis.service.spec.ts
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();
});
});
79 changes: 79 additions & 0 deletions backend/src/redis/redis.service.ts
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;
}, {});
}
}
7 changes: 7 additions & 0 deletions backend/src/room/dto/create-room.dto.ts
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;
}
18 changes: 18 additions & 0 deletions backend/src/room/room.gateway.spec.ts
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();
});
});
123 changes: 123 additions & 0 deletions backend/src/room/room.gateway.ts
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);
}
Copy link
Member

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: '상태' } 이런식으로 반환(비즈니스 로직)해서 이걸 받아서, 웹소켓 통신 처리 코드에서 상태 코드를 통해 웹소켓 통신을 처리할 수도 있을거 같아요! 이렇게 분리하면 테스트 할 때 비즈니스 로직에서는 소켓 없이도 테스트 가능해지지 않을까하는 생각이 듭니다.

Copy link
Member Author

@kcw2205 kcw2205 Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반환하는 상대에 따라서 저는 emit 말고 ACK 로 설정하는 것도 좋다고 생각합니다.

테스트 관련 언급해주셔서 감사합니다. 결국엔 테스트 가능하게 코드를 짜야 검증이 쉽기 때문에 중요한 요소라고 생각이 되네요. 참고하겠습니다.


@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);
}
}
15 changes: 15 additions & 0 deletions backend/src/room/room.model.ts
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;
}
10 changes: 10 additions & 0 deletions backend/src/room/room.module.ts
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 {}
Loading