feat: enhance map editor, refine location view, and update image assets

- Map Editor: Add box numbering (drawing/placed) and set default file
- Location View: Refine mouse interaction in view mode (readonly)
- Assets: Add MDF room support and update server room directory structure
- Backend: Add map configuration API for real-time saving
This commit is contained in:
2026-06-01 14:00:45 +09:00
parent bf7fb0ffe6
commit 590ddd0e85
19 changed files with 1456 additions and 109 deletions

View File

@@ -12,13 +12,15 @@ export function initBaseModal() {
if (e.key === 'Escape') closeModals();
});
// 배경(Overlay) 클릭 시 닫기
// 배경(Overlay) 클릭 시 닫기 (요청에 의해 비활성화됨)
/*
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('modal-overlay')) {
closeModals();
}
});
*/
return { closeAllModals: closeModals };
}

View File

@@ -15,6 +15,64 @@ import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu,
let currentHwAsset: any | null = null;
let isEditMode = false;
let dynamicMapConfig: Record<string, any[]> = {};
const IMAGE_LOCATIONS: Record<string, Record<string, string[]>> = {
'IDC': {
'서관202': ['img/location_photo/IDC/서관202.png'],
'서관203': ['img/location_photo/IDC/서관203.png'],
'서관204': ['img/location_photo/IDC/서관204.png'],
'서관205': ['img/location_photo/IDC/서관205.png'],
'동관53': ['img/location_photo/IDC/동관53.png'],
'동관54': ['img/location_photo/IDC/동관54.png'],
},
'기술개발센터': {
'서버실': [
'img/location_photo/기술개발센터/서버실/서버실_1.png',
'img/location_topic/기술개발센터/서버실/서버실_2.png'
]
},
'한맥빌딩': {
'7층': ['img/location_photo/한맥빌딩/7층_로비.png'],
'MDF실': [
'img/location_photo/한맥빌딩/MDF실/MDF_1.png',
'img/location_photo/한맥빌딩/MDF실/MDF_2.png',
'img/location_photo/한맥빌딩/MDF실/MDF_3.png',
'img/location_photo/한맥빌딩/MDF실/MDF_4.png'
]
}
};
const getImagesForLocation = (bldg: string, detail: string): string[] | null => {
if (!bldg || !detail) return null;
const b = bldg.trim();
const d = detail.trim();
return IMAGE_LOCATIONS[b]?.[d] || null;
};
async function fetchMapConfig() {
try {
const res = await fetch(`http://${location.hostname}:3000/api/maps`);
dynamicMapConfig = await res.json();
} catch (err) {
console.error('Failed to fetch map config:', err);
}
}
function generateDynamicSVG(imagePath: string): string {
const boxes = 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>
`;
}
const HW_MODAL_HTML = `
<div id="hw-asset-modal" class="modal-overlay hidden">
@@ -29,7 +87,6 @@ const HW_MODAL_HTML = `
<form id="hw-asset-form" class="grid-form">
<input type="hidden" id="hw-id" name="id" />
<!-- Group 1: 기본 및 관리 정보 -->
<div class="form-section-title">기본 및 관리 정보</div>
<div class="form-group">
<label>${ASSET_SCHEMA.ASSET_CODE.ui}</label>
@@ -92,7 +149,6 @@ const HW_MODAL_HTML = `
<input type="text" id="hw-asset_purpose" name="asset_purpose" placeholder="예: DB서버, 웹서버, 백업용 등" />
</div>
<!-- Group 2: 설치 위치 -->
<div class="form-section-title">설치 위치</div>
<div class="form-group">
<label>건물/위치</label>
@@ -100,12 +156,22 @@ const HW_MODAL_HTML = `
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.LOC_DETAIL.ui}</label>
<select id="hw-location_detail" name="location_detail">
<option value="">선택</option>
</select>
<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>
<!-- Group 3: 시스템 사양 -->
<div class="form-section-title">시스템 사양</div>
<div class="form-group">
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
@@ -160,7 +226,6 @@ const HW_MODAL_HTML = `
<input type="text" id="hw-mac_address" name="mac_address" />
</div>
<!-- Group 4: 네트워크 및 접속 정보 -->
<div class="form-section-title">네트워크 및 접속 정보</div>
<div class="form-group">
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
@@ -190,7 +255,6 @@ const HW_MODAL_HTML = `
</select>
</div>
<!-- Group 5: 구매 정보 -->
<div class="form-section-title">구매 및 증빙</div>
<div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
@@ -248,11 +312,49 @@ const HW_MODAL_HTML = `
</div>
`;
/**
* 전역적으로 버튼 가시성을 업데이트하는 공용 함수
*/
function updateMapButtonVisibility(asset?: any) {
const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
const detailSelect = document.getElementById('hw-location_detail') as HTMLSelectElement;
const regLocBtn = document.getElementById('btn-reg-loc-map')!;
const viewLocBtn = document.getElementById('btn-view-loc-map')!;
if (!bldgSelect || !detailSelect) return;
// 인자로 넘어온 asset이 있으면 우선 사용, 없으면 DOM 필드에서 읽음
const bldg = asset ? (asset.location || '') : bldgSelect.value;
const detail = asset ? (asset.location_detail || '') : detailSelect.value;
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 = !!getImagesForLocation(bldg, detail);
// 위치등록 버튼: 오직 편집 모드일 때만 노출
if (hasImage && isEditMode) {
regLocBtn.classList.remove('hidden');
} else {
regLocBtn.classList.add('hidden');
}
// 위치보기 버튼: 좌표가 있고 이미지가 있는 위치라면 모드 상관없이 노출
if (hasImage && hasCoords) {
viewLocBtn.classList.remove('hidden');
} else {
viewLocBtn.classList.add('hidden');
}
}
export function initHwModal(onSave: () => void, closeModals: () => void) {
if (!document.getElementById('hw-asset-modal')) {
document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML);
}
fetchMapConfig();
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
const saveBtn = document.getElementById('btn-save-hw-asset')!;
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
@@ -263,7 +365,6 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
bindLocationEvents('hw-bldg-select', 'hw-location_detail', '', '');
applyDateMask(document.getElementById('hw-purchase_date') as HTMLInputElement);
// Category -> Asset Type Cascading
const categorySelect = document.getElementById('hw-category') as HTMLSelectElement;
const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement;
@@ -279,16 +380,176 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
btnCloseHeader.addEventListener('click', closeModalAction);
btnCancelFooter.addEventListener('click', closeModalAction);
deleteBtn.addEventListener('click', async () => {
if (!currentHwAsset) return;
if (!confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
const detailSelect = document.getElementById('hw-location_detail') as HTMLSelectElement;
const bldgSelect = document.getElementById('hw-bldg-select') as HTMLSelectElement;
bldgSelect.addEventListener('change', () => {
setTimeout(() => updateMapButtonVisibility(), 100);
});
detailSelect.addEventListener('change', () => updateMapButtonVisibility());
const regLocBtn = document.getElementById('btn-reg-loc-map')!;
const viewLocBtn = document.getElementById('btn-view-loc-map')!;
regLocBtn.addEventListener('click', async () => {
await fetchMapConfig();
const images = getImagesForLocation(bldgSelect.value, detailSelect.value);
if (images) openImagePicker(images, `${detailSelect.value} 위치 등록`);
});
viewLocBtn.addEventListener('click', async () => {
await fetchMapConfig();
const bldg = bldgSelect.value;
const detail = detailSelect.value;
const images = getImagesForLocation(bldg, detail);
const x = getFieldValue('hw-loc_x');
const y = getFieldValue('hw-loc_y');
const savedImg = getFieldValue('hw-location_photo');
if (images) {
const imgPath = savedImg && images.includes(savedImg) ? savedImg : images[0];
openImagePreview(imgPath, `${detail} 위치 확인`, x, y);
}
});
function 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 = 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 } });
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');
});
});
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) => { e.stopPropagation(); if (currentIdx > 0) { currentIdx--; renderContent(); } });
overlay.querySelector('.picker-nav.next')?.addEventListener('click', (e) => { e.stopPropagation(); 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);
setFieldValue('hw-location_photo', imagePaths[currentIdx]);
updateMapButtonVisibility();
overlay.remove();
});
};
renderContent();
document.body.appendChild(overlay);
}
function openImagePreview(imagePath: string, title: string, x: string, y: string) {
const overlay = document.createElement('div');
overlay.className = 'image-picker-overlay';
const digitalMap = 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>
`;
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 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}%`;
}
});
}
overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove());
overlay.querySelector('#btn-preview-close')?.addEventListener('click', () => overlay.remove());
}
deleteBtn.addEventListener('click', async () => {
if (!currentHwAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
let categoryKey = 'pc';
const cat = currentHwAsset.category;
const type = currentHwAsset.asset_type;
const code = currentHwAsset.asset_code || '';
if (type === '서버PC') categoryKey = 'pc';
if (currentHwAsset.asset_type === '서버PC') categoryKey = 'pc';
else if (cat === '서버' || code.startsWith('SVR')) categoryKey = 'server';
else if (cat === '스토리지' || code.startsWith('STO')) categoryKey = 'storage';
else if (cat === '네트워크' || code.startsWith('NET')) categoryKey = 'network';
@@ -298,11 +559,8 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
else if (cat === '사무가구' || cat === '사무소모품') categoryKey = 'officeSupplies';
else if (cat === 'PC' || code.startsWith('PC')) categoryKey = 'pc';
const success = await deleteAsset(categoryKey, currentHwAsset.id);
if (success) {
alert('성공적으로 삭제되었습니다.');
onSave(); // Refresh list
closeModalAction();
if (await deleteAsset(categoryKey, currentHwAsset.id)) {
alert('성공적으로 삭제되었습니다.'); onSave(); closeModalAction();
}
});
@@ -310,49 +568,33 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = false;
if (currentHwAsset) fillHwFormData(currentHwAsset);
updateMapButtonVisibility();
});
saveBtn.addEventListener('click', async () => {
if (!currentHwAsset) return;
if (!isEditMode) {
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = true;
isEditMode = true;
updateMapButtonVisibility();
return;
}
const formData = new FormData(form);
const updated: any = { ...currentHwAsset };
formData.forEach((value, key) => {
if (key !== 'id') updated[key] = value;
});
// Handle location columns:
// 'location' stores only the building name
// 'location_detail' is already handled via the dynamic FormData loop
formData.forEach((value, key) => { if (key !== 'id') updated[key] = value; });
updated.location = getFieldValue('hw-bldg-select');
let categoryKey = 'pc';
// 서버PC인 경우 category는 PC이지만 UI상 서버로 취급되므로,
// 저장은 반드시 'pc' 엔드포인트(/api/pc)로 되어야 함.
if (updated.asset_type === '서버PC') {
categoryKey = 'pc';
} else if (updated.asset_code?.startsWith('SVR') || updated.category === '서버') {
categoryKey = 'server';
} else if (updated.asset_code?.startsWith('STO') || updated.category === '스토리지') {
categoryKey = 'storage';
} else if (updated.asset_code?.startsWith('EQP') || updated.category === '업무지원장비') {
categoryKey = 'equipment';
} else if (updated.category === '공간정보장비') {
categoryKey = 'survey';
} else if (updated.category === 'PC부품') {
categoryKey = 'pcParts';
}
if (updated.asset_type === '서버PC') categoryKey = 'pc';
else if (updated.asset_code?.startsWith('SVR') || updated.category === '서버') categoryKey = 'server';
else if (updated.asset_code?.startsWith('STO') || updated.category === '스토리지') categoryKey = 'storage';
else if (updated.asset_code?.startsWith('EQP') || updated.category === '업무지원장비') categoryKey = 'equipment';
else if (updated.category === '공간정보장비') categoryKey = 'survey';
else if (updated.category === 'PC부품') categoryKey = 'pcParts';
const success = await saveAsset(categoryKey, updated);
if (success) {
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
onSave();
if (await saveAsset(categoryKey, updated)) {
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
onSave();
closeModalAction();
}
});
@@ -363,31 +605,21 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
currentHwAsset = asset;
const modal = document.getElementById('hw-asset-modal')!;
setEditLock('hw-asset-form', mode, {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
addLogBtnId: 'btn-add-hw-log'
});
setEditLock('hw-asset-form', mode, { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', addLogBtnId: 'btn-add-hw-log' });
isEditMode = (mode === 'add' || mode === 'edit');
fillHwFormData(asset);
// Show/Hide category specific fields
const serverOnly = document.querySelectorAll('.server-only');
const nonServer = document.querySelectorAll('.non-server');
const pcOnly = document.querySelectorAll('.pc-only');
const userFields = document.querySelectorAll('.user-tracking-field');
// 조회 모드에서도 확실히 버튼을 노출시키기 위해 asset 데이터를 직접 전달
updateMapButtonVisibility(asset);
const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR') || asset.asset_type === '서버PC';
const isPc = asset.category === 'PC' || asset.asset_code?.startsWith('PC');
const isVip = asset.category === '선물' || asset.category === 'VIP';
serverOnly.forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = !isServer ? 'flex' : 'none');
pcOnly.forEach(el => (el as HTMLElement).style.display = isPc ? 'flex' : 'none');
userFields.forEach(el => (el as HTMLElement).style.display = (!isServer && !isVip) ? 'flex' : 'none');
document.querySelectorAll('.server-only').forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none');
document.querySelectorAll('.non-server').forEach(el => (el as HTMLElement).style.display = !isServer ? 'flex' : 'none');
document.querySelectorAll('.pc-only').forEach(el => (el as HTMLElement).style.display = isPc ? 'flex' : 'none');
document.querySelectorAll('.user-tracking-field').forEach(el => (el as HTMLElement).style.display = (!isServer && !isVip) ? 'flex' : 'none');
modal.classList.remove('hidden');
}
@@ -397,17 +629,11 @@ function fillHwFormData(asset: any) {
setFieldValue('hw-asset_code', asset.asset_code || '');
setFieldValue('hw-purchase_corp', asset.purchase_corp || '');
setFieldValue('hw-category', asset.category || '');
// Populate asset_type options based on category
const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement;
const types = CATEGORY_TYPE_MAP[asset.category] || [];
if (typeSelect) {
typeSelect.innerHTML = types.length > 0
? generateOptionsHTML(types, asset.asset_type, true)
: '<option value="">구분을 먼저 선택하세요</option>';
}
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 || '');
@@ -417,7 +643,6 @@ function fillHwFormData(asset: any) {
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 || '');
@@ -431,21 +656,21 @@ function fillHwFormData(asset: any) {
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 || '');
(document.getElementById('hw-approval_document_name') as HTMLElement).textContent = asset.approval_document || '';
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');
renderHwHistory(asset.id);
@@ -455,15 +680,6 @@ function renderHwHistory(assetId: string) {
const container = document.getElementById('hw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId);
if (logs.length === 0) {
container.innerHTML = '<div class="empty-history">이력이 없습니다.</div>';
return;
}
container.innerHTML = logs.map(l => `
<div class="history-item">
<div class="history-date">${l.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details}</div>
</div>
`).join('');
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `<div class=\"history-item\"><div class=\"history-date\">${l.date}</div><div class=\"history-user\">${l.user}</div><div class=\"history-details\">${l.details}</div></div>`).join('');
}