@fuzionx/framework 0.1.39 → 0.1.42
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 -99
- package/cli/index.js +494 -493
- 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 -226
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +228 -228
- 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
|
@@ -1,170 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,81 +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
|
-
}
|
|
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
|
+
}
|