feat: migrate ServerPC data to asset_pc, enhance filters with location, and standardize page headers

- 서버PC 자산을 asset_pc 테이블로 통합 마이그레이션 및 스키마 확장 (위치, IP 정보 복구 완료)

- 하드웨어 자산 페이지의 구매법인 필터를 자산위치 필터로 교체 및 동적 데이터 바인딩 적용

- 모든 자산 리스트 페이지 상단에 설명(Description) 필드 추가 및 헤더 표준화

- 상세 모달 내 삭제 버튼 기능 구현 및 서버PC 용도 필드 노출 오류 수정

- 현 사용조직 필터 리스트가 비어있던 DOM 셀렉터 버그 수정
This commit is contained in:
2026-05-26 17:33:03 +09:00
parent d34ebb8500
commit 82bbe85e23
43 changed files with 2055 additions and 1871 deletions

View File

@@ -1,4 +1,4 @@
import { state, saveAsset } from '../../core/state';
import { state, saveAsset, deleteAsset } from '../../core/state';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import {
generateOptionsHTML,
@@ -10,7 +10,7 @@ import {
getCombinedLocation,
applyDateMask
} from './ModalUtils';
import { CORP_LIST, LOCATION_DATA, ORG_LIST } from './SharedData';
import { CORP_LIST, LOCATION_DATA, ORG_LIST, CATEGORY_TYPE_MAP, HW_STATUS_LIST } from './SharedData';
import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide';
let currentHwAsset: any | null = null;
@@ -44,16 +44,20 @@ const HW_MODAL_HTML = `
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.CATEGORY.ui}</label>
<input type="text" id="hw-category" name="category" />
<select id="hw-category" name="category">
<option value="">선택</option>
${generateOptionsHTML(Object.keys(CATEGORY_TYPE_MAP), '', false)}
</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
<select id="hw-asset_type" name="asset_type">
<option value="">구분을 먼저 선택하세요</option>
</select>
</div>
<div class="form-group">
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
<select id="hw-hw_status" name="hw_status">
<option value="운영">운영</option>
<option value="재고">재고</option>
<option value="수리">수리</option>
<option value="폐기">폐기</option>
</select>
<select id="hw-hw_status" name="hw_status">${generateOptionsHTML(HW_STATUS_LIST)}</select>
</div>
<div class="form-group dept-field">
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
@@ -71,11 +75,15 @@ const HW_MODAL_HTML = `
<label>${ASSET_SCHEMA.MANAGER_SUB.ui}</label>
<input type="text" id="hw-manager_secondary" name="manager_secondary" />
</div>
<div class="form-group pc-only">
<div class="form-group user-tracking-field">
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
<input type="text" id="hw-current_user" name="current_user" />
<input type="text" id="hw-user_current" name="user_current" />
</div>
<div class="form-group pc-only">
<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>
@@ -88,15 +96,13 @@ const HW_MODAL_HTML = `
<div class="form-section-title">설치 위치</div>
<div class="form-group">
<label>건물/위치</label>
<select id="hw-bldg-select">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
<select id="hw-bldg-select" name="location">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
</div>
<div class="form-group">
<label>상세 위치(층/구역)</label>
<select id="hw-floor-select"><option value="">선택</option></select>
</div>
<div class="form-group full-width" id="hw-loc-etc-group" style="display:none;">
<label>기타 상세 위치</label>
<input type="text" id="hw-loc-etc" placeholder="직접 입력" />
<label>${ASSET_SCHEMA.LOC_DETAIL.ui}</label>
<select id="hw-location_detail" name="location_detail">
<option value="">선택</option>
</select>
</div>
<!-- Group 3: 시스템 사양 -->
@@ -250,13 +256,52 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
const btnCloseHeader = document.getElementById('btn-close-hw-modal')!;
const btnCancelFooter = document.getElementById('btn-cancel-hw-modal')!;
bindLocationEvents('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
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;
categorySelect.addEventListener('change', () => {
const selectedCat = categorySelect.value;
const types = CATEGORY_TYPE_MAP[selectedCat] || [];
typeSelect.innerHTML = types.length > 0
? generateOptionsHTML(types, '', true)
: '<option value="">구분을 먼저 선택하세요</option>';
});
const closeModalAction = () => { closeModals(); isEditMode = false; };
btnCloseHeader.addEventListener('click', closeModalAction);
btnCancelFooter.addEventListener('click', closeModalAction);
deleteBtn.addEventListener('click', async () => {
if (!currentHwAsset) return;
if (!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';
else if (cat === '서버' || code.startsWith('SVR')) categoryKey = 'server';
else if (cat === '스토리지' || code.startsWith('STO')) categoryKey = 'storage';
else if (cat === '네트워크' || code.startsWith('NET')) categoryKey = 'network';
else if (cat === '업무지원장비' || code.startsWith('EQP')) categoryKey = 'equipment';
else if (cat === '공간정보장비') categoryKey = 'survey';
else if (cat === 'PC부품') categoryKey = 'pcParts';
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();
}
});
revertBtn.addEventListener('click', () => {
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = false;
@@ -277,13 +322,28 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
if (key !== 'id') updated[key] = value;
});
// Handle combined location
updated.location = getCombinedLocation('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc');
// Handle location columns:
// 'location' stores only the building name
// 'location_detail' is already handled via the dynamic FormData loop
updated.location = getFieldValue('hw-bldg-select');
let categoryKey = 'pc';
if (updated.asset_code?.startsWith('SVR')) categoryKey = 'server';
else if (updated.asset_code?.startsWith('STO')) categoryKey = 'storage';
else if (updated.asset_code?.startsWith('EQP')) categoryKey = 'equipment';
// 서버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';
}
const success = await saveAsset(categoryKey, updated);
if (success) {
@@ -314,13 +374,16 @@ export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view')
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');
const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR');
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');
modal.classList.remove('hidden');
}
@@ -330,12 +393,24 @@ 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>';
}
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-current_user', asset.current_user || '');
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 || '');
@@ -365,8 +440,9 @@ function fillHwFormData(asset: any) {
(document.getElementById('hw-approval_document_name') as HTMLElement).textContent = asset.approval_document || '';
setFieldValue('hw-memo', asset.memo || '');
setFieldValue('hw-location_detail', asset.location_detail || '');
parseAndSetLocation(asset.location || '', 'hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc');
parseAndSetLocation(asset.location || '', asset.location_detail || '', 'hw-bldg-select', 'hw-location_detail');
renderHwHistory(asset.id);
}