Overnap
import { UserData } from "../user/types";
/**
* 방 리스트에서 사용됩니다.
*/
......@@ -19,3 +17,7 @@ export interface RoomInfo {
maxUsers: number;
users: UserData[];
}
export interface UserData {
username: string;
}
......
export * from "./message";
import { RoomDescription, RoomInfo } from "./dataType";
// 서버로 들어오는 메세지 타입을 정의합니다.
// 'result' 속성은 서버 요청 결과에만 포함되는 특별한 속성입니다.
interface ServerInboundMessageMap {
// 로그인을 시도합니다.
login: {
username: string;
};
// 방 목록을 요청합니다.
roomList: {
result: RoomDescription[];
};
// 방에 접속합니다.
joinRoom: {
uuid: string;
result: RoomInfo;
};
// 방에서 나갑니다.
leaveRoom: {};
// 채팅을 보냅니다.
chat: {
message: string;
};
// drawer가 단어를 선택합니다.
chooseWord: {
word: string;
};
// 브러시 정보를 변경합니다.
setBrush: {
size: number;
color: string;
drawing: boolean;
};
// 브러시를 이동합니다.
moveBrush: {
x: number;
y: number;
};
}
// 서버에서 나가는 메세지 타입을 정의합니다.
interface ServerOutboundMessageMap {
// 방에 접속 중인 유저 목록이 업데이트 되었습니다.
updateRoomUser: {
state: "added" | "updated" | "removed";
user: {
username: string;
};
};
// 다른 유저가 채팅을 보냈습니다.
chat: {
sender: string;
message: string;
};
// 라운드가 시작되었습니다.
startRound: {
round: number;
duration: number;
roles: {
username: string;
role: "drawer" | "guesser" | "winner" | "spectator";
};
};
// drawer에게 선택할 수 있는 단어가 주어졌습니다.
wordSet: {
words: string[];
};
// 이번 라운드의 단어가 선택되었습니다.
wordChosen: {
length: number;
};
// 라운드 타이머 정보를 동기화합니다.
timer: {
state: "started" | "stopped";
time: number;
};
// 라운드가 종료되었습니다.
finishRound: {
answer: string;
};
// 역할이 변경되었습니다.
role: {
username: string;
role: "drawer" | "guesser" | "winner" | "spectator";
};
// 보낸 단어가 정답 처리 되었습니다.
answerAccepted: {
answer: string;
};
// 게임이 종료되었습니다.
finishGame: {};
// 브러시 정보가 변경되었습니다.
setBrush: {
size: number;
color: string;
drawing: boolean;
};
// 브러시가 이동되었습니다.
moveBrush: {
x: number;
y: number;
};
}
export interface RawMessage {
type: string;
message: any;
}
export type ServerInboundMessageKey = keyof ServerInboundMessageMap;
export type ServerInboundMessage<Key extends ServerInboundMessageKey> = Omit<
ServerInboundMessageMap[Key],
"result"
>;
export interface ServerResponse<Key extends ServerInboundMessageKey> {
ok: boolean;
reason?: string;
result?: "result" extends keyof ServerInboundMessageMap[Key]
? ServerInboundMessageMap[Key]["result"]
: never;
}
export type ServerOutboundMessage<Key extends keyof ServerOutboundMessageMap> =
ServerOutboundMessageMap[Key];
export type ServerOutboundMessageKey = keyof ServerOutboundMessageMap;
{
"name": "common",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {}
}
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"declarationMap": true,
"rootDir": ".",
"composite": true,
"strict": true,
"esModuleInterop": true
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
import { Socket } from "socket.io";
import { MessageHandlerRegistry } from "../message/MessageHandlerRegistry";
import { Message } from "../message/types";
import { ServerOutboundMessage, ServerOutboundMessageKey } from "../../common";
import { MessageHandlerChain } from "../message/MessageHandlerChain";
import { Room } from "../room/Room";
import { User } from "../user/User";
......@@ -9,17 +9,20 @@ export class Connection {
public user?: User;
private messageHandlerChain: MessageHandlerChain;
constructor(socket: Socket) {
this.socket = socket;
MessageHandlerRegistry.registerHandlers(this);
}
public get authenticated(): boolean {
return this.user !== undefined;
this.messageHandlerChain = new MessageHandlerChain(this);
}
public send(message: Message) {
this.socket.emit(message.type, message);
public send<T extends ServerOutboundMessageKey>(
type: T,
message: ServerOutboundMessage<T>
) {
this.socket.emit("msg", {
type: type as string,
message: message,
});
}
}
......
......@@ -15,6 +15,7 @@ export class ConnectionMapper {
}
value = new Connection(socket);
// FIXME: Register connection to the map
return value;
}
......
import { User } from "../user/User";
export interface Game {
join(user: User): void;
leave(user: User): void;
}
import { roomChatHandler } from "../message/handler/roomChatHandler";
import { Room } from "../room/Room";
import { User } from "../user/User";
import { Game } from "./Game";
export class WorldGuessingGame implements Game {
room: Room;
maxRound: number;
round: number;
constructor(room: Room) {
this.room = room;
if (this.room.users.length < 2) {
throw new Error("인원이 부족합니다.");
}
// TODO: 방장이 설정
this.maxRound = 5;
this.round = 1;
}
join(user: User): void {
throw new Error("Method not implemented.");
}
leave(user: User): void {
throw new Error("Method not implemented.");
}
}
import { Connection } from "../connection/Connection";
import {
ServerInboundMessage,
ServerInboundMessageKey,
ServerResponse,
} from "../../common/index";
import { User } from "../user/User";
type UserHandlerMap = {
[Key in ServerInboundMessageKey]?: (
user: User,
message: ServerInboundMessage<Key>
) => ServerResponse<Key>;
};
export class MessageHandler {
private handlers: UserHandlerMap;
constructor(handlers: UserHandlerMap) {
this.handlers = handlers;
}
public handle(
type: ServerInboundMessageKey,
user: User,
message: any,
callback: Function
): boolean {
const handler = this.handlers[type];
if (!handler) return false;
const response = handler(user, message);
callback(response);
return true;
}
}
import { Connection } from "../connection/Connection";
import {
RawMessage,
ServerInboundMessage,
ServerInboundMessageKey,
ServerResponse,
} from "../../common/index";
import { User } from "../user/User";
export class MessageHandlerChain {
connection: Connection;
constructor(connection: Connection) {
this.connection = connection;
this.connection.socket.on("msg", (raw: RawMessage, callback: Function) => {
this.handleRaw(connection, raw, callback);
});
}
private handleRaw(
connection: Connection,
raw: RawMessage,
callback: Function
) {
const type = raw.type as ServerInboundMessageKey;
const message = raw.message;
// 유저 정보가 없으므로 로그인은 따로 핸들링
if (type === "login") {
this.handleLogin(connection, message, callback);
return;
}
// Game > Room > User 순으로 전달
if (
connection?.user?.room &&
connection.user.room.handler.handle(
type,
connection.user,
message,
callback
)
)
return;
if (
connection?.user &&
connection.user.handler.handle(type, connection.user, message, callback)
)
return;
}
private handleLogin(
connection: Connection,
message: ServerInboundMessage<"login">,
callback: Function
) {
connection.user = new User(message.username, connection);
console.log(`User ${message.username} has logged in!`);
callback({ ok: true });
}
}
import { Connection } from "../connection/Connection";
import { User } from "../user/User";
import { loginHandler } from "./handler/loginHandler";
import { roomChatHandler } from "./handler/roomChatHandler";
import { roomJoinHandler } from "./handler/roomJoinHandler";
import { roomLeaveHandler } from "./handler/roomLeaveHandler";
import { roomListRequestHandler } from "./handler/roomListRequestHandler";
import { Message, MessageResponse, MessageType } from "./types";
export class MessageHandlerRegistry {
static registerHandlers(connection: Connection) {
this.registerHandler(connection, MessageType.LOGIN, loginHandler);
this.registerHandlerAuthed(
connection,
MessageType.ROOM_LIST_REQUEST,
roomListRequestHandler
);
this.registerHandlerAuthed(
connection,
MessageType.ROOM_JOIN,
roomJoinHandler
);
this.registerHandlerAuthed(
connection,
MessageType.ROOM_LEAVE,
roomLeaveHandler
);
this.registerHandlerAuthed(
connection,
MessageType.ROOM_CHAT,
roomChatHandler
);
}
private static registerHandler<T extends Message, S>(
connection: Connection,
typeName: string,
handler: (connection: Connection, message: T) => MessageResponse<S>
) {
connection.socket.on(typeName, (message: T, callback: Function) => {
const response = handler(connection, message);
callback(response);
});
}
private static registerHandlerAuthed<T extends Message, S>(
connection: Connection,
typeName: string,
handler: (user: User, message: T) => MessageResponse<S>
) {
connection.socket.on(typeName, (message: T, callback: Function) => {
if (connection.user !== undefined) {
const response = handler(connection.user, message);
callback(response);
} else {
callback({ ok: false });
}
});
}
}
import { Connection } from "../../connection/Connection";
import { RoomManager } from "../../room/RoomManager";
import { User } from "../../user/User";
import { LoginMessage, MessageResponse } from "../types";
export function loginHandler(
connection: Connection,
message: LoginMessage
): MessageResponse<undefined> {
connection.user = new User(message.username, connection);
console.log(`User ${message.username} has logged in!`);
return { ok: true };
}
import { Connection } from "../../connection/Connection";
import { RoomManager } from "../../room/RoomManager";
import { User } from "../../user/User";
import { MessageResponse, RoomChatMessage, RoomJoinMessage } from "../types";
export function roomChatHandler(
user: User,
message: RoomChatMessage
): MessageResponse<undefined> {
user.room?.sendChat(user, message.message);
return { ok: true };
}
import { Connection } from "../../connection/Connection";
import { RoomManager } from "../../room/RoomManager";
import { RoomInfo } from "../../room/types";
import { User } from "../../user/User";
import { MessageResponse, RoomJoinMessage } from "../types";
export function roomJoinHandler(
user: User,
message: RoomJoinMessage
): MessageResponse<RoomInfo> {
const room = RoomManager.instance().get(message.uuid);
if (room !== undefined) {
room.connect(user);
return { ok: user.room !== undefined, result: user.room?.getInfo() };
}
return { ok: false };
}
import { Connection } from "../../connection/Connection";
import { RoomManager } from "../../room/RoomManager";
import { User } from "../../user/User";
import { MessageResponse, RoomLeaveMessage } from "../types";
export function roomLeaveHandler(
user: User,
message: RoomLeaveMessage
): MessageResponse<undefined> {
user.room?.disconnect(user);
return { ok: true };
}
import { Connection } from "../../connection/Connection";
import { RoomManager } from "../../room/RoomManager";
import { RoomDescription, RoomInfo } from "../../room/types";
import { User } from "../../user/User";
import { MessageResponse, RoomListRequestMessage } from "../types";
export function roomListRequestHandler(
user: User,
message: RoomListRequestMessage
): MessageResponse<RoomDescription[]> {
return { ok: true, result: RoomManager.instance().list() };
}
import { RoomDescription } from "../room/types";
import { UserData } from "../user/types";
export interface Message {
readonly type: string;
}
/**
* 클라 -> 서버 : 클라이언트에서 서버로 요청할 때 사용하는 메세지입니다. 요청 결과는 MessageResponse<undefined>입니다.
* 클라 -> 서버 -> T : 위와 동일하지만 요청 결과가 MessageResponse<T>입니다.
* 서버 -> 클라 : 서버에서 클라이언트로 전송되는 메세지입니다.
* 클라 <-> 서버 : 양방향으로 사용되는 메세지입니다.
*/
/**
* 서버에 Event를 보냈을 때 요청에 대한 결과를 전송받습니다.
* @param ok 요청의 성공 여부입니다.
* @param reason 요청 실패 사유입니다. 필요한 경우에만 포함됩니다.
* @param result 요청에 대한 결과 메세지입니다. 특정한 메세지에 대해 요청이 성공하였을 때만 포함됩니다.
*/
export interface MessageResponse<T> {
ok: boolean;
reason?: string;
result?: T;
}
/**
* 클라 -> 서버
* 로그인 정보를 서버에게 전송합니다.
*/
export class LoginMessage implements Message {
readonly type = MessageType.LOGIN;
constructor(public username: string) {}
}
/**
* 클라 -> 서버 -> RoomDescription[]
* 방 목록을 요청합니다.
*/
export class RoomListRequestMessage implements Message {
readonly type = MessageType.ROOM_LIST_REQUEST;
constructor() {}
}
/**
* 클라 -> 서버 -> RoomInfo
* 방에 접속합니다.
*/
export class RoomJoinMessage implements Message {
readonly type = MessageType.ROOM_JOIN;
constructor(public uuid: string) {}
}
/**
* 클라 -> 서버
* 방에서 나갑니다.
*/
export class RoomLeaveMessage implements Message {
readonly type = MessageType.ROOM_LEAVE;
constructor() {}
}
/**
* 클라 <- 서버
* 접속한 방에 새로운 유저가 들어오거나 나갈 때 전송됩니다.
* @param state 유저가 입장하면 added, 퇴장하면 removed 값을 가집니다.
* @param userdata 대상 유저입니다.
*/
export class RoomUserUpdateMessage implements Message {
readonly type = MessageType.ROOM_USER_UPDATE;
constructor(
public state: "added" | "updated" | "removed",
public userdata: UserData
) {}
}
/**
* 클라 <-> 서버
* 접속한 방에서 채팅을 보내거나 받을 때 사용됩니다. 자신이 보낸 채팅은 서버에 의해 수신되지 않습니다.
* @param message 메세지 내용입니다.
* @param sender 채팅을 보낸 유저의 username입니다. 채팅이 클라이언트로 수신 될 경우에만 값을 가집니다.
*/
export class RoomChatMessage implements Message {
readonly type = MessageType.ROOM_CHAT;
constructor(public message: string, public sender?: string) {}
}
/**
* 클라 <- 서버
* 라운드가 시작되었음을 알립니다.
* @param round 현재 라운드 넘버입니다. (1부터 시작)
* @param duration 초 단위의 라운드 시간입니다.
* @param roles 모든 방 접속 인원의 역할입니다.
*/
export class RoundStartMessage implements Message {
readonly type = MessageType.ROUND_START;
constructor(
public round: number,
public duration: number,
public roles: {
username: string;
role: "drawer" | "guesser" | "winner" | "spectator";
}[]
) {}
}
/**
* 클라 <- 서버
* 라운드 시작시에 오직 drawer에게만 전송되는 메세지로, drawer가 선택할 수 있는 단어들입니다.
*/
export class RoundWordSetMessage implements Message {
readonly type = MessageType.ROUND_WORD_SET;
constructor(public words: string[]) {}
}
/**
* 클라 -> 서버
* drawer가 단어를 선택하면 해당 메세지가 서버로 전송됩니다.
* @param word RoundWordSetMessage에서 수신받은 단어 중 drawer가 선택한 단어입니다.
*/
export class RoundChooseWordMessage implements Message {
readonly type = MessageType.ROUND_CHOOSE_WORD;
constructor(public word: string) {}
}
/**
* 클라 <- 서버
* drawer가 단어를 선택하였음을 알립니다.
* @param length 정답 단어의 길이입니다.
*/
export class RoundWordChosenMessage implements Message {
readonly type = MessageType.ROUND_WORD_CHOSEN;
constructor(public length: number) {}
}
/**
* 클라 <- 서버
* 서버가 클라이언트의 타이머를 동기화하기 위해 사용됩니다. 라운드가 시작하면 타이머는 초기화되며 기본 상태는 stopped입니다.
* @param state 타이머의 동작, 정지 여부입니다.
* @param time 타이머의 남은 시간입니다. 초 단위로 주어집니다.
*/
export class RoundTimerMessage implements Message {
readonly type = MessageType.ROUND_TIMER;
constructor(public state: "started" | "stopped", public time: number) {}
}
/**
* 클라 <- 서버
* 라운드가 종료되었음을 알립니다.
* @param answer 이번 라운드의 정답입니다.
*/
export class RoundFinishMessage implements Message {
readonly type = MessageType.ROUND_FINISH;
constructor(public answer: string) {}
}
/**
* 클라 <- 서버
* 플레이어의 역할이 바뀌었음을 알립니다.
* @param username 대상 유저의 username입니다.
* @param role 대상 유저의 새로운 역할입니다.
*/
export class RoundRoleMessage implements Message {
readonly type = MessageType.ROUND_ROLE;
constructor(
public username: string,
public role: "drawer" | "guesser" | "winner" | "spectator"
) {}
}
/**
* 클라 <- 서버
* 플레이어가 정답을 맞췄음을 알립니다.
* @param answer 이번 라운드의 정답입니다.
*/
export class AnswerAcceptedMessage implements Message {
readonly type = MessageType.ANSWER_ACCEPTED;
constructor(public answer: string) {}
}
/**
* 클라 <- 서버
* 게임이 종료되었음을 알립니다. 다시 준비 화면으로 돌아갑니다.
*/
export class GameFinishMessage implements Message {
readonly type = MessageType.GAME_FINISH;
constructor() {}
}
/**
* 클라 <-> 서버
* 브러시 설정을 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다.
* @param size 픽셀 단위의 브러시 지름입니다.
* @param color 6자리 소문자 16진수로 표현된 브러시 색상입니다.
* @param drawing 현재 브러시가 캔버스에 닿은 상태인지를 나타냅니다.
*/
export class PaintBrushMessage implements Message {
readonly type = MessageType.PAINT_BRUSH;
constructor(
public size: number,
public color: string,
public drawing: boolean
) {}
}
/**
* 클라 <-> 서버
* 브러시 위치를 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다.
* 왼쪽 하단이 원점입니다.
* @param x 픽셀 단위의 가로 위치입니다.
* @param y 픽셀 단위의 세로 위치입니다.
*/
export class PaintMoveMessage implements Message {
readonly type = MessageType.PAINT_MOVE;
constructor(public x: number, public y: number) {}
}
export class MessageType {
static readonly LOGIN = "login";
static readonly ROOM_LIST_REQUEST = "room_list_request";
static readonly ROOM_JOIN = "room_join";
static readonly ROOM_LEAVE = "room_leave";
static readonly ROOM_USER_UPDATE = "room_user_update";
static readonly ROOM_CHAT = "room_chat";
static readonly ROUND_START = "round_start";
static readonly ROUND_TIMER = "round_timer";
static readonly ROUND_FINISH = "round_finish";
static readonly ROUND_ROLE = "round_role";
static readonly ANSWER_ACCEPTED = "answer_accepted";
static readonly GAME_FINISH = "game_finish";
static readonly ROUND_WORD_SET = "round_word_set";
static readonly ROUND_CHOOSE_WORD = "round_choose_word";
static readonly ROUND_WORD_CHOSEN = "round_word_chosen";
static readonly PAINT_BRUSH = "paint_brush";
static readonly PAINT_MOVE = "paint_move";
}
import { Connection } from "../connection/Connection";
import { v4 as uuidv4 } from "uuid";
import { RoomDescription, RoomInfo } from "./types";
import {
Message,
RoomChatMessage,
RoomUserUpdateMessage,
} from "../message/types";
import { UserData } from "../user/types";
import { User } from "../user/User";
import { MessageHandlerChain } from "../message/MessageHandlerChain";
import { MessageHandler } from "../message/MessageHandler";
import {
ServerInboundMessage,
ServerOutboundMessage,
ServerOutboundMessageKey,
} from "../../common";
import { RoomDescription, RoomInfo, UserData } from "../../common/dataType";
export class Room {
public readonly uuid: string;
......@@ -15,14 +16,27 @@ export class Room {
public name: string;
public readonly maxUsers: number;
private users: User[] = [];
public users: User[] = [];
private closed: boolean = false;
public handler: MessageHandler;
constructor(name: string, maxUsers: number = 8) {
this.uuid = uuidv4();
this.name = name;
this.maxUsers = maxUsers;
this.handler = new MessageHandler({
chat: (user, message) => {
this.sendChat(user, message.message);
return { ok: true };
},
leaveRoom: (user, message) => {
this.disconnect(user);
return { ok: true };
},
});
}
public connect(user: User): void {
......@@ -30,10 +44,10 @@ export class Room {
return;
}
this.broadcast(new RoomUserUpdateMessage("added", user.getData()));
this.broadcast("updateRoomUser", { state: "added", user: user.getData() });
this.users.push(user);
user.room = this; // TODO: 더 나은 관리
user.room = this;
}
public disconnect(user: User): void {
......@@ -42,12 +56,15 @@ export class Room {
this.users.splice(index, 1);
user.room = undefined;
this.broadcast(new RoomUserUpdateMessage("removed", user.getData()));
this.broadcast("updateRoomUser", {
state: "removed",
user: user.getData(),
});
}
}
public sendChat(user: User, message: string): void {
this.broadcast(new RoomChatMessage(message, user.username), user);
this.broadcast("chat", { sender: user.username, message: message });
}
public getDescription(): RoomDescription {
......@@ -69,10 +86,14 @@ export class Room {
};
}
public broadcast(message: Message, except?: User): void {
public broadcast<T extends ServerOutboundMessageKey>(
type: T,
message: ServerOutboundMessage<T>,
except?: User
): void {
this.users.forEach((u) => {
if (u !== except) {
u.connection.send(message);
u.connection.send(type, message);
}
});
}
......
import { RoomDescription } from "../../common/dataType";
import { Room } from "./Room";
import { RoomDescription } from "./types";
export class RoomManager {
private static _instance: RoomManager;
......
import ioclient, { Socket } from "socket.io-client";
import {
LoginMessage,
MessageResponse,
MessageType,
RoomChatMessage,
RoomJoinMessage,
RoomLeaveMessage,
RoomListRequestMessage,
RoomUserUpdateMessage,
} from "./message/types";
import { expect } from "chai";
import { Server } from "./Server";
import { RoomDescription, RoomInfo } from "./room/types";
import { response } from "express";
import {
RawMessage,
ServerInboundMessage,
ServerInboundMessageKey,
ServerOutboundMessage,
ServerOutboundMessageKey,
ServerResponse,
} from "../common";
describe("server", () => {
const PORT = 3000;
......@@ -39,39 +36,58 @@ describe("server", () => {
client2.close();
});
var roomUserUpdateMessage: RoomUserUpdateMessage;
var roomChatMessage: RoomChatMessage;
var roomUserUpdateMessage: ServerOutboundMessage<"updateRoomUser">;
var roomChatMessage: ServerOutboundMessage<"chat">;
const send = <T extends ServerInboundMessageKey>(
socket: Socket,
type: T,
message: ServerInboundMessage<T>,
callback: (response: ServerResponse<T>) => void
) => {
socket.emit(
"msg",
{
type: type as string,
message: message,
},
callback
);
};
step("register listeners", () => {
client1.on(
MessageType.ROOM_USER_UPDATE,
(message: RoomUserUpdateMessage) => {
roomUserUpdateMessage = message;
}
);
client1.on("msg", (raw: RawMessage) => {
if (raw.type == "updateRoomUser") roomUserUpdateMessage = raw.message;
});
client1.on(MessageType.ROOM_CHAT, (message: RoomChatMessage) => {
roomChatMessage = message;
client1.on("msg", (raw: RawMessage) => {
if (raw.type == "chat") roomChatMessage = raw.message;
});
});
step("login 1", (done) => {
client1.emit(
MessageType.LOGIN,
new LoginMessage("guest1"),
(response: MessageResponse<undefined>) => {
expect(response.ok).to.equals(true);
send(
client1,
"login",
{
username: "guest1",
},
(response) => {
expect(response.ok).to.eq(true);
done();
}
);
});
step("login 2", (done) => {
client2.emit(
MessageType.LOGIN,
new LoginMessage("guest2"),
(response: MessageResponse<undefined>) => {
expect(response.ok).to.equals(true);
send(
client2,
"login",
{
username: "guest2",
},
(response) => {
expect(response.ok).to.eq(true);
done();
}
);
......@@ -80,52 +96,40 @@ describe("server", () => {
var roomToJoin: string;
step("room list", (done) => {
client1.emit(
MessageType.ROOM_LIST_REQUEST,
new RoomListRequestMessage(),
(response: MessageResponse<RoomDescription[]>) => {
expect(response.ok).to.eq(true);
expect(response.result !== undefined).to.eq(true);
if (response.result) {
expect(response.result[0].name).to.eq("테스트 방 #1");
roomToJoin = response.result[0].uuid;
}
done();
send(client1, "roomList", {}, (response) => {
expect(response.ok).to.eq(true);
expect(response.result !== undefined).to.eq(true);
if (response.result) {
expect(response.result[0].name).to.eq("테스트 방 #1");
roomToJoin = response.result[0].uuid;
}
);
done();
});
});
step("room join 1", (done) => {
client1.emit(
MessageType.ROOM_JOIN,
new RoomJoinMessage(roomToJoin),
(response: MessageResponse<RoomInfo>) => {
expect(response.ok).to.eq(true);
expect(response.result !== undefined).to.eq(true);
if (response.result) {
expect(response.result.uuid).to.eq(roomToJoin);
expect(response.result.users.length).to.eq(1);
expect(response.result.users[0].username).to.eq("guest1");
}
done();
send(client1, "joinRoom", { uuid: roomToJoin }, (response) => {
expect(response.ok).to.eq(true);
expect(response.result !== undefined).to.eq(true);
if (response.result) {
expect(response.result.uuid).to.eq(roomToJoin);
expect(response.result.users.length).to.eq(1);
expect(response.result.users[0].username).to.eq("guest1");
}
);
done();
});
});
step("room join 2", (done) => {
client2.emit(
MessageType.ROOM_JOIN,
new RoomJoinMessage(roomToJoin),
(response: MessageResponse<RoomInfo>) => {
expect(response.ok).to.eq(true);
expect(response.result !== undefined).to.eq(true);
if (response.result) {
expect(response.result.uuid).to.eq(roomToJoin);
expect(response.result.users.length).to.eq(2);
}
done();
send(client2, "joinRoom", { uuid: roomToJoin }, (response) => {
expect(response.ok).to.eq(true);
expect(response.result !== undefined).to.eq(true);
if (response.result) {
expect(response.result.uuid).to.eq(roomToJoin);
expect(response.result.users.length).to.eq(2);
}
);
done();
});
});
// TODO: RoomUserUpdateMessage가 아직 도착하지 않았는데 실행되는 경우
......@@ -133,19 +137,15 @@ describe("server", () => {
expect(roomUserUpdateMessage !== undefined).to.eq(true);
if (roomUserUpdateMessage) {
expect(roomUserUpdateMessage.state).to.eq("added");
expect(roomUserUpdateMessage.userdata.username).to.eq("guest2");
expect(roomUserUpdateMessage.user.username).to.eq("guest2");
}
});
step("client 2 send chat", (done) => {
client2.emit(
MessageType.ROOM_CHAT,
new RoomChatMessage("Hello World"),
(response: MessageResponse<undefined>) => {
expect(response.ok).to.eq(true);
done();
}
);
send(client2, "chat", { message: "Hello World" }, (response) => {
expect(response.ok).to.eq(true);
done();
});
});
step("client 1 received chat", () => {
......@@ -157,21 +157,17 @@ describe("server", () => {
});
step("client 2 leave", (done) => {
client2.emit(
MessageType.ROOM_LEAVE,
new RoomLeaveMessage(),
(response: MessageResponse<undefined>) => {
expect(response.ok).to.eq(true);
done();
}
);
send(client2, "leaveRoom", {}, (response) => {
expect(response.ok).to.eq(true);
done();
});
});
step("client 1 received user update", () => {
expect(roomUserUpdateMessage !== undefined).to.eq(true);
if (roomUserUpdateMessage) {
expect(roomUserUpdateMessage.state).to.eq("removed");
expect(roomUserUpdateMessage.userdata.username).to.eq("guest2");
expect(roomUserUpdateMessage.user.username).to.eq("guest2");
}
});
});
......
......@@ -4,8 +4,8 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
......@@ -25,7 +25,7 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
......@@ -50,7 +50,7 @@
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
......@@ -65,7 +65,12 @@
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"references": [
{
"path": "../common"
}
]
}
......
import { UserData } from "../../common/dataType";
import { Connection } from "../connection/Connection";
import { MessageHandler } from "../message/MessageHandler";
import { Room } from "../room/Room";
import { UserData } from "./types";
import { RoomManager } from "../room/RoomManager";
export class User {
public readonly username: string;
......@@ -9,9 +11,28 @@ export class User {
public room?: Room;
public handler: MessageHandler;
constructor(username: string, connection: Connection) {
this.username = username;
this.connection = connection;
this.handler = new MessageHandler({
roomList: (user, message) => {
return { ok: true, result: RoomManager.instance().list() };
},
joinRoom: (user, message) => {
const room = RoomManager.instance().get(message.uuid);
if (user.room || !room) {
return { ok: false };
}
// TODO: 방 접속 실패 처리
room.connect(user);
if (user.room === undefined) {
return { ok: false };
}
return { ok: true, result: room.getInfo() };
},
});
}
public getData(): UserData {
......
export interface UserData {
username: string;
}
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
......@@ -20,7 +16,10 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
"include": ["src"],
"references": [
{
"path": "../common"
}
]
}
......