create-fuzionx 0.1.48 β†’ 0.1.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fuzionx",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Create a new FuzionX application β€” npx create-fuzionx my-app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,13 @@
21
21
  "processing": "Processing",
22
22
  "store_success": "Post saved successfully.",
23
23
  "title": "Board",
24
- "update_success": "Post updated successfully."
24
+ "update_success": "Post updated successfully.",
25
+ "upload_speed": "Speed",
26
+ "upload_complete": "Upload complete!",
27
+ "upload_error": "Upload error",
28
+ "upload_network_error": "Network error",
29
+ "uploading": "Uploading...",
30
+ "thumbnail_extracting": "πŸ”„ Extracting thumbnails..."
25
31
  },
26
32
  "btn": {
27
33
  "back": "← Back",
@@ -229,7 +235,8 @@
229
235
  "login": "Login",
230
236
  "logout": "Logout",
231
237
  "profile": "Profile",
232
- "register": "Register"
238
+ "register": "Register",
239
+ "live": "Live"
233
240
  },
234
241
  "page": {
235
242
  "404": "Page not found",
@@ -246,5 +253,48 @@
246
253
  "title": "Edit Profile",
247
254
  "update_success": "Profile updated."
248
255
  },
249
- "welcome": "Welcome, {name}! πŸ‘‹"
256
+ "welcome": "Welcome, {name}! πŸ‘‹",
257
+ "live": {
258
+ "back": "Back to list",
259
+ "broadcast": "Broadcast",
260
+ "cancel": "Cancel",
261
+ "channel_id": "Channel ID",
262
+ "channel_exists": "Channel already exists.",
263
+ "chat": "Chat",
264
+ "chat_placeholder": "Type a message...",
265
+ "connecting": "Connecting...",
266
+ "connection_failed": "connection failed",
267
+ "create": "New Channel",
268
+ "create_btn": "Create",
269
+ "create_failed": "Create failed",
270
+ "create_title": "Create New Channel",
271
+ "default": "Default",
272
+ "enter_channel_id": "Please enter a channel ID.",
273
+ "file_path": "File Path",
274
+ "file_stream": "File Streaming",
275
+ "hero_sub": "WebRTC Live Broadcast Β· VideoChat",
276
+ "join": "Join",
277
+ "join_room": "Enter Room",
278
+ "leave": "Leave",
279
+ "nickname": "Nickname",
280
+ "no_channels": "No active channels.",
281
+ "options": "Encoding Options",
282
+ "publisher": "Publisher",
283
+ "refresh": "Refresh",
284
+ "resolution": "Resolution",
285
+ "role": "Select Role",
286
+ "send": "Send",
287
+ "source": "Source",
288
+ "source_url": "Streaming URL",
289
+ "start_hint": "Create a new channel to get started.",
290
+ "stop": "Stop",
291
+ "stop_failed": "Stop failed",
292
+ "type": "Type",
293
+ "url_stream": "External URL",
294
+ "video_bitrate": "Video Bitrate",
295
+ "audio_bitrate": "Audio Bitrate",
296
+ "video_codec": "Video Codec",
297
+ "videochat": "VideoChat",
298
+ "viewer": "Viewer"
299
+ }
250
300
  }
@@ -21,7 +21,13 @@
21
21
  "processing": "μ²˜λ¦¬μ€‘",
22
22
  "store_success": "κ²Œμ‹œκΈ€μ΄ μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.",
23
23
  "title": "κ²Œμ‹œνŒ",
24
- "update_success": "κ²Œμ‹œκΈ€μ΄ μ„±κ³΅μ μœΌλ‘œ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€."
24
+ "update_success": "κ²Œμ‹œκΈ€μ΄ μ„±κ³΅μ μœΌλ‘œ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.",
25
+ "upload_speed": "속도",
26
+ "upload_complete": "μ—…λ‘œλ“œ μ™„λ£Œ!",
27
+ "upload_error": "였λ₯˜ λ°œμƒ",
28
+ "upload_network_error": "λ„€νŠΈμ›Œν¬ 였λ₯˜",
29
+ "uploading": "μ—…λ‘œλ“œ 쀑...",
30
+ "thumbnail_extracting": "πŸ”„ 썸넀일 μΆ”μΆœμ€‘..."
25
31
  },
26
32
  "btn": {
27
33
  "back": "← λͺ©λ‘",
@@ -229,7 +235,8 @@
229
235
  "login": "둜그인",
230
236
  "logout": "λ‘œκ·Έμ•„μ›ƒ",
231
237
  "profile": "ν”„λ‘œν•„",
232
- "register": "νšŒμ›κ°€μž…"
238
+ "register": "νšŒμ›κ°€μž…",
239
+ "live": "Live"
233
240
  },
234
241
  "page": {
235
242
  "404": "νŽ˜μ΄μ§€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€",
@@ -246,5 +253,48 @@
246
253
  "title": "ν”„λ‘œν•„ μˆ˜μ •",
247
254
  "update_success": "ν”„λ‘œν•„μ΄ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€."
248
255
  },
249
- "welcome": "ν™˜μ˜ν•©λ‹ˆλ‹€, {name}λ‹˜ πŸ‘‹"
256
+ "welcome": "ν™˜μ˜ν•©λ‹ˆλ‹€, {name}λ‹˜ πŸ‘‹",
257
+ "live": {
258
+ "back": "λͺ©λ‘μœΌλ‘œ",
259
+ "broadcast": "방솑",
260
+ "cancel": "μ·¨μ†Œ",
261
+ "channel_id": "채널 ID",
262
+ "channel_exists": "이미 μ‘΄μž¬ν•˜λŠ” μ±„λ„μž…λ‹ˆλ‹€.",
263
+ "chat": "μ±„νŒ…",
264
+ "chat_placeholder": "λ©”μ‹œμ§€ μž…λ ₯...",
265
+ "connecting": "μ—°κ²° 쀑...",
266
+ "connection_failed": "μ—°κ²° μ‹€νŒ¨",
267
+ "create": "μƒˆ 채널",
268
+ "create_btn": "생성",
269
+ "create_failed": "생성 μ‹€νŒ¨",
270
+ "create_title": "μƒˆ 채널 λ§Œλ“€κΈ°",
271
+ "default": "κΈ°λ³Έκ°’",
272
+ "enter_channel_id": "채널 IDλ₯Ό μž…λ ₯ν•˜μ„Έμš”.",
273
+ "file_path": "파일 경둜",
274
+ "file_stream": "파일 슀트리밍",
275
+ "hero_sub": "WebRTC 라이브 방솑 Β· ν™”μƒμ±„νŒ…",
276
+ "join": "μž…μž₯",
277
+ "join_room": "μž…μž₯",
278
+ "leave": "λ‚˜κ°€κΈ°",
279
+ "nickname": "λ‹‰λ„€μž„",
280
+ "no_channels": "ν™œμ„± 채널이 μ—†μŠ΅λ‹ˆλ‹€.",
281
+ "options": "인코딩 μ˜΅μ…˜",
282
+ "publisher": "μ†‘μΆœμž",
283
+ "refresh": "μƒˆλ‘œκ³ μΉ¨",
284
+ "resolution": "해상도",
285
+ "role": "μ—­ν•  선택",
286
+ "send": "전솑",
287
+ "source": "μ†ŒμŠ€",
288
+ "source_url": "슀트리밍 URL",
289
+ "start_hint": "μƒˆ 채널을 λ§Œλ“€μ–΄ μ‹œμž‘ν•˜μ„Έμš”.",
290
+ "stop": "μ’…λ£Œ",
291
+ "stop_failed": "μ’…λ£Œ μ‹€νŒ¨",
292
+ "type": "νƒ€μž…",
293
+ "url_stream": "μ™ΈλΆ€ URL",
294
+ "video_bitrate": "μ˜μƒ λΉ„νŠΈλ ˆμ΄νŠΈ",
295
+ "audio_bitrate": "μ˜€λ””μ˜€ λΉ„νŠΈλ ˆμ΄νŠΈ",
296
+ "video_codec": "λΉ„λ””μ˜€ 코덱",
297
+ "videochat": "ν™”μƒμ±„νŒ…",
298
+ "viewer": "μ‹œμ²­μž"
299
+ }
250
300
  }
@@ -9,8 +9,8 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@fuzionx/framework": "^0.1.48",
13
- "@fuzionx/client": "^0.1.48",
12
+ "@fuzionx/framework": "^0.1.50",
13
+ "@fuzionx/client": "^0.1.50",
14
14
  "joi": "^18.1.1"
15
15
  },
16
16
  "devDependencies": {
@@ -4,7 +4,7 @@
4
4
  "description": "Vue.js 3 SPA + Tera SSR ν•˜μ΄λΈŒλ¦¬λ“œ. WASM μ•”ν˜Έν™” 톡신.",
5
5
  "features": ["auth", "board", "i18n", "asp", "wasm"],
6
6
  "dependencies": {
7
- "@fuzionx/client": "^0.1.48"
7
+ "@fuzionx/client": "^0.1.50"
8
8
  },
9
9
  "devDependencies": {},
10
10
  "spaDevDependencies": {
@@ -9,6 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
+ "@fuzionx/player": "^0.1.1",
12
13
  "pinia": "^3.0.4",
13
14
  "vue": "^3.5.0",
14
15
  "vue-router": "^4.5.0"
@@ -54,7 +54,8 @@
54
54
  </div>
55
55
  <div class="progress-info" style="display: flex; justify-content: space-between; margin-top: 6px; font-size: 0.85rem; color: #aaa;">
56
56
  <span>{{ progressText }}</span>
57
- <span>{{ speedText }}</span>
57
+ <span>{{ sizeText }}</span>
58
+ <span>{{ speedLabel }}</span>
58
59
  </div>
59
60
  </div>
60
61
  </div>
@@ -62,6 +63,9 @@
62
63
 
63
64
  <script setup>
64
65
  import { ref, computed } from 'vue';
66
+ import { useLocale } from '../composables/useLocale.js';
67
+
68
+ const { t } = useLocale();
65
69
 
66
70
  /**
67
71
  * @prop {string} label - λ ˆμ΄λΈ” ν…μŠ€νŠΈ
@@ -83,14 +87,16 @@ const selectedFiles = ref([]);
83
87
  const uploading = ref(false);
84
88
  const progress = ref(0);
85
89
  const progressText = ref('0%');
86
- const speedText = ref('');
90
+ const sizeText = ref('');
91
+ const speedLabel = ref('');
87
92
  const progressColor = ref('linear-gradient(90deg, #667eea, #764ba2)');
88
93
 
89
94
  /** 파일 크기 포맷 */
90
95
  function formatSize(bytes) {
91
96
  if (bytes < 1024) return bytes + ' B';
92
97
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
93
- return (bytes / 1048576).toFixed(1) + ' MB';
98
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
99
+ return (bytes / 1073741824).toFixed(2) + ' GB';
94
100
  }
95
101
 
96
102
  /** 파일 선택 이벀트 */
@@ -122,23 +128,32 @@ function setFiles(fileList) {
122
128
  }
123
129
 
124
130
  /** μ§„ν–‰λ₯  μ—…λ°μ΄νŠΈ (μ™ΈλΆ€μ—μ„œ 호좜) */
125
- function setProgress(pct, speed) {
131
+ function setProgress(pct, speed, loaded, total) {
126
132
  uploading.value = true;
127
133
  progress.value = pct;
128
134
  progressText.value = pct + '%';
129
- speedText.value = speed || '';
135
+
136
+ // 파일 크기 ν‘œμ‹œ
137
+ if (loaded !== undefined && total !== undefined) {
138
+ sizeText.value = formatSize(loaded) + ' / ' + formatSize(total);
139
+ }
140
+
141
+ // 속도 λ ˆμ΄λΈ”
142
+ if (speed) {
143
+ speedLabel.value = t('board.upload_speed', '속도') + ': ' + speed;
144
+ }
130
145
  }
131
146
 
132
147
  /** μ—…λ‘œλ“œ μ™„λ£Œ */
133
- function setComplete(text = 'μ—…λ‘œλ“œ μ™„λ£Œ!') {
148
+ function setComplete(text) {
134
149
  progress.value = 100;
135
- progressText.value = text;
136
- speedText.value = '';
150
+ progressText.value = text || t('board.upload_complete', 'μ—…λ‘œλ“œ μ™„λ£Œ!');
151
+ speedLabel.value = '';
137
152
  }
138
153
 
139
154
  /** μ—…λ‘œλ“œ 였λ₯˜ */
140
- function setError(text = '였λ₯˜ λ°œμƒ') {
141
- progressText.value = text;
155
+ function setError(text) {
156
+ progressText.value = text || t('board.upload_error', '였λ₯˜ λ°œμƒ');
142
157
  progressColor.value = '#e74c3c';
143
158
  }
144
159
 
@@ -153,7 +168,8 @@ function reset() {
153
168
  uploading.value = false;
154
169
  progress.value = 0;
155
170
  progressText.value = '0%';
156
- speedText.value = '';
171
+ sizeText.value = '';
172
+ speedLabel.value = '';
157
173
  progressColor.value = 'linear-gradient(90deg, #667eea, #764ba2)';
158
174
  }
159
175
 
@@ -14,6 +14,7 @@
14
14
  <router-link to="/features" class="nav-link">{{ t('nav.features', 'Features') }}</router-link>
15
15
  <router-link to="/chat" class="nav-link">{{ t('nav.chat', 'Chat Demo') }}</router-link>
16
16
  <router-link to="/board" class="nav-link">{{ t('nav.board', 'κ²Œμ‹œνŒ') }}</router-link>
17
+ <router-link to="/live" class="nav-link">{{ t('nav.live', 'Live') }}</router-link>
17
18
  <template v-if="authStore.isAuthenticated">
18
19
  <router-link to="/profile" class="nav-link">{{ t('nav.profile', 'ν”„λ‘œν•„') }}</router-link>
19
20
  <button class="nav-link btn-link" @click="handleLogout">{{ t('nav.logout', 'λ‘œκ·Έμ•„μ›ƒ') }}</button>
@@ -80,7 +80,7 @@ export function useApi() {
80
80
  * XHR μ—…λ‘œλ“œ β€” μ§„ν–‰λ₯  콜백 지원.
81
81
  * @param {string} url - μ—…λ‘œλ“œ URL
82
82
  * @param {FormData} formData - 폼 데이터
83
- * @param {Function} onProgress - μ§„ν–‰λ₯  콜백 (percent, speed)
83
+ * @param {Function} onProgress - μ§„ν–‰λ₯  콜백 (percent, speed, loaded, total)
84
84
  * @param {string} [method='POST'] - HTTP λ©”μ„œλ“œ
85
85
  * @returns {Promise<{ok: boolean, responseURL: string}>}
86
86
  */
@@ -100,7 +100,7 @@ export function useApi() {
100
100
  ? (bps / 1048576).toFixed(1) + ' MB/s'
101
101
  : (bps / 1024).toFixed(0) + ' KB/s';
102
102
  }
103
- onProgress(pct, speed);
103
+ onProgress(pct, speed, ev.loaded, ev.total);
104
104
  };
105
105
 
106
106
  xhr.onload = () => {
@@ -15,6 +15,11 @@ export const routes = [
15
15
  { path: '/', name: 'home', component: HomeView },
16
16
  { path: '/features', name: 'features', component: () => import('../views/FeaturesView.vue') },
17
17
 
18
+ // ── Live (인증 ν•„μš”) ──
19
+ { path: '/live', name: 'live', component: () => import('../views/LiveList.vue'), meta: { auth: true } },
20
+ { path: '/live/watch/:channelId', name: 'live-watch', component: () => import('../views/LiveWatch.vue'), meta: { auth: true } },
21
+ { path: '/live/room/:channelId', name: 'live-room', component: () => import('../views/LiveRoom.vue'), meta: { auth: true } },
22
+
18
23
  // ── 게슀트 μ „μš© (둜그인 μƒνƒœλ©΄ ν™ˆμœΌλ‘œ) ──
19
24
  { path: '/login', name: 'login', component: () => import('../views/Login.vue'), meta: { guest: true } },
20
25
  { path: '/register', name: 'register', component: () => import('../views/Register.vue'), meta: { guest: true } },
@@ -162,10 +162,10 @@ async function handleSubmit() {
162
162
  try {
163
163
  if (hasFiles) {
164
164
  // XHR μ—…λ‘œλ“œ + μ§„ν–‰λ₯ 
165
- const result = await api.uploadWithProgress(uploadUrl, formData, (pct, speed) => {
166
- fileUploader.value?.setProgress(pct, speed);
165
+ const result = await api.uploadWithProgress(uploadUrl, formData, (pct, speed, loaded, total) => {
166
+ fileUploader.value?.setProgress(pct, speed, loaded, total);
167
167
  }, method);
168
- fileUploader.value?.setComplete('μ—…λ‘œλ“œ μ™„λ£Œ!');
168
+ fileUploader.value?.setComplete();
169
169
  toast.success(isEdit.value ? t('board.updated', '글이 μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.') : t('board.created', '글이 μž‘μ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'));
170
170
  setTimeout(() => {
171
171
  router.push(isEdit.value ? `/board/${route.params.id}` : '/board');