Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
63c022e
Merge pull request #119 from boostcampwm-2024/feature/study-session
ShipFriend0516 Nov 14, 2024
8e3efec
feat: 소켓 연결 핸들러 추가
ShipFriend0516 Nov 14, 2024
7215f92
Merge remote-tracking branch 'origin/dev' into feature/session-enter
ShipFriend0516 Nov 14, 2024
b60e722
fix: 폼 데이터 형식 서버 데이터와 맞게 수정
ShipFriend0516 Nov 14, 2024
e51fe99
feat: 입력데이터 유효성검사 실패시 토스트 메시지 출력, 방 생성 후 입장시 토스트 메시지 출력
ShipFriend0516 Nov 14, 2024
f09ad2b
fix: lint 수정
ShipFriend0516 Nov 14, 2024
49d4775
feat: 응답 데이터 interface화 및 스터디룸 메타데이터로 방이름 및 인원 수 렌더링
ShipFriend0516 Nov 14, 2024
19ce5ec
feat: 참가자 목록에 방장 아이콘 표시하도록 구현
ShipFriend0516 Nov 14, 2024
23cc0ee
style: bg-accent-gray -> bg-point-2로 적용
ShipFriend0516 Nov 14, 2024
27d75c3
feat: 세션 페이지 헤더 컴포넌트 구현
ShipFriend0516 Nov 14, 2024
aa17f6f
feat: isHost 여부에 따른 모달창 동작 차별화
ShipFriend0516 Nov 14, 2024
e34dc81
style: 세션참가 레이아웃 조절
ShipFriend0516 Nov 14, 2024
8f6a672
feat: 호스트가 나갈시 방장 권한 양도 로직 구현
ShipFriend0516 Nov 14, 2024
df8ea64
feat: 호스트가 방 폭파시 전체 유저 세션 퇴장처리
ShipFriend0516 Nov 14, 2024
dad9f0e
feat: 이벤트 핸들러 클린업 추가
ShipFriend0516 Nov 14, 2024
56bc512
feat: 유저 나갔을 때 이벤트핸들러 추가
ShipFriend0516 Nov 14, 2024
393b173
chore: 주석제거
ShipFriend0516 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