@fuzionx/framework 0.1.30 → 0.1.32
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/cli/index.js +146 -11
- package/index.js +3 -0
- package/lib/core/Application.js +27 -5
- package/lib/core/Context.js +31 -1
- package/lib/helpers/I18nHelper.js +10 -6
- package/lib/middleware/apiAuth.js +79 -0
- package/lib/middleware/auth.js +42 -0
- package/lib/middleware/bodyParser.js +19 -0
- package/lib/middleware/cors.js +47 -0
- package/lib/middleware/csrf.js +32 -0
- package/lib/middleware/index.js +8 -277
- package/lib/middleware/session.js +27 -0
- package/lib/middleware/theme.js +20 -0
- package/lib/schedule/Job.js +4 -0
- package/lib/schedule/Queue.js +20 -8
- package/lib/schedule/Scheduler.js +84 -75
- package/lib/utilities/ArrUtil.js +112 -0
- package/lib/utilities/DateUtil.js +98 -0
- package/lib/utilities/FunctionUtil.js +119 -0
- package/lib/utilities/NumUtil.js +75 -0
- package/lib/utilities/ObjectUtil.js +170 -0
- package/lib/utilities/PaginationUtil.js +81 -0
- package/lib/utilities/StrUtil.js +105 -0
- package/lib/utilities/index.js +18 -0
- package/package.json +2 -2
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunctionUtil — 함수 순수 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Debounce — 마지막 호출 후 delay ms 뒤 실행
|
|
7
|
+
* @param {Function} fn
|
|
8
|
+
* @param {number} delay - ms
|
|
9
|
+
* @returns {Function}
|
|
10
|
+
*/
|
|
11
|
+
export function debounce(fn, delay = 300) {
|
|
12
|
+
let timer;
|
|
13
|
+
const debounced = (...args) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
16
|
+
};
|
|
17
|
+
debounced.cancel = () => clearTimeout(timer);
|
|
18
|
+
return debounced;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Throttle — delay ms 간격으로 최대 1회 실행
|
|
23
|
+
* @param {Function} fn
|
|
24
|
+
* @param {number} delay - ms
|
|
25
|
+
* @returns {Function}
|
|
26
|
+
*/
|
|
27
|
+
export function throttle(fn, delay = 300) {
|
|
28
|
+
let last = 0;
|
|
29
|
+
let timer;
|
|
30
|
+
return (...args) => {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const remaining = delay - (now - last);
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
if (remaining <= 0) {
|
|
35
|
+
last = now;
|
|
36
|
+
fn(...args);
|
|
37
|
+
} else {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
last = Date.now();
|
|
40
|
+
fn(...args);
|
|
41
|
+
}, remaining);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 한 번만 실행
|
|
48
|
+
* @param {Function} fn
|
|
49
|
+
* @returns {Function}
|
|
50
|
+
*/
|
|
51
|
+
export function once(fn) {
|
|
52
|
+
let called = false;
|
|
53
|
+
let result;
|
|
54
|
+
return (...args) => {
|
|
55
|
+
if (called) return result;
|
|
56
|
+
called = true;
|
|
57
|
+
result = fn(...args);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Promise 타임아웃 래퍼
|
|
64
|
+
* @param {Promise} promise
|
|
65
|
+
* @param {number} ms
|
|
66
|
+
* @param {string} [message='Timeout']
|
|
67
|
+
* @returns {Promise}
|
|
68
|
+
*/
|
|
69
|
+
export function timeout(promise, ms, message = 'Timeout') {
|
|
70
|
+
return Promise.race([
|
|
71
|
+
promise,
|
|
72
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms)),
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 딜레이 (async sleep)
|
|
78
|
+
* @param {number} ms
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
export function sleep(ms) {
|
|
82
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 함수 파이프라인 (왼→오) — 순차 실행
|
|
87
|
+
* @param {...Function} fns
|
|
88
|
+
* @returns {Function}
|
|
89
|
+
*
|
|
90
|
+
* @example pipe(addOne, double, square)(2) → square(double(addOne(2)))
|
|
91
|
+
*/
|
|
92
|
+
export function pipe(...fns) {
|
|
93
|
+
return (x) => fns.reduce((v, fn) => fn(v), x);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 메모이제이션 — 결과 캐싱
|
|
98
|
+
* @param {Function} fn
|
|
99
|
+
* @param {Function} [keyFn] - 캐시 키 생성 함수 (기본: 첫 인자)
|
|
100
|
+
* @returns {Function}
|
|
101
|
+
*/
|
|
102
|
+
export function memoize(fn, keyFn) {
|
|
103
|
+
const cache = new Map();
|
|
104
|
+
const memoized = (...args) => {
|
|
105
|
+
const key = keyFn ? keyFn(...args) : args[0];
|
|
106
|
+
if (cache.has(key)) return cache.get(key);
|
|
107
|
+
const result = fn(...args);
|
|
108
|
+
cache.set(key, result);
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
memoized.cache = cache;
|
|
112
|
+
memoized.clear = () => cache.clear();
|
|
113
|
+
return memoized;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* noop — 아무것도 하지 않는 함수
|
|
118
|
+
*/
|
|
119
|
+
export function noop() {}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NumUtil — 숫자 순수 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 범위 제한 (clamp)
|
|
7
|
+
* @param {number} n
|
|
8
|
+
* @param {number} min
|
|
9
|
+
* @param {number} max
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export function clamp(n, min, max) {
|
|
13
|
+
return Math.min(Math.max(n, min), max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 범위 내 랜덤 정수
|
|
18
|
+
* @param {number} min
|
|
19
|
+
* @param {number} max
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
export function random(min = 0, max = 100) {
|
|
23
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 숫자 포맷 (로케일)
|
|
28
|
+
* @param {number} n
|
|
29
|
+
* @param {string} [locale='ko-KR']
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*
|
|
32
|
+
* @example format(1234567) → '1,234,567'
|
|
33
|
+
*/
|
|
34
|
+
export function format(n, locale = 'ko-KR') {
|
|
35
|
+
return new Intl.NumberFormat(locale).format(n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 바이트 → 읽기 쉬운 단위 변환
|
|
40
|
+
* @param {number} bytes
|
|
41
|
+
* @param {number} [decimals=2]
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*
|
|
44
|
+
* @example fileSize(1536) → '1.50 KB'
|
|
45
|
+
*/
|
|
46
|
+
export function fileSize(bytes, decimals = 2) {
|
|
47
|
+
if (bytes === 0) return '0 B';
|
|
48
|
+
const k = 1024;
|
|
49
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
50
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
51
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${units[i]}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 퍼센트 계산
|
|
56
|
+
* @param {number} part
|
|
57
|
+
* @param {number} total
|
|
58
|
+
* @param {number} [decimals=1]
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
export function percent(part, total, decimals = 1) {
|
|
62
|
+
if (total === 0) return 0;
|
|
63
|
+
return parseFloat(((part / total) * 100).toFixed(decimals));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 안전한 정수 파싱
|
|
68
|
+
* @param {*} value
|
|
69
|
+
* @param {number} [fallback=0]
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
export function toInt(value, fallback = 0) {
|
|
73
|
+
const n = parseInt(value, 10);
|
|
74
|
+
return isNaN(n) ? fallback : n;
|
|
75
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUtil — 객체 순수 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deep clone (structuredClone 폴백)
|
|
7
|
+
* @param {*} obj
|
|
8
|
+
* @returns {*}
|
|
9
|
+
*/
|
|
10
|
+
export function deepClone(obj) {
|
|
11
|
+
if (typeof structuredClone === 'function') return structuredClone(obj);
|
|
12
|
+
return JSON.parse(JSON.stringify(obj));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Deep merge — 비파괴 병합
|
|
17
|
+
* @param {object} target
|
|
18
|
+
* @param {...object} sources
|
|
19
|
+
* @returns {object}
|
|
20
|
+
*/
|
|
21
|
+
export function deepMerge(target, ...sources) {
|
|
22
|
+
const result = { ...target };
|
|
23
|
+
for (const source of sources) {
|
|
24
|
+
if (!source) continue;
|
|
25
|
+
for (const key of Object.keys(source)) {
|
|
26
|
+
const sv = source[key];
|
|
27
|
+
const tv = result[key];
|
|
28
|
+
if (isPlainObject(sv) && isPlainObject(tv)) {
|
|
29
|
+
result[key] = deepMerge(tv, sv);
|
|
30
|
+
} else {
|
|
31
|
+
result[key] = sv;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 특정 키만 선택
|
|
40
|
+
* @param {object} obj
|
|
41
|
+
* @param {string[]} keys
|
|
42
|
+
* @returns {object}
|
|
43
|
+
*
|
|
44
|
+
* @example pick({ a:1, b:2, c:3 }, ['a', 'c']) → { a:1, c:3 }
|
|
45
|
+
*/
|
|
46
|
+
export function pick(obj, keys) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
if (key in obj) result[key] = obj[key];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 특정 키 제외
|
|
56
|
+
* @param {object} obj
|
|
57
|
+
* @param {string[]} keys
|
|
58
|
+
* @returns {object}
|
|
59
|
+
*/
|
|
60
|
+
export function omit(obj, keys) {
|
|
61
|
+
const keySet = new Set(keys);
|
|
62
|
+
const result = {};
|
|
63
|
+
for (const key of Object.keys(obj)) {
|
|
64
|
+
if (!keySet.has(key)) result[key] = obj[key];
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Flatten — 중첩 객체를 dot notation으로 평탄화
|
|
71
|
+
* @param {object} obj
|
|
72
|
+
* @param {string} [prefix='']
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*
|
|
75
|
+
* @example flatten({ a: { b: 1, c: { d: 2 } } }) → { 'a.b': 1, 'a.c.d': 2 }
|
|
76
|
+
*/
|
|
77
|
+
export function flatten(obj, prefix = '') {
|
|
78
|
+
const result = {};
|
|
79
|
+
for (const key of Object.keys(obj)) {
|
|
80
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
81
|
+
const val = obj[key];
|
|
82
|
+
if (isPlainObject(val)) {
|
|
83
|
+
Object.assign(result, flatten(val, path));
|
|
84
|
+
} else {
|
|
85
|
+
result[path] = val;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unflatten — dot notation → 중첩 객체
|
|
93
|
+
* @param {object} obj
|
|
94
|
+
* @returns {object}
|
|
95
|
+
*/
|
|
96
|
+
export function unflatten(obj) {
|
|
97
|
+
const result = {};
|
|
98
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
99
|
+
const parts = key.split('.');
|
|
100
|
+
let current = result;
|
|
101
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
102
|
+
if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {
|
|
103
|
+
current[parts[i]] = {};
|
|
104
|
+
}
|
|
105
|
+
current = current[parts[i]];
|
|
106
|
+
}
|
|
107
|
+
current[parts[parts.length - 1]] = val;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 객체가 비어있는지 확인
|
|
114
|
+
* @param {*} obj
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function isEmpty(obj) {
|
|
118
|
+
if (!obj) return true;
|
|
119
|
+
return Object.keys(obj).length === 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* dot notation으로 값 접근
|
|
124
|
+
* @param {object} obj
|
|
125
|
+
* @param {string} path
|
|
126
|
+
* @param {*} [defaultValue]
|
|
127
|
+
* @returns {*}
|
|
128
|
+
*
|
|
129
|
+
* @example get({ a: { b: { c: 42 } } }, 'a.b.c') → 42
|
|
130
|
+
*/
|
|
131
|
+
export function get(obj, path, defaultValue) {
|
|
132
|
+
const parts = path.split('.');
|
|
133
|
+
let current = obj;
|
|
134
|
+
for (const part of parts) {
|
|
135
|
+
if (current == null || typeof current !== 'object') return defaultValue;
|
|
136
|
+
current = current[part];
|
|
137
|
+
}
|
|
138
|
+
return current !== undefined ? current : defaultValue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* dot notation으로 값 설정 (비파괴 — 새 객체 반환)
|
|
143
|
+
* @param {object} obj
|
|
144
|
+
* @param {string} path
|
|
145
|
+
* @param {*} value
|
|
146
|
+
* @returns {object}
|
|
147
|
+
*/
|
|
148
|
+
export function set(obj, path, value) {
|
|
149
|
+
const result = deepClone(obj);
|
|
150
|
+
const parts = path.split('.');
|
|
151
|
+
let current = result;
|
|
152
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
153
|
+
if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {
|
|
154
|
+
current[parts[i]] = {};
|
|
155
|
+
}
|
|
156
|
+
current = current[parts[i]];
|
|
157
|
+
}
|
|
158
|
+
current[parts[parts.length - 1]] = value;
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 순수 객체인지 확인 (Array, Date 등 제외)
|
|
164
|
+
* @param {*} val
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
export function isPlainObject(val) {
|
|
168
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val)
|
|
169
|
+
&& Object.getPrototypeOf(val) === Object.prototype;
|
|
170
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaginationUtil — 페이지네이션 순수 유틸리티
|
|
3
|
+
*
|
|
4
|
+
* Stateless 함수들. DB Pagination 클래스와 달리
|
|
5
|
+
* 인메모리 배열/수동 메타 계산에 사용.
|
|
6
|
+
*
|
|
7
|
+
* @see lib/database/Pagination.js — DB 쿼리 결과용
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 메타 계산 — total, page, perPage로 페이지네이션 정보 생성
|
|
12
|
+
* @param {number} total - 전체 레코드 수
|
|
13
|
+
* @param {number} page - 현재 페이지 (1-based)
|
|
14
|
+
* @param {number} perPage - 페이지당 수
|
|
15
|
+
* @returns {{ total, page, perPage, lastPage, hasMore, from, to }}
|
|
16
|
+
*/
|
|
17
|
+
export function buildMeta(total, page = 1, perPage = 20) {
|
|
18
|
+
const lastPage = Math.ceil(total / perPage) || 1;
|
|
19
|
+
const from = (page - 1) * perPage + 1;
|
|
20
|
+
const to = Math.min(page * perPage, total);
|
|
21
|
+
return {
|
|
22
|
+
total,
|
|
23
|
+
page,
|
|
24
|
+
perPage,
|
|
25
|
+
lastPage,
|
|
26
|
+
hasMore: page < lastPage,
|
|
27
|
+
from: total > 0 ? from : 0,
|
|
28
|
+
to: total > 0 ? to : 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 인메모리 배열 페이지네이션
|
|
34
|
+
* @param {Array} items - 전체 배열
|
|
35
|
+
* @param {number} page - 현재 페이지 (1-based)
|
|
36
|
+
* @param {number} perPage - 페이지당 수
|
|
37
|
+
* @returns {{ data: Array, meta: object }}
|
|
38
|
+
*/
|
|
39
|
+
export function paginate(items, page = 1, perPage = 20) {
|
|
40
|
+
const total = items.length;
|
|
41
|
+
const start = (page - 1) * perPage;
|
|
42
|
+
const data = items.slice(start, start + perPage);
|
|
43
|
+
return { data, meta: buildMeta(total, page, perPage) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* offset/limit 계산
|
|
48
|
+
* @param {number} page
|
|
49
|
+
* @param {number} perPage
|
|
50
|
+
* @returns {{ offset: number, limit: number }}
|
|
51
|
+
*/
|
|
52
|
+
export function offsetLimit(page = 1, perPage = 20) {
|
|
53
|
+
return {
|
|
54
|
+
offset: (page - 1) * perPage,
|
|
55
|
+
limit: perPage,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 페이지 번호 배열 생성 (페이지 네비게이션 UI용)
|
|
61
|
+
* @param {number} current - 현재 페이지
|
|
62
|
+
* @param {number} last - 마지막 페이지
|
|
63
|
+
* @param {number} [delta=2] - 현재 페이지 앞뒤로 표시할 수
|
|
64
|
+
* @returns {Array<number|'...'>}
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* pageNumbers(5, 20, 2) → [1, '...', 3, 4, 5, 6, 7, '...', 20]
|
|
68
|
+
*/
|
|
69
|
+
export function pageNumbers(current, last, delta = 2) {
|
|
70
|
+
const pages = [];
|
|
71
|
+
const rangeStart = Math.max(2, current - delta);
|
|
72
|
+
const rangeEnd = Math.min(last - 1, current + delta);
|
|
73
|
+
|
|
74
|
+
pages.push(1);
|
|
75
|
+
if (rangeStart > 2) pages.push('...');
|
|
76
|
+
for (let i = rangeStart; i <= rangeEnd; i++) pages.push(i);
|
|
77
|
+
if (rangeEnd < last - 1) pages.push('...');
|
|
78
|
+
if (last > 1) pages.push(last);
|
|
79
|
+
|
|
80
|
+
return pages;
|
|
81
|
+
}
|