@fuzionx/framework 0.1.46 → 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.
Files changed (87) hide show
  1. package/README.md +29 -2
  2. package/cli/index.js +37 -8
  3. package/cli/templates/make/app-spa/controllers/AuthController.js +114 -0
  4. package/cli/templates/make/app-spa/controllers/HomeController.js +66 -0
  5. package/cli/templates/make/app-spa/controllers/PostController.js +191 -0
  6. package/cli/templates/make/app-spa/controllers/UserController.js +43 -0
  7. package/cli/templates/make/app-spa/public/css/style.css +1011 -0
  8. package/cli/templates/make/app-spa/routes/api.js +31 -0
  9. package/cli/templates/make/app-spa/routes/web.js +19 -0
  10. package/cli/templates/make/app-spa/services/AuthService.js +48 -0
  11. package/cli/templates/make/app-spa/services/PostService.js +372 -0
  12. package/cli/templates/make/app-spa/services/UserService.js +48 -0
  13. package/cli/templates/make/app-spa/views/default/errors/404.html +11 -0
  14. package/cli/templates/make/app-spa/views/default/errors/500.html +11 -0
  15. package/cli/templates/make/app-spa/views/default/layouts/main.html +34 -0
  16. package/cli/templates/make/app-spa/views/default/pages/home.html +22 -0
  17. package/cli/templates/make/app-spa/views/default/spa/index.html +13 -0
  18. package/cli/templates/make/app-spa/views/default/spa/package.json +20 -0
  19. package/cli/templates/make/app-spa/views/default/spa/src/App.vue +41 -0
  20. package/cli/templates/make/app-spa/views/default/spa/src/assets/landing.css +220 -0
  21. package/cli/templates/make/app-spa/views/default/spa/src/assets/style.css +1156 -0
  22. package/cli/templates/make/app-spa/views/default/spa/src/components/AlertDialog.vue +179 -0
  23. package/cli/templates/make/app-spa/views/default/spa/src/components/CodeBlock.vue +33 -0
  24. package/cli/templates/make/app-spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
  25. package/cli/templates/make/app-spa/views/default/spa/src/components/FileUpload.vue +161 -0
  26. package/cli/templates/make/app-spa/views/default/spa/src/components/FlashMessage.vue +39 -0
  27. package/cli/templates/make/app-spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
  28. package/cli/templates/make/app-spa/views/default/spa/src/components/Lightbox.vue +62 -0
  29. package/cli/templates/make/app-spa/views/default/spa/src/components/Navbar.vue +68 -0
  30. package/cli/templates/make/app-spa/views/default/spa/src/components/Pagination.vue +166 -0
  31. package/cli/templates/make/app-spa/views/default/spa/src/components/ToastContainer.vue +135 -0
  32. package/cli/templates/make/app-spa/views/default/spa/src/composables/useApi.js +129 -0
  33. package/cli/templates/make/app-spa/views/default/spa/src/composables/useClipboard.js +44 -0
  34. package/cli/templates/make/app-spa/views/default/spa/src/composables/useDate.js +73 -0
  35. package/cli/templates/make/app-spa/views/default/spa/src/composables/useDebounce.js +59 -0
  36. package/cli/templates/make/app-spa/views/default/spa/src/composables/useFlash.js +46 -0
  37. package/cli/templates/make/app-spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
  38. package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
  39. package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocale.js +79 -0
  40. package/cli/templates/make/app-spa/views/default/spa/src/composables/useWebSocket.js +93 -0
  41. package/cli/templates/make/app-spa/views/default/spa/src/main.js +106 -0
  42. package/cli/templates/make/app-spa/views/default/spa/src/plugins/alert.js +96 -0
  43. package/cli/templates/make/app-spa/views/default/spa/src/plugins/toast.js +79 -0
  44. package/cli/templates/make/app-spa/views/default/spa/src/router/index.js +29 -0
  45. package/cli/templates/make/app-spa/views/default/spa/src/stores/auth.js +58 -0
  46. package/cli/templates/make/app-spa/views/default/spa/src/views/BoardDetail.vue +169 -0
  47. package/cli/templates/make/app-spa/views/default/spa/src/views/BoardForm.vue +192 -0
  48. package/cli/templates/make/app-spa/views/default/spa/src/views/BoardList.vue +129 -0
  49. package/cli/templates/make/app-spa/views/default/spa/src/views/ChatView.vue +317 -0
  50. package/cli/templates/make/app-spa/views/default/spa/src/views/FeaturesView.vue +242 -0
  51. package/cli/templates/make/app-spa/views/default/spa/src/views/HomeView.vue +215 -0
  52. package/cli/templates/make/app-spa/views/default/spa/src/views/Login.vue +82 -0
  53. package/cli/templates/make/app-spa/views/default/spa/src/views/Profile.vue +85 -0
  54. package/cli/templates/make/app-spa/views/default/spa/src/views/Register.vue +84 -0
  55. package/cli/templates/make/app-spa/views/default/spa/vite.config.js +28 -0
  56. package/cli/templates/make/app-spa/views/default/spa/yarn.lock +633 -0
  57. package/cli/templates/make/app-spa/ws/ChatHandler.js +138 -0
  58. package/cli/templates/make/app-ssr/controllers/AuthController.js +119 -0
  59. package/cli/templates/make/app-ssr/controllers/ChatController.js +15 -0
  60. package/cli/templates/make/app-ssr/controllers/FeaturesController.js +15 -0
  61. package/cli/templates/make/app-ssr/controllers/HomeController.js +21 -0
  62. package/cli/templates/make/app-ssr/controllers/PostController.js +214 -0
  63. package/cli/templates/make/app-ssr/controllers/UserController.js +48 -0
  64. package/cli/templates/make/app-ssr/public/css/fx-ui.css +43 -0
  65. package/cli/templates/make/app-ssr/public/css/landing.css +220 -0
  66. package/cli/templates/make/app-ssr/public/css/style.css +1011 -0
  67. package/cli/templates/make/app-ssr/public/js/fx-client.js +107 -0
  68. package/cli/templates/make/app-ssr/public/js/fx-ui.js +124 -0
  69. package/cli/templates/make/app-ssr/routes/web.js +46 -0
  70. package/cli/templates/make/app-ssr/services/AuthService.js +48 -0
  71. package/cli/templates/make/app-ssr/services/PostService.js +372 -0
  72. package/cli/templates/make/app-ssr/services/UserService.js +48 -0
  73. package/cli/templates/make/app-ssr/views/default/errors/404.html +11 -0
  74. package/cli/templates/make/app-ssr/views/default/errors/500.html +48 -0
  75. package/cli/templates/make/app-ssr/views/default/layouts/main.html +96 -0
  76. package/cli/templates/make/app-ssr/views/default/pages/board/form.html +240 -0
  77. package/cli/templates/make/app-ssr/views/default/pages/board/index.html +73 -0
  78. package/cli/templates/make/app-ssr/views/default/pages/board/show.html +148 -0
  79. package/cli/templates/make/app-ssr/views/default/pages/chat.html +288 -0
  80. package/cli/templates/make/app-ssr/views/default/pages/features.html +373 -0
  81. package/cli/templates/make/app-ssr/views/default/pages/home.html +258 -0
  82. package/cli/templates/make/app-ssr/views/default/pages/login.html +27 -0
  83. package/cli/templates/make/app-ssr/views/default/pages/profile.html +36 -0
  84. package/cli/templates/make/app-ssr/views/default/pages/register.html +35 -0
  85. package/cli/templates/make/app-ssr/views/default/partials/pagination.html +75 -0
  86. package/cli/templates/make/app-ssr/ws/ChatHandler.js +138 -0
  87. package/package.json +2 -2
@@ -0,0 +1,317 @@
1
+ <!--
2
+ ChatView.vue — WebSocket 채팅 페이지.
3
+
4
+ SSR chat.html 완전 복제:
5
+ Join Screen (닉네임 입력 → 접속) + Chat Room (사이드바 유저 리스트 + 메시지 영역).
6
+ useWebSocket composable로 WASM FuzionXSocket 사용.
7
+ -->
8
+ <template>
9
+ <div class="container">
10
+ <!-- ── Join Screen ── -->
11
+ <div v-if="!joined" class="chat-join-screen">
12
+ <div class="glass-card chat-join-card">
13
+ <div class="chat-join-icon">💬</div>
14
+ <h1 class="chat-join-title">WebSocket <span class="gradient-text">{{ t('chat.demo_title', 'Chat Demo') }}</span></h1>
15
+ <p class="chat-join-sub">
16
+ {{ t('chat.demo_sub', 'Experience FuzionX real-time WebSocket in action.') }}<br>
17
+ {{ t('chat.demo_tech', 'Built with') }} <code>WsHandler</code> + <code>FuzionXSocket</code> WASM client.
18
+ </p>
19
+ <div class="form-group" style="margin-top:2rem;">
20
+ <input
21
+ ref="nicknameInput"
22
+ v-model="nickname"
23
+ type="text"
24
+ class="chat-nickname-input"
25
+ :placeholder="t('chat.nickname_placeholder', 'Enter your nickname...')"
26
+ maxlength="20"
27
+ autocomplete="off"
28
+ @keydown.enter="joinChat"
29
+ />
30
+ </div>
31
+ <button
32
+ class="btn btn-hero-primary btn-full"
33
+ :disabled="joining"
34
+ @click="joinChat"
35
+ >
36
+ <span>{{ joining ? t('chat.connecting', 'connecting...') : t('chat.btn_join', 'Join Chat') }}</span>
37
+ </button>
38
+ <div class="chat-join-tech">
39
+ <span class="feature-tag tag-ws">FuzionXSocket</span>
40
+ <span class="feature-tag tag-rust">WASM</span>
41
+ <span class="feature-tag tag-node">Auto ASP</span>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- ── Chat Room ── -->
47
+ <div v-else class="chat-room">
48
+ <div class="chat-layout">
49
+ <!-- Sidebar: User List -->
50
+ <aside class="chat-sidebar glass-card">
51
+ <div class="chat-sidebar-header">
52
+ <h3>{{ t('chat.online', 'Online') }} <span class="chat-user-count">{{ userCount }}</span></h3>
53
+ </div>
54
+ <ul class="chat-user-list">
55
+ <li
56
+ v-for="user in users"
57
+ :key="user.sid"
58
+ :class="['chat-user-item', { 'is-me': user.sid === mySessionId }]"
59
+ >
60
+ <span class="chat-user-dot"></span>
61
+ <span class="chat-user-name">{{ user.name }}</span>
62
+ <span v-if="user.sid === mySessionId" class="chat-user-me">you</span>
63
+ </li>
64
+ </ul>
65
+ <div class="chat-sidebar-footer">
66
+ <button class="btn btn-outline btn-full" style="font-size:0.85rem;" @click="leaveChat">
67
+ {{ t('chat.btn_leave', 'Leave Chat') }}
68
+ </button>
69
+ </div>
70
+ </aside>
71
+
72
+ <!-- Main: Messages -->
73
+ <div class="chat-main glass-card">
74
+ <div class="chat-messages-header">
75
+ <h3>💬 {{ t('chat.room_title', 'Chat Room') }}</h3>
76
+ <span :class="['chat-status', `chat-status-${statusClass}`]">{{ statusText }}</span>
77
+ </div>
78
+ <div ref="messagesContainer" class="chat-messages">
79
+ <template v-for="(msg, i) in messages" :key="i">
80
+ <div v-if="msg.system" class="chat-msg-system">{{ msg.text }}</div>
81
+ <div v-else :class="['chat-msg', msg.isMe ? 'chat-msg-me' : 'chat-msg-other']">
82
+ <div class="chat-msg-bubble">
83
+ <span v-if="!msg.isMe" class="chat-msg-sender">{{ msg.sender }}</span>
84
+ <span class="chat-msg-text">{{ msg.message }}</span>
85
+ <span class="chat-msg-time">{{ msg.time }}</span>
86
+ </div>
87
+ </div>
88
+ </template>
89
+ </div>
90
+ <div v-if="typingUser" class="chat-typing">{{ typingUser }} {{ t('chat.typing', 'is typing...') }}</div>
91
+ <form class="chat-input-form" @submit.prevent="sendMessage">
92
+ <input
93
+ ref="messageInput"
94
+ v-model="messageText"
95
+ type="text"
96
+ class="chat-message-input"
97
+ :placeholder="t('chat.msg_placeholder', 'Type a message...')"
98
+ autocomplete="off"
99
+ maxlength="500"
100
+ @input="onTyping"
101
+ />
102
+ <button type="submit" class="btn btn-primary chat-send-btn">{{ t('chat.btn_send', 'Send') }}</button>
103
+ </form>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- ── Footer ── -->
110
+ <footer class="site-footer">
111
+ <div class="container">
112
+ <div class="footer-content">
113
+ <span class="footer-brand">FuzionX</span>
114
+ <span class="footer-text">{{ t('chat.footer', 'Powered by WsHandler') }} — <router-link to="/features#websocket" style="color:var(--accent-solid);">{{ t('chat.footer_link', 'WebSocket Features') }}</router-link></span>
115
+ </div>
116
+ </div>
117
+ </footer>
118
+ </template>
119
+
120
+ <script setup>
121
+ import { ref, computed, nextTick, onMounted, onUnmounted, inject } from 'vue';
122
+ import { useLocale } from '../composables/useLocale.js';
123
+ import { useAuthStore } from '../stores/auth.js';
124
+
125
+ const { t } = useLocale();
126
+ const authStore = useAuthStore();
127
+ const fxConfig = inject('fxConfig', {});
128
+ const injectedMasterSecret = inject('masterSecret', '');
129
+
130
+ /* ── State ── */
131
+ const joined = ref(false);
132
+ const joining = ref(false);
133
+ const nickname = ref('');
134
+ const mySessionId = ref(null);
135
+ const users = ref([]);
136
+ const messages = ref([]);
137
+ const messageText = ref('');
138
+ const statusClass = ref('connecting');
139
+ const statusText = ref(t('chat.connecting', 'connecting...'));
140
+ const typingUser = ref('');
141
+ const nicknameInput = ref(null);
142
+ const messageInput = ref(null);
143
+ const messagesContainer = ref(null);
144
+
145
+ let ws = null;
146
+ let typingTimeout = null;
147
+ let lastTypingSent = 0;
148
+
149
+ const userCount = computed(() => users.value.length);
150
+
151
+ /* ── HTML 이스케이프 ── */
152
+ const ESC_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
153
+ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ESC_MAP[c]); }
154
+
155
+ /* ── 로그인 사용자 닉네임 자동 입력 ── */
156
+ onMounted(() => {
157
+ if (authStore.user?.name) {
158
+ nickname.value = authStore.user.name;
159
+ }
160
+ });
161
+
162
+ /* ── 컴포넌트 언마운트 시 정리 ── */
163
+ onUnmounted(() => {
164
+ if (ws) { ws.disconnect(); ws = null; }
165
+ if (typingTimeout) clearTimeout(typingTimeout);
166
+ });
167
+
168
+ /* ── Join Chat ── */
169
+ async function joinChat() {
170
+ const name = nickname.value.trim();
171
+ if (!name) {
172
+ nicknameInput.value?.focus();
173
+ return;
174
+ }
175
+ joining.value = true;
176
+ await connectWS();
177
+ }
178
+
179
+ /* ── WebSocket 연결 ── */
180
+ async function connectWS() {
181
+ const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
182
+ const url = `${wsProto}//${location.host}/ws/chat`;
183
+
184
+ // WASM 로드 대기
185
+ if (window._aspReady) await window._aspReady;
186
+
187
+ // FuzionXSocket이 아직 없으면 직접 WASM 로드
188
+ if (!window.FuzionXSocket) {
189
+ try {
190
+ const _v = Date.now();
191
+ const mod = await import(/* @vite-ignore */ `/wasm/fuzionx_client_wasm.js?v=${_v}`);
192
+ await mod.default({ module_or_path: `/wasm/fuzionx_client_wasm_bg.wasm?v=${_v}` });
193
+ window.FuzionXSocket = mod.FuzionXSocket;
194
+ } catch (e) {
195
+ console.error('[Chat] WASM load failed:', e);
196
+ joining.value = false;
197
+ return;
198
+ }
199
+ }
200
+
201
+ // masterSecret는 main.js에서 inject('masterSecret')로 제공됨
202
+ const masterSecret = injectedMasterSecret || '';
203
+
204
+ ws = new window.FuzionXSocket(url, masterSecret, window.__FX_ENC_ENABLED__);
205
+
206
+ ws.on('connect', () => { setStatus('connected', window.__FX_ENC_ENABLED__ ? 'connected (ASP 🔐)' : 'connected'); });
207
+
208
+ ws.on('message', (raw) => { console.log('[Chat] raw:', JSON.stringify(raw)); });
209
+
210
+ ws.on('chat_ready', (data) => {
211
+ if (!data || !data.sessionId) return;
212
+ mySessionId.value = data.sessionId;
213
+ joined.value = true;
214
+ joining.value = false;
215
+ appendSystemMessage(`${nickname.value} 접속`);
216
+ nextTick(() => { messageInput.value?.focus(); });
217
+
218
+ // 서버에 사용자 정보 전송
219
+ const payload = { name: nickname.value };
220
+ if (authStore.user?.email) payload.email = authStore.user.email;
221
+ if (authStore.user?.id) payload.id = authStore.user.id;
222
+ ws.send('setUser', payload);
223
+ });
224
+
225
+ ws.on('userlist', (data) => {
226
+ users.value = data.users || [];
227
+ });
228
+
229
+ ws.on('user_joined', (data) => {
230
+ if (!users.value.find(u => u.sid === data.sid)) {
231
+ users.value.push(data);
232
+ }
233
+ appendSystemMessage(`${data.name || 'unknown'} 입장`);
234
+ });
235
+
236
+ ws.on('user_left', (data) => {
237
+ users.value = users.value.filter(u => u.sid !== data.sid);
238
+ appendSystemMessage(`${data.name || 'unknown'} 퇴장`);
239
+ });
240
+
241
+ ws.on('system', (data) => { appendSystemMessage(data.text); });
242
+
243
+ ws.on('typing', (data) => {
244
+ typingUser.value = data.user;
245
+ clearTimeout(typingTimeout);
246
+ typingTimeout = setTimeout(() => { typingUser.value = ''; }, 2000);
247
+ });
248
+
249
+ ws.on('broadcast', (data) => { appendSystemMessage(`📢 ${data.user}: ${data.text}`); });
250
+
251
+ ws.on('chat_msg', (data) => {
252
+ appendMsg({ sender: data.user, message: data.text, timestamp: data.timestamp }, false);
253
+ });
254
+
255
+ ws.on('disconnect', () => { setStatus('disconnected', 'disconnected'); });
256
+ ws.on('reconnect', () => { setStatus('connecting', 'reconnecting...'); });
257
+ ws.on('error', () => { setStatus('error', 'connection error'); joining.value = false; });
258
+
259
+ ws.connect();
260
+ }
261
+
262
+ /* ── 메시지 전송 ── */
263
+ function sendMessage() {
264
+ const text = messageText.value.trim();
265
+ if (!text || !ws || !ws.is_connected()) return;
266
+ ws.send('message', { text });
267
+ appendMsg({ sender: nickname.value, message: text, timestamp: Date.now() }, true);
268
+ messageText.value = '';
269
+ messageInput.value?.focus();
270
+ }
271
+
272
+ /* ── 타이핑 인디케이터 ── */
273
+ function onTyping() {
274
+ if (!ws || !ws.is_connected()) return;
275
+ const now = Date.now();
276
+ if (now - lastTypingSent > 1500) {
277
+ ws.send('typing', {});
278
+ lastTypingSent = now;
279
+ }
280
+ }
281
+
282
+ /* ── Leave Chat ── */
283
+ function leaveChat() {
284
+ if (ws) { ws.disconnect(); ws = null; }
285
+ mySessionId.value = null;
286
+ joined.value = false;
287
+ joining.value = false;
288
+ users.value = [];
289
+ messages.value = [];
290
+ }
291
+
292
+ /* ── Helpers ── */
293
+ function setStatus(cls, text) {
294
+ statusClass.value = cls;
295
+ statusText.value = text;
296
+ }
297
+
298
+ function appendMsg(data, isMe) {
299
+ const time = data.timestamp
300
+ ? new Date(data.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
301
+ : '';
302
+ messages.value.push({ sender: data.sender, message: data.message, time, isMe, system: false });
303
+ scrollToBottom();
304
+ }
305
+
306
+ function appendSystemMessage(text) {
307
+ messages.value.push({ text, system: true });
308
+ scrollToBottom();
309
+ }
310
+
311
+ function scrollToBottom() {
312
+ nextTick(() => {
313
+ const el = messagesContainer.value;
314
+ if (el) el.scrollTop = el.scrollHeight;
315
+ });
316
+ }
317
+ </script>
@@ -0,0 +1,242 @@
1
+ <!--
2
+ FeaturesView.vue — 기능 소개 페이지.
3
+
4
+ SSR features.html 완전 복제:
5
+ 7개 feature 섹션 (Bridge, ORM, SSR+SPA, WebSocket, TaskQueue, Security, DX)
6
+ 각 섹션에 4개 카드 + 코드 블록 포함. CTA + Footer.
7
+ -->
8
+ <template>
9
+ <!-- ── Page Header ── -->
10
+ <section class="section">
11
+ <div class="container">
12
+ <div class="section-header">
13
+ <div class="hero-badge" style="margin-bottom:1rem;">📖 {{ t('feat.badge', 'Framework Features') }}</div>
14
+ <h1 class="section-title">{{ t('feat.header_title_1', 'Built for') }} <span class="gradient-text">{{ t('feat.header_title_2', 'Production') }}</span></h1>
15
+ <p class="section-subtitle">{{ t('feat.header_sub', 'Every feature designed for performance, developer experience, and enterprise readiness.') }}</p>
16
+ </div>
17
+ </div>
18
+ </section>
19
+
20
+ <!-- ── Feature Sections ── -->
21
+ <section
22
+ v-for="(feat, idx) in featureSections"
23
+ :key="feat.id"
24
+ :id="feat.id"
25
+ :class="['section', { 'section-dark': idx % 2 === 0 }]"
26
+ >
27
+ <div class="container">
28
+ <div class="feat-detail">
29
+ <div class="feat-detail-header">
30
+ <span class="feat-detail-icon">{{ feat.icon }}</span>
31
+ <div>
32
+ <h2 class="feat-detail-title">{{ feat.title }}</h2>
33
+ <p class="feat-detail-sub">{{ feat.sub }}</p>
34
+ </div>
35
+ </div>
36
+ <div :class="['feat-detail-grid', { 'feat-detail-grid-3': feat.cards.length === 3 }]">
37
+ <div v-for="card in feat.cards" :key="card.title" class="feat-detail-card">
38
+ <h4 v-html="card.title"></h4>
39
+ <p v-html="card.desc"></p>
40
+ </div>
41
+ </div>
42
+ <CodeBlock v-if="feat.code" :title="feat.code.title">
43
+ <span v-html="feat.code.body"></span>
44
+ </CodeBlock>
45
+ <!-- WebSocket 섹션 — Try Chat 버튼 -->
46
+ <div v-if="feat.id === 'websocket'" style="text-align:center; margin-top:2rem;">
47
+ <router-link to="/chat" class="btn btn-hero-primary"><span>{{ t('feat.btn_try_chat', 'Try Live Chat Demo →') }}</span></router-link>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </section>
52
+
53
+ <!-- ── CTA ── -->
54
+ <section class="cta-section">
55
+ <div class="container">
56
+ <div class="cta-card glass-card">
57
+ <h2 class="cta-title">{{ t('feat.cta_title_1', 'Start Building') }} <span class="gradient-text">{{ t('feat.cta_title_2', 'Today') }}</span></h2>
58
+ <p class="cta-subtitle">{{ t('feat.cta_sub', 'One command to scaffold. Zero configuration to start.') }}</p>
59
+ <div class="cta-install"><code>npx create-fuzionx my-app</code></div>
60
+ <div class="hero-actions" style="margin-top: 1.5rem;">
61
+ <router-link to="/" class="btn btn-hero-outline"><span>← {{ t('feat.btn_back', 'Back to Home') }}</span></router-link>
62
+ <router-link to="/chat" class="btn btn-hero-primary"><span>{{ t('feat.btn_chat', 'Try Chat Demo') }}</span></router-link>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </section>
67
+
68
+ <!-- ── Footer ── -->
69
+ <footer class="site-footer">
70
+ <div class="container">
71
+ <div class="footer-content">
72
+ <span class="footer-brand">FuzionX</span>
73
+ <span class="footer-text">{{ t('home.footer', 'Rust-powered Node.js framework for the modern web.') }}</span>
74
+ </div>
75
+ </div>
76
+ </footer>
77
+ </template>
78
+
79
+ <script setup>
80
+ import { computed } from 'vue';
81
+ import { useLocale } from '../composables/useLocale.js';
82
+ import CodeBlock from '../components/CodeBlock.vue';
83
+
84
+ const { t } = useLocale();
85
+
86
+ /** 7개 Feature 섹션 데이터 — SSR features.html */
87
+ const featureSections = computed(() => [
88
+ {
89
+ id: 'bridge', icon: '⚡',
90
+ title: t('feat.bridge_title', 'Rust N-API Bridge'),
91
+ sub: t('feat.bridge_sub', 'Native performance for core operations — no JavaScript overhead.'),
92
+ cards: [
93
+ { title: '500K+ RPS', desc: t('feat.bridge_rps', 'The HTTP engine runs entirely in Rust through N-API, delivering throughput that JavaScript-based servers cannot match. Request parsing, routing, and response serialization happen at native speed.') },
94
+ { title: t('feat.bridge_zerocopy', 'Zero-Copy File Uploads'), desc: t('feat.bridge_zerocopy_desc', 'Multipart parsing happens in Rust. Files are written directly to disk — the file buffer never enters JavaScript memory, making large file uploads safe and efficient.') },
95
+ { title: t('feat.bridge_crypto', 'Native Crypto'), desc: t('feat.bridge_crypto_desc', 'bcrypt, argon2id, AES-256-GCM, SHA-256, MD5, UUID v4 — all executed in Rust. No crypto module overhead, no C++ addon compilation issues.') },
96
+ { title: t('feat.bridge_media', 'Media Processing'), desc: t('feat.bridge_media_desc', 'Image resizing (Lanczos3), WebP conversion, and video thumbnail extraction powered by Rust image crate and ffmpeg bindings.') },
97
+ ],
98
+ code: {
99
+ title: t('feat.bridge_code_title', 'Native Performance — Zero JS Overhead'),
100
+ body: `<span class="code-cmt">// All these operations run in Rust — no JS overhead</span>
101
+ <span class="code-kw">const</span> hash = app.hash.<span class="code-fn">argon2</span>(password); <span class="code-cmt">// Rust argon2id</span>
102
+ <span class="code-kw">const</span> id = app.crypto.<span class="code-fn">uuid</span>(); <span class="code-cmt">// Rust UUID v4</span>
103
+ <span class="code-kw">const</span> enc = app.crypto.<span class="code-fn">encrypt</span>(key, plaintext); <span class="code-cmt">// Rust AES-256-GCM</span>
104
+ app.media.<span class="code-fn">resize</span>(input, output, <span class="code-num">800</span>, <span class="code-num">600</span>, <span class="code-str">'webp'</span>); <span class="code-cmt">// Rust image crate</span>`,
105
+ },
106
+ },
107
+ {
108
+ id: 'orm', icon: '🗄️',
109
+ title: t('feat.orm_title', 'Multi-Database ORM'),
110
+ sub: t('feat.orm_sub', 'One API for MariaDB, PostgreSQL, and MongoDB. Model = Schema = Single source of truth.'),
111
+ cards: [
112
+ { title: t('feat.orm_unified', 'Unified Query API'), desc: t('feat.orm_unified_desc', "Write User.where('active', true).paginate(1, 20) and it works across all databases. No database-specific code needed for CRUD operations.") },
113
+ { title: t('feat.orm_schema', 'Model-Based Schema Sync'), desc: t('feat.orm_schema_desc', 'Define columns in the model with static columns, then run fx db:sync. No more writing migration files — the model IS the schema.') },
114
+ { title: t('feat.orm_relations', 'Relationships'), desc: t('feat.orm_relations_desc', "hasMany, hasOne, belongsTo, belongsToMany — define relations as methods. Eager loading with .with('posts.comments').") },
115
+ { title: t('feat.orm_access', '3-Level Access'), desc: t('feat.orm_access_desc', 'Unified API for 99% of cases → Raw Query for DB-specific features → Native driver access when you need full control.') },
116
+ ],
117
+ code: {
118
+ title: 'models/User.js',
119
+ body: `<span class="code-kw">import</span> { Model } <span class="code-kw">from</span> <span class="code-str">'@fuzionx/framework'</span>;
120
+
121
+ <span class="code-kw">export default class</span> <span class="code-fn">User</span> <span class="code-kw">extends</span> Model {
122
+ <span class="code-kw">static</span> table = <span class="code-str">'users'</span>;
123
+ <span class="code-kw">static</span> timestamps = <span class="code-num">true</span>;
124
+
125
+ <span class="code-kw">static</span> columns = {
126
+ id: { type: <span class="code-str">'increments'</span> },
127
+ name: { type: <span class="code-str">'string'</span>, length: <span class="code-num">100</span> },
128
+ email: { type: <span class="code-str">'string'</span>, length: <span class="code-num">150</span>, unique: <span class="code-num">true</span> },
129
+ };
130
+
131
+ <span class="code-fn">posts</span>() { <span class="code-kw">return</span> <span class="code-kw">this</span>.hasMany(<span class="code-str">'Post'</span>); }
132
+ <span class="code-fn">profile</span>() { <span class="code-kw">return</span> <span class="code-kw">this</span>.hasOne(<span class="code-str">'Profile'</span>); }
133
+ }`,
134
+ },
135
+ },
136
+ {
137
+ id: 'rendering', icon: '🎨',
138
+ title: t('feat.ssr_title', 'SSR + SPA Hybrid'),
139
+ sub: t('feat.ssr_sub', 'Server-rendered pages and single-page apps in one project with domain routing.'),
140
+ cards: [
141
+ { title: t('feat.ssr_tera', 'Tera Template Engine'), desc: t('feat.ssr_tera_desc', 'Jinja2-compatible template engine compiled in Rust. Layouts, blocks, inheritance, filters, and macros for fast SSR rendering.') },
142
+ { title: t('feat.ssr_theme', 'Theme System'), desc: t('feat.ssr_theme_desc', 'Multiple view themes with views/{theme}/ structure. Switch themes via config without changing any controller code.') },
143
+ { title: t('feat.ssr_multiapp', 'Multi-App Routing'), desc: t('feat.ssr_multiapp_desc', 'Route different domains to different apps: api.example.com → spa, www.example.com → ssr. One codebase, multiple frontends.') },
144
+ { title: t('feat.ssr_i18n', 'Built-in i18n'), desc: t('feat.ssr_i18n_desc', "JSON-based translations with auto-detection, fallback locales, and missing key auto-complete.") + ` <code>ctx.t('key')</code> / <code>{{ '{{' }} t(key="...") }}</code>` },
145
+ ],
146
+ code: null,
147
+ },
148
+ {
149
+ id: 'websocket', icon: '🔌',
150
+ title: t('feat.ws_title', 'Real-time WebSocket'),
151
+ sub: t('feat.ws_sub', 'Namespace-based event routing with rooms, middleware sharing, and multi-server Hub.'),
152
+ cards: [
153
+ { title: t('feat.ws_dsl', 'Event Routing DSL'), desc: t('feat.ws_dsl_desc', 'static events(e) declares event → handler mappings, just like controller routes. Clean, declarative, and auto-scanned from ws/ folder.') },
154
+ { title: t('feat.ws_rooms', 'Rooms'), desc: t('feat.ws_rooms_desc', "socket.join('room:123'), socket.to('room:123').send({...}) — built-in room management without external libraries.") },
155
+ { title: t('feat.ws_middleware', 'Middleware Sharing'), desc: t('feat.ws_middleware_desc', 'Reuse the same HTTP middleware for WebSocket handshake. Auth middleware works for both HTTP and WS connections.') },
156
+ { title: t('feat.ws_hub', 'Hub Broadcasting'), desc: t('feat.ws_hub_desc', 'Multi-server? Just add { hub: true } to broadcast. Messages route through Hub server to all connected instances.') },
157
+ ],
158
+ code: {
159
+ title: 'ws/ChatHandler.js',
160
+ body: `<span class="code-kw">import</span> { WsHandler } <span class="code-kw">from</span> <span class="code-str">'@fuzionx/framework'</span>;
161
+
162
+ <span class="code-kw">export default class</span> <span class="code-fn">ChatHandler</span> <span class="code-kw">extends</span> WsHandler {
163
+ <span class="code-kw">static</span> namespace = <span class="code-str">'/chat'</span>;
164
+ <span class="code-kw">static</span> middleware = [<span class="code-str">'auth'</span>];
165
+
166
+ <span class="code-kw">static</span> <span class="code-fn">events</span>(e) {
167
+ e.on(<span class="code-str">'chat'</span>, <span class="code-kw">this</span>.handleChat);
168
+ e.on(<span class="code-str">'typing'</span>, <span class="code-kw">this</span>.handleTyping);
169
+ }
170
+
171
+ <span class="code-fn">handleChat</span>(socket, data) {
172
+ socket.to(<span class="code-str">\`room:\${data.roomId}\`</span>).send({
173
+ type: <span class="code-str">'chat'</span>,
174
+ data: { user: socket.user, message: data.message }
175
+ });
176
+ }
177
+ }`,
178
+ },
179
+ },
180
+ {
181
+ id: 'queue', icon: '⏰',
182
+ title: t('feat.queue_title', 'Task Queue + Workers'),
183
+ sub: t('feat.queue_sub', 'Three ways to handle background work — scheduled, queued, and CPU-isolated.'),
184
+ cards: [
185
+ { title: '📅 ' + t('feat.queue_scheduled', 'Scheduled Jobs'), desc: t('feat.queue_scheduled_desc', "Cron-based recurring tasks. static schedule = 'daily:02:00'. Runs on master process with Redis distributed lock for multi-server.") },
186
+ { title: '📬 ' + t('feat.queue_queued', 'Queued Tasks'), desc: t('feat.queue_queued_desc', "Async dispatch with retries: app.dispatch('SendEmail', data). Memory or Redis backend. Failed tasks call failed() hook.") },
187
+ { title: '🧵 WorkerPool', desc: t('feat.queue_worker_desc', "CPU-heavy work in worker_threads: app.worker.run('csv-parser', data). Prevents event loop blocking. Timeout + auto-cleanup.") },
188
+ ],
189
+ code: {
190
+ title: 'jobs/SendWelcomeEmail.js',
191
+ body: `<span class="code-kw">import</span> { Task } <span class="code-kw">from</span> <span class="code-str">'@fuzionx/framework'</span>;
192
+
193
+ <span class="code-kw">export default class</span> <span class="code-fn">SendWelcomeEmail</span> <span class="code-kw">extends</span> Task {
194
+ <span class="code-kw">static</span> queue = <span class="code-str">'emails'</span>;
195
+ <span class="code-kw">static</span> retries = <span class="code-num">3</span>;
196
+
197
+ <span class="code-kw">async</span> <span class="code-fn">handle</span>(data) {
198
+ <span class="code-kw">const</span> user = <span class="code-kw">await</span> <span class="code-kw">this</span>.db.User.find(data.userId);
199
+ <span class="code-kw">await</span> <span class="code-kw">this</span>.service(<span class="code-str">'MailService'</span>).sendWelcome(user.email);
200
+ }
201
+
202
+ <span class="code-kw">async</span> <span class="code-fn">failed</span>(data, error) {
203
+ <span class="code-kw">this</span>.logger.error(<span class="code-str">'Email failed'</span>, { userId: data.userId });
204
+ }
205
+ }`,
206
+ },
207
+ },
208
+ {
209
+ id: 'security', icon: '🔐',
210
+ title: t('feat.sec_title', 'Enterprise Security'),
211
+ sub: t('feat.sec_sub', 'Production-grade security built into every layer — from wire encryption to password hashing.'),
212
+ cards: [
213
+ { title: t('feat.sec_asp', 'ASP Wire Encryption'), desc: t('feat.sec_asp_desc', 'FuzionX Stealth Protocol encrypts all HTTP request/response bodies on the wire. Transparent to application code, impenetrable to MITM attacks.') },
214
+ { title: t('feat.sec_hash', 'Native Password Hashing'), desc: t('feat.sec_hash_desc', 'bcrypt (cost 12) and argon2id hashing in Rust — orders of magnitude faster than JavaScript implementations with the same security guarantees.') },
215
+ { title: t('feat.sec_session', 'Session + JWT Auth'), desc: t('feat.sec_session_desc', 'File-based or Redis session store. JWT access/refresh token rotation. Built-in auth() middleware with route guards.') },
216
+ { title: t('feat.sec_rate', 'Rate Limiting + CSRF'), desc: t('feat.sec_rate_desc', 'Per-IP rate limiting in the Rust bridge layer. CSRF token generation and validation. IP whitelist/blacklist. HSTS and CSP headers.') },
217
+ ],
218
+ code: null,
219
+ },
220
+ {
221
+ id: 'dx', icon: '🛠️',
222
+ title: t('feat.dx_title', 'Developer Experience'),
223
+ sub: t('feat.dx_sub', 'CLI scaffolding, auto-scanning, hot reload, OpenAPI docs — everything to keep you productive.'),
224
+ cards: [
225
+ { title: t('feat.dx_cli', 'CLI Code Generator'), desc: t('feat.dx_cli_desc', 'fx make:controller, fx make:model, fx make:service — scaffold any component instantly with proper boilerplate.') },
226
+ { title: t('feat.dx_autoscan', 'Auto-Scanning'), desc: t('feat.dx_autoscan_desc', 'Drop a file in controllers/, services/, ws/, or jobs/ — the framework discovers and registers it automatically. Zero manual wiring.') },
227
+ { title: t('feat.dx_openapi', 'OpenAPI Auto-Generation'), desc: t('feat.dx_openapi_desc', 'Swagger UI at /docs with zero configuration. Route definitions are automatically converted to OpenAPI 3.0 spec.') },
228
+ { title: t('feat.dx_test', 'Testing Helpers'), desc: t('feat.dx_test_desc', 'In-process HTTP test client, database transaction rollbacks, isolated test contexts. vitest integration out of the box.') },
229
+ ],
230
+ code: {
231
+ title: 'Terminal',
232
+ body: `<span class="code-cmt">$ npx create-fuzionx my-app</span>
233
+ <span class="code-cmt">$ cd my-app</span>
234
+ <span class="code-cmt">$ fx make:controller User</span> <span class="code-str"># → controllers/UserController.js</span>
235
+ <span class="code-cmt">$ fx make:model User</span> <span class="code-str"># → models/User.js</span>
236
+ <span class="code-cmt">$ fx make:service User</span> <span class="code-str"># → services/UserService.js</span>
237
+ <span class="code-cmt">$ fx db:sync --apply</span> <span class="code-str"># → Schema synced ✓</span>
238
+ <span class="code-cmt">$ fx dev</span> <span class="code-str"># → 🚀 Running on http://localhost:3000</span>`,
239
+ },
240
+ },
241
+ ]);
242
+ </script>