@fuzionx/framework 0.1.48 → 0.1.49

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.
@@ -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
 
@@ -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 = () => {
@@ -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');
@@ -1,4 +1,4 @@
1
- import { auth } from '@fuzionx/framework';
1
+ import { auth, loadUser } from '@fuzionx/framework';
2
2
  import HomeController from '../controllers/HomeController.js';
3
3
  import FeaturesController from '../controllers/FeaturesController.js';
4
4
  import ChatController from '../controllers/ChatController.js';
@@ -7,9 +7,11 @@ import UserController from '../controllers/UserController.js';
7
7
  import PostController from '../controllers/PostController.js';
8
8
 
9
9
  export default (r) => {
10
- // ── 공개 페이지 (인증 불필요) ──
11
- r.get('/', HomeController.index);
12
- r.get('/features', FeaturesController.index);
10
+ // ── 공개 페이지 (인증 불필요, 세션 유저 로드) ──
11
+ r.group('', { middleware: [loadUser()] }, (r) => {
12
+ r.get('/', HomeController.index);
13
+ r.get('/features', FeaturesController.index);
14
+ });
13
15
 
14
16
  // ── 인증 ──
15
17
  r.get('/login', AuthController.loginPage);
@@ -81,6 +81,7 @@
81
81
  </div>
82
82
  <div class="progress-info" style="display:flex;justify-content:space-between;margin-top:6px;font-size:0.85rem;color:#aaa">
83
83
  <span id="progressText">0%</span>
84
+ <span id="progressSize"></span>
84
85
  <span id="progressSpeed"></span>
85
86
  </div>
86
87
  </div>
@@ -128,7 +129,8 @@ function editorInsert(text) {
128
129
  function formatSize(bytes) {
129
130
  if (bytes < 1024) return bytes + ' B';
130
131
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
131
- return (bytes / 1048576).toFixed(1) + ' MB';
132
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
133
+ return (bytes / 1073741824).toFixed(2) + ' GB';
132
134
  }
133
135
 
134
136
  function renderFiles() {
@@ -168,7 +170,25 @@ function editorInsert(text) {
168
170
  });
169
171
  })();
170
172
 
171
- /** XHR 업로드 — 진행률 + 자동 리다이렉트 */
173
+ /** i18n 텍스트 */
174
+ var _uploadLabels = {
175
+ speed: '{{ t(key="board.upload_speed", default="속도") }}',
176
+ complete: '{{ t(key="board.upload_complete", default="업로드 완료!") }}',
177
+ error: '{{ t(key="board.upload_error", default="오류 발생") }}',
178
+ networkError: '{{ t(key="board.upload_network_error", default="네트워크 오류") }}',
179
+ uploading: '{{ t(key="board.uploading", default="업로드 중...") }}',
180
+ thumbnailExtracting: '{{ t(key="board.thumbnail_extracting", default="🔄 썸네일 추출중...") }}'
181
+ };
182
+
183
+ /** 파일 크기 포맷 (진행률용) */
184
+ function _fmtSize(bytes) {
185
+ if (bytes < 1024) return bytes + ' B';
186
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
187
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
188
+ return (bytes / 1073741824).toFixed(2) + ' GB';
189
+ }
190
+
191
+ /** XHR 업로드 — 진행률 + 파일 크기 + 속도 + 자동 리다이렉트 */
172
192
  function handleUpload(e) {
173
193
  var fileInput = document.getElementById('fileInput');
174
194
  if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
@@ -181,10 +201,11 @@ function handleUpload(e) {
181
201
  var progress = document.getElementById('uploadProgress');
182
202
  var fill = document.getElementById('progressFill');
183
203
  var pText = document.getElementById('progressText');
204
+ var pSize = document.getElementById('progressSize');
184
205
  var pSpeed = document.getElementById('progressSpeed');
185
206
 
186
207
  btn.disabled = true;
187
- btn.textContent = '업로드 중...';
208
+ btn.textContent = _uploadLabels.uploading;
188
209
  progress.style.display = 'block';
189
210
 
190
211
  var formData = new FormData(form);
@@ -197,30 +218,35 @@ function handleUpload(e) {
197
218
  fill.style.width = pct + '%';
198
219
  pText.textContent = pct + '%';
199
220
 
221
+ // 파일 크기 표시 (loaded / total)
222
+ pSize.textContent = _fmtSize(ev.loaded) + ' / ' + _fmtSize(ev.total);
223
+
200
224
  var elapsed = (Date.now() - startTime) / 1000;
201
225
  if (elapsed > 0.5) {
202
226
  var bps = ev.loaded / elapsed;
227
+ var speedVal;
203
228
  if (bps > 1048576) {
204
- pSpeed.textContent = (bps / 1048576).toFixed(1) + ' MB/s';
229
+ speedVal = (bps / 1048576).toFixed(1) + ' MB/s';
205
230
  } else {
206
- pSpeed.textContent = (bps / 1024).toFixed(0) + ' KB/s';
231
+ speedVal = (bps / 1024).toFixed(0) + ' KB/s';
207
232
  }
233
+ pSpeed.textContent = _uploadLabels.speed + ': ' + speedVal;
208
234
  }
209
235
  };
210
236
 
211
237
  xhr.onload = function() {
212
238
  if (xhr.status >= 200 && xhr.status < 400) {
213
239
  fill.style.width = '100%';
214
- pText.textContent = '업로드 완료!';
240
+ pText.textContent = _uploadLabels.complete;
215
241
  pSpeed.textContent = '';
216
- btn.textContent = '🔄 썸네일 추출중...';
242
+ btn.textContent = _uploadLabels.thumbnailExtracting;
217
243
  setTimeout(function() {
218
244
  window.location.href = xhr.responseURL || '/board';
219
245
  }, 1500);
220
246
  } else {
221
247
  btn.disabled = false;
222
248
  btn.textContent = '재시도';
223
- pText.textContent = '오류 발생';
249
+ pText.textContent = _uploadLabels.error;
224
250
  fill.style.background = '#e74c3c';
225
251
  }
226
252
  };
@@ -228,7 +254,7 @@ function handleUpload(e) {
228
254
  xhr.onerror = function() {
229
255
  btn.disabled = false;
230
256
  btn.textContent = '재시도';
231
- pText.textContent = '네트워크 오류';
257
+ pText.textContent = _uploadLabels.networkError;
232
258
  fill.style.background = '#e74c3c';
233
259
  };
234
260
 
package/index.js CHANGED
@@ -64,4 +64,4 @@ export { default as OpenAPI } from './lib/view/OpenAPI.js';
64
64
  export { PaginationUtil, StrUtil, NumUtil, DateUtil, ArrUtil, FunctionUtil, ObjectUtil } from './lib/utilities/index.js';
65
65
 
66
66
  // ── Built-in Middleware ──
67
- export { bodyParser, cors, auth, apiAuth, csrf, session, theme } from './lib/middleware/index.js';
67
+ export { bodyParser, cors, auth, apiAuth, csrf, session, theme, loadUser } from './lib/middleware/index.js';
@@ -11,3 +11,4 @@ export { apiAuth } from './apiAuth.js';
11
11
  export { csrf } from './csrf.js';
12
12
  export { session } from './session.js';
13
13
  export { theme } from './theme.js';
14
+ export { loadUser } from './loadUser.js';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * loadUser — 세션 유저 로드 미들웨어 (비강제)
3
+ *
4
+ * auth 미들웨어와 동일하게 ctx.session.userId → db.User.find() → ctx.user 설정.
5
+ * 차이점: 세션이 없거나 userId가 없으면 redirect/401 없이 그냥 통과.
6
+ *
7
+ * 공개 페이지에서도 로그인 상태를 확인하여 네비게이션 메뉴 등에 반영할 때 사용.
8
+ *
9
+ * @example
10
+ * // 글로벌 미들웨어로 등록 (모든 라우트에 적용)
11
+ * import { loadUser } from '@fuzionx/framework';
12
+ * app.use(loadUser());
13
+ *
14
+ * // 또는 특정 라우트/그룹에만 적용
15
+ * r.group('', { middleware: [loadUser()] }, (r) => {
16
+ * r.get('/', HomeController.index);
17
+ * });
18
+ *
19
+ * @see docs/framework/14-authentication.md
20
+ *
21
+ * @param {object} [opts]
22
+ * @param {string} [opts.sessionKey='userId'] - 세션에서 유저 ID를 읽을 키
23
+ * @param {string} [opts.model='User'] - DB 모델명
24
+ */
25
+ export function loadUser(opts = {}) {
26
+ const sessionKey = opts.sessionKey || 'userId';
27
+ const modelName = opts.model || 'User';
28
+
29
+ return async (ctx, next) => {
30
+ // 이미 auth 미들웨어로 ctx.user가 설정된 경우 스킵
31
+ if (ctx.user) {
32
+ await next();
33
+ return;
34
+ }
35
+
36
+ const userId = ctx._rawReq?.session?.[sessionKey];
37
+
38
+ if (userId && ctx.app?.db?.[modelName]) {
39
+ try {
40
+ ctx.user = await ctx.app.db[modelName].find(userId);
41
+ } catch {
42
+ ctx.user = null;
43
+ }
44
+ }
45
+
46
+ await next();
47
+ };
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/framework",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "type": "module",
5
5
  "description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
6
6
  "main": "index.js",
@@ -34,7 +34,7 @@
34
34
  "url": "https://github.com/saytohenry/fuzionx"
35
35
  },
36
36
  "dependencies": {
37
- "@fuzionx/core": "^0.1.48",
37
+ "@fuzionx/core": "^0.1.49",
38
38
  "better-sqlite3": "^12.8.0",
39
39
  "knex": "^3.2.5",
40
40
  "mongoose": "^9.3.2",