인앱 스캐너 카메라로 스캔한 결과에 웹 주소(URL)가 포함되어 있는 경우, 쿼리 스트링 파라미터에서 순수 위치 코드(loc) 및 자산 코드(asset) 값을 자동으로 추출하여 서버로 전송하도록 수정
198 lines
6.9 KiB
TypeScript
198 lines
6.9 KiB
TypeScript
// 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);
|
|
}
|
|
}
|
|
});
|