create-fuzionx 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.
Files changed (124) hide show
  1. package/fx.js +20 -16
  2. package/index.js +103 -86
  3. package/package.json +1 -1
  4. package/templates/{fuzionx.yaml.tpl → common/fuzionx.yaml.tpl} +3 -1
  5. package/templates/{package.json.tpl → common/package.json.tpl} +5 -1
  6. package/templates/spa/controllers/AuthController.js +114 -0
  7. package/templates/spa/controllers/HomeController.js +68 -0
  8. package/templates/spa/controllers/PostController.js +191 -0
  9. package/templates/spa/controllers/UserController.js +43 -0
  10. package/templates/spa/meta.json +24 -0
  11. package/templates/spa/routes/api.js +31 -0
  12. package/templates/spa/routes/web.js +19 -0
  13. package/templates/spa/services/AuthService.js +48 -0
  14. package/templates/spa/services/PostService.js +372 -0
  15. package/templates/spa/services/UserService.js +48 -0
  16. package/templates/spa/views/default/errors/404.html +11 -0
  17. package/templates/spa/views/default/errors/500.html +11 -0
  18. package/templates/spa/views/default/layouts/main.html +34 -0
  19. package/templates/spa/views/default/pages/home.html +22 -0
  20. package/templates/spa/views/default/spa/index.html +13 -0
  21. package/templates/spa/views/default/spa/package.json +20 -0
  22. package/templates/spa/views/default/spa/src/App.vue +41 -0
  23. package/templates/spa/views/default/spa/src/assets/landing.css +220 -0
  24. package/templates/spa/views/default/spa/src/assets/style.css +1156 -0
  25. package/templates/spa/views/default/spa/src/components/AlertDialog.vue +179 -0
  26. package/templates/spa/views/default/spa/src/components/CodeBlock.vue +33 -0
  27. package/templates/spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
  28. package/templates/spa/views/default/spa/src/components/FileUpload.vue +161 -0
  29. package/templates/spa/views/default/spa/src/components/FlashMessage.vue +39 -0
  30. package/templates/spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
  31. package/templates/spa/views/default/spa/src/components/Lightbox.vue +62 -0
  32. package/templates/spa/views/default/spa/src/components/Navbar.vue +68 -0
  33. package/templates/spa/views/default/spa/src/components/Pagination.vue +166 -0
  34. package/templates/spa/views/default/spa/src/components/ToastContainer.vue +135 -0
  35. package/templates/spa/views/default/spa/src/composables/useApi.js +129 -0
  36. package/templates/spa/views/default/spa/src/composables/useClipboard.js +44 -0
  37. package/templates/spa/views/default/spa/src/composables/useDate.js +73 -0
  38. package/templates/spa/views/default/spa/src/composables/useDebounce.js +59 -0
  39. package/templates/spa/views/default/spa/src/composables/useFlash.js +46 -0
  40. package/templates/spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
  41. package/templates/spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
  42. package/templates/spa/views/default/spa/src/composables/useLocale.js +79 -0
  43. package/templates/spa/views/default/spa/src/composables/useWebSocket.js +93 -0
  44. package/templates/spa/views/default/spa/src/main.js +108 -0
  45. package/templates/spa/views/default/spa/src/plugins/alert.js +96 -0
  46. package/templates/spa/views/default/spa/src/plugins/toast.js +79 -0
  47. package/templates/spa/views/default/spa/src/router/index.js +29 -0
  48. package/templates/spa/views/default/spa/src/stores/auth.js +58 -0
  49. package/templates/spa/views/default/spa/src/views/BoardDetail.vue +169 -0
  50. package/templates/spa/views/default/spa/src/views/BoardForm.vue +192 -0
  51. package/templates/spa/views/default/spa/src/views/BoardList.vue +129 -0
  52. package/templates/spa/views/default/spa/src/views/ChatView.vue +327 -0
  53. package/templates/spa/views/default/spa/src/views/FeaturesView.vue +242 -0
  54. package/templates/spa/views/default/spa/src/views/HomeView.vue +215 -0
  55. package/templates/spa/views/default/spa/src/views/Login.vue +82 -0
  56. package/templates/spa/views/default/spa/src/views/Profile.vue +85 -0
  57. package/templates/spa/views/default/spa/src/views/Register.vue +84 -0
  58. package/templates/spa/views/default/spa/vite.config.js +28 -0
  59. package/templates/spa/views/default/spa/yarn.lock +633 -0
  60. package/templates/spa/ws/ChatHandler.js +7 -0
  61. package/templates/ssr/controllers/AuthController.js +119 -0
  62. package/templates/ssr/controllers/ChatController.js +15 -0
  63. package/templates/ssr/controllers/FeaturesController.js +15 -0
  64. package/templates/ssr/controllers/HomeController.js +21 -0
  65. package/templates/ssr/controllers/PostController.js +214 -0
  66. package/templates/ssr/controllers/UserController.js +48 -0
  67. package/templates/ssr/meta.json +11 -0
  68. package/templates/ssr/public/css/fx-ui.css +43 -0
  69. package/templates/ssr/public/css/landing.css +220 -0
  70. package/templates/ssr/public/css/style.css +1011 -0
  71. package/templates/ssr/public/js/fx-ui.js +124 -0
  72. package/templates/ssr/routes/web.js +46 -0
  73. package/templates/ssr/services/AuthService.js +48 -0
  74. package/templates/ssr/services/PostService.js +372 -0
  75. package/templates/ssr/services/UserService.js +48 -0
  76. package/templates/ssr/views/default/errors/404.html +11 -0
  77. package/templates/ssr/views/default/errors/500.html +48 -0
  78. package/templates/ssr/views/default/layouts/main.html +93 -0
  79. package/templates/ssr/views/default/pages/board/form.html +240 -0
  80. package/templates/ssr/views/default/pages/board/index.html +73 -0
  81. package/templates/ssr/views/default/pages/board/show.html +148 -0
  82. package/templates/ssr/views/default/pages/chat.html +288 -0
  83. package/templates/ssr/views/default/pages/features.html +373 -0
  84. package/templates/ssr/views/default/pages/home.html +258 -0
  85. package/templates/ssr/views/default/pages/login.html +27 -0
  86. package/templates/ssr/views/default/pages/profile.html +36 -0
  87. package/templates/ssr/views/default/pages/register.html +35 -0
  88. package/templates/ssr/views/default/partials/pagination.html +75 -0
  89. package/templates/ssr/ws/ChatHandler.js +138 -0
  90. package/templates/.env.example.tpl +0 -14
  91. package/templates/.gitignore.tpl +0 -5
  92. package/templates/fuzionx/controllers/HomeController.js +0 -13
  93. package/templates/fuzionx/routes/api.js.tpl +0 -7
  94. package/templates/fuzionx/routes/web.js.tpl +0 -5
  95. package/templates/fuzionx/views/default/layouts/main.html +0 -22
  96. package/templates/fuzionx/views/default/pages/home.html +0 -188
  97. package/templates/tester/views/default/errors/404.html +0 -15
  98. package/templates/tester/views/default/errors/500.html +0 -14
  99. /package/templates/{app.js.tpl → common/app.js.tpl} +0 -0
  100. /package/templates/{database → common/database}/models/User.js +0 -0
  101. /package/templates/{locales → common/locales}/en.json +0 -0
  102. /package/templates/{locales → common/locales}/ko.json +0 -0
  103. /package/templates/{shared → common/shared}/events/userEvents.js +0 -0
  104. /package/templates/{shared → common/shared}/jobs/CleanupJob.js +0 -0
  105. /package/templates/{shared → common/shared}/jobs/EmailTask.js +0 -0
  106. /package/templates/{shared → common/shared}/jobs/VideoPreviewTask.js +0 -0
  107. /package/templates/{shared → common/shared}/workers/heavy.js +0 -0
  108. /package/templates/{tester → common/tester}/controllers/FileController.js +0 -0
  109. /package/templates/{tester → common/tester}/controllers/HomeController.js +0 -0
  110. /package/templates/{tester → common/tester}/controllers/UserController.js +0 -0
  111. /package/templates/{tester → common/tester}/middleware/RequestLogger.js +0 -0
  112. /package/templates/{tester → common/tester}/routes/api.js +0 -0
  113. /package/templates/{tester → common/tester}/routes/web.js +0 -0
  114. /package/templates/{tester → common/tester}/services/UserService.js +0 -0
  115. /package/templates/{fuzionx → common/tester}/views/default/errors/404.html +0 -0
  116. /package/templates/{fuzionx → common/tester}/views/default/errors/500.html +0 -0
  117. /package/templates/{tester → common/tester}/views/default/layouts/main.html +0 -0
  118. /package/templates/{tester → common/tester}/views/default/pages/home.html +0 -0
  119. /package/templates/{tester → common/tester}/views/default/pages/i18n.html +0 -0
  120. /package/templates/{tester → common/tester}/views/default/pages/upload.html +0 -0
  121. /package/templates/{tester → common/tester}/views/default/pages/websocket.html +0 -0
  122. /package/templates/{tester → common/tester}/views/default/partials/footer.html +0 -0
  123. /package/templates/{tester → common/tester}/views/default/partials/header.html +0 -0
  124. /package/templates/{tester → common/tester}/ws/ChatHandler.js +0 -0
package/fx.js CHANGED
@@ -48,22 +48,26 @@ if (existsSync(localBin) || existsSync(localCli)) {
48
48
  fx new <name> 새 프로젝트 스캐폴딩
49
49
 
50
50
  프로젝트 내 명령어 (@fuzionx/framework 필요):
51
- fx make:app <name> 새 디렉토리 생성
52
- fx make:controller <Name> 컨트롤러 생성
53
- fx make:model <Name> 모델 생성
54
- fx make:service <Name> 서비스 생성
55
- fx make:middleware <Name> 미들웨어 생성
56
- fx make:job <Name> Job 생성
57
- fx make:task <Name> Task 생성
58
- fx make:ws <Name> WsHandler 생성
59
- fx make:event <Name> 이벤트 핸들러 생성
60
- fx dev 개발 서버 (--watch)
61
- fx stop 서버 종료 (graceful)
62
- fx restart 서버 재시작 (graceful)
63
- fx test 테스트 실행
64
- fx routes 라우트 테이블
65
- fx config 설정 출력
66
- fx db:sync 모델 DB 동기화
51
+ fx make:app --type=ssr|spa Create app (fixed name: app/ssr or app/spa)
52
+ fx make:controller <Name> --app= Create controller (app-specific)
53
+ fx make:service <Name> --app= Create service (app-specific)
54
+ fx make:model <Name> Create model (database/models)
55
+ fx make:middleware <Name> --app= Create middleware (app-specific)
56
+ fx make:job <Name> Create job (shared/jobs)
57
+ fx make:task <Name> Create task (shared/jobs)
58
+ fx make:ws <Name> --app= Create WsHandler (app-specific)
59
+ fx make:event <Name> Create event handler (shared/events)
60
+ fx make:worker <Name> Create worker (shared/workers)
61
+ fx make:test <Name> Create test
62
+ fx dev Start dev server (--watch)
63
+ fx dev:spa Start dev server + Vite HMR
64
+ fx build:spa Build SPA for production
65
+ fx stop Stop server (graceful)
66
+ fx restart Restart server (graceful)
67
+ fx test Run tests
68
+ fx routes Print route table
69
+ fx config Print fuzionx.yaml
70
+ fx db:sync Sync models → DB (--apply)
67
71
  `);
68
72
  } else {
69
73
  console.error(`❌ @fuzionx/framework가 설치되지 않았습니다.`);
package/index.js CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Usage:
6
6
  * npx create-fuzionx my-app
7
- * npx create-fuzionx my-app /path/to/dir
7
+ * npx create-fuzionx my-app --type=spa
8
+ * npx create-fuzionx my-app --type=ssr
8
9
  */
9
10
  import { promises as fs } from 'node:fs';
10
11
  import path from 'node:path';
@@ -24,65 +25,62 @@ async function loadTemplate(relPath, vars) {
24
25
  return render(raw, vars);
25
26
  }
26
27
 
27
- /** 디렉토리 재귀 복사 */
28
- async function copyDirRecursive(src, dst) {
28
+ /** 디렉토리 재귀 복사 — .tpl 파일은 변수 치환 */
29
+ async function copyDirRecursive(src, dst, vars = {}) {
29
30
  await fs.mkdir(dst, { recursive: true });
30
31
  const entries = await fs.readdir(src, { withFileTypes: true });
31
32
  for (const entry of entries) {
32
33
  const srcPath = path.join(src, entry.name);
33
34
  const dstPath = path.join(dst, entry.name);
34
35
  if (entry.isDirectory()) {
35
- await copyDirRecursive(srcPath, dstPath);
36
+ await copyDirRecursive(srcPath, dstPath, vars);
37
+ } else if (entry.name === 'meta.json') {
38
+ // meta.json은 스캐폴딩에 포함하지 않음
39
+ continue;
40
+ } else if (entry.name.endsWith('.tpl')) {
41
+ // .tpl 파일은 변수 치환 후 확장자 제거
42
+ const content = await fs.readFile(srcPath, 'utf-8');
43
+ const rendered = render(content, vars);
44
+ const destName = entry.name.replace(/\.tpl$/, '');
45
+ await fs.writeFile(path.join(dst, destName), rendered);
36
46
  } else {
37
47
  await fs.copyFile(srcPath, dstPath);
38
48
  }
39
49
  }
40
50
  }
41
51
 
42
- // ── 스캐폴딩 파일 목록 (fuzionx + tester 2-app 구조) ──
52
+ // ── 디렉토리 (.gitkeep 포함) ──
43
53
 
44
- const APP_FILES = [
45
- { tpl: 'package.json.tpl', dest: 'package.json' },
46
- { tpl: 'fuzionx.yaml.tpl', dest: 'fuzionx.yaml' },
47
- { tpl: 'app.js.tpl', dest: 'app.js' },
48
- { tpl: '.env.example.tpl', dest: '.env.example' },
49
- { tpl: '.env.example.tpl', dest: '.env' },
50
- { tpl: '.gitignore.tpl', dest: '.gitignore' },
51
- ];
52
-
53
- /** fuzionx 앱 — .tpl 파일 (치환 필요) */
54
- const FUZIONX_TPL_FILES = [
55
- { tpl: 'fuzionx/routes/web.js.tpl', dest: 'app/fuzionx/routes/web.js' },
56
- { tpl: 'fuzionx/routes/api.js.tpl', dest: 'app/fuzionx/routes/api.js' },
57
- ];
58
-
59
- /** 빈 디렉토리 (.gitkeep 포함) */
60
- const APP_DIRS = [
61
- // fuzionx 앱
62
- 'app/fuzionx/services',
63
- 'app/fuzionx/middleware',
64
- 'app/fuzionx/ws',
65
- // 인프라
54
+ const EMPTY_DIRS = [
66
55
  'database/migrations',
67
56
  'database/seeds',
68
57
  'storage/logs',
69
58
  'storage/uploads',
70
- 'public/css',
71
- 'public/js',
72
59
  'tests',
73
60
  ];
74
61
 
75
62
  // ── createApp ──
76
63
 
77
- async function createApp(name, targetDir) {
64
+ async function createApp(name, targetDir, type = 'spa') {
78
65
  const dir = targetDir || path.resolve(name);
66
+ const appName = type; // 앱 디렉토리명은 타입과 동일
79
67
  const vars = {
80
68
  name,
81
69
  dbName: name.replace(/-/g, '_'),
82
70
  };
83
71
 
84
- // 디렉토리 생성 (.gitkeep 포함)
85
- for (const d of APP_DIRS) {
72
+ const commonDir = path.join(TPL_DIR, 'common');
73
+ const typeDir = path.join(TPL_DIR, type);
74
+
75
+ // meta.json 로드
76
+ let meta = {};
77
+ try {
78
+ const metaRaw = await fs.readFile(path.join(typeDir, 'meta.json'), 'utf-8');
79
+ meta = JSON.parse(metaRaw);
80
+ } catch { /* meta.json 없어도 진행 */ }
81
+
82
+ // 1. 빈 디렉토리 생성
83
+ for (const d of EMPTY_DIRS) {
86
84
  const fullDir = path.join(dir, d);
87
85
  await fs.mkdir(fullDir, { recursive: true });
88
86
  const files = await fs.readdir(fullDir).catch(() => []);
@@ -91,86 +89,105 @@ async function createApp(name, targetDir) {
91
89
  }
92
90
  }
93
91
 
94
- // 루트 템플릿 파일 생성 (.tpl 치환)
95
- for (const { tpl, dest } of APP_FILES) {
96
- const content = await loadTemplate(tpl, vars);
97
- const fullPath = path.join(dir, dest);
98
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
99
- await fs.writeFile(fullPath, content);
92
+ // 전용 디렉토리
93
+ for (const d of ['services', 'middleware', 'ws']) {
94
+ const fullDir = path.join(dir, `app/${appName}`, d);
95
+ await fs.mkdir(fullDir, { recursive: true });
96
+ const files = await fs.readdir(fullDir).catch(() => []);
97
+ if (files.length === 0) {
98
+ await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
99
+ }
100
100
  }
101
101
 
102
- // fuzionx .tpl 파일 (치환 필요)
103
- for (const { tpl, dest } of FUZIONX_TPL_FILES) {
104
- const content = await loadTemplate(tpl, vars);
105
- const fullPath = path.join(dir, dest);
106
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
107
- await fs.writeFile(fullPath, content);
102
+ // 2. common/ 루트 .tpl 파일 프로젝트 루트
103
+ const commonEntries = await fs.readdir(commonDir, { withFileTypes: true });
104
+ for (const entry of commonEntries) {
105
+ if (entry.isFile() && entry.name.endsWith('.tpl')) {
106
+ const content = await fs.readFile(path.join(commonDir, entry.name), 'utf-8');
107
+ const rendered = render(content, vars);
108
+ const destName = entry.name.replace(/\.tpl$/, '');
109
+ await fs.writeFile(path.join(dir, destName), rendered);
110
+ }
108
111
  }
109
112
 
110
- // fuzionx 직접 복사 (controllers, views)
111
- const fuzionxSrc = path.join(TPL_DIR, 'fuzionx');
112
- await copyDirRecursive(
113
- path.join(fuzionxSrc, 'controllers'),
114
- path.join(dir, 'app/fuzionx/controllers'),
115
- );
116
- await copyDirRecursive(
117
- path.join(fuzionxSrc, 'views'),
118
- path.join(dir, 'app/fuzionx/views'),
119
- );
120
-
121
- // tester 앱 전체 복사
122
- await copyDirRecursive(
123
- path.join(TPL_DIR, 'tester'),
124
- path.join(dir, 'app/tester'),
125
- );
126
-
127
- // shared — 전체 복사
128
- await copyDirRecursive(
129
- path.join(TPL_DIR, 'shared'),
130
- path.join(dir, 'shared'),
131
- );
132
-
133
- // database/models 복사
134
- await copyDirRecursive(
135
- path.join(TPL_DIR, 'database'),
136
- path.join(dir, 'database'),
137
- );
138
-
139
- // locales — 복사
140
- await copyDirRecursive(
141
- path.join(TPL_DIR, 'locales'),
142
- path.join(dir, 'locales'),
143
- );
113
+ // 3. common/ 하위 디렉토리 복사 (database, locales, shared)
114
+ for (const entry of commonEntries) {
115
+ if (!entry.isDirectory()) continue;
116
+ if (entry.name === 'tester') continue; // tester는 별도 처리
117
+ await copyDirRecursive(
118
+ path.join(commonDir, entry.name),
119
+ path.join(dir, entry.name),
120
+ vars,
121
+ );
122
+ }
123
+
124
+ // 4. tester 앱 복사 (common/tester/ → app/tester/)
125
+ const testerSrc = path.join(commonDir, 'tester');
126
+ try {
127
+ await fs.access(testerSrc);
128
+ await copyDirRecursive(testerSrc, path.join(dir, 'app/tester'), vars);
129
+ } catch { /* tester 없으면 스킵 */ }
130
+
131
+ // 5. 타입별 템플릿 복사 → app/{type}/
132
+ // controllers, services, middleware, routes, ws, views
133
+ const typeEntries = await fs.readdir(typeDir, { withFileTypes: true });
134
+ for (const entry of typeEntries) {
135
+ if (!entry.isDirectory()) continue;
136
+ if (entry.name === 'public') continue; // public은 프로젝트 루트로 복사
137
+ await copyDirRecursive(
138
+ path.join(typeDir, entry.name),
139
+ path.join(dir, `app/${appName}`, entry.name),
140
+ vars,
141
+ );
142
+ }
143
+
144
+ // 6. public/ 디렉토리 복사 (SSR의 경우 CSS/JS assets)
145
+ const publicSrc = path.join(typeDir, 'public');
146
+ try {
147
+ await fs.access(publicSrc);
148
+ await copyDirRecursive(publicSrc, path.join(dir, 'public'), vars);
149
+ } catch { /* public 없으면 스킵 */ }
144
150
 
145
151
  return dir;
146
152
  }
147
153
 
148
154
  // ── CLI 엔트리 ──
149
155
 
150
- const name = process.argv[2];
151
- const targetDir = process.argv[3];
156
+ const allArgs = process.argv.slice(2);
157
+ const name = allArgs.find(a => !a.startsWith('--'));
158
+ const typeFlag = allArgs.find(a => a.startsWith('--type='));
159
+ const type = typeFlag?.split('=')[1] || 'spa';
152
160
 
153
161
  if (!name) {
154
162
  console.error(`
155
- Usage: npx create-fuzionx <app-name> [target-dir]
163
+ Usage: npx create-fuzionx <app-name> [--type=ssr|spa]
156
164
 
157
165
  Examples:
158
- npx create-fuzionx my-app
159
- npx create-fuzionx my-app /path/to/dir
166
+ npx create-fuzionx my-app # default: spa
167
+ npx create-fuzionx my-app --type=ssr # MPA (SSR)
168
+ npx create-fuzionx my-app --type=spa # SPA (Vue.js 3 + SSR)
160
169
  `);
161
170
  process.exit(1);
162
171
  }
163
172
 
173
+ const validTypes = ['ssr', 'spa'];
174
+ if (!validTypes.includes(type)) {
175
+ console.error(`❌ Unknown type: ${type}. Available: ${validTypes.join(', ')}`);
176
+ process.exit(1);
177
+ }
178
+
164
179
  try {
165
- const dir = await createApp(name, targetDir);
180
+ const dir = await createApp(name, null, type);
181
+ const nextCmd = type === 'spa' ? 'npx fx dev:spa' : 'npx fx dev';
166
182
  console.log(`
167
- ✅ Created ${name} at ${dir}
183
+ ✅ Created ${name} at ${dir} (type: ${type})
168
184
 
169
185
  Next steps:
170
186
 
171
187
  cd ${name}
172
188
  npm install
173
- npx fx dev
189
+ ${type === 'spa' ? `cd app/spa/views/default/spa && npm install && cd ../../../../..
190
+ ` : ''}${nextCmd}
174
191
  `);
175
192
  } catch (err) {
176
193
  console.error(`❌ Failed to create app: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fuzionx",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Create a new FuzionX application — npx create-fuzionx my-app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,6 +87,8 @@ bridge:
87
87
  static:
88
88
  - url: /public
89
89
  path: ./public
90
+ - url: /wasm
91
+ path: ./node_modules/@fuzionx/client
90
92
 
91
93
  # ── 로깅 ──
92
94
  logging:
@@ -125,7 +127,7 @@ bridge:
125
127
 
126
128
  # ── ASP 암호화 ──
127
129
  asp:
128
- enabled: false
130
+ enabled: true
129
131
  master_secret: "${ASP_SECRET:change-me-in-production}"
130
132
  header_signal: Ruxy-Enc-Mode
131
133
 
@@ -4,13 +4,17 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "fx dev",
7
+ "dev:spa": "fx dev:spa",
8
+ "build:spa": "fx build:spa",
7
9
  "test": "vitest run"
8
10
  },
9
11
  "dependencies": {
10
- "@fuzionx/framework": "^0.1.30",
12
+ "@fuzionx/framework": "^0.1.32",
13
+ "@fuzionx/client": "^0.1.32",
11
14
  "joi": "^18.1.1"
12
15
  },
13
16
  "devDependencies": {
17
+ "concurrently": "^9.0.0",
14
18
  "vitest": "^3.0.0"
15
19
  }
16
20
  }
@@ -0,0 +1,114 @@
1
+ import { Controller } from '@fuzionx/framework';
2
+
3
+ /**
4
+ * SPA AuthController — JSON API 전용 인증
5
+ *
6
+ * 모든 페이지는 Vue에서 렌더링. 서버는 API만 제공.
7
+ * 세션 기반 인증. JSON 요청/응답.
8
+ */
9
+ export default class AuthController extends Controller {
10
+ /** @type {import('@fuzionx/framework').RouteHandler} 로그인 API */
11
+ static login;
12
+ /** @type {import('@fuzionx/framework').RouteHandler} 회원가입 API */
13
+ static register;
14
+ /** @type {import('@fuzionx/framework').RouteHandler} 로그아웃 API */
15
+ static logout;
16
+ /** @type {import('@fuzionx/framework').RouteHandler} 인증 상태 확인 API */
17
+ static check;
18
+ /** @type {import('@fuzionx/framework').RouteHandler} 하트비트 (세션 연장) */
19
+ static heartbeat;
20
+
21
+ /**
22
+ * POST /api/auth/login — 로그인
23
+ *
24
+ * email/password 검증 후 세션에 userId 저장.
25
+ * 성공 → { user }, 실패 → 401 { error }.
26
+ *
27
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
28
+ * @returns {void}
29
+ */
30
+ async login(ctx) {
31
+ const { email, password } = ctx.body;
32
+ const user = await this.service('AuthService').login({ email, password });
33
+
34
+ if (!user) {
35
+ return ctx.status(401).json({ error: ctx.t('auth.login_failed') });
36
+ }
37
+
38
+ ctx.session.set('userId', user.id);
39
+ ctx.json({ user: { id: user.id, name: user.name, email: user.email, role: user.role } });
40
+ }
41
+
42
+ /**
43
+ * POST /api/auth/register — 회원가입
44
+ *
45
+ * name/email/password/password_confirm 검증.
46
+ * 성공 → 201 { user }, 실패 → 400 { error }.
47
+ *
48
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
49
+ * @returns {void}
50
+ */
51
+ async register(ctx) {
52
+ const { name, email, password, password_confirm } = ctx.body;
53
+
54
+ if (password !== password_confirm) {
55
+ return ctx.status(400).json({ error: ctx.t('auth.password_mismatch') });
56
+ }
57
+
58
+ try {
59
+ const user = await this.service('AuthService').register({ name, email, password });
60
+ ctx.session.set('userId', user.id);
61
+ ctx.status(201).json({ user: { id: user.id, name: user.name, email: user.email } });
62
+ } catch (e) {
63
+ ctx.status(e.status || 400).json({ error: ctx.t(e.message) || e.message });
64
+ }
65
+ }
66
+
67
+ /**
68
+ * POST /api/auth/logout — 로그아웃
69
+ *
70
+ * 세션 파기 후 { success: true } 반환.
71
+ *
72
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
73
+ * @returns {void}
74
+ */
75
+ async logout(ctx) {
76
+ ctx.session.destroy();
77
+ ctx.json({ success: true });
78
+ }
79
+
80
+ /**
81
+ * GET /api/auth/check — 인증 상태 확인
82
+ *
83
+ * 세션에 userId 존재 → DB 조회 → { authenticated, user }.
84
+ * 미인증 시 { authenticated: false, user: null }.
85
+ *
86
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
87
+ * @returns {void}
88
+ */
89
+ async check(ctx) {
90
+ const userId = ctx.session.get('userId');
91
+ if (!userId) return ctx.json({ authenticated: false, user: null });
92
+
93
+ try {
94
+ const user = await this.db.User.find(userId);
95
+ if (!user) return ctx.json({ authenticated: false, user: null });
96
+ ctx.json({ authenticated: true, user: user.toJSON() });
97
+ } catch {
98
+ ctx.json({ authenticated: false, user: null });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * GET /api/heartbeat — 세션 연장 (하트비트)
104
+ *
105
+ * 인증된 사용자의 세션 유지. { alive, user }.
106
+ *
107
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
108
+ * @returns {void}
109
+ */
110
+ async heartbeat(ctx) {
111
+ if (!ctx.user) return ctx.status(401).json({ alive: false });
112
+ ctx.json({ alive: true, user: { id: ctx.user.id } });
113
+ }
114
+ }
@@ -0,0 +1,68 @@
1
+ import { Controller } from '@fuzionx/framework';
2
+
3
+ /**
4
+ * SPA HomeController — Vue 셸 렌더링
5
+ *
6
+ * 모든 프론트엔드 라우트에 대해 Vue 앱 셸을 서빙.
7
+ * 인증 여부와 관계없이 셸을 렌더링하며,
8
+ * 유저 데이터를 ASP 암호화하여 Vue에 전달.
9
+ * Vue가 user=null이면 로그인 화면, 아니면 대시보드를 표시.
10
+ *
11
+ * bridge.cryptoEncryptCustom(key, plaintext) → WASM decrypt_custom(key, ciphertext)
12
+ */
13
+ export default class HomeController extends Controller {
14
+ /** @type {import('@fuzionx/framework').RouteHandler} SPA 셸 렌더링 */
15
+ static index;
16
+
17
+ /**
18
+ * GET / | /* — Vue SPA 셸 렌더링
19
+ *
20
+ * 세션에서 유저 조회 후 ASP 암호화된 payload와 함께 렌더링.
21
+ * payload: { user, locale, isDev, asp, app }
22
+ *
23
+ * @param {import('@fuzionx/framework').Context} ctx - 요청 컨텍스트
24
+ * @returns {void}
25
+ */
26
+ async index(ctx) {
27
+ const clientSecret = this.config.get('app.client_secret');
28
+ const bridge = this.app._bridge;
29
+
30
+ // ASP config에서 masterSecret 가져오기
31
+ const aspConfig = JSON.parse(bridge.getAspConfig());
32
+ const masterSecret = aspConfig.masterSecret || '';
33
+
34
+ // 세션에서 유저 조회 (인증 안 되어 있으면 null)
35
+ let user = null;
36
+ const userId = ctx.session?.get('userId');
37
+ if (userId && this.db?.User) {
38
+ try {
39
+ const u = await this.db.User.find(userId);
40
+ if (u) user = { id: u.id, name: u.name, email: u.email, role: u.role };
41
+ } catch {}
42
+ }
43
+
44
+ const isDev = this.config.get('app.environment') === 'development';
45
+
46
+ // SPA에 전달할 payload
47
+ const payload = {
48
+ user,
49
+ locale: ctx.locale,
50
+ locales: this.app?.i18n?.locales?.() || [],
51
+ translations: ctx.t.all(),
52
+ isDev,
53
+ asp: { headerSignal: aspConfig.headerSignal || 'Ruxy-Enc-Mode' },
54
+ app: { name: this.config.get('app.name'), theme: this.config.get('app.themes.default') },
55
+ };
56
+
57
+ // 암호화 (bridge crypto_encrypt_custom — WASM decrypt_custom과 호환)
58
+ const encryptedPayload = bridge.cryptoEncryptCustom(clientSecret, JSON.stringify(payload));
59
+ const encryptedSecret = bridge.cryptoEncryptCustom(clientSecret, masterSecret);
60
+
61
+ ctx.render('home', {
62
+ __fx__: encryptedPayload,
63
+ __fx_secret__: encryptedSecret,
64
+ client_secret: clientSecret,
65
+ isDev,
66
+ });
67
+ }
68
+ }