@fuzionx/framework 0.1.29 → 0.1.31
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 +230 -114
- package/cli/templates/{app/fuzionx → make/app}/controllers/HomeController.js +1 -0
- package/cli/templates/{app/tester → make/app}/views/default/errors/404.html +0 -4
- package/cli/templates/{app/fuzionx/views/default/errors/404.html → make/app/views/default/errors/500.html} +1 -2
- package/cli/templates/make/app/views/default/pages/home.html +11 -0
- package/index.js +3 -0
- package/lib/core/Application.js +31 -6
- package/lib/core/Context.js +30 -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
- package/cli/templates/app/.env.example.tpl +0 -14
- package/cli/templates/app/.gitignore.tpl +0 -4
- package/cli/templates/app/app.js.tpl +0 -6
- package/cli/templates/app/database/models/User.js +0 -9
- package/cli/templates/app/fuzionx/views/default/errors/500.html +0 -14
- package/cli/templates/app/fuzionx/views/default/pages/home.html +0 -188
- package/cli/templates/app/fuzionx.yaml.tpl +0 -202
- package/cli/templates/app/locales/en.json +0 -52
- package/cli/templates/app/locales/ko.json +0 -52
- package/cli/templates/app/package.json.tpl +0 -16
- package/cli/templates/app/shared/events/userEvents.js +0 -10
- package/cli/templates/app/shared/jobs/CleanupJob.js +0 -18
- package/cli/templates/app/shared/jobs/EmailTask.js +0 -17
- package/cli/templates/app/shared/jobs/VideoPreviewTask.js +0 -47
- package/cli/templates/app/shared/workers/heavy.js +0 -18
- package/cli/templates/app/tester/controllers/FileController.js +0 -288
- package/cli/templates/app/tester/controllers/HomeController.js +0 -36
- package/cli/templates/app/tester/controllers/UserController.js +0 -43
- package/cli/templates/app/tester/middleware/RequestLogger.js +0 -13
- package/cli/templates/app/tester/routes/api.js +0 -397
- package/cli/templates/app/tester/routes/web.js +0 -8
- package/cli/templates/app/tester/services/UserService.js +0 -52
- package/cli/templates/app/tester/views/default/errors/500.html +0 -14
- package/cli/templates/app/tester/views/default/layouts/main.html +0 -82
- package/cli/templates/app/tester/views/default/pages/home.html +0 -56
- package/cli/templates/app/tester/views/default/pages/i18n.html +0 -104
- package/cli/templates/app/tester/views/default/pages/upload.html +0 -149
- package/cli/templates/app/tester/views/default/pages/websocket.html +0 -239
- package/cli/templates/app/tester/views/default/partials/footer.html +0 -8
- package/cli/templates/app/tester/views/default/partials/header.html +0 -20
- package/cli/templates/app/tester/ws/ChatHandler.js +0 -98
- /package/cli/templates/{app/fuzionx/routes/api.js.tpl → make/app/routes/api.js} +0 -0
- /package/cli/templates/{app/fuzionx/routes/web.js.tpl → make/app/routes/web.js} +0 -0
- /package/cli/templates/{app/fuzionx → make/app}/views/default/layouts/main.html +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StrUtil — 문자열 순수 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* URL-safe slug 생성
|
|
7
|
+
* @param {string} str
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function slug(str) {
|
|
11
|
+
return str
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/[^\w\s가-힣-]/g, '')
|
|
15
|
+
.replace(/[\s_]+/g, '-')
|
|
16
|
+
.replace(/^-+|-+$/g, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 말줄임 (한글/이모지 안전)
|
|
21
|
+
* @param {string} str
|
|
22
|
+
* @param {number} len
|
|
23
|
+
* @param {string} [suffix='…']
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function truncate(str, len, suffix = '…') {
|
|
27
|
+
if (!str || str.length <= len) return str || '';
|
|
28
|
+
return str.slice(0, len) + suffix;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* camelCase 변환
|
|
33
|
+
* @param {string} str
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function camelCase(str) {
|
|
37
|
+
return str
|
|
38
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '')
|
|
39
|
+
.replace(/^[A-Z]/, c => c.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* snake_case 변환
|
|
44
|
+
* @param {string} str
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function snakeCase(str) {
|
|
48
|
+
return str
|
|
49
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
50
|
+
.replace(/[-\s]+/g, '_')
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* PascalCase 변환
|
|
56
|
+
* @param {string} str
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function pascalCase(str) {
|
|
60
|
+
return str
|
|
61
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '')
|
|
62
|
+
.replace(/^[a-z]/, c => c.toUpperCase());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 첫 글자 대문자
|
|
67
|
+
* @param {string} str
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function capitalize(str) {
|
|
71
|
+
if (!str) return '';
|
|
72
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 랜덤 문자열 생성
|
|
77
|
+
* @param {number} len
|
|
78
|
+
* @param {string} [chars]
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function random(len = 16, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
|
|
82
|
+
let result = '';
|
|
83
|
+
for (let i = 0; i < len; i++) {
|
|
84
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 마스킹 (이메일, 전화번호 등)
|
|
91
|
+
* @param {string} str
|
|
92
|
+
* @param {number} [visibleStart=2]
|
|
93
|
+
* @param {number} [visibleEnd=2]
|
|
94
|
+
* @param {string} [mask='*']
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*
|
|
97
|
+
* @example mask('hello@test.com', 2, 4) → 'he*****t.com'
|
|
98
|
+
*/
|
|
99
|
+
export function mask(str, visibleStart = 2, visibleEnd = 2, maskChar = '*') {
|
|
100
|
+
if (!str || str.length <= visibleStart + visibleEnd) return str || '';
|
|
101
|
+
const start = str.slice(0, visibleStart);
|
|
102
|
+
const end = str.slice(-visibleEnd);
|
|
103
|
+
const middle = maskChar.repeat(str.length - visibleStart - visibleEnd);
|
|
104
|
+
return start + middle + end;
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities — 순수 함수 유틸리티 배럴 export
|
|
3
|
+
*
|
|
4
|
+
* Helper와 달리 Bridge/App 의존 없이 import 즉시 사용 가능.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { PaginationUtil, StrUtil, ObjectUtil } from '@fuzionx/framework';
|
|
8
|
+
* const meta = PaginationUtil.buildMeta(100, 1, 20);
|
|
9
|
+
* const s = StrUtil.slug('Hello World');
|
|
10
|
+
* const cleaned = ObjectUtil.omit(user, ['password']);
|
|
11
|
+
*/
|
|
12
|
+
export * as PaginationUtil from './PaginationUtil.js';
|
|
13
|
+
export * as StrUtil from './StrUtil.js';
|
|
14
|
+
export * as NumUtil from './NumUtil.js';
|
|
15
|
+
export * as DateUtil from './DateUtil.js';
|
|
16
|
+
export * as ArrUtil from './ArrUtil.js';
|
|
17
|
+
export * as FunctionUtil from './FunctionUtil.js';
|
|
18
|
+
export * as ObjectUtil from './ObjectUtil.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "https://github.com/saytohenry/fuzionx"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fuzionx/core": "^0.1.
|
|
37
|
+
"@fuzionx/core": "^0.1.31",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { MariaModel } from '@fuzionx/framework';
|
|
2
|
-
|
|
3
|
-
export default class User extends MariaModel {
|
|
4
|
-
static table = 'users';
|
|
5
|
-
static connection = 'main';
|
|
6
|
-
static softDelete = true;
|
|
7
|
-
static hidden = ['password'];
|
|
8
|
-
static fillable = ['name', 'email', 'password', 'role'];
|
|
9
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{% extends "layouts/main.html" %}
|
|
2
|
-
|
|
3
|
-
{% block title %}500 — 서버 오류{% endblock %}
|
|
4
|
-
|
|
5
|
-
{% block content %}
|
|
6
|
-
<div style="text-align:center;padding:80px 20px;">
|
|
7
|
-
<h1 style="font-size:72px;color:#e74c3c;">500</h1>
|
|
8
|
-
<p style="font-size:20px;margin:16px 0;">{{ error.message | default(value='Internal Server Error') }}</p>
|
|
9
|
-
{% if config.debug and error.stack %}
|
|
10
|
-
<pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
|
|
11
|
-
{% endif %}
|
|
12
|
-
<a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
|
|
13
|
-
</div>
|
|
14
|
-
{% endblock %}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="ko">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>FuzionX</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap');
|
|
10
|
-
|
|
11
|
-
body {
|
|
12
|
-
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
13
|
-
min-height: 100vh;
|
|
14
|
-
display: flex;
|
|
15
|
-
align-items: center;
|
|
16
|
-
justify-content: center;
|
|
17
|
-
background: linear-gradient(135deg, #0f0c29 0%, #1a1a3e 40%, #24243e 100%);
|
|
18
|
-
color: #e0e0e0;
|
|
19
|
-
overflow: hidden;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.container {
|
|
23
|
-
text-align: center;
|
|
24
|
-
z-index: 1;
|
|
25
|
-
animation: fadeInUp 0.8s ease-out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.logo {
|
|
29
|
-
font-size: 4rem;
|
|
30
|
-
font-weight: 800;
|
|
31
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
32
|
-
-webkit-background-clip: text;
|
|
33
|
-
background-clip: text;
|
|
34
|
-
-webkit-text-fill-color: transparent;
|
|
35
|
-
letter-spacing: -2px;
|
|
36
|
-
margin-bottom: 0.5rem;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.subtitle {
|
|
40
|
-
font-size: 1.1rem;
|
|
41
|
-
font-weight: 300;
|
|
42
|
-
color: rgba(255, 255, 255, 0.5);
|
|
43
|
-
margin-bottom: 2.5rem;
|
|
44
|
-
letter-spacing: 2px;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.card {
|
|
48
|
-
background: rgba(255, 255, 255, 0.05);
|
|
49
|
-
backdrop-filter: blur(20px);
|
|
50
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
51
|
-
border-radius: 16px;
|
|
52
|
-
padding: 2rem 3rem;
|
|
53
|
-
max-width: 480px;
|
|
54
|
-
margin: 0 auto 2rem;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.version {
|
|
58
|
-
display: inline-block;
|
|
59
|
-
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
60
|
-
color: white;
|
|
61
|
-
padding: 4px 14px;
|
|
62
|
-
border-radius: 20px;
|
|
63
|
-
font-size: 0.75rem;
|
|
64
|
-
font-weight: 600;
|
|
65
|
-
letter-spacing: 1px;
|
|
66
|
-
margin-bottom: 1.5rem;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.features {
|
|
70
|
-
display: grid;
|
|
71
|
-
grid-template-columns: 1fr 1fr;
|
|
72
|
-
gap: 1rem;
|
|
73
|
-
text-align: left;
|
|
74
|
-
margin-top: 1.5rem;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.feature {
|
|
78
|
-
display: flex;
|
|
79
|
-
align-items: center;
|
|
80
|
-
gap: 8px;
|
|
81
|
-
font-size: 0.85rem;
|
|
82
|
-
color: rgba(255, 255, 255, 0.7);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.feature span {
|
|
86
|
-
font-size: 1.1rem;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.links {
|
|
90
|
-
display: flex;
|
|
91
|
-
gap: 1rem;
|
|
92
|
-
justify-content: center;
|
|
93
|
-
margin-top: 1rem;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.links a {
|
|
97
|
-
color: rgba(255, 255, 255, 0.6);
|
|
98
|
-
text-decoration: none;
|
|
99
|
-
font-size: 0.85rem;
|
|
100
|
-
padding: 8px 20px;
|
|
101
|
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
102
|
-
border-radius: 8px;
|
|
103
|
-
transition: all 0.3s ease;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.links a:hover {
|
|
107
|
-
color: #fff;
|
|
108
|
-
border-color: #667eea;
|
|
109
|
-
background: rgba(102, 126, 234, 0.1);
|
|
110
|
-
transform: translateY(-2px);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.hint {
|
|
114
|
-
margin-top: 2rem;
|
|
115
|
-
font-size: 0.75rem;
|
|
116
|
-
color: rgba(255, 255, 255, 0.3);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.hint code {
|
|
120
|
-
background: rgba(255, 255, 255, 0.08);
|
|
121
|
-
padding: 2px 8px;
|
|
122
|
-
border-radius: 4px;
|
|
123
|
-
font-family: 'JetBrains Mono', monospace;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* Background orbs */
|
|
127
|
-
.orb {
|
|
128
|
-
position: fixed;
|
|
129
|
-
border-radius: 50%;
|
|
130
|
-
filter: blur(80px);
|
|
131
|
-
opacity: 0.3;
|
|
132
|
-
animation: float 8s ease-in-out infinite;
|
|
133
|
-
}
|
|
134
|
-
.orb-1 { width: 400px; height: 400px; background: #667eea; top: -100px; right: -100px; }
|
|
135
|
-
.orb-2 { width: 300px; height: 300px; background: #764ba2; bottom: -80px; left: -80px; animation-delay: -4s; }
|
|
136
|
-
.orb-3 { width: 200px; height: 200px; background: #f093fb; top: 50%; left: 60%; animation-delay: -2s; }
|
|
137
|
-
|
|
138
|
-
@keyframes fadeInUp {
|
|
139
|
-
from { opacity: 0; transform: translateY(30px); }
|
|
140
|
-
to { opacity: 1; transform: translateY(0); }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
@keyframes float {
|
|
144
|
-
0%, 100% { transform: translate(0, 0); }
|
|
145
|
-
50% { transform: translate(30px, -30px); }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@media (max-width: 600px) {
|
|
149
|
-
.logo { font-size: 2.5rem; }
|
|
150
|
-
.card { padding: 1.5rem; margin: 0 1rem; }
|
|
151
|
-
.features { grid-template-columns: 1fr; }
|
|
152
|
-
}
|
|
153
|
-
</style>
|
|
154
|
-
</head>
|
|
155
|
-
<body>
|
|
156
|
-
<div class="orb orb-1"></div>
|
|
157
|
-
<div class="orb orb-2"></div>
|
|
158
|
-
<div class="orb orb-3"></div>
|
|
159
|
-
|
|
160
|
-
<div class="container">
|
|
161
|
-
<h1 class="logo">FuzionX</h1>
|
|
162
|
-
<p class="subtitle">HIGH-PERFORMANCE NODE.JS FRAMEWORK</p>
|
|
163
|
-
|
|
164
|
-
<div class="card">
|
|
165
|
-
<div class="version">v0.1.0 · POWERED BY RUST</div>
|
|
166
|
-
|
|
167
|
-
<div class="features">
|
|
168
|
-
<div class="feature"><span>⚡</span> 500K+ RPS</div>
|
|
169
|
-
<div class="feature"><span>🦀</span> Rust N-API Bridge</div>
|
|
170
|
-
<div class="feature"><span>🎯</span> MVC Architecture</div>
|
|
171
|
-
<div class="feature"><span>🔌</span> WebSocket</div>
|
|
172
|
-
<div class="feature"><span>🗄️</span> Multi-DB ORM</div>
|
|
173
|
-
<div class="feature"><span>🔐</span> Auth & Session</div>
|
|
174
|
-
<div class="feature"><span>📡</span> Event System</div>
|
|
175
|
-
<div class="feature"><span>⏰</span> Job Scheduler</div>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
<div class="links">
|
|
180
|
-
<a href="https://github.com/saytohenry/fuzionx">GitHub</a>
|
|
181
|
-
<a href="/docs">API Docs</a>
|
|
182
|
-
<a href="/api/health">Health Check</a>
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
<p class="hint">Edit <code>routes/web.js</code> to get started</p>
|
|
186
|
-
</div>
|
|
187
|
-
</body>
|
|
188
|
-
</html>
|