Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
blu3fishez Nov 12, 2024
93f7a94
feat: `Redis Service` 추가
blu3fishez Nov 13, 2024
f187e89
feat: 스터디 세션 모델 추가
blu3fishez Nov 13, 2024
6427dd1
feat: 방 생성 DTO 추가
blu3fishez Nov 13, 2024
043ebae
feat: 스터디 세션 서비스 추가
blu3fishez Nov 13, 2024
e955a20
feat: 스터디 세션 레포지토리 추가
blu3fishez Nov 13, 2024
4e39b5a
feat: 스터디 세션 게이트웨이 작업 갱신
blu3fishez Nov 13, 2024
d86a74e
feat: 스터디 세션 모듈 업데이트
blu3fishez Nov 13, 2024
202312d
fix: `getValues` 함수 수정
blu3fishez Nov 13, 2024
d723def
fix: 스터디 세션 게이트웨이 버그 수정
blu3fishez Nov 13, 2024
048ea94
refactor: 스터디 세션 레포지토리 리팩토링
blu3fishez Nov 13, 2024
be3d83b
refactor: 스터디 세션 서비스 로그문 삭제
blu3fishez 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` 로직 수정
blu3fishez Nov 14, 2024
e7818b9
fix: 스터디 세션 레포지 토리 수정
blu3fishez Nov 14, 2024
039126c
feat: 스터디 세션 최대 인원 설정 기능 추가
blu3fishez Nov 14, 2024
fe9039f
feat: 감정표현(reaction) 기능 추가
blu3fishez Nov 14, 2024
9edeb22
feat: 스터디 공개방 API 기능 추가
blu3fishez Nov 14, 2024
c251438
feat: 방 멤버 정보 반환 값 변경
blu3fishez Nov 14, 2024
236cef0
feat: 변경된 웹소켓 API 명세서에 따라 반환값 수정
blu3fishez 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.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from "@nestjs/testing";
import { RoomController } from "./room.controller";

describe("RoomController", () => {
let controller: RoomController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RoomController],
}).compile();

controller = module.get<RoomController>(RoomController);
});

it("should be defined", () => {
expect(controller).toBeDefined();
});
});
12 changes: 12 additions & 0 deletions backend/src/room/room.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Controller, Get } from "@nestjs/common";
import { RoomService } from "./room.service";

@Controller("rooms")
export class RoomController {
constructor(private readonly roomService: RoomService) {}

@Get()
getPublicRooms() {
return this.roomService.getPublicRoom();
}
}
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();
});
});
135 changes: 135 additions & 0 deletions backend/src/room/room.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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 roomData = await this.roomService.createRoom({
title,
status,
socketId: client.id,
nickname,
maxParticipants,
});

client.join(roomData.roomId);

this.server
.to(roomData.roomId)
.emit(EVENT_NAME.ROOM_CREATED, roomData);
} 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(client.id, roomId))) {
// client joins full room
client.emit(EVENT_NAME.ROOM_FULL);
return;
}

const room = await this.roomService.joinRoom(
client.id,
roomId,
nickname
);

client.join(roomId);

console.log(`[${data.roomId}]: ${client.id} enter`);

const usersInThisRoom = await this.roomService.getRoomMemberConnection(
client.id,
roomId
);

client.emit(EVENT_NAME.ALL_USERS, {
roomMetadata: room,
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);
}
}
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;
}
12 changes: 12 additions & 0 deletions backend/src/room/room.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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";
import { RoomController } from "./room.controller";

@Module({
providers: [RoomService, RoomGateway, RedisService, RoomRepository],
controllers: [RoomController],
})
export class RoomModule {}
Loading