@fuzionx/framework 0.1.45 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/cli/index.js +37 -8
- package/cli/templates/make/app-spa/controllers/AuthController.js +114 -0
- package/cli/templates/make/app-spa/controllers/HomeController.js +66 -0
- package/cli/templates/make/app-spa/controllers/PostController.js +191 -0
- package/cli/templates/make/app-spa/controllers/UserController.js +43 -0
- package/cli/templates/make/app-spa/public/css/style.css +1011 -0
- package/cli/templates/make/app-spa/routes/api.js +31 -0
- package/cli/templates/make/app-spa/routes/web.js +19 -0
- package/cli/templates/make/app-spa/services/AuthService.js +48 -0
- package/cli/templates/make/app-spa/services/PostService.js +372 -0
- package/cli/templates/make/app-spa/services/UserService.js +48 -0
- package/cli/templates/make/app-spa/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-spa/views/default/errors/500.html +11 -0
- package/cli/templates/make/app-spa/views/default/layouts/main.html +34 -0
- package/cli/templates/make/app-spa/views/default/pages/home.html +22 -0
- package/cli/templates/make/app-spa/views/default/spa/index.html +13 -0
- package/cli/templates/make/app-spa/views/default/spa/package.json +20 -0
- package/cli/templates/make/app-spa/views/default/spa/src/App.vue +41 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/landing.css +220 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/style.css +1156 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/AlertDialog.vue +179 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/CodeBlock.vue +33 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FileUpload.vue +161 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FlashMessage.vue +39 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Lightbox.vue +62 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Navbar.vue +68 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Pagination.vue +166 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/ToastContainer.vue +135 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useApi.js +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useClipboard.js +44 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDate.js +73 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDebounce.js +59 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useFlash.js +46 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocale.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useWebSocket.js +93 -0
- package/cli/templates/make/app-spa/views/default/spa/src/main.js +106 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/alert.js +96 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/toast.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/router/index.js +29 -0
- package/cli/templates/make/app-spa/views/default/spa/src/stores/auth.js +58 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardDetail.vue +169 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardForm.vue +192 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardList.vue +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/ChatView.vue +317 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/FeaturesView.vue +242 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/HomeView.vue +215 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Login.vue +82 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Profile.vue +85 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Register.vue +84 -0
- package/cli/templates/make/app-spa/views/default/spa/vite.config.js +28 -0
- package/cli/templates/make/app-spa/views/default/spa/yarn.lock +633 -0
- package/cli/templates/make/app-spa/ws/ChatHandler.js +138 -0
- package/cli/templates/make/app-ssr/controllers/AuthController.js +119 -0
- package/cli/templates/make/app-ssr/controllers/ChatController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/FeaturesController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/HomeController.js +21 -0
- package/cli/templates/make/app-ssr/controllers/PostController.js +214 -0
- package/cli/templates/make/app-ssr/controllers/UserController.js +48 -0
- package/cli/templates/make/app-ssr/public/css/fx-ui.css +43 -0
- package/cli/templates/make/app-ssr/public/css/landing.css +220 -0
- package/cli/templates/make/app-ssr/public/css/style.css +1011 -0
- package/cli/templates/make/app-ssr/public/js/fx-client.js +107 -0
- package/cli/templates/make/app-ssr/public/js/fx-ui.js +124 -0
- package/cli/templates/make/app-ssr/routes/web.js +46 -0
- package/cli/templates/make/app-ssr/services/AuthService.js +48 -0
- package/cli/templates/make/app-ssr/services/PostService.js +372 -0
- package/cli/templates/make/app-ssr/services/UserService.js +48 -0
- package/cli/templates/make/app-ssr/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-ssr/views/default/errors/500.html +48 -0
- package/cli/templates/make/app-ssr/views/default/layouts/main.html +96 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/form.html +240 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/index.html +73 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/show.html +148 -0
- package/cli/templates/make/app-ssr/views/default/pages/chat.html +288 -0
- package/cli/templates/make/app-ssr/views/default/pages/features.html +373 -0
- package/cli/templates/make/app-ssr/views/default/pages/home.html +258 -0
- package/cli/templates/make/app-ssr/views/default/pages/login.html +27 -0
- package/cli/templates/make/app-ssr/views/default/pages/profile.html +36 -0
- package/cli/templates/make/app-ssr/views/default/pages/register.html +35 -0
- package/cli/templates/make/app-ssr/views/default/partials/pagination.html +75 -0
- package/cli/templates/make/app-ssr/ws/ChatHandler.js +138 -0
- package/lib/core/Application.js +425 -138
- package/lib/core/Context.js +540 -236
- package/lib/middleware/auth.js +1 -1
- package/lib/middleware/csrf.js +1 -1
- package/lib/middleware/session.js +5 -4
- package/package.json +2 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{{ t(key="page.login", default="로그인") }} — FuzionX{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="auth-container">
|
|
5
|
+
<div class="glass-card auth-card">
|
|
6
|
+
<div class="auth-logo">FuzionX</div>
|
|
7
|
+
<p class="auth-subtitle">{{ t(key="page.login", default="로그인") }}</p>
|
|
8
|
+
|
|
9
|
+
<form class="auth-form" action="/login" method="POST">
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label for="email">{{ t(key="label.email", default="이메일") }}</label>
|
|
12
|
+
<input type="email" id="email" name="email" required placeholder="name@example.com">
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form-group">
|
|
15
|
+
<label for="password">{{ t(key="label.password", default="비밀번호") }}</label>
|
|
16
|
+
<input type="password" id="password" name="password" required>
|
|
17
|
+
</div>
|
|
18
|
+
<button type="submit" class="btn btn-primary btn-full">{{ t(key="btn.login", default="로그인") }}</button>
|
|
19
|
+
</form>
|
|
20
|
+
|
|
21
|
+
<div class="auth-footer">
|
|
22
|
+
{{ t(key="auth.no_account", default="계정이 없으신가요?") }}
|
|
23
|
+
<a href="/register">{{ t(key="btn.register", default="회원가입") }}</a>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
{% endblock %}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{{ t(key="profile.title", default="프로필 수정") }} — FuzionX{% endblock %}
|
|
3
|
+
{% block flash_messages %}{% endblock %}
|
|
4
|
+
{% block content %}
|
|
5
|
+
<div class="container">
|
|
6
|
+
<div class="glass-card">
|
|
7
|
+
<h1 class="page-title">{{ t(key="profile.title", default="프로필 수정") }}</h1>
|
|
8
|
+
|
|
9
|
+
<form class="form" action="/profile" method="POST">
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label for="name">{{ t(key="label.name", default="이름") }}</label>
|
|
12
|
+
<input type="text" id="name" name="name" value="{{ auth.user.name }}" required>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form-group">
|
|
15
|
+
<label for="email">{{ t(key="label.email", default="이메일") }}</label>
|
|
16
|
+
<input type="email" id="email" name="email" value="{{ auth.user.email }}" required>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label for="password">{{ t(key="profile.new_password", default="새 비밀번호 (변경 시에만)") }}</label>
|
|
20
|
+
<input type="password" id="password" name="password">
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="password_confirm">{{ t(key="label.password_confirm", default="비밀번호 확인") }}</label>
|
|
24
|
+
<input type="password" id="password_confirm" name="password_confirm">
|
|
25
|
+
</div>
|
|
26
|
+
{% if flash.error %}
|
|
27
|
+
<div class="alert alert-error">{{ flash.error }}</div>
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% if flash.success %}
|
|
30
|
+
<div class="alert alert-success">{{ flash.success }}</div>
|
|
31
|
+
{% endif %}
|
|
32
|
+
<button type="submit" class="btn btn-primary">{{ t(key="btn.save", default="저장") }}</button>
|
|
33
|
+
</form>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
{% endblock %}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{{ t(key="page.register", default="회원가입") }} — FuzionX{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="auth-container">
|
|
5
|
+
<div class="glass-card auth-card">
|
|
6
|
+
<div class="auth-logo">FuzionX</div>
|
|
7
|
+
<p class="auth-subtitle">{{ t(key="page.register", default="회원가입") }}</p>
|
|
8
|
+
|
|
9
|
+
<form class="auth-form" action="/register" method="POST">
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label for="name">{{ t(key="label.name", default="이름") }}</label>
|
|
12
|
+
<input type="text" id="name" name="name" required>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form-group">
|
|
15
|
+
<label for="email">{{ t(key="label.email", default="이메일") }}</label>
|
|
16
|
+
<input type="email" id="email" name="email" required placeholder="name@example.com">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label for="password">{{ t(key="label.password", default="비밀번호") }}</label>
|
|
20
|
+
<input type="password" id="password" name="password" required>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="password_confirm">{{ t(key="label.password_confirm", default="비밀번호 확인") }}</label>
|
|
24
|
+
<input type="password" id="password_confirm" name="password_confirm" required>
|
|
25
|
+
</div>
|
|
26
|
+
<button type="submit" class="btn btn-primary btn-full">{{ t(key="btn.register", default="회원가입") }}</button>
|
|
27
|
+
</form>
|
|
28
|
+
|
|
29
|
+
<div class="auth-footer">
|
|
30
|
+
{{ t(key="auth.has_account", default="이미 계정이 있으신가요?") }}
|
|
31
|
+
<a href="/login">{{ t(key="btn.login", default="로그인") }}</a>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
{% endblock %}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{#
|
|
2
|
+
pagination.html — 범용 페이지네이션 partial.
|
|
3
|
+
|
|
4
|
+
개발자가 include로 쉽게 사용할 수 있는 템플릿.
|
|
5
|
+
번호 페이지 버튼 + 이전/다음 + ellipsis 지원.
|
|
6
|
+
|
|
7
|
+
필요 변수:
|
|
8
|
+
- page: 현재 페이지 (number)
|
|
9
|
+
- lastPage: 마지막 페이지 (number)
|
|
10
|
+
- baseUrl: 페이지 링크 기본 URL (예: "/board")
|
|
11
|
+
|
|
12
|
+
사용법:
|
|
13
|
+
{% set baseUrl = "/board" %}
|
|
14
|
+
{% include "partials/pagination.html" %}
|
|
15
|
+
#}
|
|
16
|
+
|
|
17
|
+
{% if lastPage > 1 %}
|
|
18
|
+
<nav class="pagination" role="navigation" aria-label="Pagination">
|
|
19
|
+
{# 이전 #}
|
|
20
|
+
{% if page > 1 %}
|
|
21
|
+
<a href="{{ baseUrl }}?page={{ page - 1 }}" class="page-btn page-prev">←</a>
|
|
22
|
+
{% else %}
|
|
23
|
+
<span class="page-btn page-prev disabled">←</span>
|
|
24
|
+
{% endif %}
|
|
25
|
+
|
|
26
|
+
{# 첫 페이지 #}
|
|
27
|
+
{% if page == 1 %}
|
|
28
|
+
<span class="page-btn page-num active" aria-current="page">1</span>
|
|
29
|
+
{% else %}
|
|
30
|
+
<a href="{{ baseUrl }}?page=1" class="page-btn page-num">1</a>
|
|
31
|
+
{% endif %}
|
|
32
|
+
|
|
33
|
+
{# 앞쪽 ellipsis #}
|
|
34
|
+
{% if page > 3 %}
|
|
35
|
+
<span class="page-ellipsis">…</span>
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
{# 중간 범위 (page-1, page, page+1) #}
|
|
39
|
+
{% if page - 1 > 1 and page - 1 < lastPage %}
|
|
40
|
+
<a href="{{ baseUrl }}?page={{ page - 1 }}" class="page-btn page-num">{{ page - 1 }}</a>
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
{% if page != 1 and page != lastPage %}
|
|
44
|
+
<span class="page-btn page-num active" aria-current="page">{{ page }}</span>
|
|
45
|
+
{% endif %}
|
|
46
|
+
|
|
47
|
+
{% if page + 1 < lastPage and page + 1 > 1 %}
|
|
48
|
+
<a href="{{ baseUrl }}?page={{ page + 1 }}" class="page-btn page-num">{{ page + 1 }}</a>
|
|
49
|
+
{% endif %}
|
|
50
|
+
|
|
51
|
+
{# 뒤쪽 ellipsis #}
|
|
52
|
+
{% if page < lastPage - 2 %}
|
|
53
|
+
<span class="page-ellipsis">…</span>
|
|
54
|
+
{% endif %}
|
|
55
|
+
|
|
56
|
+
{# 마지막 페이지 #}
|
|
57
|
+
{% if lastPage > 1 %}
|
|
58
|
+
{% if page == lastPage %}
|
|
59
|
+
<span class="page-btn page-num active" aria-current="page">{{ lastPage }}</span>
|
|
60
|
+
{% else %}
|
|
61
|
+
<a href="{{ baseUrl }}?page={{ lastPage }}" class="page-btn page-num">{{ lastPage }}</a>
|
|
62
|
+
{% endif %}
|
|
63
|
+
{% endif %}
|
|
64
|
+
|
|
65
|
+
{# 다음 #}
|
|
66
|
+
{% if page < lastPage %}
|
|
67
|
+
<a href="{{ baseUrl }}?page={{ page + 1 }}" class="page-btn page-next">→</a>
|
|
68
|
+
{% else %}
|
|
69
|
+
<span class="page-btn page-next disabled">→</span>
|
|
70
|
+
{% endif %}
|
|
71
|
+
|
|
72
|
+
{# 모바일 정보 #}
|
|
73
|
+
<span class="page-info-mobile">{{ page }} / {{ lastPage }}</span>
|
|
74
|
+
</nav>
|
|
75
|
+
{% endif %}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { WsHandler } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
/** 채팅 WebSocket 핸들러 */
|
|
4
|
+
export default class ChatHandler extends WsHandler {
|
|
5
|
+
static namespace = '/chat';
|
|
6
|
+
static middleware = ['auth'];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* disconnect 시 metadata가 이미 삭제되므로,
|
|
10
|
+
* setUser 시점에 이름을 캐시하여 disconnect에서 사용.
|
|
11
|
+
* 연결/해제는 항상 같은 워커에서 발생하므로 per-worker 캐시로 충분.
|
|
12
|
+
*/
|
|
13
|
+
static _nameCache = new Map();
|
|
14
|
+
|
|
15
|
+
static events(e) {
|
|
16
|
+
e.on('message', ChatHandler.prototype.handleMessage);
|
|
17
|
+
e.on('broadcast', ChatHandler.prototype.handleBroadcast);
|
|
18
|
+
e.on('typing', ChatHandler.prototype.handleTyping);
|
|
19
|
+
e.on('userlist', ChatHandler.prototype.handleUserList);
|
|
20
|
+
e.on('setUser', ChatHandler.prototype.handleSetUser);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── helpers ── */
|
|
24
|
+
|
|
25
|
+
/** 사용자 이름 조회 (캐시 → metadata → UUID 폴백) */
|
|
26
|
+
_getName(socket, sid) {
|
|
27
|
+
return ChatHandler._nameCache.get(sid)
|
|
28
|
+
|| socket.getMetadataFor(sid, 'name')
|
|
29
|
+
|| sid;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 단일 사용자 정보 빌드 */
|
|
33
|
+
_buildUserInfo(socket, sid) {
|
|
34
|
+
return {
|
|
35
|
+
sid,
|
|
36
|
+
name: this._getName(socket, sid),
|
|
37
|
+
email: socket.getMetadataFor(sid, 'email') || '',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 전체 접속자 목록 빌드 */
|
|
42
|
+
_buildUserList(socket) {
|
|
43
|
+
const sessions = socket.sessionIds;
|
|
44
|
+
return {
|
|
45
|
+
count: socket.onlineCount,
|
|
46
|
+
users: sessions.map(sid => this._buildUserInfo(socket, sid)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── lifecycle ── */
|
|
51
|
+
|
|
52
|
+
async onConnect(socket) {
|
|
53
|
+
const sid = socket.sessionId;
|
|
54
|
+
console.log(`[Chat] 연결: ${sid}`);
|
|
55
|
+
|
|
56
|
+
socket.send(JSON.stringify({
|
|
57
|
+
type: 'chat_ready',
|
|
58
|
+
data: { sessionId: sid, timestamp: Date.now() },
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async onDisconnect(socket, code, reason) {
|
|
63
|
+
const sid = socket.sessionId;
|
|
64
|
+
const name = ChatHandler._nameCache.get(sid);
|
|
65
|
+
ChatHandler._nameCache.delete(sid);
|
|
66
|
+
|
|
67
|
+
// setUser 미완료 세션은 user_joined도 안 보냈으므로 user_left 불필요
|
|
68
|
+
if (!name) return;
|
|
69
|
+
|
|
70
|
+
console.log(`[Chat] 해제: ${name} code=${code ?? 'N/A'}`);
|
|
71
|
+
socket.broadcastExcluding(JSON.stringify({
|
|
72
|
+
type: 'user_left',
|
|
73
|
+
data: { sid, name, timestamp: Date.now() },
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── event handlers ── */
|
|
78
|
+
|
|
79
|
+
/** setUser — 사용자 정보 등록 + userlist 응답 */
|
|
80
|
+
async handleSetUser(socket, data) {
|
|
81
|
+
const sid = socket.sessionId;
|
|
82
|
+
const name = (data.name || '').trim() || sid;
|
|
83
|
+
|
|
84
|
+
// Bridge metadata + 로컬 캐시 동시 저장
|
|
85
|
+
socket.setMetadata('name', name);
|
|
86
|
+
if (data.email) socket.setMetadata('email', data.email);
|
|
87
|
+
if (data.id) socket.setMetadata('userId', String(data.id));
|
|
88
|
+
ChatHandler._nameCache.set(sid, name);
|
|
89
|
+
console.log(`[Chat] 사용자 등록: ${sid} → ${name}`);
|
|
90
|
+
|
|
91
|
+
// 다른 사용자에게 참여 알림
|
|
92
|
+
socket.broadcastExcluding(JSON.stringify({
|
|
93
|
+
type: 'user_joined',
|
|
94
|
+
data: this._buildUserInfo(socket, sid),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// 본인에게 전체 목록 응답
|
|
98
|
+
return {
|
|
99
|
+
type: 'userlist',
|
|
100
|
+
data: this._buildUserList(socket),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** chat_msg — 본인 제외 전체 전송 */
|
|
105
|
+
async handleMessage(socket, data) {
|
|
106
|
+
const name = this._getName(socket, socket.sessionId);
|
|
107
|
+
socket.broadcastExcluding(JSON.stringify({
|
|
108
|
+
type: 'chat_msg',
|
|
109
|
+
data: { user: name, text: data.text || data, timestamp: Date.now() },
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** broadcast — 공지 */
|
|
114
|
+
async handleBroadcast(socket, data) {
|
|
115
|
+
const name = this._getName(socket, socket.sessionId);
|
|
116
|
+
socket.broadcastExcluding(JSON.stringify({
|
|
117
|
+
type: 'broadcast',
|
|
118
|
+
data: { user: name, text: data.text || data, timestamp: Date.now() },
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** typing — 타이핑 표시 */
|
|
123
|
+
async handleTyping(socket) {
|
|
124
|
+
const name = this._getName(socket, socket.sessionId);
|
|
125
|
+
socket.broadcastExcluding(JSON.stringify({
|
|
126
|
+
type: 'typing',
|
|
127
|
+
data: { user: name },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** userlist — 목록 요청 */
|
|
132
|
+
async handleUserList(socket) {
|
|
133
|
+
return {
|
|
134
|
+
type: 'userlist',
|
|
135
|
+
data: this._buildUserList(socket),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|