feat: DB V3 정규화 및 용도 기반 동적 UI 구현

- 백엔드: asset_core(마스터), asset_spec(사양), asset_volume(스토리지), asset_location(위치), asset_network(네트워크/원격) 5개 테이블로 V3 정규화 완료

- 백엔드: /api/assets/master 단일 엔드포인트로 통합 및 서브쿼리 최적화를 통한 UI 하위 호환성 유지

- 백엔드: 저장 로직(save) V3 스키마 분산 저장 및 cascade 기반 삭제 로직 적용

- 프론트엔드(HWModal): '현 용도(current_role)' 필드 추가 및 서버/개인용에 따른 네트워크/위치 섹션 동적 렌더링 구현

- 프론트엔드(state): 분산된 API 호출을 단일 호출로 통합하여 렌더링 성능 최적화

- 레거시 백업 파일 및 불필요한 구형 테이블 완벽 정리 완료
This commit is contained in:
2026-06-08 17:58:48 +09:00
parent 3b9b2ea598
commit 3ab587d342
4 changed files with 303 additions and 296 deletions

View File

@@ -359,6 +359,7 @@ 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;
@@ -408,19 +409,50 @@ class HwAssetModal extends BaseModal {
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');
}
protected onAfterOpen(asset: any, mode: string): void {
this.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';
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);
});
}
private applyRoleVisibility(role: string): void {
const isServer = role === 'Server';
const isPersonal = role === 'Personal';
// 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;
// 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;
}
};
// 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('.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');
document.querySelectorAll('.pc-only').forEach(el => (el as HTMLElement).style.display = isPersonal ? 'flex' : 'none');
}
private updateMapButtonVisibility(asset?: any) {