feat: QR 자산 스캔 점검, 모바일 웹뷰 및 관리자 승인 시스템 구현 (DB 기반 맵 좌표 저장 단일화 포함)

This commit is contained in:
이태훈
2026-06-23 16:39:14 +09:00
parent 9f165faf13
commit f36e8e93e2
21 changed files with 2357 additions and 46 deletions

View File

@@ -11,6 +11,7 @@ import {
} from './ModalUtils';
import { CORP_LIST, LOCATION_DATA, CATEGORY_TYPE_MAP, HW_STATUS_LIST, ORG_LIST, IMAGE_LOCATIONS, TYPE_PREFIX_MAP } from './SharedData';
import { BaseModal } from './BaseModal';
import { QRPrinter } from '../../core/qr_print';
/**
* 하드웨어 자산 상세 모달 (Styled Main Edition)
@@ -30,9 +31,10 @@ class HwAssetModal extends BaseModal {
<div id="hw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<div class="header-left">
<h2 id="hw-modal-title" class="modal-title">${this.title}</h2>
<div id="hw-header-identity" class="header-identity"></div>
<div class="header-left" style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;">
<h2 id="hw-modal-title" class="modal-title" style="display: none;">${this.title}</h2>
<div id="hw-header-identity" class="header-identity" style="display: inline-flex; gap: 0.5rem; align-items: center;"></div>
<button id="btn-print-hw-qr" class="btn btn-outline btn-primary hidden" style="padding: 2px 8px; font-size: 11px; height: 22px; margin: 0; line-height: 1; display: inline-flex; align-items: center; justify-content: center; cursor: pointer;">QR 인쇄</button>
</div>
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기">&times;</button>
</div>
@@ -264,6 +266,21 @@ class HwAssetModal extends BaseModal {
const detailSelect = document.getElementById('hw-location_detail') as HTMLSelectElement;
this.fetchMapConfig();
const qrPrintBtn = document.getElementById('btn-print-hw-qr');
qrPrintBtn?.addEventListener('click', () => {
if (this.currentAsset && this.currentAsset.asset_code) {
QRPrinter.print([{
type: 'asset',
code: this.currentAsset.asset_code,
title: '[ HM IT ASSET ]',
subtitle: this.currentAsset.model_name || this.currentAsset.asset_purpose || this.currentAsset.category || 'IT 자산',
dept: this.currentAsset.current_dept || '-',
user: this.currentAsset.user_current || '-'
}]);
}
});
this.fetchMasterComponents().then(() => {
this.bindAutocomplete('hw-cpu', 'hw-cpu-list', 'CPU');
this.bindAutocomplete('hw-ram', 'hw-ram-list', 'RAM');
@@ -299,7 +316,7 @@ class HwAssetModal extends BaseModal {
const prefix = TYPE_PREFIX_MAP[cat] || 'ETC';
const purchaseDate = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || '';
try {
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
const res = await fetch(`/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
const data = await res.json();
if (data.nextCode) setFieldValue('hw-asset_code', data.nextCode);
} catch (err) { console.error('코드 생성 실패:', err); }
@@ -317,7 +334,7 @@ class HwAssetModal extends BaseModal {
const reader = new FileReader();
reader.onload = async () => {
try {
const res = await fetch(`http://${location.hostname}:3000/api/upload`, {
const res = await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, fileData: reader.result })
@@ -326,7 +343,7 @@ class HwAssetModal extends BaseModal {
if (data.success) {
setFieldValue('hw-approval_document', data.filePath);
if (fileLinkContainer) {
fileLinkContainer.innerHTML = `<a href="http://${location.hostname}:3000${data.filePath}" target="_blank" class="btn btn-outline btn-sm">[파일 보기]</a>`;
fileLinkContainer.innerHTML = `<a href="${data.filePath}" target="_blank" class="btn btn-outline btn-sm">[파일 보기]</a>`;
}
}
} catch (err) { console.error('파일 업로드 실패:', err); alert('파일 업로드 중 오류가 발생했습니다.'); }
@@ -385,7 +402,7 @@ class HwAssetModal extends BaseModal {
const prefix = TYPE_PREFIX_MAP[cat] || 'ETC';
const purchaseDate = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || '';
try {
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
const res = await fetch(`/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
const data = await res.json();
if (data.nextCode) {
setFieldValue('hw-asset_code', data.nextCode);
@@ -621,7 +638,7 @@ class HwAssetModal extends BaseModal {
if (docName) docName.textContent = asset.approval_document ? asset.approval_document.split('/').pop() : '파일 선택...';
const fileLinkContainer = document.getElementById('hw-file-link-container');
if (fileLinkContainer && asset.approval_document) {
fileLinkContainer.innerHTML = `<a href="http://${location.hostname}:3000${asset.approval_document}" target="_blank" class="btn btn-outline btn-sm">[파일 보기]</a>`;
fileLinkContainer.innerHTML = `<a href="${asset.approval_document}" target="_blank" class="btn btn-outline btn-sm">[파일 보기]</a>`;
} else if (fileLinkContainer) {
fileLinkContainer.innerHTML = '';
}
@@ -642,6 +659,13 @@ class HwAssetModal extends BaseModal {
protected onAfterOpen(asset: any, mode: string): void {
const genBtn = document.getElementById('btn-gen-hw-code');
if (genBtn) genBtn.style.display = (mode === 'add') ? 'inline-flex' : 'none';
const qrBtn = document.getElementById('btn-print-hw-qr');
if (qrBtn) {
const hasCode = asset && asset.asset_code && asset.asset_code.trim() !== '';
qrBtn.classList.toggle('hidden', mode !== 'view' || !hasCode);
}
this.toggleFileUploadUI(mode !== 'view');
this.toggleEditOnlyBtns(mode !== 'view');
this.updateMapButtonVisibility();