|
|
|
|
@@ -10,7 +10,6 @@ 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 { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide';
|
|
|
|
|
|
|
|
|
|
class HwAssetModal extends BaseModal {
|
|
|
|
|
private dynamicMapConfig: Record<string, any[]> = {};
|
|
|
|
|
@@ -20,12 +19,15 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected renderFrameHTML(): string {
|
|
|
|
|
// CSS 명세(modal.css)의 input 패딩(0.625rem)과 일치시켜 정렬을 완벽하게 잡는 스타일
|
|
|
|
|
const standardBtnStyle = 'height: auto !important; padding: 0.625rem 1.25rem; font-size: 0.875rem; line-height: 1.2; display: inline-flex; align-items: center; justify-content: center;';
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div id="hw-asset-modal" class="modal-overlay hidden">
|
|
|
|
|
<div class="modal-content wide">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h2 id="hw-modal-title">${this.title}</h2>
|
|
|
|
|
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
|
|
|
|
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기" style="font-size: 24px; color: white; background: none; border: none; cursor: pointer;">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="modal-body-split">
|
|
|
|
|
@@ -33,12 +35,12 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
<form id="hw-asset-form" class="grid-form">
|
|
|
|
|
<input type="hidden" id="hw-id" name="id" />
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title">기본 및 관리 정보</div>
|
|
|
|
|
<div class="form-section-title" style="padding-top: 0;">기본 정보</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.ASSET_CODE.ui}</label>
|
|
|
|
|
<div class="input-with-btn">
|
|
|
|
|
<input type="text" id="hw-asset_code" name="asset_code" placeholder="자동 생성" readonly />
|
|
|
|
|
<button type="button" id="btn-gen-hw-code" class="btn btn-outline btn-sm btn-helper">생성</button>
|
|
|
|
|
<button type="button" id="btn-gen-hw-code" class="btn btn-outline" style="${standardBtnStyle}">생성</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
@@ -62,71 +64,45 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
|
|
|
|
|
<select id="hw-hw_status" name="hw_status">${generateOptionsHTML(HW_STATUS_LIST)}</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group dept-field">
|
|
|
|
|
<div class="form-group server-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.MONITORING.ui}</label>
|
|
|
|
|
<select id="hw-monitoring" name="monitoring">
|
|
|
|
|
<option value="비대상">비대상</option>
|
|
|
|
|
<option value="대상">대상</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title user-tracking-field">담당 및 조직</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
|
|
|
|
<select id="hw-current_dept" name="current_dept">${generateOptionsHTML(ORG_LIST)}</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group dept-field">
|
|
|
|
|
<label>${ASSET_SCHEMA.PREV_DEPT.ui}</label>
|
|
|
|
|
<select id="hw-previous_dept" name="previous_dept">${generateOptionsHTML(ORG_LIST)}</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.MANAGER_MAIN.ui}</label>
|
|
|
|
|
<input type="text" id="hw-manager_primary" name="manager_primary" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group user-tracking-field">
|
|
|
|
|
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
|
|
|
|
<input type="text" id="hw-user_current" name="user_current" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group user-tracking-field">
|
|
|
|
|
<label>${ASSET_SCHEMA.USER_POSITION.ui}</label>
|
|
|
|
|
<input type="text" id="hw-user_position" name="user_position" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.MANAGER_SUB.ui}</label>
|
|
|
|
|
<input type="text" id="hw-manager_secondary" name="manager_secondary" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group user-tracking-field">
|
|
|
|
|
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
|
|
|
|
<input type="text" id="hw-user_current" name="user_current" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group user-tracking-field pc-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.USER_POSITION.ui}</label>
|
|
|
|
|
<input type="text" id="hw-user_position" name="user_position" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group user-tracking-field">
|
|
|
|
|
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
|
|
|
|
|
<input type="text" id="hw-previous_user" name="previous_user" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group full-width server-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.ASSET_PURPOSE.ui}</label>
|
|
|
|
|
<input type="text" id="hw-asset_purpose" name="asset_purpose" placeholder="예: DB서버, 웹서버, 백업용 등" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title">설치 위치</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>건물/위치</label>
|
|
|
|
|
<select id="hw-bldg-select" name="location">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.LOC_DETAIL.ui}</label>
|
|
|
|
|
<div class="location-detail-container">
|
|
|
|
|
<select id="hw-location_detail" name="location_detail">
|
|
|
|
|
<option value="">선택</option>
|
|
|
|
|
</select>
|
|
|
|
|
<button type="button" id="btn-reg-loc-map" class="btn-loc-action btn-loc-view hidden" style="background-color: var(--primary-color);">
|
|
|
|
|
위치등록
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" id="btn-view-loc-map" class="btn-loc-action btn-loc-view hidden">
|
|
|
|
|
위치보기
|
|
|
|
|
</button>
|
|
|
|
|
<input type="hidden" id="hw-loc_x" name="loc_x" />
|
|
|
|
|
<input type="hidden" id="hw-loc_y" name="loc_y" />
|
|
|
|
|
<input type="hidden" id="hw-location_photo" name="location_photo" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title">시스템 사양</div>
|
|
|
|
|
<div class="form-section-title">시스템 사양 및 접속 정보</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
|
|
|
|
|
<input type="text" id="hw-model_name" name="model_name" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
|
|
|
|
|
<input type="text" id="hw-mainboard" name="mainboard" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.OS.ui}</label>
|
|
|
|
|
<input type="text" id="hw-os" name="os" />
|
|
|
|
|
@@ -139,40 +115,6 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
<label>${ASSET_SCHEMA.RAM.ui}</label>
|
|
|
|
|
<input type="text" id="hw-ram" name="ram" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.GPU.ui}</label>
|
|
|
|
|
<input type="text" id="hw-gpu" name="gpu" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>SSD 1</label>
|
|
|
|
|
<input type="text" id="hw-ssd_1" name="ssd_1" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>SSD 2</label>
|
|
|
|
|
<input type="text" id="hw-ssd_2" name="ssd_2" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>HDD 1</label>
|
|
|
|
|
<input type="text" id="hw-hdd_1" name="hdd_1" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>HDD 2</label>
|
|
|
|
|
<input type="text" id="hw-hdd_2" name="hdd_2" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>HDD 3</label>
|
|
|
|
|
<input type="text" id="hw-hdd_3" name="hdd_3" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>HDD 4</label>
|
|
|
|
|
<input type="text" id="hw-hdd_4" name="hdd_4" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group pc-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
|
|
|
|
|
<input type="text" id="hw-mac_address" name="mac_address" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title">네트워크 및 접속 정보</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
|
|
|
|
|
<input type="text" id="hw-ip_address" name="ip_address" />
|
|
|
|
|
@@ -181,9 +123,13 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
<label>${ASSET_SCHEMA.IP_ADDR2.ui}</label>
|
|
|
|
|
<input type="text" id="hw-ip_address_2" name="ip_address_2" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
|
|
|
|
|
<input type="text" id="hw-mac_address" name="mac_address" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group server-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.REMOTE_TOOL.ui}</label>
|
|
|
|
|
<input type="text" id="hw-remote_tool" name="remote_tool" placeholder="Anydesk, Chrome 등" />
|
|
|
|
|
<input type="text" id="hw-remote_tool" name="remote_tool" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group server-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.REMOTE_ID.ui}</label>
|
|
|
|
|
@@ -193,24 +139,26 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
<label>${ASSET_SCHEMA.REMOTE_PW.ui}</label>
|
|
|
|
|
<input type="text" id="hw-remote_pw" name="remote_pw" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group server-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.MONITORING.ui}</label>
|
|
|
|
|
<select id="hw-monitoring" name="monitoring">
|
|
|
|
|
<option value="대상">대상</option>
|
|
|
|
|
<option value="비대상">비대상</option>
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title infra-only">설치 위치</div>
|
|
|
|
|
<div class="form-group infra-only">
|
|
|
|
|
<label>건물/위치</label>
|
|
|
|
|
<select id="hw-bldg-select" name="location">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group infra-only">
|
|
|
|
|
<label>${ASSET_SCHEMA.LOC_DETAIL.ui}</label>
|
|
|
|
|
<div class="input-with-btn">
|
|
|
|
|
<select id="hw-location_detail" name="location_detail" style="flex: 1;"><option value="">선택</option></select>
|
|
|
|
|
<button type="button" id="btn-reg-loc-map" class="btn btn-primary" style="${standardBtnStyle} display: none;">위치등록</button>
|
|
|
|
|
<button type="button" id="btn-view-loc-map" class="btn btn-primary btn-loc-action" style="${standardBtnStyle} display: none; pointer-events: auto !important;">위치보기</button>
|
|
|
|
|
</div>
|
|
|
|
|
<input type="hidden" id="hw-loc_x" name="loc_x" /><input type="hidden" id="hw-loc_y" name="loc_y" /><input type="hidden" id="hw-location_photo" name="location_photo" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title">구매 및 증빙</div>
|
|
|
|
|
<div class="form-section-title">구매 및 증빙 정보</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
|
|
|
|
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
|
|
|
|
<input type="text" id="hw-purchase_date" name="purchase_date" style="flex:1;" />
|
|
|
|
|
<button type="button" class="btn-icon btn-helper" onclick="const p = document.getElementById('hw-purchase_date-picker'); p.value = document.getElementById('hw-purchase_date').value; p.showPicker();" style="padding:0.25rem;">
|
|
|
|
|
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<input type="date" id="hw-purchase_date-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('hw-purchase_date').value = this.value" tabindex="-1" />
|
|
|
|
|
</div>
|
|
|
|
|
<input type="text" id="hw-purchase_date" name="purchase_date" placeholder="YYYY-MM-DD" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
|
|
|
|
@@ -218,29 +166,32 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
|
|
|
|
|
<input type="text" id="hw-purchase_amount" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
|
|
|
|
<input type="text" id="hw-purchase_amount" name="purchase_amount" placeholder="0" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group full-width">
|
|
|
|
|
<label>${ASSET_SCHEMA.APPROVAL_DOC.ui}</label>
|
|
|
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
|
|
|
<input type="file" id="hw-approval_document_file" style="font-size:12px;" />
|
|
|
|
|
<span id="hw-approval_document_name" style="font-size:12px; color:var(--text-muted);"></span>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>${ASSET_SCHEMA.APPROVAL_DOC.ui} (첨부파일)</label>
|
|
|
|
|
<div class="file-upload-wrapper">
|
|
|
|
|
<input type="file" id="hw-approval_document_file" style="display:none;" />
|
|
|
|
|
<div class="input-with-btn">
|
|
|
|
|
<button type="button" id="btn-file-select" onclick="document.getElementById('hw-approval_document_file').click()" class="btn btn-outline btn-loc-action" style="${standardBtnStyle} flex: 1; justify-content: flex-start; pointer-events: auto !important;">
|
|
|
|
|
<span id="hw-file-name-display">파일 선택...</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<input type="hidden" id="hw-approval_document" name="approval_document" />
|
|
|
|
|
<div id="hw-file-link-container" style="margin-top: 4px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-group full-width">
|
|
|
|
|
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
|
|
|
|
<textarea id="hw-memo" name="memo" rows="2"></textarea>
|
|
|
|
|
<textarea id="hw-memo" name="memo" rows="3"></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="modal-history-area">
|
|
|
|
|
<div class="history-header">
|
|
|
|
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 자산 변동 이력</h3>
|
|
|
|
|
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">
|
|
|
|
|
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<h3>자산 변동 이력</h3>
|
|
|
|
|
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">이력 추가</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="hw-history-list" class="history-timeline"></div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -251,7 +202,7 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
<div class="footer-actions">
|
|
|
|
|
<button id="btn-revert-hw-edit" class="btn btn-outline hidden">수정 취소</button>
|
|
|
|
|
<button id="btn-cancel-hw-modal" class="btn btn-outline">닫기</button>
|
|
|
|
|
<button id="btn-save-hw-asset" class="btn btn-primary">수정</button>
|
|
|
|
|
<button id="btn-save-hw-asset" class="btn btn-primary">저장</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -273,48 +224,48 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
|
|
|
|
|
categorySelect.addEventListener('change', () => {
|
|
|
|
|
const types = CATEGORY_TYPE_MAP[categorySelect.value] || [];
|
|
|
|
|
typeSelect.innerHTML = types.length > 0
|
|
|
|
|
? generateOptionsHTML(types, '', true)
|
|
|
|
|
: '<option value="">구분을 먼저 선택하세요</option>';
|
|
|
|
|
typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, '', true) : '<option value="">구분을 먼저 선택하세요</option>';
|
|
|
|
|
this.applyRoleVisibility();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
typeSelect.addEventListener('change', () => {
|
|
|
|
|
this.applyRoleVisibility();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-gen-hw-code')?.addEventListener('click', async () => {
|
|
|
|
|
const cat = categorySelect.value;
|
|
|
|
|
if (!cat) { alert('구분을 먼저 선택해주세요.'); return; }
|
|
|
|
|
|
|
|
|
|
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 data = await res.json();
|
|
|
|
|
if (data.nextCode) {
|
|
|
|
|
setFieldValue('hw-asset_code', data.nextCode);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('코드 생성 실패:', err);
|
|
|
|
|
}
|
|
|
|
|
if (data.nextCode) setFieldValue('hw-asset_code', data.nextCode);
|
|
|
|
|
} catch (err) { console.error('코드 생성 실패:', err); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100));
|
|
|
|
|
detailSelect.addEventListener('change', () => this.updateMapButtonVisibility());
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-reg-loc-map')?.addEventListener('click', async () => {
|
|
|
|
|
document.getElementById('btn-reg-loc-map')?.addEventListener('click', async (e) => {
|
|
|
|
|
e.preventDefault(); e.stopPropagation();
|
|
|
|
|
await this.fetchMapConfig();
|
|
|
|
|
const images = this.getImagesForLocation(bldgSelect.value, detailSelect.value);
|
|
|
|
|
if (images) this.openImagePicker(images, `${detailSelect.value} 위치 등록`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-view-loc-map')?.addEventListener('click', async () => {
|
|
|
|
|
document.getElementById('btn-view-loc-map')?.addEventListener('click', async (e) => {
|
|
|
|
|
e.preventDefault(); e.stopPropagation();
|
|
|
|
|
await this.fetchMapConfig();
|
|
|
|
|
const images = this.getImagesForLocation(bldgSelect.value, detailSelect.value);
|
|
|
|
|
const x = getFieldValue('hw-loc_x');
|
|
|
|
|
const y = getFieldValue('hw-loc_y');
|
|
|
|
|
const savedImg = getFieldValue('hw-location_photo');
|
|
|
|
|
|
|
|
|
|
const bldg = getFieldValue('hw-bldg-select');
|
|
|
|
|
const detail = getFieldValue('hw-location_detail');
|
|
|
|
|
const images = this.getImagesForLocation(bldg, detail);
|
|
|
|
|
if (images) {
|
|
|
|
|
const imgPath = savedImg && images.includes(savedImg) ? savedImg : images[0];
|
|
|
|
|
this.openImagePreview(imgPath, `${detailSelect.value} 위치 확인`, x, y);
|
|
|
|
|
this.openImagePreview(imgPath, `${detail} 위치 확인`, x, y);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -331,27 +282,52 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
this.updateMapButtonVisibility();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const fileInput = document.getElementById('hw-approval_document_file') as HTMLInputElement;
|
|
|
|
|
const fileNameDisplay = document.getElementById('hw-file-name-display');
|
|
|
|
|
const fileLinkContainer = document.getElementById('hw-file-link-container');
|
|
|
|
|
|
|
|
|
|
fileInput?.addEventListener('change', async (e) => {
|
|
|
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
if (fileNameDisplay) fileNameDisplay.textContent = file.name;
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`http://${location.hostname}:3000/api/upload`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ fileName: file.name, fileData: reader.result })
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
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-loc-action" style="color:var(--primary-color); font-size:12px; text-decoration:underline; pointer-events: auto !important;">[업로드 완료: 파일 보기]</a>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) { console.error('파일 업로드 실패:', err); alert('파일 업로드 중 오류가 발생했습니다.'); }
|
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
saveBtn.addEventListener('click', async () => {
|
|
|
|
|
if (!this.currentAsset) return;
|
|
|
|
|
if (!this.isEditMode) {
|
|
|
|
|
this.setEditLockMode('edit');
|
|
|
|
|
this.isEditMode = true;
|
|
|
|
|
this.updateMapButtonVisibility();
|
|
|
|
|
this.toggleFileUploadUI(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formData = new FormData(this.formEl!);
|
|
|
|
|
const updated = { ...this.currentAsset };
|
|
|
|
|
formData.forEach((value, key) => { if (key !== 'id') updated[key] = value; });
|
|
|
|
|
updated.location = getFieldValue('hw-bldg-select');
|
|
|
|
|
|
|
|
|
|
if (await saveAsset(this.getCategoryKey(updated), updated)) {
|
|
|
|
|
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
|
|
|
|
onSave(); this.close(); closeModals();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
createIcons({ icons: { History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected fillFormData(asset: any): void {
|
|
|
|
|
@@ -359,100 +335,72 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
setFieldValue('hw-asset_code', asset.asset_code || '');
|
|
|
|
|
setFieldValue('hw-purchase_corp', asset.purchase_corp || '');
|
|
|
|
|
setFieldValue('hw-category', asset.category || '');
|
|
|
|
|
setFieldValue('hw-current_role', asset.current_role || 'Normal');
|
|
|
|
|
|
|
|
|
|
const types = CATEGORY_TYPE_MAP[asset.category] || [];
|
|
|
|
|
const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement;
|
|
|
|
|
if (typeSelect) typeSelect.innerHTML = types.length > 0 ? generateOptionsHTML(types, asset.asset_type, true) : '<option value="">구분을 먼저 선택하세요</option>';
|
|
|
|
|
|
|
|
|
|
setFieldValue('hw-asset_type', asset.asset_type || '');
|
|
|
|
|
setFieldValue('hw-hw_status', asset.hw_status || '운영');
|
|
|
|
|
setFieldValue('hw-current_dept', asset.current_dept || '');
|
|
|
|
|
setFieldValue('hw-previous_dept', asset.previous_dept || '');
|
|
|
|
|
setFieldValue('hw-manager_primary', asset.manager_primary || '');
|
|
|
|
|
setFieldValue('hw-manager_secondary', asset.manager_secondary || '');
|
|
|
|
|
setFieldValue('hw-user_current', asset.user_current || '');
|
|
|
|
|
setFieldValue('hw-user_position', asset.user_position || '');
|
|
|
|
|
setFieldValue('hw-previous_user', asset.previous_user || '');
|
|
|
|
|
setFieldValue('hw-asset_purpose', asset.asset_purpose || '');
|
|
|
|
|
setFieldValue('hw-model_name', asset.model_name || '');
|
|
|
|
|
setFieldValue('hw-cpu', asset.cpu || '');
|
|
|
|
|
setFieldValue('hw-ram', asset.ram || '');
|
|
|
|
|
setFieldValue('hw-gpu', asset.gpu || '');
|
|
|
|
|
setFieldValue('hw-ssd_1', asset.ssd_1 || '');
|
|
|
|
|
setFieldValue('hw-ssd_2', asset.ssd_2 || '');
|
|
|
|
|
setFieldValue('hw-hdd_1', asset.hdd_1 || '');
|
|
|
|
|
setFieldValue('hw-hdd_2', asset.hdd_2 || '');
|
|
|
|
|
setFieldValue('hw-hdd_3', asset.hdd_3 || '');
|
|
|
|
|
setFieldValue('hw-hdd_4', asset.hdd_4 || '');
|
|
|
|
|
setFieldValue('hw-mainboard', asset.mainboard || '');
|
|
|
|
|
setFieldValue('hw-os', asset.os || '');
|
|
|
|
|
setFieldValue('hw-mac_address', asset.mac_address || '');
|
|
|
|
|
setFieldValue('hw-ip_address', asset.ip_address || '');
|
|
|
|
|
setFieldValue('hw-ip_address_2', asset.ip_address_2 || '');
|
|
|
|
|
setFieldValue('hw-remote_tool', asset.remote_tool || '');
|
|
|
|
|
setFieldValue('hw-remote_id', asset.remote_id || '');
|
|
|
|
|
setFieldValue('hw-remote_pw', asset.remote_pw || '');
|
|
|
|
|
setFieldValue('hw-monitoring', asset.monitoring || '비대상');
|
|
|
|
|
setFieldValue('hw-purchase_date', asset.purchase_date || '');
|
|
|
|
|
setFieldValue('hw-purchase_vendor', asset.purchase_vendor || '');
|
|
|
|
|
setFieldValue('hw-purchase_amount', asset.purchase_amount || '');
|
|
|
|
|
|
|
|
|
|
const docName = document.getElementById('hw-approval_document_name');
|
|
|
|
|
if (docName) docName.textContent = asset.approval_document || '';
|
|
|
|
|
|
|
|
|
|
setFieldValue('hw-approval_document', asset.approval_document || '');
|
|
|
|
|
const docName = document.getElementById('hw-file-name-display');
|
|
|
|
|
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-loc-action" style="color:var(--primary-color); font-size:12px; text-decoration:underline; pointer-events: auto !important;">[파일 보기]</a>`;
|
|
|
|
|
} else if (fileLinkContainer) {
|
|
|
|
|
fileLinkContainer.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
setFieldValue('hw-memo', asset.memo || '');
|
|
|
|
|
setFieldValue('hw-location_detail', asset.location_detail || '');
|
|
|
|
|
setFieldValue('hw-loc_x', asset.loc_x || '');
|
|
|
|
|
setFieldValue('hw-loc_y', asset.loc_y || '');
|
|
|
|
|
setFieldValue('hw-location_photo', asset.location_photo || asset.loc_img || '');
|
|
|
|
|
|
|
|
|
|
parseAndSetLocation(asset.location || '', asset.location_detail || '', 'hw-bldg-select', 'hw-location_detail');
|
|
|
|
|
this.renderHistory(asset.id);
|
|
|
|
|
|
|
|
|
|
// Initial visibility check based on role
|
|
|
|
|
this.applyRoleVisibility(asset.current_role || 'Normal');
|
|
|
|
|
this.applyRoleVisibility();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
this.toggleFileUploadUI(mode !== 'view');
|
|
|
|
|
this.updateMapButtonVisibility(asset);
|
|
|
|
|
|
|
|
|
|
const role = asset.current_role || 'Normal';
|
|
|
|
|
this.applyRoleVisibility(role);
|
|
|
|
|
|
|
|
|
|
// Role change event
|
|
|
|
|
const roleSelect = document.getElementById('hw-current_role') as HTMLSelectElement;
|
|
|
|
|
roleSelect?.addEventListener('change', (e) => {
|
|
|
|
|
this.applyRoleVisibility((e.target as HTMLSelectElement).value);
|
|
|
|
|
});
|
|
|
|
|
this.applyRoleVisibility();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private applyRoleVisibility(role: string): void {
|
|
|
|
|
const isServer = role === 'Server';
|
|
|
|
|
const isPersonal = role === 'Personal';
|
|
|
|
|
private toggleFileUploadUI(showUpload: boolean) {
|
|
|
|
|
const fileBtn = document.getElementById('btn-file-select') as HTMLElement;
|
|
|
|
|
if (fileBtn) fileBtn.style.display = showUpload ? 'inline-flex' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Section Visibility
|
|
|
|
|
const networkSectionTitle = document.evaluate("//div[contains(text(), '네트워크 및 접속 정보')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue as HTMLElement;
|
|
|
|
|
const locationSectionTitle = document.evaluate("//div[contains(text(), '설치 위치')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue as HTMLElement;
|
|
|
|
|
private applyRoleVisibility(): void {
|
|
|
|
|
const category = (document.getElementById('hw-category') as HTMLSelectElement)?.value || '';
|
|
|
|
|
const type = (document.getElementById('hw-asset_type') as HTMLSelectElement)?.value || '';
|
|
|
|
|
|
|
|
|
|
// Helper to toggle visibility of elements after a title until next section title
|
|
|
|
|
const toggleSection = (titleEl: HTMLElement, show: boolean) => {
|
|
|
|
|
if (!titleEl) return;
|
|
|
|
|
titleEl.style.display = show ? 'block' : 'none';
|
|
|
|
|
let next = titleEl.nextElementSibling as HTMLElement;
|
|
|
|
|
while (next && !next.classList.contains('form-section-title')) {
|
|
|
|
|
next.style.display = show ? 'flex' : 'none';
|
|
|
|
|
next = next.nextElementSibling as HTMLElement;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// 인프라 장비 (서버, 서버PC, 저장시스템 등)
|
|
|
|
|
const isInfra = ['서버', '저장매체', '네트워크', '공간정보장비'].includes(category) || type.includes('서버') || type.includes('저장시스템');
|
|
|
|
|
// 개인 장비 (PC, 노트북) - '서버PC'는 제외
|
|
|
|
|
const isPersonal = (['PC', '노트북'].includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC');
|
|
|
|
|
|
|
|
|
|
// Show/Hide based on role
|
|
|
|
|
toggleSection(networkSectionTitle, isServer);
|
|
|
|
|
toggleSection(locationSectionTitle, !isPersonal);
|
|
|
|
|
|
|
|
|
|
// Specific fields
|
|
|
|
|
document.querySelectorAll('.server-only').forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none');
|
|
|
|
|
document.querySelectorAll('.pc-only').forEach(el => (el as HTMLElement).style.display = isPersonal ? 'flex' : 'none');
|
|
|
|
|
// JS에서 display: block을 강제하지 않고, 빈 문자열로 설정하여 CSS의 flex가 작동하게 함
|
|
|
|
|
document.querySelectorAll('.server-only').forEach(el => (el as HTMLElement).style.display = isInfra ? '' : 'none');
|
|
|
|
|
document.querySelectorAll('.infra-only').forEach(el => (el as HTMLElement).style.display = isInfra ? '' : 'none');
|
|
|
|
|
document.querySelectorAll('.user-tracking-field').forEach(el => (el as HTMLElement).style.display = isPersonal ? '' : 'none');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateMapButtonVisibility(asset?: any) {
|
|
|
|
|
@@ -460,18 +408,19 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
const detail = asset ? (asset.location_detail || '') : getFieldValue('hw-location_detail');
|
|
|
|
|
const x = asset ? (asset.loc_x || '') : getFieldValue('hw-loc_x');
|
|
|
|
|
const y = asset ? (asset.loc_y || '') : getFieldValue('hw-loc_y');
|
|
|
|
|
|
|
|
|
|
const hasCoords = (x !== '' && y !== '' && x !== 'null' && y !== 'null');
|
|
|
|
|
const hasImage = !!this.getImagesForLocation(bldg, detail);
|
|
|
|
|
|
|
|
|
|
const regLocBtn = document.getElementById('btn-reg-loc-map')!;
|
|
|
|
|
const viewLocBtn = document.getElementById('btn-view-loc-map')!;
|
|
|
|
|
|
|
|
|
|
if (hasImage && this.isEditMode) regLocBtn.classList.remove('hidden');
|
|
|
|
|
else regLocBtn.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
if (hasImage && hasCoords) viewLocBtn.classList.remove('hidden');
|
|
|
|
|
else viewLocBtn.classList.add('hidden');
|
|
|
|
|
if (hasImage && this.isEditMode) regLocBtn.style.display = 'inline-flex'; else regLocBtn.style.display = 'none';
|
|
|
|
|
if (hasImage && hasCoords) {
|
|
|
|
|
viewLocBtn.style.display = 'inline-flex';
|
|
|
|
|
viewLocBtn.style.pointerEvents = 'auto';
|
|
|
|
|
viewLocBtn.style.opacity = '1';
|
|
|
|
|
} else {
|
|
|
|
|
viewLocBtn.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getImagesForLocation(bldg: string, detail: string): string[] | null {
|
|
|
|
|
@@ -489,78 +438,32 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
private generateDynamicSVG(imagePath: string): string {
|
|
|
|
|
const boxes = this.dynamicMapConfig[imagePath] || [];
|
|
|
|
|
if (boxes.length === 0) return '';
|
|
|
|
|
return `
|
|
|
|
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" class="digital-map-svg">
|
|
|
|
|
<g class="seat-group">
|
|
|
|
|
${boxes.map((b, i) => `<rect class="map-seat-obj" data-id="seat-${i+1}" x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="0.5" />`).join('')}
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
return `<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%; height:100%; position:absolute; top:0; left:0; pointer-events:none;"><g>${boxes.map((b) => `<rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="0.5" style="fill:rgba(30,81,73,0.05); stroke:rgba(30,81,73,0.2); stroke-width:0.2;" />`).join('')}</g></svg>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private openImagePicker(imagePaths: string[], title: string) {
|
|
|
|
|
let currentIdx = 0;
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
overlay.className = 'image-picker-overlay';
|
|
|
|
|
|
|
|
|
|
const renderContent = () => {
|
|
|
|
|
const imgPath = imagePaths[currentIdx];
|
|
|
|
|
const isMulti = imagePaths.length > 1;
|
|
|
|
|
const digitalMap = this.generateDynamicSVG(imgPath);
|
|
|
|
|
overlay.innerHTML = `
|
|
|
|
|
<div class="image-picker-header">
|
|
|
|
|
<h3>${title} ${isMulti ? `(${currentIdx + 1}/${imagePaths.length})` : ''}</h3>
|
|
|
|
|
<button class="btn-icon btn-close-picker" style="color:white !important;"><i data-lucide="x"></i></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="image-picker-content">
|
|
|
|
|
${isMulti ? `<div class=\"picker-nav prev ${currentIdx === 0 ? 'disabled' : ''}\">◀</div><div class=\"picker-nav next ${currentIdx === imagePaths.length - 1 ? 'disabled' : ''}\">▶</div>` : ''}
|
|
|
|
|
<div class="layout-map-container" id="picker-container">
|
|
|
|
|
<img src="${imgPath}" class="layout-map-img" />
|
|
|
|
|
<div id="picker-marker" class="layout-marker hidden"></div>
|
|
|
|
|
<div class="digital-overlay-layer">${digitalMap}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="image-picker-footer">
|
|
|
|
|
<p style="color:#ddd; font-size:12px; margin:0; flex:1;">배치도의 네모 칸을 클릭하면 위치가 자동으로 지정됩니다.</p>
|
|
|
|
|
<button id="btn-picker-cancel" class="btn btn-outline" style="color:white; border-color:white;">취소</button>
|
|
|
|
|
<button id="btn-picker-save" class="btn btn-primary">위치 확정</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
createIcons({ icons: { X } });
|
|
|
|
|
|
|
|
|
|
<div class="image-picker-header"><h3>${title}</h3><button class="btn-close-picker" style="background:none; border:none; color:white; font-size:24px; cursor:pointer;">×</button></div>
|
|
|
|
|
<div class="image-picker-content"><div class="layout-map-container" id="picker-container"><img src="${imgPath}" class="layout-map-img" /><div id="picker-marker" class="layout-marker hidden"></div><div class="digital-overlay-layer">${digitalMap}</div></div></div>
|
|
|
|
|
<div class="image-picker-footer"><button id="btn-picker-cancel" class="btn btn-outline" style="color:white; border-color:white;">취소</button><button id="btn-picker-save" class="btn btn-primary">위치 확정</button></div>`;
|
|
|
|
|
let selectedX = ''; let selectedY = '';
|
|
|
|
|
const container = overlay.querySelector('#picker-container') as HTMLElement;
|
|
|
|
|
const marker = overlay.querySelector('#picker-marker') as HTMLElement;
|
|
|
|
|
|
|
|
|
|
overlay.querySelectorAll('.map-seat-obj').forEach(seat => {
|
|
|
|
|
seat.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const target = e.currentTarget as SVGRectElement;
|
|
|
|
|
selectedX = target.getAttribute('x') || ''; selectedY = target.getAttribute('y') || '';
|
|
|
|
|
const w = target.getAttribute('width') || '0'; const h = target.getAttribute('height') || '0';
|
|
|
|
|
marker.style.left = `${parseFloat(selectedX) + parseFloat(w)/2}%`;
|
|
|
|
|
marker.style.top = `${parseFloat(selectedY) + parseFloat(h)/2}%`;
|
|
|
|
|
marker.classList.remove('hidden');
|
|
|
|
|
});
|
|
|
|
|
container.addEventListener('click', (e) => {
|
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
|
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
|
|
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
|
|
|
|
selectedX = x.toFixed(2); selectedY = y.toFixed(2);
|
|
|
|
|
marker.style.left = `${selectedX}%`; marker.style.top = `${selectedY}%`; marker.classList.remove('hidden');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!digitalMap) {
|
|
|
|
|
container.addEventListener('click', (e) => {
|
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
|
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
|
|
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
|
|
|
|
selectedX = x.toFixed(2); selectedY = y.toFixed(2);
|
|
|
|
|
marker.style.left = `${selectedX}%`; marker.style.top = `${selectedY}%`;
|
|
|
|
|
marker.classList.remove('hidden');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove());
|
|
|
|
|
overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => overlay.remove());
|
|
|
|
|
if (isMulti) {
|
|
|
|
|
overlay.querySelector('.picker-nav.prev')?.addEventListener('click', (e) => { if (currentIdx > 0) { currentIdx--; renderContent(); } });
|
|
|
|
|
overlay.querySelector('.picker-nav.next')?.addEventListener('click', (e) => { if (currentIdx < imagePaths.length - 1) { currentIdx++; renderContent(); } });
|
|
|
|
|
}
|
|
|
|
|
overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
|
|
|
|
|
if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
|
|
|
|
|
setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
|
|
|
|
|
@@ -575,33 +478,22 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
overlay.className = 'image-picker-overlay';
|
|
|
|
|
const digitalMap = this.generateDynamicSVG(imagePath);
|
|
|
|
|
|
|
|
|
|
overlay.innerHTML = `
|
|
|
|
|
<div class="image-picker-header">
|
|
|
|
|
<h3>${title}</h3>
|
|
|
|
|
<button class="btn-icon btn-close-picker" style="color:white !important;"><i data-lucide="x"></i></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="image-picker-content">
|
|
|
|
|
<div class="layout-map-container readonly">
|
|
|
|
|
<img src="${imagePath}" class="layout-map-img" />
|
|
|
|
|
<div id="preview-marker" class="layout-marker pulse-marker" style="left:${x}%; top:${y}%;"></div>
|
|
|
|
|
<div class="digital-overlay-layer">${digitalMap}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="image-picker-footer"><button id="btn-preview-close" class="btn btn-primary">확인</button></div>
|
|
|
|
|
`;
|
|
|
|
|
<div class="image-picker-header"><h3>${title}</h3><button class="btn-close-picker" style="background:none; border:none; color:white; font-size:24px; cursor:pointer;">×</button></div>
|
|
|
|
|
<div class="image-picker-content"><div class="layout-map-container readonly"><img src="${imagePath}" class="layout-map-img" /><div id="preview-marker" class="layout-marker pulse-marker" style="left:${x}%; top:${y}%;"></div><div class="digital-overlay-layer">${digitalMap}</div></div></div>
|
|
|
|
|
<div class="image-picker-footer"><button id="btn-preview-close" class="btn btn-primary">확인</button></div>`;
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
createIcons({ icons: { X } });
|
|
|
|
|
|
|
|
|
|
if (digitalMap) {
|
|
|
|
|
overlay.querySelectorAll('.map-seat-obj').forEach(seat => {
|
|
|
|
|
const sx = seat.getAttribute('x'); const sy = seat.getAttribute('y');
|
|
|
|
|
if (sx === x && sy === y) {
|
|
|
|
|
(seat as SVGRectElement).style.fill = 'rgba(255, 61, 0, 0.4)';
|
|
|
|
|
(seat as SVGRectElement).style.stroke = '#FF3D00'; (seat as SVGRectElement).style.strokeWidth = '0.8';
|
|
|
|
|
const curX = parseFloat(x || '0'); const curY = parseFloat(y || '0');
|
|
|
|
|
overlay.querySelectorAll('rect').forEach(rect => {
|
|
|
|
|
const sx = parseFloat(rect.getAttribute('x') || '0');
|
|
|
|
|
const sy = parseFloat(rect.getAttribute('y') || '0');
|
|
|
|
|
if (Math.abs(sx - curX) < 0.01 && Math.abs(sy - curY) < 0.01) {
|
|
|
|
|
rect.style.fill = 'rgba(255, 61, 0, 0.4)'; rect.style.stroke = '#FF3D00'; rect.style.strokeWidth = '0.8';
|
|
|
|
|
const w = parseFloat(rect.getAttribute('width') || '0');
|
|
|
|
|
const h = parseFloat(rect.getAttribute('height') || '0');
|
|
|
|
|
const marker = overlay.querySelector('#preview-marker') as HTMLElement;
|
|
|
|
|
const w = seat.getAttribute('width') || '0'; const h = seat.getAttribute('height') || '0';
|
|
|
|
|
marker.style.left = `${parseFloat(sx!) + parseFloat(w)/2}%`; marker.style.top = `${parseFloat(sy!) + parseFloat(h)/2}%`;
|
|
|
|
|
if (marker) { marker.style.left = `${sx + w/2}%`; marker.style.top = `${sy + h/2}%`; }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@@ -631,14 +523,6 @@ class HwAssetModal extends BaseModal {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 싱글톤 인스턴스 생성 및 익스포트
|
|
|
|
|
export const hwModal = new HwAssetModal();
|
|
|
|
|
|
|
|
|
|
// 레거시 호환성을 위한 함수 래퍼
|
|
|
|
|
export function initHwModal(onSave: () => void, closeModals: () => void) {
|
|
|
|
|
hwModal.init(onSave, closeModals);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
|
|
|
|
hwModal.open(asset, mode);
|
|
|
|
|
}
|
|
|
|
|
export function initHwModal(onSave: () => void, closeModals: () => void) { hwModal.init(onSave, closeModals); }
|
|
|
|
|
export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { hwModal.open(asset, mode); }
|
|
|
|
|
|