// ITAM Mobile Audit Scanner Main Business Logic const SESSION_LOC_KEY = 'itam_audit_locked_location'; document.addEventListener('DOMContentLoaded', () => { const locDisplay = document.getElementById('loc-display')!; const unlockBtn = document.getElementById('btn-unlock-loc') as HTMLButtonElement; const feedbackEl = document.getElementById('scan-feedback')!; const manualToggleBtn = document.getElementById('btn-toggle-manual')!; const manualForm = document.getElementById('manual-form')!; const manualInput = document.getElementById('manual-code-input') as HTMLInputElement; const manualSubmitBtn = document.getElementById('btn-submit-manual') as HTMLButtonElement; let html5QrcodeScanner: any = null; // Initialize UI based on current session lock updateLocationUI(); // Parse URL parameters for immediate processing (convenience for direct QR scans) parseUrlParams(); // Initialize HTML5 QR Code Scanner initScanner(); // Bind UI Events unlockBtn.addEventListener('click', () => { sessionStorage.removeItem(SESSION_LOC_KEY); showFeedback('위치 잠금이 해제되었습니다.', 'success'); updateLocationUI(); }); manualToggleBtn.addEventListener('click', () => { const isHidden = window.getComputedStyle(manualForm).display === 'none'; manualForm.style.display = isHidden ? 'flex' : 'none'; manualToggleBtn.textContent = isHidden ? '스캐너 카메라로 스캔하기' : '카메라가 안 되나요? 수동 코드로 입력'; }); manualSubmitBtn.addEventListener('click', () => { const code = manualInput.value.trim(); if (!code) return; processScannedCode(code); manualInput.value = ''; }); // --- Core Scanner Functions --- function initScanner() { try { // Create Html5Qrcode instance // Using Html5Qrcode directly instead of Html5QrcodeScanner for customized viewport control const html5QrCode = new (window as any).Html5Qrcode("reader"); const config = { fps: 10, qrbox: (width: number, height: number) => { const size = Math.min(width, height) * 0.7; return { width: size, height: size }; }, aspectRatio: 1.0 }; // Start scanning using the rear camera html5QrCode.start( { facingMode: "environment" }, config, (decodedText: string) => { processScannedCode(decodedText); }, (errorMessage: string) => { // Silent failure during continuous scanning to avoid log flooding } ).catch((err: any) => { console.error("Camera startup failed:", err); showFeedback("카메라 시작 실패: 권한을 허용했는지 확인하세요.", "error"); }); } catch (e) { console.error("Failed to initialize html5-qrcode:", e); showFeedback("QR 라이브러리 로드 오류", "error"); } } function processScannedCode(rawCode: string) { // QR 코드 인쇄 폼 등으로 인한 개행 문자(\r, \n) 및 모든 공백 문자(\s)를 제거 let code = rawCode.replace(/[\r\n]/g, '').replace(/\s+/g, '').trim(); // 만약 스캔된 텍스트가 전체 URL 주소 형식이라면 파라미터 값만 추출하여 정제 if (code.includes('http://') || code.includes('https://') || code.includes('/mobile')) { try { const urlObj = new URL(code, window.location.origin); const locParam = urlObj.searchParams.get('loc'); const assetParam = urlObj.searchParams.get('asset'); if (locParam) code = locParam; else if (assetParam) code = assetParam; } catch (e) { console.error("URL 파싱 에러:", e); } } // 1. Check if the code is a physical location code if (code.startsWith('LOC-')) { sessionStorage.setItem(SESSION_LOC_KEY, code); showFeedback(`위치 [${code}] 잠금 설정 완료!`, 'success'); updateLocationUI(); vibrateDevice(100); return; } // 2. Otherwise treat it as an asset code const lockedLoc = sessionStorage.getItem(SESSION_LOC_KEY); if (!lockedLoc) { showFeedback('위치 QR 코드를 먼저 스캔하여 잠금을 설정해야 자산을 스캔할 수 있습니다.', 'error'); vibrateDevice([100, 50, 100]); return; } // Submit matching info to server submitAssetAudit(code, lockedLoc); } async function submitAssetAudit(assetCode: string, locationCode: string) { showFeedback(`자산 ${assetCode} 전송 중...`, 'success'); try { // Request is sent relative to host, which resolves dynamically through server proxy const res = await fetch('/api/audit/scan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asset_code: assetCode, physical_location_code: locationCode }) }); const data = await res.json(); if (res.ok && data.success) { showFeedback(`자산 [${assetCode}] 실사 전송 성공! (관리자 승인 대기)`, 'success'); vibrateDevice([200]); } else { showFeedback(`전송 실패: ${data.error || '알 수 없는 서버 오류'}`, 'error'); vibrateDevice([100, 100, 100]); } } catch (err) { console.error("Failed to submit scan:", err); showFeedback('서버 전송 중 통신 네트워크 오류가 발생했습니다.', 'error'); vibrateDevice([100, 100, 100]); } } function updateLocationUI() { const lockedLoc = sessionStorage.getItem(SESSION_LOC_KEY); if (lockedLoc) { locDisplay.textContent = lockedLoc; locDisplay.className = 'badge-lock'; unlockBtn.style.display = 'inline-block'; } else { locDisplay.textContent = '위치 QR 코드를 먼저 스캔하세요.'; locDisplay.className = 'badge-empty'; unlockBtn.style.display = 'none'; } } function parseUrlParams() { const params = new URLSearchParams(window.location.search); const loc = params.get('loc'); const asset = params.get('asset'); if (loc) { processScannedCode(loc); // Clean query parameters to avoid re-triggering on page refresh window.history.replaceState({}, document.title, window.location.pathname); } else if (asset) { processScannedCode(asset); window.history.replaceState({}, document.title, window.location.pathname); } } function showFeedback(msg: string, type: 'success' | 'error') { feedbackEl.textContent = msg; feedbackEl.style.display = 'block'; feedbackEl.className = `feedback-message ${type === 'success' ? 'feedback-success' : 'feedback-error'}`; // Auto-hide feedback after 4 seconds setTimeout(() => { if (feedbackEl.textContent === msg) { feedbackEl.style.display = 'none'; } }, 4000); } function vibrateDevice(pattern: number | number[]) { if ('vibrate' in navigator) { navigator.vibrate(pattern); } } });