@fuzionx/framework 0.1.46 → 0.1.47
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 +29 -2
- package/cli/index.js +37 -8
- package/cli/templates/make/app-spa/controllers/AuthController.js +114 -0
- package/cli/templates/make/app-spa/controllers/HomeController.js +66 -0
- package/cli/templates/make/app-spa/controllers/PostController.js +191 -0
- package/cli/templates/make/app-spa/controllers/UserController.js +43 -0
- package/cli/templates/make/app-spa/public/css/style.css +1011 -0
- package/cli/templates/make/app-spa/routes/api.js +31 -0
- package/cli/templates/make/app-spa/routes/web.js +19 -0
- package/cli/templates/make/app-spa/services/AuthService.js +48 -0
- package/cli/templates/make/app-spa/services/PostService.js +372 -0
- package/cli/templates/make/app-spa/services/UserService.js +48 -0
- package/cli/templates/make/app-spa/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-spa/views/default/errors/500.html +11 -0
- package/cli/templates/make/app-spa/views/default/layouts/main.html +34 -0
- package/cli/templates/make/app-spa/views/default/pages/home.html +22 -0
- package/cli/templates/make/app-spa/views/default/spa/index.html +13 -0
- package/cli/templates/make/app-spa/views/default/spa/package.json +20 -0
- package/cli/templates/make/app-spa/views/default/spa/src/App.vue +41 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/landing.css +220 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/style.css +1156 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/AlertDialog.vue +179 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/CodeBlock.vue +33 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FileUpload.vue +161 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FlashMessage.vue +39 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Lightbox.vue +62 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Navbar.vue +68 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Pagination.vue +166 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/ToastContainer.vue +135 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useApi.js +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useClipboard.js +44 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDate.js +73 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDebounce.js +59 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useFlash.js +46 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocale.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useWebSocket.js +93 -0
- package/cli/templates/make/app-spa/views/default/spa/src/main.js +106 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/alert.js +96 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/toast.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/router/index.js +29 -0
- package/cli/templates/make/app-spa/views/default/spa/src/stores/auth.js +58 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardDetail.vue +169 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardForm.vue +192 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardList.vue +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/ChatView.vue +317 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/FeaturesView.vue +242 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/HomeView.vue +215 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Login.vue +82 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Profile.vue +85 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Register.vue +84 -0
- package/cli/templates/make/app-spa/views/default/spa/vite.config.js +28 -0
- package/cli/templates/make/app-spa/views/default/spa/yarn.lock +633 -0
- package/cli/templates/make/app-spa/ws/ChatHandler.js +138 -0
- package/cli/templates/make/app-ssr/controllers/AuthController.js +119 -0
- package/cli/templates/make/app-ssr/controllers/ChatController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/FeaturesController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/HomeController.js +21 -0
- package/cli/templates/make/app-ssr/controllers/PostController.js +214 -0
- package/cli/templates/make/app-ssr/controllers/UserController.js +48 -0
- package/cli/templates/make/app-ssr/public/css/fx-ui.css +43 -0
- package/cli/templates/make/app-ssr/public/css/landing.css +220 -0
- package/cli/templates/make/app-ssr/public/css/style.css +1011 -0
- package/cli/templates/make/app-ssr/public/js/fx-client.js +107 -0
- package/cli/templates/make/app-ssr/public/js/fx-ui.js +124 -0
- package/cli/templates/make/app-ssr/routes/web.js +46 -0
- package/cli/templates/make/app-ssr/services/AuthService.js +48 -0
- package/cli/templates/make/app-ssr/services/PostService.js +372 -0
- package/cli/templates/make/app-ssr/services/UserService.js +48 -0
- package/cli/templates/make/app-ssr/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-ssr/views/default/errors/500.html +48 -0
- package/cli/templates/make/app-ssr/views/default/layouts/main.html +96 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/form.html +240 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/index.html +73 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/show.html +148 -0
- package/cli/templates/make/app-ssr/views/default/pages/chat.html +288 -0
- package/cli/templates/make/app-ssr/views/default/pages/features.html +373 -0
- package/cli/templates/make/app-ssr/views/default/pages/home.html +258 -0
- package/cli/templates/make/app-ssr/views/default/pages/login.html +27 -0
- package/cli/templates/make/app-ssr/views/default/pages/profile.html +36 -0
- package/cli/templates/make/app-ssr/views/default/pages/register.html +35 -0
- package/cli/templates/make/app-ssr/views/default/partials/pagination.html +75 -0
- package/cli/templates/make/app-ssr/ws/ChatHandler.js +138 -0
- package/package.json +2 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFlash — Flash 메시지 상태 관리 composable.
|
|
3
|
+
*
|
|
4
|
+
* 성공/에러 메시지를 표시하고 자동으로 사라지게 한다.
|
|
5
|
+
* SSR의 flash.error / flash.success와 동일한 역할.
|
|
6
|
+
*/
|
|
7
|
+
import { ref } from 'vue';
|
|
8
|
+
|
|
9
|
+
/** 현재 Flash 메시지 (모듈 레벨 싱글톤) */
|
|
10
|
+
const flash = ref({ type: '', message: '' });
|
|
11
|
+
let _timer = null;
|
|
12
|
+
|
|
13
|
+
export function useFlash() {
|
|
14
|
+
/**
|
|
15
|
+
* 성공 메시지 표시.
|
|
16
|
+
* @param {string} message
|
|
17
|
+
* @param {number} duration - 표시 시간 (ms), 기본 3000
|
|
18
|
+
*/
|
|
19
|
+
function success(message, duration = 3000) {
|
|
20
|
+
flash.value = { type: 'success', message };
|
|
21
|
+
_autoClear(duration);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 에러 메시지 표시.
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @param {number} duration - 표시 시간 (ms), 기본 5000
|
|
28
|
+
*/
|
|
29
|
+
function error(message, duration = 5000) {
|
|
30
|
+
flash.value = { type: 'error', message };
|
|
31
|
+
_autoClear(duration);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Flash 메시지 수동 제거 */
|
|
35
|
+
function clear() {
|
|
36
|
+
flash.value = { type: '', message: '' };
|
|
37
|
+
if (_timer) clearTimeout(_timer);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _autoClear(duration) {
|
|
41
|
+
if (_timer) clearTimeout(_timer);
|
|
42
|
+
_timer = setTimeout(clear, duration);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { flash, success, error, clear };
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useHeartbeat — 세션 연장 heartbeat composable.
|
|
3
|
+
*
|
|
4
|
+
* SSR main.html의 setInterval(fxFetch('/api/heartbeat'), 5분) 패턴.
|
|
5
|
+
* Pinia auth store와 연동하여 401 시 로그아웃.
|
|
6
|
+
*/
|
|
7
|
+
import { onMounted, onUnmounted } from 'vue';
|
|
8
|
+
import { useApi } from './useApi.js';
|
|
9
|
+
import { useAuthStore } from '../stores/auth.js';
|
|
10
|
+
|
|
11
|
+
/** Heartbeat 간격 (5분) */
|
|
12
|
+
const INTERVAL_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
export function useHeartbeat() {
|
|
15
|
+
let timer = null;
|
|
16
|
+
const api = useApi();
|
|
17
|
+
const authStore = useAuthStore();
|
|
18
|
+
|
|
19
|
+
function start() {
|
|
20
|
+
if (!authStore.isAuthenticated) return;
|
|
21
|
+
timer = setInterval(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const res = await api.get('/api/heartbeat');
|
|
24
|
+
if (res && res.status === 401) {
|
|
25
|
+
authStore.logout();
|
|
26
|
+
window.location.href = '/#/login';
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// 네트워크 오류 무시
|
|
30
|
+
}
|
|
31
|
+
}, INTERVAL_MS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stop() {
|
|
35
|
+
if (timer) {
|
|
36
|
+
clearInterval(timer);
|
|
37
|
+
timer = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMounted(start);
|
|
42
|
+
onUnmounted(stop);
|
|
43
|
+
|
|
44
|
+
return { start, stop };
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLocalStorage — 타입 안전한 localStorage composable.
|
|
3
|
+
*
|
|
4
|
+
* ref와 localStorage를 동기화. JSON 직렬화/역직렬화 자동 처리.
|
|
5
|
+
* @example
|
|
6
|
+
* const theme = useLocalStorage('fx_theme', 'dark');
|
|
7
|
+
* theme.value = 'light'; // localStorage + ref 동시 업데이트
|
|
8
|
+
*/
|
|
9
|
+
import { ref, watch } from 'vue';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* localStorage와 동기화된 ref 생성.
|
|
13
|
+
* @param {string} key - localStorage 키
|
|
14
|
+
* @param {*} defaultValue - 기본값
|
|
15
|
+
* @returns {import('vue').Ref}
|
|
16
|
+
*/
|
|
17
|
+
export function useLocalStorage(key, defaultValue) {
|
|
18
|
+
// 초기값 로드
|
|
19
|
+
let initial = defaultValue;
|
|
20
|
+
try {
|
|
21
|
+
const stored = localStorage.getItem(key);
|
|
22
|
+
if (stored !== null) {
|
|
23
|
+
initial = JSON.parse(stored);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// 파싱 실패 시 기본값 사용
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = ref(initial);
|
|
30
|
+
|
|
31
|
+
// ref 변경 시 localStorage에 저장
|
|
32
|
+
watch(data, (val) => {
|
|
33
|
+
try {
|
|
34
|
+
if (val === null || val === undefined) {
|
|
35
|
+
localStorage.removeItem(key);
|
|
36
|
+
} else {
|
|
37
|
+
localStorage.setItem(key, JSON.stringify(val));
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}, { deep: true });
|
|
41
|
+
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLocale — i18n 다국어 지원 composable.
|
|
3
|
+
*
|
|
4
|
+
* fxConfig.translations에서 번역문을 가져오고,
|
|
5
|
+
* 로케일 전환 시 서버에 알려 쿠키를 설정한다.
|
|
6
|
+
* locales() 로 서버에서 제공하는 사용 가능한 로케일 목록 조회.
|
|
7
|
+
*/
|
|
8
|
+
import { ref } from 'vue';
|
|
9
|
+
|
|
10
|
+
/** 현재 로케일 (모듈 레벨 싱글톤) */
|
|
11
|
+
const currentLocale = ref('ko');
|
|
12
|
+
let _translations = {};
|
|
13
|
+
let _availableLocales = [];
|
|
14
|
+
|
|
15
|
+
/** ISO 639-1 → 표시 이름 매핑 */
|
|
16
|
+
const LOCALE_LABELS = {
|
|
17
|
+
ko: '한국어',
|
|
18
|
+
en: 'English',
|
|
19
|
+
ja: '日本語',
|
|
20
|
+
zh: '中文',
|
|
21
|
+
es: 'Español',
|
|
22
|
+
fr: 'Français',
|
|
23
|
+
de: 'Deutsch',
|
|
24
|
+
pt: 'Português',
|
|
25
|
+
ru: 'Русский',
|
|
26
|
+
ar: 'العربية',
|
|
27
|
+
vi: 'Tiếng Việt',
|
|
28
|
+
th: 'ภาษาไทย',
|
|
29
|
+
id: 'Bahasa Indonesia',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* i18n composable 초기화.
|
|
34
|
+
* main.js에서 fxConfig 로드 후 호출.
|
|
35
|
+
* @param {string} locale - 현재 로케일
|
|
36
|
+
* @param {object} translations - 번역 데이터
|
|
37
|
+
* @param {string[]} [locales] - 사용 가능한 로케일 목록
|
|
38
|
+
*/
|
|
39
|
+
export function initLocale(locale, translations, locales) {
|
|
40
|
+
currentLocale.value = locale || 'ko';
|
|
41
|
+
_translations = translations || {};
|
|
42
|
+
_availableLocales = locales || [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useLocale() {
|
|
46
|
+
/**
|
|
47
|
+
* 번역 키 조회.
|
|
48
|
+
* @param {string} key - 번역 키 (예: 'home.hero_title_1')
|
|
49
|
+
* @param {string} fallback - 키가 없을 때 기본값
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function t(key, fallback) {
|
|
53
|
+
return _translations[key] || fallback || key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 로케일 전환 (서버 쿠키 + 페이지 새로고침) */
|
|
57
|
+
function switchLocale(locale) {
|
|
58
|
+
currentLocale.value = locale;
|
|
59
|
+
window.location.href = `?lang=${locale}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 사용 가능한 로케일 목록 (서버에서 제공).
|
|
64
|
+
* @returns {{ code: string, label: string }[]}
|
|
65
|
+
*/
|
|
66
|
+
function locales() {
|
|
67
|
+
return _availableLocales.map(code => ({
|
|
68
|
+
code,
|
|
69
|
+
label: LOCALE_LABELS[code] || code.toUpperCase(),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
locale: currentLocale,
|
|
75
|
+
t,
|
|
76
|
+
switchLocale,
|
|
77
|
+
locales,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWebSocket — WASM FuzionXSocket 래퍼 composable.
|
|
3
|
+
*
|
|
4
|
+
* SSR chat.html의 WebSocket 로직을 Vue composable로 캡슐화.
|
|
5
|
+
* WASM 로드 대기, connect, event listener, send, disconnect 제공.
|
|
6
|
+
*/
|
|
7
|
+
import { ref, onUnmounted, inject } from 'vue';
|
|
8
|
+
|
|
9
|
+
export function useWebSocket() {
|
|
10
|
+
/** WebSocket 인스턴스 */
|
|
11
|
+
const ws = ref(null);
|
|
12
|
+
/** 연결 상태 */
|
|
13
|
+
const connected = ref(false);
|
|
14
|
+
/** 내 세션 ID */
|
|
15
|
+
const mySessionId = ref(null);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WebSocket 연결.
|
|
19
|
+
* @param {string} path - WS 경로 (예: '/ws/chat')
|
|
20
|
+
* @param {string} masterSecret - ASP master secret
|
|
21
|
+
* @param {boolean} aspEnabled - ASP 활성화 여부
|
|
22
|
+
*/
|
|
23
|
+
async function connect(path, masterSecret = '', aspEnabled = false) {
|
|
24
|
+
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
25
|
+
const url = `${wsProto}//${location.host}${path}`;
|
|
26
|
+
|
|
27
|
+
// WASM 로드 대기
|
|
28
|
+
if (window._aspReady) await window._aspReady;
|
|
29
|
+
|
|
30
|
+
// FuzionXSocket 로드
|
|
31
|
+
if (!window.FuzionXSocket) {
|
|
32
|
+
try {
|
|
33
|
+
const _v = Date.now();
|
|
34
|
+
const mod = await import(/* @vite-ignore */ `/wasm/fuzionx_client_wasm.js?v=${_v}`);
|
|
35
|
+
await mod.default({ module_or_path: `/wasm/fuzionx_client_wasm_bg.wasm?v=${_v}` });
|
|
36
|
+
window.FuzionXSocket = mod.FuzionXSocket;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('[WS] WASM load failed:', e);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ws.value = new window.FuzionXSocket(url, masterSecret, aspEnabled);
|
|
44
|
+
ws.value.on('connect', () => { connected.value = true; });
|
|
45
|
+
ws.value.on('disconnect', () => { connected.value = false; });
|
|
46
|
+
ws.value.connect();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 이벤트 리스너 등록.
|
|
51
|
+
* @param {string} event - 이벤트명
|
|
52
|
+
* @param {Function} handler - 핸들러
|
|
53
|
+
*/
|
|
54
|
+
function on(event, handler) {
|
|
55
|
+
if (ws.value) ws.value.on(event, handler);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 메시지 전송.
|
|
60
|
+
* @param {string} type - 메시지 타입
|
|
61
|
+
* @param {object} data - 메시지 데이터
|
|
62
|
+
*/
|
|
63
|
+
function send(type, data) {
|
|
64
|
+
if (ws.value && ws.value.is_connected()) {
|
|
65
|
+
ws.value.send(type, data);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 연결 종료 */
|
|
70
|
+
function disconnect() {
|
|
71
|
+
if (ws.value) {
|
|
72
|
+
ws.value.disconnect();
|
|
73
|
+
ws.value = null;
|
|
74
|
+
}
|
|
75
|
+
connected.value = false;
|
|
76
|
+
mySessionId.value = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 컴포넌트 언마운트 시 자동 정리 */
|
|
80
|
+
onUnmounted(() => {
|
|
81
|
+
disconnect();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ws,
|
|
86
|
+
connected,
|
|
87
|
+
mySessionId,
|
|
88
|
+
connect,
|
|
89
|
+
on,
|
|
90
|
+
send,
|
|
91
|
+
disconnect,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* main.js — SPA 부트스트랩.
|
|
3
|
+
*
|
|
4
|
+
* WASM 초기화, ASP 클라이언트 설정, Pinia 등록,
|
|
5
|
+
* 인증 상태 확인, i18n 초기화, Vue 앱 마운트.
|
|
6
|
+
*/
|
|
7
|
+
import { createApp } from 'vue';
|
|
8
|
+
import { createPinia } from 'pinia';
|
|
9
|
+
import { createRouter, createWebHashHistory } from 'vue-router';
|
|
10
|
+
import App from './App.vue';
|
|
11
|
+
import { routes } from './router/index.js';
|
|
12
|
+
import { useAuthStore } from './stores/auth.js';
|
|
13
|
+
import { setAspClient, setAspReady, useApi } from './composables/useApi.js';
|
|
14
|
+
import { initLocale } from './composables/useLocale.js';
|
|
15
|
+
import ToastPlugin from './plugins/toast.js';
|
|
16
|
+
import AlertPlugin from './plugins/alert.js';
|
|
17
|
+
import './assets/style.css';
|
|
18
|
+
import './assets/landing.css';
|
|
19
|
+
|
|
20
|
+
// ── Pinia 생성 ──
|
|
21
|
+
const pinia = createPinia();
|
|
22
|
+
|
|
23
|
+
// ── 라우터 생성 ──
|
|
24
|
+
const router = createRouter({
|
|
25
|
+
history: createWebHashHistory(),
|
|
26
|
+
routes,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ── 네비게이션 가드 (인증 상태에 따라 리다이렉트) ──
|
|
30
|
+
router.beforeEach((to) => {
|
|
31
|
+
const authStore = useAuthStore(pinia);
|
|
32
|
+
if (to.meta.auth && !authStore.isAuthenticated) {
|
|
33
|
+
return { name: 'login' };
|
|
34
|
+
}
|
|
35
|
+
if (to.meta.guest && authStore.isAuthenticated) {
|
|
36
|
+
return { name: 'home' };
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── 부트스트랩 ──
|
|
41
|
+
async function bootstrap() {
|
|
42
|
+
const clientSecret = window.__FX_CLIENT_SECRET__;
|
|
43
|
+
const encPayload = window.__FX__;
|
|
44
|
+
let masterSecret = '';
|
|
45
|
+
let fxConfig = {};
|
|
46
|
+
|
|
47
|
+
// WASM 초기화 Promise를 useApi에 등록 (aspFetch가 대기할 수 있도록)
|
|
48
|
+
const wasmInit = (async () => {
|
|
49
|
+
const wasmJsUrl = '/wasm/fuzionx_client_wasm.js';
|
|
50
|
+
const wasmBgUrl = '/wasm/fuzionx_client_wasm_bg.wasm';
|
|
51
|
+
const wasm = await import(/* @vite-ignore */ wasmJsUrl);
|
|
52
|
+
await wasm.default({ module_or_path: wasmBgUrl });
|
|
53
|
+
|
|
54
|
+
// 복호화용 임시 클라이언트
|
|
55
|
+
const client = new wasm.FuzionXClient('_init_');
|
|
56
|
+
const configJson = client.decrypt_custom(clientSecret, encPayload);
|
|
57
|
+
fxConfig = JSON.parse(configJson);
|
|
58
|
+
|
|
59
|
+
masterSecret = fxConfig.asp.masterSecret;
|
|
60
|
+
|
|
61
|
+
// ASP 활성 클라이언트
|
|
62
|
+
const headerSignal = fxConfig.asp?.headerSignal || 'Ruxy-Enc-Mode';
|
|
63
|
+
const aspClient = wasm.FuzionXClient.new_with_options(masterSecret, headerSignal);
|
|
64
|
+
setAspClient(aspClient);
|
|
65
|
+
console.log('[ASP] WASM client initialized');
|
|
66
|
+
})();
|
|
67
|
+
|
|
68
|
+
setAspReady(wasmInit);
|
|
69
|
+
// WASM 초기화 Promise를 전역에 노출 (chat에서 사용)
|
|
70
|
+
window._aspReady = wasmInit;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await wasmInit;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('[FuzionX] WASM init failed:', e);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── i18n 초기화 ──
|
|
79
|
+
initLocale(fxConfig.locale || 'ko', fxConfig.translations || {}, fxConfig.locales || []);
|
|
80
|
+
|
|
81
|
+
// ── Pinia 스토어 초기화 ──
|
|
82
|
+
const authStore = useAuthStore(pinia);
|
|
83
|
+
authStore.restoreFromStorage();
|
|
84
|
+
|
|
85
|
+
// ── 인증 상태 확인 (서버 세션 검증) ──
|
|
86
|
+
try {
|
|
87
|
+
const api = useApi();
|
|
88
|
+
const authData = await api.get('/api/auth/check');
|
|
89
|
+
authStore.setUser(authData?.user || fxConfig.user || null);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn('[FX] auth/check failed:', e);
|
|
92
|
+
authStore.setUser(fxConfig.user || null);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Vue 앱 생성 + 마운트 ──
|
|
96
|
+
const app = createApp(App);
|
|
97
|
+
app.use(pinia);
|
|
98
|
+
app.use(ToastPlugin);
|
|
99
|
+
app.use(AlertPlugin);
|
|
100
|
+
app.provide('fxConfig', fxConfig);
|
|
101
|
+
app.provide('masterSecret', masterSecret);
|
|
102
|
+
app.use(router);
|
|
103
|
+
app.mount('#app');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bootstrap();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FuzionX Alert Plugin
|
|
3
|
+
*
|
|
4
|
+
* Vue 3 global plugin — import 없이 전역 사용.
|
|
5
|
+
*
|
|
6
|
+
* Options API: this.$alert.show('제목', '내용')
|
|
7
|
+
* Composition API: const { $alert } = getCurrentInstance().appContext.config.globalProperties
|
|
8
|
+
* 또는 useAlert() composable 사용.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* await this.$alert.confirm('삭제', '정말 삭제하시겠습니까?'); // true/false
|
|
12
|
+
* await this.$alert.show('알림', '처리 완료'); // void
|
|
13
|
+
* await this.$alert.confirm('확인', '계속?', { ok: '네', cancel: '아니오' });
|
|
14
|
+
*/
|
|
15
|
+
import { reactive } from 'vue';
|
|
16
|
+
|
|
17
|
+
/** 알럿 상태 (reactive) */
|
|
18
|
+
export const alertState = reactive({
|
|
19
|
+
visible: false,
|
|
20
|
+
title: '',
|
|
21
|
+
message: '',
|
|
22
|
+
type: 'info', // 'info' | 'warning' | 'error' | 'confirm'
|
|
23
|
+
okText: '확인',
|
|
24
|
+
cancelText: '취소',
|
|
25
|
+
showCancel: false,
|
|
26
|
+
resolve: null,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 알럿 표시 (Promise 반환).
|
|
31
|
+
* @param {string} title - 제목
|
|
32
|
+
* @param {string} message - 내용
|
|
33
|
+
* @param {object} opts - { type, okText, cancelText }
|
|
34
|
+
* @returns {Promise<boolean>} confirm 모드일 때 true/false
|
|
35
|
+
*/
|
|
36
|
+
function show(title, message, opts = {}) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
alertState.title = title || '';
|
|
39
|
+
alertState.message = message || '';
|
|
40
|
+
alertState.type = opts.type || 'info';
|
|
41
|
+
alertState.okText = opts.ok || opts.okText || '확인';
|
|
42
|
+
alertState.cancelText = opts.cancel || opts.cancelText || '취소';
|
|
43
|
+
alertState.showCancel = false;
|
|
44
|
+
alertState.visible = true;
|
|
45
|
+
alertState.resolve = resolve;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 확인/취소 다이얼로그.
|
|
51
|
+
*/
|
|
52
|
+
function confirm(title, message, opts = {}) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
alertState.title = title || '';
|
|
55
|
+
alertState.message = message || '';
|
|
56
|
+
alertState.type = opts.type || 'confirm';
|
|
57
|
+
alertState.okText = opts.ok || opts.okText || '확인';
|
|
58
|
+
alertState.cancelText = opts.cancel || opts.cancelText || '취소';
|
|
59
|
+
alertState.showCancel = true;
|
|
60
|
+
alertState.visible = true;
|
|
61
|
+
alertState.resolve = resolve;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 확인 버튼 핸들러 */
|
|
66
|
+
export function handleOk() {
|
|
67
|
+
alertState.visible = false;
|
|
68
|
+
alertState.resolve?.(true);
|
|
69
|
+
alertState.resolve = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 취소 버튼 핸들러 */
|
|
73
|
+
export function handleCancel() {
|
|
74
|
+
alertState.visible = false;
|
|
75
|
+
alertState.resolve?.(false);
|
|
76
|
+
alertState.resolve = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 퍼블릭 API */
|
|
80
|
+
export const alert = {
|
|
81
|
+
show,
|
|
82
|
+
confirm,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Composition API 용 */
|
|
86
|
+
export function useAlert() {
|
|
87
|
+
return alert;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Vue Plugin */
|
|
91
|
+
export default {
|
|
92
|
+
install(app) {
|
|
93
|
+
app.config.globalProperties.$alert = alert;
|
|
94
|
+
app.provide('alert', alert);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FuzionX Toast Plugin
|
|
3
|
+
*
|
|
4
|
+
* Vue 3 global plugin — import 없이 전역 사용.
|
|
5
|
+
*
|
|
6
|
+
* Options API: this.$toast.success('저장됨!')
|
|
7
|
+
* Composition API: const { $toast } = getCurrentInstance().appContext.config.globalProperties
|
|
8
|
+
* 또는 useToast() composable 사용.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* this.$toast.success('성공!');
|
|
12
|
+
* this.$toast.error('실패!');
|
|
13
|
+
* this.$toast.info('안내');
|
|
14
|
+
* this.$toast.warning('주의');
|
|
15
|
+
* this.$toast.show({ message: '커스텀', type: 'info', duration: 5000 });
|
|
16
|
+
*/
|
|
17
|
+
import { reactive } from 'vue';
|
|
18
|
+
|
|
19
|
+
/** 토스트 상태 (reactive) */
|
|
20
|
+
export const toastState = reactive({
|
|
21
|
+
items: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let _id = 0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 토스트 추가.
|
|
28
|
+
* @param {string|object} opts - 메시지 문자열 또는 { message, type, duration }
|
|
29
|
+
*/
|
|
30
|
+
function show(opts) {
|
|
31
|
+
const config = typeof opts === 'string' ? { message: opts } : { ...opts };
|
|
32
|
+
const item = {
|
|
33
|
+
id: ++_id,
|
|
34
|
+
message: config.message || '',
|
|
35
|
+
type: config.type || 'info',
|
|
36
|
+
duration: config.duration ?? 3000,
|
|
37
|
+
};
|
|
38
|
+
toastState.items.push(item);
|
|
39
|
+
|
|
40
|
+
if (item.duration > 0) {
|
|
41
|
+
setTimeout(() => dismiss(item.id), item.duration);
|
|
42
|
+
}
|
|
43
|
+
return item.id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 토스트 제거 */
|
|
47
|
+
function dismiss(id) {
|
|
48
|
+
const idx = toastState.items.findIndex(t => t.id === id);
|
|
49
|
+
if (idx !== -1) toastState.items.splice(idx, 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 전체 제거 */
|
|
53
|
+
function clear() {
|
|
54
|
+
toastState.items.splice(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 퍼블릭 API */
|
|
58
|
+
export const toast = {
|
|
59
|
+
show,
|
|
60
|
+
dismiss,
|
|
61
|
+
clear,
|
|
62
|
+
success: (msg, duration) => show({ message: msg, type: 'success', duration }),
|
|
63
|
+
error: (msg, duration) => show({ message: msg, type: 'error', duration }),
|
|
64
|
+
info: (msg, duration) => show({ message: msg, type: 'info', duration }),
|
|
65
|
+
warning: (msg, duration) => show({ message: msg, type: 'warning', duration }),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Composition API 용 */
|
|
69
|
+
export function useToast() {
|
|
70
|
+
return toast;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Vue Plugin */
|
|
74
|
+
export default {
|
|
75
|
+
install(app) {
|
|
76
|
+
app.config.globalProperties.$toast = toast;
|
|
77
|
+
app.provide('toast', toast);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* router/index.js — SPA 라우트 정의.
|
|
3
|
+
*
|
|
4
|
+
* SSR web.js 라우트 구조와 동일:
|
|
5
|
+
* - 공개: /, /features (guest 불가)
|
|
6
|
+
* - 게스트: /login, /register
|
|
7
|
+
* - 인증: /chat, /profile, /board/...
|
|
8
|
+
*
|
|
9
|
+
* Lazy loading으로 초기 번들 크기 최적화.
|
|
10
|
+
*/
|
|
11
|
+
import HomeView from '../views/HomeView.vue';
|
|
12
|
+
|
|
13
|
+
export const routes = [
|
|
14
|
+
// ── 공개 페이지 (인증 불필요) ──
|
|
15
|
+
{ path: '/', name: 'home', component: HomeView },
|
|
16
|
+
{ path: '/features', name: 'features', component: () => import('../views/FeaturesView.vue') },
|
|
17
|
+
|
|
18
|
+
// ── 게스트 전용 (로그인 상태면 홈으로) ──
|
|
19
|
+
{ path: '/login', name: 'login', component: () => import('../views/Login.vue'), meta: { guest: true } },
|
|
20
|
+
{ path: '/register', name: 'register', component: () => import('../views/Register.vue'), meta: { guest: true } },
|
|
21
|
+
|
|
22
|
+
// ── 인증 필요 ──
|
|
23
|
+
{ path: '/chat', name: 'chat', component: () => import('../views/ChatView.vue'), meta: { auth: true } },
|
|
24
|
+
{ path: '/profile', name: 'profile', component: () => import('../views/Profile.vue'), meta: { auth: true } },
|
|
25
|
+
{ path: '/board', name: 'board', component: () => import('../views/BoardList.vue'), meta: { auth: true } },
|
|
26
|
+
{ path: '/board/new', name: 'board-new', component: () => import('../views/BoardForm.vue'), meta: { auth: true } },
|
|
27
|
+
{ path: '/board/:id', name: 'board-detail', component: () => import('../views/BoardDetail.vue'), meta: { auth: true } },
|
|
28
|
+
{ path: '/board/:id/edit', name: 'board-edit', component: () => import('../views/BoardForm.vue'), meta: { auth: true } },
|
|
29
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stores/auth — Pinia 사용자 상태 관리 스토어.
|
|
3
|
+
*
|
|
4
|
+
* 사용자 정보, 인증 상태, 로그인/로그아웃 액션을 관리한다.
|
|
5
|
+
* DevTools 디버깅 지원 및 $reset()으로 깔끔한 상태 초기화.
|
|
6
|
+
*/
|
|
7
|
+
import { defineStore } from 'pinia';
|
|
8
|
+
import { ref, computed } from 'vue';
|
|
9
|
+
|
|
10
|
+
export const useAuthStore = defineStore('auth', () => {
|
|
11
|
+
/* ── State ── */
|
|
12
|
+
const user = ref(null);
|
|
13
|
+
|
|
14
|
+
/* ── Getters ── */
|
|
15
|
+
const isAuthenticated = computed(() => !!user.value);
|
|
16
|
+
const userName = computed(() => user.value?.name || '');
|
|
17
|
+
const userEmail = computed(() => user.value?.email || '');
|
|
18
|
+
|
|
19
|
+
/* ── Actions ── */
|
|
20
|
+
|
|
21
|
+
/** 사용자 정보 설정 (로그인 성공 또는 초기 로드 시) */
|
|
22
|
+
function setUser(userData) {
|
|
23
|
+
user.value = userData || null;
|
|
24
|
+
if (userData) {
|
|
25
|
+
localStorage.setItem('fx_auth', JSON.stringify(userData));
|
|
26
|
+
} else {
|
|
27
|
+
localStorage.removeItem('fx_auth');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 로그아웃 — 상태 초기화 + localStorage 정리 */
|
|
32
|
+
function logout() {
|
|
33
|
+
user.value = null;
|
|
34
|
+
localStorage.removeItem('fx_auth');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** localStorage에서 사용자 정보 복원 (앱 시작 시) */
|
|
38
|
+
function restoreFromStorage() {
|
|
39
|
+
try {
|
|
40
|
+
const stored = localStorage.getItem('fx_auth');
|
|
41
|
+
if (stored) {
|
|
42
|
+
user.value = JSON.parse(stored);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
localStorage.removeItem('fx_auth');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
user,
|
|
51
|
+
isAuthenticated,
|
|
52
|
+
userName,
|
|
53
|
+
userEmail,
|
|
54
|
+
setUser,
|
|
55
|
+
logout,
|
|
56
|
+
restoreFromStorage,
|
|
57
|
+
};
|
|
58
|
+
});
|