window.taxbaikAdminSession = { clientLogState: { enabled: true, windowStart: 0, sentCount: 0, suppressedCount: 0, fingerprints: {}, eventCounts: {}, screen: '', feature: '', action: '', step: '', entity: '', entityId: '', dataKey: '' }, initErrorLogging: function () { if (window._taxbaikClientLogInitialized) return; window._taxbaikClientLogInitialized = true; const postLog = function (payload) { try { if (!window.taxbaikAdminSession.shouldSendClientLog(payload)) { return; } const body = JSON.stringify(payload); if (navigator.sendBeacon) { const blob = new Blob([body], { type: 'application/json' }); if (navigator.sendBeacon(window.taxbaikAdminSession.getApiUrl('/client-logs'), blob)) { return; } } fetch(window.taxbaikAdminSession.getApiUrl('/client-logs'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, keepalive: true }).catch(function () { }); } catch { // Logging must never break the UI. } }; window.taxbaikAdminSession.postClientLog = postLog; if (!window._taxbaikConsoleErrorPatched) { window._taxbaikConsoleErrorPatched = true; const originalConsoleError = console.error.bind(console); console.error = function (...args) { try { const text = args.map(arg => { if (arg instanceof Error) { return `${arg.message}\n${arg.stack || ''}`; } if (typeof arg === 'string') return arg; try { return JSON.stringify(arg); } catch { return String(arg); } }).join(' '); if (text && !text.includes('taxbaikAdminSession.postClientLog') && !text.includes('client-logs')) { postLog({ level: 'error', source: 'console.error', message: text.slice(0, 1000), url: window.location.href, route: window.location.pathname + window.location.search, screen: window.taxbaikAdminSession.clientLogState.screen || '', feature: window.taxbaikAdminSession.clientLogState.feature || '', action: window.taxbaikAdminSession.clientLogState.action || '', step: window.taxbaikAdminSession.clientLogState.step || '', entity: window.taxbaikAdminSession.clientLogState.entity || '', entityId: window.taxbaikAdminSession.clientLogState.entityId || '', dataKey: window.taxbaikAdminSession.clientLogState.dataKey || '', buildVersion: window.taxbaikAdminBuildVersion || '', component: window.taxbaikAdminComponent || '', viewportWidth: window.taxbaikAdminSession.getViewportWidth(), userAgent: navigator.userAgent || '', stack: args.find(arg => arg instanceof Error)?.stack || '' }); } } catch { // Logging must never break the UI. } return originalConsoleError(...args); }; } window.addEventListener('error', function (event) { postLog({ level: 'error', source: 'window.error', message: event.message || 'unknown error', url: event.filename || window.location.href, route: window.location.pathname + window.location.search, screen: window.taxbaikAdminSession.clientLogState.screen || '', feature: window.taxbaikAdminSession.clientLogState.feature || '', action: window.taxbaikAdminSession.clientLogState.action || '', step: window.taxbaikAdminSession.clientLogState.step || '', entity: window.taxbaikAdminSession.clientLogState.entity || '', entityId: window.taxbaikAdminSession.clientLogState.entityId || '', dataKey: window.taxbaikAdminSession.clientLogState.dataKey || '', buildVersion: window.taxbaikAdminBuildVersion || '', component: window.taxbaikAdminComponent || '', viewportWidth: window.taxbaikAdminSession.getViewportWidth(), userAgent: navigator.userAgent || '', stack: event.error?.stack || '' }); }); window.addEventListener('unhandledrejection', function (event) { const reason = event.reason; postLog({ level: 'error', source: 'window.unhandledrejection', message: reason?.message || String(reason || 'unknown rejection'), url: window.location.href, route: window.location.pathname + window.location.search, screen: window.taxbaikAdminSession.clientLogState.screen || '', feature: window.taxbaikAdminSession.clientLogState.feature || '', action: window.taxbaikAdminSession.clientLogState.action || '', step: window.taxbaikAdminSession.clientLogState.step || '', entity: window.taxbaikAdminSession.clientLogState.entity || '', entityId: window.taxbaikAdminSession.clientLogState.entityId || '', dataKey: window.taxbaikAdminSession.clientLogState.dataKey || '', buildVersion: window.taxbaikAdminBuildVersion || '', component: window.taxbaikAdminComponent || '', viewportWidth: window.taxbaikAdminSession.getViewportWidth(), userAgent: navigator.userAgent || '', stack: reason?.stack || '' }); }); window.addEventListener('load', function () { window.taxbaikAdminSession.traceUiState('startup', 'window.load'); }, { once: true }); window.taxbaikAdminSession.traceUiState('startup', 'initErrorLogging attached'); }, setContext: function (screen, feature, action, step, entity, entityId, dataKey) { const state = window.taxbaikAdminSession.clientLogState; state.screen = screen || ''; state.feature = feature || ''; state.action = action || ''; state.step = step || ''; state.entity = entity || ''; state.entityId = entityId || ''; state.dataKey = dataKey || ''; }, shouldSendClientLog: function (payload) { try { const state = window.taxbaikAdminSession.clientLogState; if (!state.enabled) return false; const now = Date.now(); if (!state.windowStart || now - state.windowStart >= 60000) { state.windowStart = now; state.sentCount = 0; state.suppressedCount = 0; state.fingerprints = {}; } const fingerprint = [ payload?.source || '', payload?.message || '', payload?.route || '', payload?.component || '', payload?.screen || '', payload?.feature || '', payload?.action || '', payload?.entity || '', payload?.entityId || '' ].join('|').slice(0, 256); state.fingerprints[fingerprint] = (state.fingerprints[fingerprint] || 0) + 1; if (state.sentCount >= 8) { state.suppressedCount += 1; return false; } if (state.fingerprints[fingerprint] > 2) { state.suppressedCount += 1; return false; } state.sentCount += 1; return true; } catch { return false; } }, traceUiState: function (source, details) { try { const payload = { level: 'info', source: source || 'ui-state', message: details || '', url: window.location.href, route: window.location.pathname + window.location.search, screen: window.taxbaikAdminSession.clientLogState.screen || '', feature: window.taxbaikAdminSession.clientLogState.feature || '', action: window.taxbaikAdminSession.clientLogState.action || '', step: window.taxbaikAdminSession.clientLogState.step || '', entity: window.taxbaikAdminSession.clientLogState.entity || '', entityId: window.taxbaikAdminSession.clientLogState.entityId || '', dataKey: window.taxbaikAdminSession.clientLogState.dataKey || '', buildVersion: window.taxbaikAdminBuildVersion || '', component: window.taxbaikAdminComponent || '', viewportWidth: window.taxbaikAdminSession.getViewportWidth(), userAgent: navigator.userAgent || '', stack: '' }; const state = window.taxbaikAdminSession.clientLogState; const key = `${payload.source}|${payload.route}|${payload.message}`.slice(0, 256); state.eventCounts[key] = (state.eventCounts[key] || 0) + 1; if (state.eventCounts[key] > 1) { return; } window.taxbaikAdminSession.postClientLog(payload); } catch { // diagnostics must never break UI. } }, postClientLog: function () { // Replaced during initialization. }, syncRouteClass: function () { document.documentElement.classList.toggle( 'admin-login-route', window.location.pathname.toLowerCase().endsWith('/admin/login')); }, getViewportWidth: function () { return window.innerWidth || document.documentElement.clientWidth || 0; }, getAppRootPath: function () { try { const base = new URL(document.baseURI); let pathname = base.pathname || '/'; if (pathname.endsWith('/admin/')) { pathname = pathname.slice(0, -'/admin/'.length) || '/'; } else if (pathname.endsWith('/portal/')) { pathname = pathname.slice(0, -'/portal/'.length) || '/'; } else if (pathname.endsWith('/admin')) { pathname = pathname.slice(0, -'/admin'.length) || '/'; } else if (pathname.endsWith('/portal')) { pathname = pathname.slice(0, -'/portal'.length) || '/'; } return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; } catch { return ''; } }, getAdminUrl: function (path) { const rootPath = window.taxbaikAdminSession.getAppRootPath(); const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`; return `${window.location.origin}${rootPath}/admin${normalizedPath}`; }, getApiUrl: function (path) { const rootPath = window.taxbaikAdminSession.getAppRootPath(); const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`; return `${window.location.origin}${rootPath}/api${normalizedPath}`; }, clearAuthToken: function () { try { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('tokenExpiry'); localStorage.removeItem('auth_token'); } catch { // Ignore storage errors; redirect still recovers the session. } }, getLocalStorageItem: function (key) { try { return localStorage.getItem(key); } catch { return null; } }, get: function (key) { return window.taxbaikAdminSession.getLocalStorageItem(key); }, setLocalStorageItem: function (key, value) { try { localStorage.setItem(key, value); } catch { // Ignore storage errors. } }, set: function (key, value) { return window.taxbaikAdminSession.setLocalStorageItem(key, value); }, removeLocalStorageItem: function (key) { try { localStorage.removeItem(key); } catch { // Ignore storage errors. } }, remove: function (key) { return window.taxbaikAdminSession.removeLocalStorageItem(key); }, showLoading: function () { // Route transitions are handled by Blazor; avoid full-screen overlays // that block drawer interaction and make the app feel frozen. window.taxbaikAdminSession.traceUiState('admin-loading', 'showLoading requested'); window.taxbaikAdminSession.hideLoading(); }, hideLoading: function () { const overlay = document.getElementById('blazor-loading'); if (overlay) { overlay.classList.remove('show'); } if (window._taxbaikLoadingTimeout) { clearTimeout(window._taxbaikLoadingTimeout); window._taxbaikLoadingTimeout = null; } if (window._taxbaikLoadingObserver) { window._taxbaikLoadingObserver.disconnect(); window._taxbaikLoadingObserver = null; } window.taxbaikAdminSession.traceUiState('admin-loading', 'hideLoading completed'); }, watchReconnect: function () { window.taxbaikAdminSession.syncRouteClass(); window.addEventListener('popstate', window.taxbaikAdminSession.syncRouteClass); window.addEventListener('hashchange', window.taxbaikAdminSession.syncRouteClass); if (document.documentElement.classList.contains('admin-login-route')) { // Login prerenders immediately; no boot splash needed. window.taxbaikAdminSession.hideLoading(); } // Non-login routes: leave the overlay showing until AdminShell's // OnAfterRenderAsync(firstRender) calls hideLoading once WASM has // actually rendered the authenticated shell. const modal = document.getElementById('components-reconnect-modal'); if (!modal) return; const reloadOnRejectedCircuit = function () { const className = modal.className || ''; if (className.includes('components-reconnect-failed') || className.includes('components-reconnect-rejected')) { window.setTimeout(function () { window.location.reload(); }, 1500); } }; new MutationObserver(reloadOnRejectedCircuit) .observe(modal, { attributes: true, attributeFilter: ['class'] }); }, bindLoginForm: function () { const form = document.getElementById('admin-login-form'); if (!form) return; console.info('taxbaikAdminSession.bindLoginForm ready', { path: window.location.pathname, hasSession: !!window.taxbaikAdminSession, hasGet: typeof window.taxbaikAdminSession.getLocalStorageItem === 'function', hasAliasGet: typeof window.taxbaikAdminSession.get === 'function' }); // 업데이트 스플래시: 매번(재호출되어도) 무조건 다시 적용한다. const readyButton = form.querySelector('#admin-login-submit'); if (readyButton) { readyButton.disabled = false; const label = readyButton.querySelector('span'); if (label) label.textContent = '로그인'; } // 체크박스 상태 복원 const rememberCheckbox = form.querySelector('input[type="checkbox"]'); if (rememberCheckbox) { try { const savedState = localStorage.getItem('admin-remember-checkbox'); if (savedState === 'true') { rememberCheckbox.checked = true; } } catch { // localStorage access error } } if (form.dataset.bound === '1') return; form.dataset.bound = '1'; window.taxbaikAdminSession.traceUiState('admin-login', 'bindLoginForm attached'); // 프리렌더 후 Blazor 하이드레이션이 이벤트 리스너를 제거할 수 있으므로, // document 전체에서 submit 이벤트를 캡처하고, form ID로 우리의 form인지 확인 // (객체 참조 비교는 Blazor 하이드레이션 후 stale이 될 수 있으므로 ID 비교 사용) document.addEventListener('submit', async function (event) { if (event.target.id !== 'admin-login-form') return; // 다른 form은 무시 event.preventDefault(); const currentForm = event.target; // Blazor 하이드레이션 후 최신 form 참조 const username = currentForm.querySelector('input[name="username"]')?.value?.trim() || ''; const password = currentForm.querySelector('input[name="password"]')?.value || ''; const rememberMe = currentForm.querySelector('input[type="checkbox"]')?.checked || false; const existing = currentForm.parentElement.querySelector('.login-error-message'); const submitButton = currentForm.querySelector('button[type="submit"]'); if (existing) existing.remove(); if (submitButton) submitButton.disabled = true; try { window.taxbaikAdminSession.traceUiState('admin-login', `submit: username=${username ? 'yes' : 'no'}, password=${password ? 'yes' : 'no'}`); if (!username || !password) { throw new Error('username/password missing'); } window.taxbaikAdminSession.traceUiState('admin-login', 'submit started'); const loginUrl = window.taxbaikAdminSession.getApiUrl('/auth/login'); const response = await fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (!response.ok) { let errorDetail = ''; try { errorDetail = await response.text(); } catch { // ignore response body read errors } const suffix = errorDetail ? `: ${errorDetail}` : ''; const error = new Error(`login failed (${response.status} ${response.statusText})${suffix}`); error.loginStatus = response.status; error.loginStatusText = response.statusText; error.loginResponse = errorDetail; throw error; } const data = await response.json(); if (!data?.accessToken || !data?.refreshToken) { throw new Error('invalid response'); } window.taxbaikAdminSession.traceUiState('admin-login', 'submit success'); const expiryTicks = 621355968000000000 + ((Date.now() + (data.expiresIn || 3600) * 1000) * 10000); localStorage.setItem('accessToken', data.accessToken); localStorage.setItem('refreshToken', data.refreshToken); localStorage.setItem('tokenExpiry', String(expiryTicks)); if (rememberMe) { localStorage.setItem('admin-remembered-username', username); localStorage.setItem('admin-remember-checkbox', 'true'); } else { localStorage.removeItem('admin-remembered-username'); localStorage.removeItem('admin-remember-checkbox'); } // 토큰 저장 후 대시보드로 이동 // Blazor가 대시보드 페이지를 로드할 때 CustomAuthenticationStateProvider가 // 자동으로 localStorage에서 토큰을 복원합니다 setTimeout(() => { window.location.href = window.taxbaikAdminSession.getAdminUrl('/dashboard'); }, 200); } catch (error) { window.taxbaikAdminSession.traceUiState('admin-login', `submit failed: ${error?.message || 'login failed'}`); if (window.taxbaikAdminSession.postClientLog) { window.taxbaikAdminSession.postClientLog({ level: 'error', source: 'admin-login-form', message: error?.message || 'login failed', url: window.location.href, route: window.location.pathname + window.location.search, buildVersion: window.taxbaikAdminBuildVersion || '', component: 'AdminLoginForm', viewportWidth: window.taxbaikAdminSession.getViewportWidth(), userAgent: navigator.userAgent || '', stack: error?.stack || '', responseStatus: error?.loginStatus || '', responseStatusText: error?.loginStatusText || '', responseBody: (error?.loginResponse || '').slice(0, 500) }); } const errorMessage = document.createElement('div'); errorMessage.className = 'mud-alert mud-alert-filled-error login-error-message mb-4'; errorMessage.textContent = '로그인 중 오류가 발생했습니다.'; currentForm.parentElement.insertBefore(errorMessage, currentForm); } finally { if (submitButton) submitButton.disabled = false; } }); } }; // 더존 ERP 스타일 엔터 키 포커스 이동 및 단축키 바인딩 document.addEventListener('keydown', function (e) { if (e.key === 'Enter') { const active = document.activeElement; if (!active) return; // 특정 영역(편집 폼 또는 다이얼로그) 내의 입력 필드만 포커스 이동 처리 const container = active.closest('.admin-editor-panel, .mud-form, .mud-dialog'); if (!container) return; // textarea나 button, submit 타입 등은 기본 동작(줄바꿈/제출) 유지 if (active.tagName === 'TEXTAREA' || active.tagName === 'BUTTON' || active.getAttribute('type') === 'submit' || active.classList.contains('mud-button-root')) { return; } e.preventDefault(); // 포커스 이동 가능한 모든 입력 요소 수집 const focusables = Array.from(container.querySelectorAll('input, select, textarea, button')) .filter(el => { const style = window.getComputedStyle(el); return el.tabIndex >= 0 && !el.disabled && el.getAttribute('aria-disabled') !== 'true' && style.display !== 'none' && style.visibility !== 'hidden'; }); const index = focusables.indexOf(active); if (index > -1 && index < focusables.length - 1) { const nextEl = focusables[index + 1]; nextEl.focus(); if (typeof nextEl.select === 'function') { nextEl.select(); } } } });