Overnap
1 -import { UserData } from "../user/types";
2 -
3 /** 1 /**
4 * 방 리스트에서 사용됩니다. 2 * 방 리스트에서 사용됩니다.
5 */ 3 */
...@@ -19,3 +17,7 @@ export interface RoomInfo { ...@@ -19,3 +17,7 @@ export interface RoomInfo {
19 maxUsers: number; 17 maxUsers: number;
20 users: UserData[]; 18 users: UserData[];
21 } 19 }
20 +
21 +export interface UserData {
22 + username: string;
23 +}
......
1 +export * from "./message";
1 +import { RoomDescription, RoomInfo } from "./dataType";
2 +
3 +// 서버로 들어오는 메세지 타입을 정의합니다.
4 +// 'result' 속성은 서버 요청 결과에만 포함되는 특별한 속성입니다.
5 +interface ServerInboundMessageMap {
6 + // 로그인을 시도합니다.
7 + login: {
8 + username: string;
9 + };
10 +
11 + // 방 목록을 요청합니다.
12 + roomList: {
13 + result: RoomDescription[];
14 + };
15 +
16 + // 방에 접속합니다.
17 + joinRoom: {
18 + uuid: string;
19 + result: RoomInfo;
20 + };
21 +
22 + // 방에서 나갑니다.
23 + leaveRoom: {};
24 +
25 + // 채팅을 보냅니다.
26 + chat: {
27 + message: string;
28 + };
29 +
30 + // drawer가 단어를 선택합니다.
31 + chooseWord: {
32 + word: string;
33 + };
34 +
35 + // 브러시 정보를 변경합니다.
36 + setBrush: {
37 + size: number;
38 + color: string;
39 + drawing: boolean;
40 + };
41 +
42 + // 브러시를 이동합니다.
43 + moveBrush: {
44 + x: number;
45 + y: number;
46 + };
47 +}
48 +
49 +// 서버에서 나가는 메세지 타입을 정의합니다.
50 +interface ServerOutboundMessageMap {
51 + // 방에 접속 중인 유저 목록이 업데이트 되었습니다.
52 + updateRoomUser: {
53 + state: "added" | "updated" | "removed";
54 + user: {
55 + username: string;
56 + };
57 + };
58 +
59 + // 다른 유저가 채팅을 보냈습니다.
60 + chat: {
61 + sender: string;
62 + message: string;
63 + };
64 +
65 + // 라운드가 시작되었습니다.
66 + startRound: {
67 + round: number;
68 + duration: number;
69 + roles: {
70 + username: string;
71 + role: "drawer" | "guesser" | "winner" | "spectator";
72 + };
73 + };
74 +
75 + // drawer에게 선택할 수 있는 단어가 주어졌습니다.
76 + wordSet: {
77 + words: string[];
78 + };
79 +
80 + // 이번 라운드의 단어가 선택되었습니다.
81 + wordChosen: {
82 + length: number;
83 + };
84 +
85 + // 라운드 타이머 정보를 동기화합니다.
86 + timer: {
87 + state: "started" | "stopped";
88 + time: number;
89 + };
90 +
91 + // 라운드가 종료되었습니다.
92 + finishRound: {
93 + answer: string;
94 + };
95 +
96 + // 역할이 변경되었습니다.
97 + role: {
98 + username: string;
99 + role: "drawer" | "guesser" | "winner" | "spectator";
100 + };
101 +
102 + // 보낸 단어가 정답 처리 되었습니다.
103 + answerAccepted: {
104 + answer: string;
105 + };
106 +
107 + // 게임이 종료되었습니다.
108 + finishGame: {};
109 +
110 + // 브러시 정보가 변경되었습니다.
111 + setBrush: {
112 + size: number;
113 + color: string;
114 + drawing: boolean;
115 + };
116 +
117 + // 브러시가 이동되었습니다.
118 + moveBrush: {
119 + x: number;
120 + y: number;
121 + };
122 +}
123 +
124 +export interface RawMessage {
125 + type: string;
126 + message: any;
127 +}
128 +
129 +export type ServerInboundMessageKey = keyof ServerInboundMessageMap;
130 +
131 +export type ServerInboundMessage<Key extends ServerInboundMessageKey> = Omit<
132 + ServerInboundMessageMap[Key],
133 + "result"
134 +>;
135 +
136 +export interface ServerResponse<Key extends ServerInboundMessageKey> {
137 + ok: boolean;
138 + reason?: string;
139 + result?: "result" extends keyof ServerInboundMessageMap[Key]
140 + ? ServerInboundMessageMap[Key]["result"]
141 + : never;
142 +}
143 +
144 +export type ServerOutboundMessage<Key extends keyof ServerOutboundMessageMap> =
145 + ServerOutboundMessageMap[Key];
146 +
147 +export type ServerOutboundMessageKey = keyof ServerOutboundMessageMap;
1 +{
2 + "name": "common",
3 + "version": "1.0.0",
4 + "main": "index.js",
5 + "license": "MIT",
6 + "dependencies": {}
7 +}
1 +{
2 + "compilerOptions": {
3 + "target": "es5",
4 + "module": "commonjs",
5 + "declaration": true,
6 + "declarationMap": true,
7 + "rootDir": ".",
8 + "composite": true,
9 + "strict": true,
10 + "esModuleInterop": true
11 + }
12 +}
1 +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 +# yarn lockfile v1
3 +
4 +
1 import { Socket } from "socket.io"; 1 import { Socket } from "socket.io";
2 -import { MessageHandlerRegistry } from "../message/MessageHandlerRegistry"; 2 +import { ServerOutboundMessage, ServerOutboundMessageKey } from "../../common";
3 -import { Message } from "../message/types"; 3 +import { MessageHandlerChain } from "../message/MessageHandlerChain";
4 import { Room } from "../room/Room"; 4 import { Room } from "../room/Room";
5 import { User } from "../user/User"; 5 import { User } from "../user/User";
6 6
...@@ -9,17 +9,20 @@ export class Connection { ...@@ -9,17 +9,20 @@ export class Connection {
9 9
10 public user?: User; 10 public user?: User;
11 11
12 + private messageHandlerChain: MessageHandlerChain;
13 +
12 constructor(socket: Socket) { 14 constructor(socket: Socket) {
13 this.socket = socket; 15 this.socket = socket;
14 - 16 + this.messageHandlerChain = new MessageHandlerChain(this);
15 - MessageHandlerRegistry.registerHandlers(this);
16 - }
17 -
18 - public get authenticated(): boolean {
19 - return this.user !== undefined;
20 } 17 }
21 18
22 - public send(message: Message) { 19 + public send<T extends ServerOutboundMessageKey>(
23 - this.socket.emit(message.type, message); 20 + type: T,
21 + message: ServerOutboundMessage<T>
22 + ) {
23 + this.socket.emit("msg", {
24 + type: type as string,
25 + message: message,
26 + });
24 } 27 }
25 } 28 }
......
...@@ -15,6 +15,7 @@ export class ConnectionMapper { ...@@ -15,6 +15,7 @@ export class ConnectionMapper {
15 } 15 }
16 16
17 value = new Connection(socket); 17 value = new Connection(socket);
18 + // FIXME: Register connection to the map
18 return value; 19 return value;
19 } 20 }
20 21
......
1 +import { User } from "../user/User";
2 +
3 +export interface Game {
4 + join(user: User): void;
5 + leave(user: User): void;
6 +}
1 +import { roomChatHandler } from "../message/handler/roomChatHandler";
2 +import { Room } from "../room/Room";
3 +import { User } from "../user/User";
4 +import { Game } from "./Game";
5 +
6 +export class WorldGuessingGame implements Game {
7 + room: Room;
8 + maxRound: number;
9 + round: number;
10 +
11 + constructor(room: Room) {
12 + this.room = room;
13 + if (this.room.users.length < 2) {
14 + throw new Error("인원이 부족합니다.");
15 + }
16 +
17 + // TODO: 방장이 설정
18 + this.maxRound = 5;
19 + this.round = 1;
20 + }
21 +
22 + join(user: User): void {
23 + throw new Error("Method not implemented.");
24 + }
25 +
26 + leave(user: User): void {
27 + throw new Error("Method not implemented.");
28 + }
29 +}
1 +import { Connection } from "../connection/Connection";
2 +import {
3 + ServerInboundMessage,
4 + ServerInboundMessageKey,
5 + ServerResponse,
6 +} from "../../common/index";
7 +import { User } from "../user/User";
8 +
9 +type UserHandlerMap = {
10 + [Key in ServerInboundMessageKey]?: (
11 + user: User,
12 + message: ServerInboundMessage<Key>
13 + ) => ServerResponse<Key>;
14 +};
15 +
16 +export class MessageHandler {
17 + private handlers: UserHandlerMap;
18 +
19 + constructor(handlers: UserHandlerMap) {
20 + this.handlers = handlers;
21 + }
22 +
23 + public handle(
24 + type: ServerInboundMessageKey,
25 + user: User,
26 + message: any,
27 + callback: Function
28 + ): boolean {
29 + const handler = this.handlers[type];
30 + if (!handler) return false;
31 + const response = handler(user, message);
32 + callback(response);
33 + return true;
34 + }
35 +}
1 +import { Connection } from "../connection/Connection";
2 +import {
3 + RawMessage,
4 + ServerInboundMessage,
5 + ServerInboundMessageKey,
6 + ServerResponse,
7 +} from "../../common/index";
8 +import { User } from "../user/User";
9 +
10 +export class MessageHandlerChain {
11 + connection: Connection;
12 +
13 + constructor(connection: Connection) {
14 + this.connection = connection;
15 +
16 + this.connection.socket.on("msg", (raw: RawMessage, callback: Function) => {
17 + this.handleRaw(connection, raw, callback);
18 + });
19 + }
20 +
21 + private handleRaw(
22 + connection: Connection,
23 + raw: RawMessage,
24 + callback: Function
25 + ) {
26 + const type = raw.type as ServerInboundMessageKey;
27 + const message = raw.message;
28 +
29 + // 유저 정보가 없으므로 로그인은 따로 핸들링
30 + if (type === "login") {
31 + this.handleLogin(connection, message, callback);
32 + return;
33 + }
34 +
35 + // Game > Room > User 순으로 전달
36 + if (
37 + connection?.user?.room &&
38 + connection.user.room.handler.handle(
39 + type,
40 + connection.user,
41 + message,
42 + callback
43 + )
44 + )
45 + return;
46 +
47 + if (
48 + connection?.user &&
49 + connection.user.handler.handle(type, connection.user, message, callback)
50 + )
51 + return;
52 + }
53 +
54 + private handleLogin(
55 + connection: Connection,
56 + message: ServerInboundMessage<"login">,
57 + callback: Function
58 + ) {
59 + connection.user = new User(message.username, connection);
60 + console.log(`User ${message.username} has logged in!`);
61 +
62 + callback({ ok: true });
63 + }
64 +}
1 -import { Connection } from "../connection/Connection";
2 -import { User } from "../user/User";
3 -import { loginHandler } from "./handler/loginHandler";
4 -import { roomChatHandler } from "./handler/roomChatHandler";
5 -import { roomJoinHandler } from "./handler/roomJoinHandler";
6 -import { roomLeaveHandler } from "./handler/roomLeaveHandler";
7 -import { roomListRequestHandler } from "./handler/roomListRequestHandler";
8 -import { Message, MessageResponse, MessageType } from "./types";
9 -
10 -export class MessageHandlerRegistry {
11 - static registerHandlers(connection: Connection) {
12 - this.registerHandler(connection, MessageType.LOGIN, loginHandler);
13 - this.registerHandlerAuthed(
14 - connection,
15 - MessageType.ROOM_LIST_REQUEST,
16 - roomListRequestHandler
17 - );
18 - this.registerHandlerAuthed(
19 - connection,
20 - MessageType.ROOM_JOIN,
21 - roomJoinHandler
22 - );
23 - this.registerHandlerAuthed(
24 - connection,
25 - MessageType.ROOM_LEAVE,
26 - roomLeaveHandler
27 - );
28 - this.registerHandlerAuthed(
29 - connection,
30 - MessageType.ROOM_CHAT,
31 - roomChatHandler
32 - );
33 - }
34 -
35 - private static registerHandler<T extends Message, S>(
36 - connection: Connection,
37 - typeName: string,
38 - handler: (connection: Connection, message: T) => MessageResponse<S>
39 - ) {
40 - connection.socket.on(typeName, (message: T, callback: Function) => {
41 - const response = handler(connection, message);
42 - callback(response);
43 - });
44 - }
45 -
46 - private static registerHandlerAuthed<T extends Message, S>(
47 - connection: Connection,
48 - typeName: string,
49 - handler: (user: User, message: T) => MessageResponse<S>
50 - ) {
51 - connection.socket.on(typeName, (message: T, callback: Function) => {
52 - if (connection.user !== undefined) {
53 - const response = handler(connection.user, message);
54 - callback(response);
55 - } else {
56 - callback({ ok: false });
57 - }
58 - });
59 - }
60 -}
1 -import { Connection } from "../../connection/Connection";
2 -import { RoomManager } from "../../room/RoomManager";
3 -import { User } from "../../user/User";
4 -import { LoginMessage, MessageResponse } from "../types";
5 -
6 -export function loginHandler(
7 - connection: Connection,
8 - message: LoginMessage
9 -): MessageResponse<undefined> {
10 - connection.user = new User(message.username, connection);
11 - console.log(`User ${message.username} has logged in!`);
12 -
13 - return { ok: true };
14 -}
1 -import { Connection } from "../../connection/Connection";
2 -import { RoomManager } from "../../room/RoomManager";
3 -import { User } from "../../user/User";
4 -import { MessageResponse, RoomChatMessage, RoomJoinMessage } from "../types";
5 -
6 -export function roomChatHandler(
7 - user: User,
8 - message: RoomChatMessage
9 -): MessageResponse<undefined> {
10 - user.room?.sendChat(user, message.message);
11 - return { ok: true };
12 -}
1 -import { Connection } from "../../connection/Connection";
2 -import { RoomManager } from "../../room/RoomManager";
3 -import { RoomInfo } from "../../room/types";
4 -import { User } from "../../user/User";
5 -import { MessageResponse, RoomJoinMessage } from "../types";
6 -
7 -export function roomJoinHandler(
8 - user: User,
9 - message: RoomJoinMessage
10 -): MessageResponse<RoomInfo> {
11 - const room = RoomManager.instance().get(message.uuid);
12 - if (room !== undefined) {
13 - room.connect(user);
14 - return { ok: user.room !== undefined, result: user.room?.getInfo() };
15 - }
16 - return { ok: false };
17 -}
1 -import { Connection } from "../../connection/Connection";
2 -import { RoomManager } from "../../room/RoomManager";
3 -import { User } from "../../user/User";
4 -import { MessageResponse, RoomLeaveMessage } from "../types";
5 -
6 -export function roomLeaveHandler(
7 - user: User,
8 - message: RoomLeaveMessage
9 -): MessageResponse<undefined> {
10 - user.room?.disconnect(user);
11 - return { ok: true };
12 -}
1 -import { Connection } from "../../connection/Connection";
2 -import { RoomManager } from "../../room/RoomManager";
3 -import { RoomDescription, RoomInfo } from "../../room/types";
4 -import { User } from "../../user/User";
5 -import { MessageResponse, RoomListRequestMessage } from "../types";
6 -
7 -export function roomListRequestHandler(
8 - user: User,
9 - message: RoomListRequestMessage
10 -): MessageResponse<RoomDescription[]> {
11 - return { ok: true, result: RoomManager.instance().list() };
12 -}
1 -import { RoomDescription } from "../room/types";
2 -import { UserData } from "../user/types";
3 -
4 -export interface Message {
5 - readonly type: string;
6 -}
7 -
8 -/**
9 - * 클라 -> 서버 : 클라이언트에서 서버로 요청할 때 사용하는 메세지입니다. 요청 결과는 MessageResponse<undefined>입니다.
10 - * 클라 -> 서버 -> T : 위와 동일하지만 요청 결과가 MessageResponse<T>입니다.
11 - * 서버 -> 클라 : 서버에서 클라이언트로 전송되는 메세지입니다.
12 - * 클라 <-> 서버 : 양방향으로 사용되는 메세지입니다.
13 - */
14 -
15 -/**
16 - * 서버에 Event를 보냈을 때 요청에 대한 결과를 전송받습니다.
17 - * @param ok 요청의 성공 여부입니다.
18 - * @param reason 요청 실패 사유입니다. 필요한 경우에만 포함됩니다.
19 - * @param result 요청에 대한 결과 메세지입니다. 특정한 메세지에 대해 요청이 성공하였을 때만 포함됩니다.
20 - */
21 -export interface MessageResponse<T> {
22 - ok: boolean;
23 - reason?: string;
24 - result?: T;
25 -}
26 -
27 -/**
28 - * 클라 -> 서버
29 - * 로그인 정보를 서버에게 전송합니다.
30 - */
31 -export class LoginMessage implements Message {
32 - readonly type = MessageType.LOGIN;
33 - constructor(public username: string) {}
34 -}
35 -
36 -/**
37 - * 클라 -> 서버 -> RoomDescription[]
38 - * 방 목록을 요청합니다.
39 - */
40 -export class RoomListRequestMessage implements Message {
41 - readonly type = MessageType.ROOM_LIST_REQUEST;
42 - constructor() {}
43 -}
44 -
45 -/**
46 - * 클라 -> 서버 -> RoomInfo
47 - * 방에 접속합니다.
48 - */
49 -export class RoomJoinMessage implements Message {
50 - readonly type = MessageType.ROOM_JOIN;
51 - constructor(public uuid: string) {}
52 -}
53 -
54 -/**
55 - * 클라 -> 서버
56 - * 방에서 나갑니다.
57 - */
58 -export class RoomLeaveMessage implements Message {
59 - readonly type = MessageType.ROOM_LEAVE;
60 - constructor() {}
61 -}
62 -
63 -/**
64 - * 클라 <- 서버
65 - * 접속한 방에 새로운 유저가 들어오거나 나갈 때 전송됩니다.
66 - * @param state 유저가 입장하면 added, 퇴장하면 removed 값을 가집니다.
67 - * @param userdata 대상 유저입니다.
68 - */
69 -export class RoomUserUpdateMessage implements Message {
70 - readonly type = MessageType.ROOM_USER_UPDATE;
71 - constructor(
72 - public state: "added" | "updated" | "removed",
73 - public userdata: UserData
74 - ) {}
75 -}
76 -
77 -/**
78 - * 클라 <-> 서버
79 - * 접속한 방에서 채팅을 보내거나 받을 때 사용됩니다. 자신이 보낸 채팅은 서버에 의해 수신되지 않습니다.
80 - * @param message 메세지 내용입니다.
81 - * @param sender 채팅을 보낸 유저의 username입니다. 채팅이 클라이언트로 수신 될 경우에만 값을 가집니다.
82 - */
83 -export class RoomChatMessage implements Message {
84 - readonly type = MessageType.ROOM_CHAT;
85 - constructor(public message: string, public sender?: string) {}
86 -}
87 -
88 -/**
89 - * 클라 <- 서버
90 - * 라운드가 시작되었음을 알립니다.
91 - * @param round 현재 라운드 넘버입니다. (1부터 시작)
92 - * @param duration 초 단위의 라운드 시간입니다.
93 - * @param roles 모든 방 접속 인원의 역할입니다.
94 - */
95 -export class RoundStartMessage implements Message {
96 - readonly type = MessageType.ROUND_START;
97 - constructor(
98 - public round: number,
99 - public duration: number,
100 - public roles: {
101 - username: string;
102 - role: "drawer" | "guesser" | "winner" | "spectator";
103 - }[]
104 - ) {}
105 -}
106 -
107 -/**
108 - * 클라 <- 서버
109 - * 라운드 시작시에 오직 drawer에게만 전송되는 메세지로, drawer가 선택할 수 있는 단어들입니다.
110 - */
111 -export class RoundWordSetMessage implements Message {
112 - readonly type = MessageType.ROUND_WORD_SET;
113 - constructor(public words: string[]) {}
114 -}
115 -
116 -/**
117 - * 클라 -> 서버
118 - * drawer가 단어를 선택하면 해당 메세지가 서버로 전송됩니다.
119 - * @param word RoundWordSetMessage에서 수신받은 단어 중 drawer가 선택한 단어입니다.
120 - */
121 -export class RoundChooseWordMessage implements Message {
122 - readonly type = MessageType.ROUND_CHOOSE_WORD;
123 - constructor(public word: string) {}
124 -}
125 -
126 -/**
127 - * 클라 <- 서버
128 - * drawer가 단어를 선택하였음을 알립니다.
129 - * @param length 정답 단어의 길이입니다.
130 - */
131 -export class RoundWordChosenMessage implements Message {
132 - readonly type = MessageType.ROUND_WORD_CHOSEN;
133 - constructor(public length: number) {}
134 -}
135 -
136 -/**
137 - * 클라 <- 서버
138 - * 서버가 클라이언트의 타이머를 동기화하기 위해 사용됩니다. 라운드가 시작하면 타이머는 초기화되며 기본 상태는 stopped입니다.
139 - * @param state 타이머의 동작, 정지 여부입니다.
140 - * @param time 타이머의 남은 시간입니다. 초 단위로 주어집니다.
141 - */
142 -export class RoundTimerMessage implements Message {
143 - readonly type = MessageType.ROUND_TIMER;
144 - constructor(public state: "started" | "stopped", public time: number) {}
145 -}
146 -
147 -/**
148 - * 클라 <- 서버
149 - * 라운드가 종료되었음을 알립니다.
150 - * @param answer 이번 라운드의 정답입니다.
151 - */
152 -export class RoundFinishMessage implements Message {
153 - readonly type = MessageType.ROUND_FINISH;
154 - constructor(public answer: string) {}
155 -}
156 -
157 -/**
158 - * 클라 <- 서버
159 - * 플레이어의 역할이 바뀌었음을 알립니다.
160 - * @param username 대상 유저의 username입니다.
161 - * @param role 대상 유저의 새로운 역할입니다.
162 - */
163 -export class RoundRoleMessage implements Message {
164 - readonly type = MessageType.ROUND_ROLE;
165 - constructor(
166 - public username: string,
167 - public role: "drawer" | "guesser" | "winner" | "spectator"
168 - ) {}
169 -}
170 -
171 -/**
172 - * 클라 <- 서버
173 - * 플레이어가 정답을 맞췄음을 알립니다.
174 - * @param answer 이번 라운드의 정답입니다.
175 - */
176 -export class AnswerAcceptedMessage implements Message {
177 - readonly type = MessageType.ANSWER_ACCEPTED;
178 - constructor(public answer: string) {}
179 -}
180 -
181 -/**
182 - * 클라 <- 서버
183 - * 게임이 종료되었음을 알립니다. 다시 준비 화면으로 돌아갑니다.
184 - */
185 -export class GameFinishMessage implements Message {
186 - readonly type = MessageType.GAME_FINISH;
187 - constructor() {}
188 -}
189 -
190 -/**
191 - * 클라 <-> 서버
192 - * 브러시 설정을 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다.
193 - * @param size 픽셀 단위의 브러시 지름입니다.
194 - * @param color 6자리 소문자 16진수로 표현된 브러시 색상입니다.
195 - * @param drawing 현재 브러시가 캔버스에 닿은 상태인지를 나타냅니다.
196 - */
197 -export class PaintBrushMessage implements Message {
198 - readonly type = MessageType.PAINT_BRUSH;
199 - constructor(
200 - public size: number,
201 - public color: string,
202 - public drawing: boolean
203 - ) {}
204 -}
205 -
206 -/**
207 - * 클라 <-> 서버
208 - * 브러시 위치를 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다.
209 - * 왼쪽 하단이 원점입니다.
210 - * @param x 픽셀 단위의 가로 위치입니다.
211 - * @param y 픽셀 단위의 세로 위치입니다.
212 - */
213 -export class PaintMoveMessage implements Message {
214 - readonly type = MessageType.PAINT_MOVE;
215 - constructor(public x: number, public y: number) {}
216 -}
217 -
218 -export class MessageType {
219 - static readonly LOGIN = "login";
220 - static readonly ROOM_LIST_REQUEST = "room_list_request";
221 - static readonly ROOM_JOIN = "room_join";
222 - static readonly ROOM_LEAVE = "room_leave";
223 - static readonly ROOM_USER_UPDATE = "room_user_update";
224 - static readonly ROOM_CHAT = "room_chat";
225 - static readonly ROUND_START = "round_start";
226 - static readonly ROUND_TIMER = "round_timer";
227 - static readonly ROUND_FINISH = "round_finish";
228 - static readonly ROUND_ROLE = "round_role";
229 - static readonly ANSWER_ACCEPTED = "answer_accepted";
230 - static readonly GAME_FINISH = "game_finish";
231 - static readonly ROUND_WORD_SET = "round_word_set";
232 - static readonly ROUND_CHOOSE_WORD = "round_choose_word";
233 - static readonly ROUND_WORD_CHOSEN = "round_word_chosen";
234 - static readonly PAINT_BRUSH = "paint_brush";
235 - static readonly PAINT_MOVE = "paint_move";
236 -}
1 import { Connection } from "../connection/Connection"; 1 import { Connection } from "../connection/Connection";
2 import { v4 as uuidv4 } from "uuid"; 2 import { v4 as uuidv4 } from "uuid";
3 -import { RoomDescription, RoomInfo } from "./types";
4 -import {
5 - Message,
6 - RoomChatMessage,
7 - RoomUserUpdateMessage,
8 -} from "../message/types";
9 -import { UserData } from "../user/types";
10 import { User } from "../user/User"; 3 import { User } from "../user/User";
4 +import { MessageHandlerChain } from "../message/MessageHandlerChain";
5 +import { MessageHandler } from "../message/MessageHandler";
6 +import {
7 + ServerInboundMessage,
8 + ServerOutboundMessage,
9 + ServerOutboundMessageKey,
10 +} from "../../common";
11 +import { RoomDescription, RoomInfo, UserData } from "../../common/dataType";
11 12
12 export class Room { 13 export class Room {
13 public readonly uuid: string; 14 public readonly uuid: string;
...@@ -15,14 +16,27 @@ export class Room { ...@@ -15,14 +16,27 @@ export class Room {
15 public name: string; 16 public name: string;
16 public readonly maxUsers: number; 17 public readonly maxUsers: number;
17 18
18 - private users: User[] = []; 19 + public users: User[] = [];
19 20
20 private closed: boolean = false; 21 private closed: boolean = false;
21 22
23 + public handler: MessageHandler;
24 +
22 constructor(name: string, maxUsers: number = 8) { 25 constructor(name: string, maxUsers: number = 8) {
23 this.uuid = uuidv4(); 26 this.uuid = uuidv4();
24 this.name = name; 27 this.name = name;
25 this.maxUsers = maxUsers; 28 this.maxUsers = maxUsers;
29 +
30 + this.handler = new MessageHandler({
31 + chat: (user, message) => {
32 + this.sendChat(user, message.message);
33 + return { ok: true };
34 + },
35 + leaveRoom: (user, message) => {
36 + this.disconnect(user);
37 + return { ok: true };
38 + },
39 + });
26 } 40 }
27 41
28 public connect(user: User): void { 42 public connect(user: User): void {
...@@ -30,10 +44,10 @@ export class Room { ...@@ -30,10 +44,10 @@ export class Room {
30 return; 44 return;
31 } 45 }
32 46
33 - this.broadcast(new RoomUserUpdateMessage("added", user.getData())); 47 + this.broadcast("updateRoomUser", { state: "added", user: user.getData() });
34 48
35 this.users.push(user); 49 this.users.push(user);
36 - user.room = this; // TODO: 더 나은 관리 50 + user.room = this;
37 } 51 }
38 52
39 public disconnect(user: User): void { 53 public disconnect(user: User): void {
...@@ -42,12 +56,15 @@ export class Room { ...@@ -42,12 +56,15 @@ export class Room {
42 this.users.splice(index, 1); 56 this.users.splice(index, 1);
43 user.room = undefined; 57 user.room = undefined;
44 58
45 - this.broadcast(new RoomUserUpdateMessage("removed", user.getData())); 59 + this.broadcast("updateRoomUser", {
60 + state: "removed",
61 + user: user.getData(),
62 + });
46 } 63 }
47 } 64 }
48 65
49 public sendChat(user: User, message: string): void { 66 public sendChat(user: User, message: string): void {
50 - this.broadcast(new RoomChatMessage(message, user.username), user); 67 + this.broadcast("chat", { sender: user.username, message: message });
51 } 68 }
52 69
53 public getDescription(): RoomDescription { 70 public getDescription(): RoomDescription {
...@@ -69,10 +86,14 @@ export class Room { ...@@ -69,10 +86,14 @@ export class Room {
69 }; 86 };
70 } 87 }
71 88
72 - public broadcast(message: Message, except?: User): void { 89 + public broadcast<T extends ServerOutboundMessageKey>(
90 + type: T,
91 + message: ServerOutboundMessage<T>,
92 + except?: User
93 + ): void {
73 this.users.forEach((u) => { 94 this.users.forEach((u) => {
74 if (u !== except) { 95 if (u !== except) {
75 - u.connection.send(message); 96 + u.connection.send(type, message);
76 } 97 }
77 }); 98 });
78 } 99 }
......
1 +import { RoomDescription } from "../../common/dataType";
1 import { Room } from "./Room"; 2 import { Room } from "./Room";
2 -import { RoomDescription } from "./types";
3 3
4 export class RoomManager { 4 export class RoomManager {
5 private static _instance: RoomManager; 5 private static _instance: RoomManager;
......
1 import ioclient, { Socket } from "socket.io-client"; 1 import ioclient, { Socket } from "socket.io-client";
2 -import {
3 - LoginMessage,
4 - MessageResponse,
5 - MessageType,
6 - RoomChatMessage,
7 - RoomJoinMessage,
8 - RoomLeaveMessage,
9 - RoomListRequestMessage,
10 - RoomUserUpdateMessage,
11 -} from "./message/types";
12 import { expect } from "chai"; 2 import { expect } from "chai";
13 import { Server } from "./Server"; 3 import { Server } from "./Server";
14 -import { RoomDescription, RoomInfo } from "./room/types";
15 import { response } from "express"; 4 import { response } from "express";
5 +import {
6 + RawMessage,
7 + ServerInboundMessage,
8 + ServerInboundMessageKey,
9 + ServerOutboundMessage,
10 + ServerOutboundMessageKey,
11 + ServerResponse,
12 +} from "../common";
16 13
17 describe("server", () => { 14 describe("server", () => {
18 const PORT = 3000; 15 const PORT = 3000;
...@@ -39,39 +36,58 @@ describe("server", () => { ...@@ -39,39 +36,58 @@ describe("server", () => {
39 client2.close(); 36 client2.close();
40 }); 37 });
41 38
42 - var roomUserUpdateMessage: RoomUserUpdateMessage; 39 + var roomUserUpdateMessage: ServerOutboundMessage<"updateRoomUser">;
43 - var roomChatMessage: RoomChatMessage; 40 + var roomChatMessage: ServerOutboundMessage<"chat">;
41 +
42 + const send = <T extends ServerInboundMessageKey>(
43 + socket: Socket,
44 + type: T,
45 + message: ServerInboundMessage<T>,
46 + callback: (response: ServerResponse<T>) => void
47 + ) => {
48 + socket.emit(
49 + "msg",
50 + {
51 + type: type as string,
52 + message: message,
53 + },
54 + callback
55 + );
56 + };
44 57
45 step("register listeners", () => { 58 step("register listeners", () => {
46 - client1.on( 59 + client1.on("msg", (raw: RawMessage) => {
47 - MessageType.ROOM_USER_UPDATE, 60 + if (raw.type == "updateRoomUser") roomUserUpdateMessage = raw.message;
48 - (message: RoomUserUpdateMessage) => { 61 + });
49 - roomUserUpdateMessage = message;
50 - }
51 - );
52 62
53 - client1.on(MessageType.ROOM_CHAT, (message: RoomChatMessage) => { 63 + client1.on("msg", (raw: RawMessage) => {
54 - roomChatMessage = message; 64 + if (raw.type == "chat") roomChatMessage = raw.message;
55 }); 65 });
56 }); 66 });
57 67
58 step("login 1", (done) => { 68 step("login 1", (done) => {
59 - client1.emit( 69 + send(
60 - MessageType.LOGIN, 70 + client1,
61 - new LoginMessage("guest1"), 71 + "login",
62 - (response: MessageResponse<undefined>) => { 72 + {
63 - expect(response.ok).to.equals(true); 73 + username: "guest1",
74 + },
75 + (response) => {
76 + expect(response.ok).to.eq(true);
64 done(); 77 done();
65 } 78 }
66 ); 79 );
67 }); 80 });
68 81
69 step("login 2", (done) => { 82 step("login 2", (done) => {
70 - client2.emit( 83 + send(
71 - MessageType.LOGIN, 84 + client2,
72 - new LoginMessage("guest2"), 85 + "login",
73 - (response: MessageResponse<undefined>) => { 86 + {
74 - expect(response.ok).to.equals(true); 87 + username: "guest2",
88 + },
89 + (response) => {
90 + expect(response.ok).to.eq(true);
75 done(); 91 done();
76 } 92 }
77 ); 93 );
...@@ -80,52 +96,40 @@ describe("server", () => { ...@@ -80,52 +96,40 @@ describe("server", () => {
80 var roomToJoin: string; 96 var roomToJoin: string;
81 97
82 step("room list", (done) => { 98 step("room list", (done) => {
83 - client1.emit( 99 + send(client1, "roomList", {}, (response) => {
84 - MessageType.ROOM_LIST_REQUEST, 100 + expect(response.ok).to.eq(true);
85 - new RoomListRequestMessage(), 101 + expect(response.result !== undefined).to.eq(true);
86 - (response: MessageResponse<RoomDescription[]>) => { 102 + if (response.result) {
87 - expect(response.ok).to.eq(true); 103 + expect(response.result[0].name).to.eq("테스트 방 #1");
88 - expect(response.result !== undefined).to.eq(true); 104 + roomToJoin = response.result[0].uuid;
89 - if (response.result) {
90 - expect(response.result[0].name).to.eq("테스트 방 #1");
91 - roomToJoin = response.result[0].uuid;
92 - }
93 - done();
94 } 105 }
95 - ); 106 + done();
107 + });
96 }); 108 });
97 109
98 step("room join 1", (done) => { 110 step("room join 1", (done) => {
99 - client1.emit( 111 + send(client1, "joinRoom", { uuid: roomToJoin }, (response) => {
100 - MessageType.ROOM_JOIN, 112 + expect(response.ok).to.eq(true);
101 - new RoomJoinMessage(roomToJoin), 113 + expect(response.result !== undefined).to.eq(true);
102 - (response: MessageResponse<RoomInfo>) => { 114 + if (response.result) {
103 - expect(response.ok).to.eq(true); 115 + expect(response.result.uuid).to.eq(roomToJoin);
104 - expect(response.result !== undefined).to.eq(true); 116 + expect(response.result.users.length).to.eq(1);
105 - if (response.result) { 117 + expect(response.result.users[0].username).to.eq("guest1");
106 - expect(response.result.uuid).to.eq(roomToJoin);
107 - expect(response.result.users.length).to.eq(1);
108 - expect(response.result.users[0].username).to.eq("guest1");
109 - }
110 - done();
111 } 118 }
112 - ); 119 + done();
120 + });
113 }); 121 });
114 122
115 step("room join 2", (done) => { 123 step("room join 2", (done) => {
116 - client2.emit( 124 + send(client2, "joinRoom", { uuid: roomToJoin }, (response) => {
117 - MessageType.ROOM_JOIN, 125 + expect(response.ok).to.eq(true);
118 - new RoomJoinMessage(roomToJoin), 126 + expect(response.result !== undefined).to.eq(true);
119 - (response: MessageResponse<RoomInfo>) => { 127 + if (response.result) {
120 - expect(response.ok).to.eq(true); 128 + expect(response.result.uuid).to.eq(roomToJoin);
121 - expect(response.result !== undefined).to.eq(true); 129 + expect(response.result.users.length).to.eq(2);
122 - if (response.result) {
123 - expect(response.result.uuid).to.eq(roomToJoin);
124 - expect(response.result.users.length).to.eq(2);
125 - }
126 - done();
127 } 130 }
128 - ); 131 + done();
132 + });
129 }); 133 });
130 134
131 // TODO: RoomUserUpdateMessage가 아직 도착하지 않았는데 실행되는 경우 135 // TODO: RoomUserUpdateMessage가 아직 도착하지 않았는데 실행되는 경우
...@@ -133,19 +137,15 @@ describe("server", () => { ...@@ -133,19 +137,15 @@ describe("server", () => {
133 expect(roomUserUpdateMessage !== undefined).to.eq(true); 137 expect(roomUserUpdateMessage !== undefined).to.eq(true);
134 if (roomUserUpdateMessage) { 138 if (roomUserUpdateMessage) {
135 expect(roomUserUpdateMessage.state).to.eq("added"); 139 expect(roomUserUpdateMessage.state).to.eq("added");
136 - expect(roomUserUpdateMessage.userdata.username).to.eq("guest2"); 140 + expect(roomUserUpdateMessage.user.username).to.eq("guest2");
137 } 141 }
138 }); 142 });
139 143
140 step("client 2 send chat", (done) => { 144 step("client 2 send chat", (done) => {
141 - client2.emit( 145 + send(client2, "chat", { message: "Hello World" }, (response) => {
142 - MessageType.ROOM_CHAT, 146 + expect(response.ok).to.eq(true);
143 - new RoomChatMessage("Hello World"), 147 + done();
144 - (response: MessageResponse<undefined>) => { 148 + });
145 - expect(response.ok).to.eq(true);
146 - done();
147 - }
148 - );
149 }); 149 });
150 150
151 step("client 1 received chat", () => { 151 step("client 1 received chat", () => {
...@@ -157,21 +157,17 @@ describe("server", () => { ...@@ -157,21 +157,17 @@ describe("server", () => {
157 }); 157 });
158 158
159 step("client 2 leave", (done) => { 159 step("client 2 leave", (done) => {
160 - client2.emit( 160 + send(client2, "leaveRoom", {}, (response) => {
161 - MessageType.ROOM_LEAVE, 161 + expect(response.ok).to.eq(true);
162 - new RoomLeaveMessage(), 162 + done();
163 - (response: MessageResponse<undefined>) => { 163 + });
164 - expect(response.ok).to.eq(true);
165 - done();
166 - }
167 - );
168 }); 164 });
169 165
170 step("client 1 received user update", () => { 166 step("client 1 received user update", () => {
171 expect(roomUserUpdateMessage !== undefined).to.eq(true); 167 expect(roomUserUpdateMessage !== undefined).to.eq(true);
172 if (roomUserUpdateMessage) { 168 if (roomUserUpdateMessage) {
173 expect(roomUserUpdateMessage.state).to.eq("removed"); 169 expect(roomUserUpdateMessage.state).to.eq("removed");
174 - expect(roomUserUpdateMessage.userdata.username).to.eq("guest2"); 170 + expect(roomUserUpdateMessage.user.username).to.eq("guest2");
175 } 171 }
176 }); 172 });
177 }); 173 });
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
4 4
5 /* Basic Options */ 5 /* Basic Options */
6 // "incremental": true, /* Enable incremental compilation */ 6 // "incremental": true, /* Enable incremental compilation */
7 - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 7 + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 8 + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 // "lib": [], /* Specify library files to be included in the compilation. */ 9 // "lib": [], /* Specify library files to be included in the compilation. */
10 // "allowJs": true, /* Allow javascript files to be compiled. */ 10 // "allowJs": true, /* Allow javascript files to be compiled. */
11 // "checkJs": true, /* Report errors in .js files. */ 11 // "checkJs": true, /* Report errors in .js files. */
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
25 // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 26
27 /* Strict Type-Checking Options */ 27 /* Strict Type-Checking Options */
28 - "strict": true, /* Enable all strict type-checking options. */ 28 + "strict": true /* Enable all strict type-checking options. */,
29 // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 // "strictNullChecks": true, /* Enable strict null checks. */ 30 // "strictNullChecks": true, /* Enable strict null checks. */
31 // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 // "strictFunctionTypes": true, /* Enable strict checking of function types. */
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
50 // "typeRoots": [], /* List of folders to include type definitions from. */ 50 // "typeRoots": [], /* List of folders to include type definitions from. */
51 // "types": [], /* Type declaration files to be included in compilation. */ 51 // "types": [], /* Type declaration files to be included in compilation. */
52 // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
53 - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
54 // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
55 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
56 56
...@@ -65,7 +65,12 @@ ...@@ -65,7 +65,12 @@
65 // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
66 66
67 /* Advanced Options */ 67 /* Advanced Options */
68 - "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 + "skipLibCheck": true /* Skip type checking of declaration files. */,
69 - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
70 - } 70 + },
71 + "references": [
72 + {
73 + "path": "../common"
74 + }
75 + ]
71 } 76 }
......
1 +import { UserData } from "../../common/dataType";
1 import { Connection } from "../connection/Connection"; 2 import { Connection } from "../connection/Connection";
3 +import { MessageHandler } from "../message/MessageHandler";
2 import { Room } from "../room/Room"; 4 import { Room } from "../room/Room";
3 -import { UserData } from "./types"; 5 +import { RoomManager } from "../room/RoomManager";
4 6
5 export class User { 7 export class User {
6 public readonly username: string; 8 public readonly username: string;
...@@ -9,9 +11,28 @@ export class User { ...@@ -9,9 +11,28 @@ export class User {
9 11
10 public room?: Room; 12 public room?: Room;
11 13
14 + public handler: MessageHandler;
15 +
12 constructor(username: string, connection: Connection) { 16 constructor(username: string, connection: Connection) {
13 this.username = username; 17 this.username = username;
14 this.connection = connection; 18 this.connection = connection;
19 + this.handler = new MessageHandler({
20 + roomList: (user, message) => {
21 + return { ok: true, result: RoomManager.instance().list() };
22 + },
23 + joinRoom: (user, message) => {
24 + const room = RoomManager.instance().get(message.uuid);
25 + if (user.room || !room) {
26 + return { ok: false };
27 + }
28 + // TODO: 방 접속 실패 처리
29 + room.connect(user);
30 + if (user.room === undefined) {
31 + return { ok: false };
32 + }
33 + return { ok: true, result: room.getInfo() };
34 + },
35 + });
15 } 36 }
16 37
17 public getData(): UserData { 38 public getData(): UserData {
......
1 -export interface UserData {
2 - username: string;
3 -}
1 { 1 {
2 "compilerOptions": { 2 "compilerOptions": {
3 "target": "es5", 3 "target": "es5",
4 - "lib": [ 4 + "lib": ["dom", "dom.iterable", "esnext"],
5 - "dom",
6 - "dom.iterable",
7 - "esnext"
8 - ],
9 "allowJs": true, 5 "allowJs": true,
10 "skipLibCheck": true, 6 "skipLibCheck": true,
11 "esModuleInterop": true, 7 "esModuleInterop": true,
...@@ -20,7 +16,10 @@ ...@@ -20,7 +16,10 @@
20 "noEmit": true, 16 "noEmit": true,
21 "jsx": "react-jsx" 17 "jsx": "react-jsx"
22 }, 18 },
23 - "include": [ 19 + "include": ["src"],
24 - "src" 20 + "references": [
21 + {
22 + "path": "../common"
23 + }
25 ] 24 ]
26 } 25 }
......