@fuzionx/framework 0.1.43 → 0.1.45
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 +501 -501
- package/bin/fx.js +12 -12
- package/cli/db-sync.js +100 -100
- package/cli/index.js +494 -494
- package/cli/templates/make/app/controllers/HomeController.js +14 -14
- package/cli/templates/make/app/routes/api.js +7 -7
- package/cli/templates/make/app/routes/web.js +5 -5
- package/cli/templates/make/app/views/default/errors/404.html +11 -11
- package/cli/templates/make/app/views/default/errors/500.html +14 -14
- package/cli/templates/make/app/views/default/layouts/main.html +22 -22
- package/cli/templates/make/app/views/default/pages/home.html +11 -11
- package/cli/templates/make/controller.js.tpl +40 -40
- package/cli/templates/make/event.js.tpl +8 -8
- package/cli/templates/make/job.js.tpl +10 -10
- package/cli/templates/make/middleware.js.tpl +10 -10
- package/cli/templates/make/model.js.tpl +15 -15
- package/cli/templates/make/service.js.tpl +15 -15
- package/cli/templates/make/task.js.tpl +15 -15
- package/cli/templates/make/test.js.tpl +7 -7
- package/cli/templates/make/worker.js.tpl +14 -14
- package/cli/templates/make/ws.js.tpl +18 -18
- package/index.js +67 -67
- package/lib/core/AppError.js +46 -46
- package/lib/core/Application.js +1006 -1006
- package/lib/core/AutoLoader.js +227 -227
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +331 -331
- package/lib/core/Context.js +484 -484
- package/lib/database/ConnectionManager.js +208 -208
- package/lib/database/MariaModel.js +29 -29
- package/lib/database/Model.js +247 -247
- package/lib/database/ModelRegistry.js +72 -72
- package/lib/database/MongoModel.js +232 -232
- package/lib/database/Pagination.js +37 -37
- package/lib/database/PostgreModel.js +29 -29
- package/lib/database/QueryBuilder.js +172 -172
- package/lib/database/SQLiteModel.js +27 -27
- package/lib/database/SqlModel.js +257 -257
- package/lib/database/SqlQueryBuilder.js +332 -332
- package/lib/helpers/CryptoHelper.js +48 -48
- package/lib/helpers/FileHelper.js +61 -61
- package/lib/helpers/HashHelper.js +39 -39
- package/lib/helpers/I18nHelper.js +174 -174
- package/lib/helpers/Logger.js +108 -108
- package/lib/helpers/MediaHelper.js +84 -84
- package/lib/http/Controller.js +34 -34
- package/lib/http/ErrorHandler.js +136 -136
- package/lib/http/Middleware.js +43 -43
- package/lib/http/Router.js +109 -109
- package/lib/http/Validation.js +125 -125
- package/lib/middleware/apiAuth.js +79 -79
- package/lib/middleware/auth.js +42 -42
- package/lib/middleware/bodyParser.js +19 -19
- package/lib/middleware/cors.js +47 -47
- package/lib/middleware/csrf.js +32 -32
- package/lib/middleware/index.js +13 -13
- package/lib/middleware/session.js +27 -27
- package/lib/middleware/theme.js +20 -20
- package/lib/realtime/RoomManager.js +85 -85
- package/lib/realtime/WsHandler.js +107 -107
- package/lib/schedule/Job.js +38 -38
- package/lib/schedule/Queue.js +103 -103
- package/lib/schedule/Scheduler.js +171 -171
- package/lib/schedule/Task.js +39 -39
- package/lib/schedule/WorkerPool.js +225 -225
- package/lib/services/EventBus.js +94 -94
- package/lib/services/Service.js +261 -261
- package/lib/services/Storage.js +112 -112
- package/lib/utilities/ArrUtil.js +112 -112
- package/lib/utilities/DateUtil.js +98 -98
- package/lib/utilities/FunctionUtil.js +119 -119
- package/lib/utilities/NumUtil.js +75 -75
- package/lib/utilities/ObjectUtil.js +170 -170
- package/lib/utilities/PaginationUtil.js +81 -81
- package/lib/utilities/StrUtil.js +105 -105
- package/lib/utilities/index.js +18 -18
- package/lib/view/OpenAPI.js +231 -231
- package/lib/view/View.js +83 -83
- package/package.json +2 -2
- package/testing/index.js +232 -232
package/lib/services/Storage.js
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage — 파일 저장 추상화 (Local / S3)
|
|
3
|
-
*
|
|
4
|
-
* put(path, tempPath) — 임시파일 → 최종 위치 이동.
|
|
5
|
-
* Local: file.move(tempPath, fullPath)
|
|
6
|
-
* S3: Node.js 스트리밍 업로드 (큐 Task 권장)
|
|
7
|
-
*
|
|
8
|
-
* @see docs/framework/15-file-upload.md
|
|
9
|
-
* @see docs/framework/class-design.mm.md (Storage)
|
|
10
|
-
*/
|
|
11
|
-
import { promises as fs } from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
|
|
14
|
-
export default class Storage {
|
|
15
|
-
/**
|
|
16
|
-
* @param {object} opts
|
|
17
|
-
* @param {string} [opts.driver='local'] - 'local' | 's3'
|
|
18
|
-
* @param {string} [opts.basePath='./storage'] - 로컬 저장 경로
|
|
19
|
-
* @param {object} [opts.s3] - S3 설정 { bucket, region, credentials }
|
|
20
|
-
* @param {import('./FileHelper.js').default} [opts.fileHelper] - FileHelper 인스턴스
|
|
21
|
-
*/
|
|
22
|
-
constructor(opts = {}) {
|
|
23
|
-
this.driver = opts.driver || 'local';
|
|
24
|
-
this.basePath = opts.basePath || './storage';
|
|
25
|
-
this._s3 = opts.s3 || null;
|
|
26
|
-
this._file = opts.fileHelper || null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 임시파일 → 최종 위치 이동
|
|
31
|
-
* @param {string} filePath - 상대 경로 (e.g. 'uploads/avatar.jpg')
|
|
32
|
-
* @param {string} tempPath - 임시 파일 경로 (/tmp/fuzionx_upload_xxx)
|
|
33
|
-
* @returns {Promise<string>} - 저장된 URL 또는 경로
|
|
34
|
-
*/
|
|
35
|
-
async put(filePath, tempPath) {
|
|
36
|
-
if (this.driver === 's3') {
|
|
37
|
-
return this._s3Put(filePath, tempPath);
|
|
38
|
-
}
|
|
39
|
-
const fullPath = path.join(this.basePath, filePath);
|
|
40
|
-
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
41
|
-
// FileHelper로 이동 (Bridge file.move → Rust 직접)
|
|
42
|
-
if (this._file) {
|
|
43
|
-
await this._file.move(tempPath, fullPath);
|
|
44
|
-
} else {
|
|
45
|
-
// 폴백: fs.rename (cross-device 시 copy+unlink)
|
|
46
|
-
await fs.rename(tempPath, fullPath).catch(async () => {
|
|
47
|
-
await fs.copyFile(tempPath, fullPath);
|
|
48
|
-
await fs.unlink(tempPath);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return fullPath;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 파일 읽기
|
|
56
|
-
* @param {string} filePath
|
|
57
|
-
* @returns {Promise<Buffer>}
|
|
58
|
-
*/
|
|
59
|
-
async get(filePath) {
|
|
60
|
-
if (this.driver === 's3') {
|
|
61
|
-
return this._s3Get(filePath);
|
|
62
|
-
}
|
|
63
|
-
return fs.readFile(path.join(this.basePath, filePath));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 파일 삭제
|
|
68
|
-
* @param {string} filePath
|
|
69
|
-
* @returns {Promise<void>}
|
|
70
|
-
*/
|
|
71
|
-
async delete(filePath) {
|
|
72
|
-
if (this.driver === 's3') {
|
|
73
|
-
return this._s3Delete(filePath);
|
|
74
|
-
}
|
|
75
|
-
await fs.unlink(path.join(this.basePath, filePath));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 파일 존재 여부
|
|
80
|
-
* @param {string} filePath
|
|
81
|
-
* @returns {Promise<boolean>}
|
|
82
|
-
*/
|
|
83
|
-
async exists(filePath) {
|
|
84
|
-
try {
|
|
85
|
-
if (this.driver === 's3') {
|
|
86
|
-
return this._s3Exists(filePath);
|
|
87
|
-
}
|
|
88
|
-
await fs.access(path.join(this.basePath, filePath));
|
|
89
|
-
return true;
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 파일 URL (public 접근용)
|
|
97
|
-
* @param {string} filePath
|
|
98
|
-
* @returns {string}
|
|
99
|
-
*/
|
|
100
|
-
url(filePath) {
|
|
101
|
-
if (this.driver === 's3' && this._s3) {
|
|
102
|
-
return `https://${this._s3.bucket}.s3.${this._s3.region}.amazonaws.com/${filePath}`;
|
|
103
|
-
}
|
|
104
|
-
return `/storage/${filePath}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── S3 스텁 (Phase 5+ 구현) ──
|
|
108
|
-
async _s3Put(filePath, tempPath) { throw new Error('S3 driver not implemented'); }
|
|
109
|
-
async _s3Get(filePath) { throw new Error('S3 driver not implemented'); }
|
|
110
|
-
async _s3Delete(filePath) { throw new Error('S3 driver not implemented'); }
|
|
111
|
-
async _s3Exists(filePath) { throw new Error('S3 driver not implemented'); }
|
|
112
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Storage — 파일 저장 추상화 (Local / S3)
|
|
3
|
+
*
|
|
4
|
+
* put(path, tempPath) — 임시파일 → 최종 위치 이동.
|
|
5
|
+
* Local: file.move(tempPath, fullPath)
|
|
6
|
+
* S3: Node.js 스트리밍 업로드 (큐 Task 권장)
|
|
7
|
+
*
|
|
8
|
+
* @see docs/framework/15-file-upload.md
|
|
9
|
+
* @see docs/framework/class-design.mm.md (Storage)
|
|
10
|
+
*/
|
|
11
|
+
import { promises as fs } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
export default class Storage {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {string} [opts.driver='local'] - 'local' | 's3'
|
|
18
|
+
* @param {string} [opts.basePath='./storage'] - 로컬 저장 경로
|
|
19
|
+
* @param {object} [opts.s3] - S3 설정 { bucket, region, credentials }
|
|
20
|
+
* @param {import('./FileHelper.js').default} [opts.fileHelper] - FileHelper 인스턴스
|
|
21
|
+
*/
|
|
22
|
+
constructor(opts = {}) {
|
|
23
|
+
this.driver = opts.driver || 'local';
|
|
24
|
+
this.basePath = opts.basePath || './storage';
|
|
25
|
+
this._s3 = opts.s3 || null;
|
|
26
|
+
this._file = opts.fileHelper || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 임시파일 → 최종 위치 이동
|
|
31
|
+
* @param {string} filePath - 상대 경로 (e.g. 'uploads/avatar.jpg')
|
|
32
|
+
* @param {string} tempPath - 임시 파일 경로 (/tmp/fuzionx_upload_xxx)
|
|
33
|
+
* @returns {Promise<string>} - 저장된 URL 또는 경로
|
|
34
|
+
*/
|
|
35
|
+
async put(filePath, tempPath) {
|
|
36
|
+
if (this.driver === 's3') {
|
|
37
|
+
return this._s3Put(filePath, tempPath);
|
|
38
|
+
}
|
|
39
|
+
const fullPath = path.join(this.basePath, filePath);
|
|
40
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
41
|
+
// FileHelper로 이동 (Bridge file.move → Rust 직접)
|
|
42
|
+
if (this._file) {
|
|
43
|
+
await this._file.move(tempPath, fullPath);
|
|
44
|
+
} else {
|
|
45
|
+
// 폴백: fs.rename (cross-device 시 copy+unlink)
|
|
46
|
+
await fs.rename(tempPath, fullPath).catch(async () => {
|
|
47
|
+
await fs.copyFile(tempPath, fullPath);
|
|
48
|
+
await fs.unlink(tempPath);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return fullPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 파일 읽기
|
|
56
|
+
* @param {string} filePath
|
|
57
|
+
* @returns {Promise<Buffer>}
|
|
58
|
+
*/
|
|
59
|
+
async get(filePath) {
|
|
60
|
+
if (this.driver === 's3') {
|
|
61
|
+
return this._s3Get(filePath);
|
|
62
|
+
}
|
|
63
|
+
return fs.readFile(path.join(this.basePath, filePath));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 파일 삭제
|
|
68
|
+
* @param {string} filePath
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
*/
|
|
71
|
+
async delete(filePath) {
|
|
72
|
+
if (this.driver === 's3') {
|
|
73
|
+
return this._s3Delete(filePath);
|
|
74
|
+
}
|
|
75
|
+
await fs.unlink(path.join(this.basePath, filePath));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 파일 존재 여부
|
|
80
|
+
* @param {string} filePath
|
|
81
|
+
* @returns {Promise<boolean>}
|
|
82
|
+
*/
|
|
83
|
+
async exists(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
if (this.driver === 's3') {
|
|
86
|
+
return this._s3Exists(filePath);
|
|
87
|
+
}
|
|
88
|
+
await fs.access(path.join(this.basePath, filePath));
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 파일 URL (public 접근용)
|
|
97
|
+
* @param {string} filePath
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
url(filePath) {
|
|
101
|
+
if (this.driver === 's3' && this._s3) {
|
|
102
|
+
return `https://${this._s3.bucket}.s3.${this._s3.region}.amazonaws.com/${filePath}`;
|
|
103
|
+
}
|
|
104
|
+
return `/storage/${filePath}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── S3 스텁 (Phase 5+ 구현) ──
|
|
108
|
+
async _s3Put(filePath, tempPath) { throw new Error('S3 driver not implemented'); }
|
|
109
|
+
async _s3Get(filePath) { throw new Error('S3 driver not implemented'); }
|
|
110
|
+
async _s3Delete(filePath) { throw new Error('S3 driver not implemented'); }
|
|
111
|
+
async _s3Exists(filePath) { throw new Error('S3 driver not implemented'); }
|
|
112
|
+
}
|
package/lib/utilities/ArrUtil.js
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ArrUtil — 배열 순수 유틸리티
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 배열을 n개씩 분할
|
|
7
|
-
* @param {Array} arr
|
|
8
|
-
* @param {number} size
|
|
9
|
-
* @returns {Array<Array>}
|
|
10
|
-
*
|
|
11
|
-
* @example chunk([1,2,3,4,5], 2) → [[1,2], [3,4], [5]]
|
|
12
|
-
*/
|
|
13
|
-
export function chunk(arr, size) {
|
|
14
|
-
const result = [];
|
|
15
|
-
for (let i = 0; i < arr.length; i += size) {
|
|
16
|
-
result.push(arr.slice(i, i + size));
|
|
17
|
-
}
|
|
18
|
-
return result;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 중복 제거
|
|
23
|
-
* @param {Array} arr
|
|
24
|
-
* @param {Function} [keyFn] - 비교 키 추출 함수
|
|
25
|
-
* @returns {Array}
|
|
26
|
-
*/
|
|
27
|
-
export function unique(arr, keyFn) {
|
|
28
|
-
if (!keyFn) return [...new Set(arr)];
|
|
29
|
-
const seen = new Set();
|
|
30
|
-
return arr.filter(item => {
|
|
31
|
-
const key = keyFn(item);
|
|
32
|
-
if (seen.has(key)) return false;
|
|
33
|
-
seen.add(key);
|
|
34
|
-
return true;
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 키 기준 그룹핑
|
|
40
|
-
* @param {Array<object>} arr
|
|
41
|
-
* @param {string|Function} key
|
|
42
|
-
* @returns {object}
|
|
43
|
-
*
|
|
44
|
-
* @example groupBy([{a:1,b:'x'},{a:2,b:'x'},{a:3,b:'y'}], 'b') → { x: [...], y: [...] }
|
|
45
|
-
*/
|
|
46
|
-
export function groupBy(arr, key) {
|
|
47
|
-
const fn = typeof key === 'function' ? key : (item) => item[key];
|
|
48
|
-
return arr.reduce((acc, item) => {
|
|
49
|
-
const k = fn(item);
|
|
50
|
-
(acc[k] = acc[k] || []).push(item);
|
|
51
|
-
return acc;
|
|
52
|
-
}, {});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 특정 키 값만 추출
|
|
57
|
-
* @param {Array<object>} arr
|
|
58
|
-
* @param {string} key
|
|
59
|
-
* @returns {Array}
|
|
60
|
-
*
|
|
61
|
-
* @example pluck([{id:1,name:'a'},{id:2,name:'b'}], 'name') → ['a', 'b']
|
|
62
|
-
*/
|
|
63
|
-
export function pluck(arr, key) {
|
|
64
|
-
return arr.map(item => item[key]);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 키맵 생성 (배열 → 객체)
|
|
69
|
-
* @param {Array<object>} arr
|
|
70
|
-
* @param {string} key
|
|
71
|
-
* @returns {object}
|
|
72
|
-
*
|
|
73
|
-
* @example keyBy([{id:1,name:'a'},{id:2,name:'b'}], 'id') → { 1: {id:1,...}, 2: {id:2,...} }
|
|
74
|
-
*/
|
|
75
|
-
export function keyBy(arr, key) {
|
|
76
|
-
return arr.reduce((acc, item) => {
|
|
77
|
-
acc[item[key]] = item;
|
|
78
|
-
return acc;
|
|
79
|
-
}, {});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 배열 셔플 (Fisher-Yates — JS 폴백용, 프로덕션은 WASM FuzionXUtil.shuffle 사용)
|
|
84
|
-
* @param {Array} arr
|
|
85
|
-
* @returns {Array} 새 배열 (원본 미변경)
|
|
86
|
-
*/
|
|
87
|
-
export function shuffle(arr) {
|
|
88
|
-
const result = [...arr];
|
|
89
|
-
for (let i = result.length - 1; i > 0; i--) {
|
|
90
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
91
|
-
[result[i], result[j]] = [result[j], result[i]];
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 배열 flatten (1 depth)
|
|
98
|
-
* @param {Array} arr
|
|
99
|
-
* @returns {Array}
|
|
100
|
-
*/
|
|
101
|
-
export function flatten(arr) {
|
|
102
|
-
return arr.flat();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 배열이 비어있는지 확인
|
|
107
|
-
* @param {*} arr
|
|
108
|
-
* @returns {boolean}
|
|
109
|
-
*/
|
|
110
|
-
export function isEmpty(arr) {
|
|
111
|
-
return !Array.isArray(arr) || arr.length === 0;
|
|
112
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ArrUtil — 배열 순수 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 배열을 n개씩 분할
|
|
7
|
+
* @param {Array} arr
|
|
8
|
+
* @param {number} size
|
|
9
|
+
* @returns {Array<Array>}
|
|
10
|
+
*
|
|
11
|
+
* @example chunk([1,2,3,4,5], 2) → [[1,2], [3,4], [5]]
|
|
12
|
+
*/
|
|
13
|
+
export function chunk(arr, size) {
|
|
14
|
+
const result = [];
|
|
15
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
16
|
+
result.push(arr.slice(i, i + size));
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 중복 제거
|
|
23
|
+
* @param {Array} arr
|
|
24
|
+
* @param {Function} [keyFn] - 비교 키 추출 함수
|
|
25
|
+
* @returns {Array}
|
|
26
|
+
*/
|
|
27
|
+
export function unique(arr, keyFn) {
|
|
28
|
+
if (!keyFn) return [...new Set(arr)];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
return arr.filter(item => {
|
|
31
|
+
const key = keyFn(item);
|
|
32
|
+
if (seen.has(key)) return false;
|
|
33
|
+
seen.add(key);
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 키 기준 그룹핑
|
|
40
|
+
* @param {Array<object>} arr
|
|
41
|
+
* @param {string|Function} key
|
|
42
|
+
* @returns {object}
|
|
43
|
+
*
|
|
44
|
+
* @example groupBy([{a:1,b:'x'},{a:2,b:'x'},{a:3,b:'y'}], 'b') → { x: [...], y: [...] }
|
|
45
|
+
*/
|
|
46
|
+
export function groupBy(arr, key) {
|
|
47
|
+
const fn = typeof key === 'function' ? key : (item) => item[key];
|
|
48
|
+
return arr.reduce((acc, item) => {
|
|
49
|
+
const k = fn(item);
|
|
50
|
+
(acc[k] = acc[k] || []).push(item);
|
|
51
|
+
return acc;
|
|
52
|
+
}, {});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 특정 키 값만 추출
|
|
57
|
+
* @param {Array<object>} arr
|
|
58
|
+
* @param {string} key
|
|
59
|
+
* @returns {Array}
|
|
60
|
+
*
|
|
61
|
+
* @example pluck([{id:1,name:'a'},{id:2,name:'b'}], 'name') → ['a', 'b']
|
|
62
|
+
*/
|
|
63
|
+
export function pluck(arr, key) {
|
|
64
|
+
return arr.map(item => item[key]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 키맵 생성 (배열 → 객체)
|
|
69
|
+
* @param {Array<object>} arr
|
|
70
|
+
* @param {string} key
|
|
71
|
+
* @returns {object}
|
|
72
|
+
*
|
|
73
|
+
* @example keyBy([{id:1,name:'a'},{id:2,name:'b'}], 'id') → { 1: {id:1,...}, 2: {id:2,...} }
|
|
74
|
+
*/
|
|
75
|
+
export function keyBy(arr, key) {
|
|
76
|
+
return arr.reduce((acc, item) => {
|
|
77
|
+
acc[item[key]] = item;
|
|
78
|
+
return acc;
|
|
79
|
+
}, {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 배열 셔플 (Fisher-Yates — JS 폴백용, 프로덕션은 WASM FuzionXUtil.shuffle 사용)
|
|
84
|
+
* @param {Array} arr
|
|
85
|
+
* @returns {Array} 새 배열 (원본 미변경)
|
|
86
|
+
*/
|
|
87
|
+
export function shuffle(arr) {
|
|
88
|
+
const result = [...arr];
|
|
89
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
90
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
91
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 배열 flatten (1 depth)
|
|
98
|
+
* @param {Array} arr
|
|
99
|
+
* @returns {Array}
|
|
100
|
+
*/
|
|
101
|
+
export function flatten(arr) {
|
|
102
|
+
return arr.flat();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 배열이 비어있는지 확인
|
|
107
|
+
* @param {*} arr
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
export function isEmpty(arr) {
|
|
111
|
+
return !Array.isArray(arr) || arr.length === 0;
|
|
112
|
+
}
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DateUtil — 날짜 순수 유틸리티
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* ISO → 'YYYY-MM-DD HH:mm:ss' 포맷
|
|
7
|
-
* @param {Date|string|number} date
|
|
8
|
-
* @param {string} [fmt='YYYY-MM-DD HH:mm:ss']
|
|
9
|
-
* @returns {string}
|
|
10
|
-
*/
|
|
11
|
-
export function format(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
|
|
12
|
-
const d = date instanceof Date ? date : new Date(date);
|
|
13
|
-
if (isNaN(d.getTime())) return '';
|
|
14
|
-
|
|
15
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
16
|
-
const tokens = {
|
|
17
|
-
YYYY: d.getFullYear(),
|
|
18
|
-
MM: pad(d.getMonth() + 1),
|
|
19
|
-
DD: pad(d.getDate()),
|
|
20
|
-
HH: pad(d.getHours()),
|
|
21
|
-
mm: pad(d.getMinutes()),
|
|
22
|
-
ss: pad(d.getSeconds()),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
let result = fmt;
|
|
26
|
-
for (const [token, value] of Object.entries(tokens)) {
|
|
27
|
-
result = result.replace(token, value);
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 상대 시간 (n분 전, n시간 전, n일 전)
|
|
34
|
-
* @param {Date|string|number} date
|
|
35
|
-
* @param {string} [locale='ko']
|
|
36
|
-
* @returns {string}
|
|
37
|
-
*/
|
|
38
|
-
export function ago(date, locale = 'ko') {
|
|
39
|
-
const d = date instanceof Date ? date : new Date(date);
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
const diffMs = now - d.getTime();
|
|
42
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
43
|
-
|
|
44
|
-
const units = locale === 'ko'
|
|
45
|
-
? { just: '방금 전', sec: '초 전', min: '분 전', hour: '시간 전', day: '일 전', month: '개월 전', year: '년 전' }
|
|
46
|
-
: { just: 'just now', sec: 's ago', min: 'm ago', hour: 'h ago', day: 'd ago', month: 'mo ago', year: 'y ago' };
|
|
47
|
-
|
|
48
|
-
if (diffSec < 10) return units.just;
|
|
49
|
-
if (diffSec < 60) return `${diffSec}${units.sec}`;
|
|
50
|
-
if (diffSec < 3600) return `${Math.floor(diffSec / 60)}${units.min}`;
|
|
51
|
-
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}${units.hour}`;
|
|
52
|
-
if (diffSec < 2592000) return `${Math.floor(diffSec / 86400)}${units.day}`;
|
|
53
|
-
if (diffSec < 31536000) return `${Math.floor(diffSec / 2592000)}${units.month}`;
|
|
54
|
-
return `${Math.floor(diffSec / 31536000)}${units.year}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 오늘인지 확인
|
|
59
|
-
* @param {Date|string|number} date
|
|
60
|
-
* @returns {boolean}
|
|
61
|
-
*/
|
|
62
|
-
export function isToday(date) {
|
|
63
|
-
const d = date instanceof Date ? date : new Date(date);
|
|
64
|
-
const now = new Date();
|
|
65
|
-
return d.getFullYear() === now.getFullYear()
|
|
66
|
-
&& d.getMonth() === now.getMonth()
|
|
67
|
-
&& d.getDate() === now.getDate();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 두 날짜 간 일수 차이
|
|
72
|
-
* @param {Date|string|number} a
|
|
73
|
-
* @param {Date|string|number} b
|
|
74
|
-
* @returns {number}
|
|
75
|
-
*/
|
|
76
|
-
export function diffDays(a, b) {
|
|
77
|
-
const da = a instanceof Date ? a : new Date(a);
|
|
78
|
-
const db = b instanceof Date ? b : new Date(b);
|
|
79
|
-
return Math.round((da.getTime() - db.getTime()) / 86400000);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 날짜 범위 배열 생성
|
|
84
|
-
* @param {Date|string} start
|
|
85
|
-
* @param {Date|string} end
|
|
86
|
-
* @returns {Date[]}
|
|
87
|
-
*/
|
|
88
|
-
export function range(start, end) {
|
|
89
|
-
const s = new Date(start);
|
|
90
|
-
const e = new Date(end);
|
|
91
|
-
const dates = [];
|
|
92
|
-
const d = new Date(s);
|
|
93
|
-
while (d <= e) {
|
|
94
|
-
dates.push(new Date(d));
|
|
95
|
-
d.setDate(d.getDate() + 1);
|
|
96
|
-
}
|
|
97
|
-
return dates;
|
|
98
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DateUtil — 날짜 순수 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ISO → 'YYYY-MM-DD HH:mm:ss' 포맷
|
|
7
|
+
* @param {Date|string|number} date
|
|
8
|
+
* @param {string} [fmt='YYYY-MM-DD HH:mm:ss']
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function format(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
|
|
12
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
13
|
+
if (isNaN(d.getTime())) return '';
|
|
14
|
+
|
|
15
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
16
|
+
const tokens = {
|
|
17
|
+
YYYY: d.getFullYear(),
|
|
18
|
+
MM: pad(d.getMonth() + 1),
|
|
19
|
+
DD: pad(d.getDate()),
|
|
20
|
+
HH: pad(d.getHours()),
|
|
21
|
+
mm: pad(d.getMinutes()),
|
|
22
|
+
ss: pad(d.getSeconds()),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let result = fmt;
|
|
26
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
27
|
+
result = result.replace(token, value);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 상대 시간 (n분 전, n시간 전, n일 전)
|
|
34
|
+
* @param {Date|string|number} date
|
|
35
|
+
* @param {string} [locale='ko']
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function ago(date, locale = 'ko') {
|
|
39
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const diffMs = now - d.getTime();
|
|
42
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
43
|
+
|
|
44
|
+
const units = locale === 'ko'
|
|
45
|
+
? { just: '방금 전', sec: '초 전', min: '분 전', hour: '시간 전', day: '일 전', month: '개월 전', year: '년 전' }
|
|
46
|
+
: { just: 'just now', sec: 's ago', min: 'm ago', hour: 'h ago', day: 'd ago', month: 'mo ago', year: 'y ago' };
|
|
47
|
+
|
|
48
|
+
if (diffSec < 10) return units.just;
|
|
49
|
+
if (diffSec < 60) return `${diffSec}${units.sec}`;
|
|
50
|
+
if (diffSec < 3600) return `${Math.floor(diffSec / 60)}${units.min}`;
|
|
51
|
+
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}${units.hour}`;
|
|
52
|
+
if (diffSec < 2592000) return `${Math.floor(diffSec / 86400)}${units.day}`;
|
|
53
|
+
if (diffSec < 31536000) return `${Math.floor(diffSec / 2592000)}${units.month}`;
|
|
54
|
+
return `${Math.floor(diffSec / 31536000)}${units.year}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 오늘인지 확인
|
|
59
|
+
* @param {Date|string|number} date
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
export function isToday(date) {
|
|
63
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
64
|
+
const now = new Date();
|
|
65
|
+
return d.getFullYear() === now.getFullYear()
|
|
66
|
+
&& d.getMonth() === now.getMonth()
|
|
67
|
+
&& d.getDate() === now.getDate();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 두 날짜 간 일수 차이
|
|
72
|
+
* @param {Date|string|number} a
|
|
73
|
+
* @param {Date|string|number} b
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
export function diffDays(a, b) {
|
|
77
|
+
const da = a instanceof Date ? a : new Date(a);
|
|
78
|
+
const db = b instanceof Date ? b : new Date(b);
|
|
79
|
+
return Math.round((da.getTime() - db.getTime()) / 86400000);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 날짜 범위 배열 생성
|
|
84
|
+
* @param {Date|string} start
|
|
85
|
+
* @param {Date|string} end
|
|
86
|
+
* @returns {Date[]}
|
|
87
|
+
*/
|
|
88
|
+
export function range(start, end) {
|
|
89
|
+
const s = new Date(start);
|
|
90
|
+
const e = new Date(end);
|
|
91
|
+
const dates = [];
|
|
92
|
+
const d = new Date(s);
|
|
93
|
+
while (d <= e) {
|
|
94
|
+
dates.push(new Date(d));
|
|
95
|
+
d.setDate(d.getDate() + 1);
|
|
96
|
+
}
|
|
97
|
+
return dates;
|
|
98
|
+
}
|