@fuzionx/framework 0.1.38 → 0.1.41

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 (81) hide show
  1. package/README.md +501 -501
  2. package/bin/fx.js +12 -12
  3. package/cli/db-sync.js +99 -99
  4. package/cli/index.js +493 -493
  5. package/cli/templates/make/app/controllers/HomeController.js +14 -14
  6. package/cli/templates/make/app/routes/api.js +7 -7
  7. package/cli/templates/make/app/routes/web.js +5 -5
  8. package/cli/templates/make/app/views/default/errors/404.html +11 -11
  9. package/cli/templates/make/app/views/default/errors/500.html +14 -14
  10. package/cli/templates/make/app/views/default/layouts/main.html +22 -22
  11. package/cli/templates/make/app/views/default/pages/home.html +11 -11
  12. package/cli/templates/make/controller.js.tpl +40 -40
  13. package/cli/templates/make/event.js.tpl +8 -8
  14. package/cli/templates/make/job.js.tpl +10 -10
  15. package/cli/templates/make/middleware.js.tpl +10 -10
  16. package/cli/templates/make/model.js.tpl +15 -15
  17. package/cli/templates/make/service.js.tpl +15 -15
  18. package/cli/templates/make/task.js.tpl +15 -15
  19. package/cli/templates/make/test.js.tpl +7 -7
  20. package/cli/templates/make/worker.js.tpl +14 -14
  21. package/cli/templates/make/ws.js.tpl +18 -18
  22. package/index.js +67 -67
  23. package/lib/core/AppError.js +46 -46
  24. package/lib/core/Application.js +1006 -998
  25. package/lib/core/AutoLoader.js +226 -226
  26. package/lib/core/Base.js +64 -64
  27. package/lib/core/Config.js +228 -228
  28. package/lib/core/Context.js +484 -460
  29. package/lib/database/ConnectionManager.js +208 -208
  30. package/lib/database/MariaModel.js +29 -29
  31. package/lib/database/Model.js +247 -247
  32. package/lib/database/ModelRegistry.js +72 -72
  33. package/lib/database/MongoModel.js +232 -232
  34. package/lib/database/Pagination.js +37 -37
  35. package/lib/database/PostgreModel.js +29 -29
  36. package/lib/database/QueryBuilder.js +172 -172
  37. package/lib/database/SQLiteModel.js +27 -27
  38. package/lib/database/SqlModel.js +257 -257
  39. package/lib/database/SqlQueryBuilder.js +332 -321
  40. package/lib/helpers/CryptoHelper.js +48 -48
  41. package/lib/helpers/FileHelper.js +61 -61
  42. package/lib/helpers/HashHelper.js +39 -39
  43. package/lib/helpers/I18nHelper.js +174 -174
  44. package/lib/helpers/Logger.js +108 -105
  45. package/lib/helpers/MediaHelper.js +84 -84
  46. package/lib/http/Controller.js +34 -34
  47. package/lib/http/ErrorHandler.js +136 -136
  48. package/lib/http/Middleware.js +43 -43
  49. package/lib/http/Router.js +109 -109
  50. package/lib/http/Validation.js +125 -124
  51. package/lib/middleware/apiAuth.js +79 -79
  52. package/lib/middleware/auth.js +42 -42
  53. package/lib/middleware/bodyParser.js +19 -19
  54. package/lib/middleware/cors.js +47 -47
  55. package/lib/middleware/csrf.js +32 -32
  56. package/lib/middleware/index.js +13 -13
  57. package/lib/middleware/session.js +27 -27
  58. package/lib/middleware/theme.js +20 -20
  59. package/lib/realtime/RoomManager.js +85 -85
  60. package/lib/realtime/WsHandler.js +107 -107
  61. package/lib/schedule/Job.js +38 -38
  62. package/lib/schedule/Queue.js +103 -102
  63. package/lib/schedule/Scheduler.js +171 -170
  64. package/lib/schedule/Task.js +39 -39
  65. package/lib/schedule/WorkerPool.js +225 -225
  66. package/lib/services/EventBus.js +94 -94
  67. package/lib/services/Service.js +261 -261
  68. package/lib/services/Storage.js +112 -112
  69. package/lib/utilities/ArrUtil.js +112 -112
  70. package/lib/utilities/DateUtil.js +98 -98
  71. package/lib/utilities/FunctionUtil.js +119 -119
  72. package/lib/utilities/NumUtil.js +75 -75
  73. package/lib/utilities/ObjectUtil.js +170 -170
  74. package/lib/utilities/PaginationUtil.js +81 -81
  75. package/lib/utilities/StrUtil.js +105 -105
  76. package/lib/utilities/index.js +18 -18
  77. package/lib/view/OpenAPI.js +231 -231
  78. package/lib/view/View.js +83 -83
  79. package/package.json +2 -2
  80. package/testing/index.js +232 -232
  81. package/cli/fx.js +0 -3
@@ -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
+ }
@@ -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
+ }