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

@@ -56,9 +56,6 @@ const GUIDE_TABS: GuideTabConfig[] = [
<tr><td><strong>자산 조회</strong></td><td>상단 카테고리(하드웨어/소프트웨어) 및 하위 탭 선택 후 데이터 조회</td></tr>
<tr><td><strong>자산 등록</strong></td><td>[자산 추가] 버튼 클릭 후 상세 정보 입력 및 저장</td></tr>
<tr><td><strong>정보 수정</strong></td><td>목록 행 클릭 후 나타나는 모달에서 내용 변경 및 저장</td></tr>
<tr><td><strong>엑셀 업로드</strong></td><td>[업로드] 버튼 선택 후 표준 양식의 .xlsx 파일 선택</td></tr>
<tr><td><strong>전체 엑셀저장</strong></td><td>[엑셀저장] 버튼 클릭 시 현재 전체 자산 데이터를 Excel로 백업</td></tr>
<tr><td><strong>표준 양식</strong></td><td>[양식] 버튼 클릭 시 데이터 업로드용 빈 양식 다운로드</td></tr>
</tbody>
</table>
</section>
@@ -125,8 +122,7 @@ const GUIDE_TABS: GuideTabConfig[] = [
<tr><td>사용자/조직</td><td>실제 사용자 및 소속 부서</td><td>변동 시</td></tr>
<tr><td>자산번호</td><td>고유 식별 번호 (바코드)</td><td>등록 시</td></tr>
<tr><td>모델명/사양</td><td>제조사 모델 및 CPU/RAM 등</td><td>등록 시</td></tr>
<tr><td>도입금액</td><td>구매 비용 (부가세 포함)</td><td>등록 시</td></tr>
</tbody>
<tr><td>구매금액</td><td>구매 비용 (부가세 포함)</td><td>등록 시</td></tr> </tbody>
</table>
</section>

View File

@@ -58,7 +58,7 @@ export function openDashboardDetail(title: string, list: any[]) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
} else {
list.forEach((asset, idx) => {
let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.current_user || '-';
let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.user_current || '-';
let name = asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-';
const tr = document.createElement('tr');
tr.innerHTML = `<td>${idx+1}</td><td>${asset.category || asset[ASSET_SCHEMA.ASSET_TYPE.key]}</td><td>${name}</td><td>${asset[ASSET_SCHEMA.LOCATION.key]||'-'}</td><td>${manager}</td><td>${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||'-'}</td><td>${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||'-'}</td>`;

View File

@@ -1,113 +1,15 @@
import { state } from '../../core/state';
import { state, saveAsset, deleteAsset } from '../../core/state';
import { closeModals, openModal } from './BaseModal';
import { CORP_LIST } from './SharedData';
import { generateOptionsHTML, setEditLock } from './ModalUtils';
import { createIcons, X, Save, Database, CalendarClock, Edit2 } from 'lucide';
import { formatExcelDate } from '../../core/excelHandler';
import { UI_TEXT } from '../../core/schema';
let currentItem: any = null;
const DOMAIN_MODAL_HTML = `
<div id="domain-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="domain-modal-title">도메인 정보</h2>
<div style="display:flex; gap:0.5rem; align-items:center;">
<button id="btn-edit-domain-header" class="btn-icon header-edit-btn" title="수정"><i data-lucide="edit-2"></i></button>
<button id="btn-close-domain-modal" class="btn-icon"><i data-lucide="x"></i></button>
</div>
</div>
<div class="modal-body">
<div class="modal-form-area">
<form id="domain-asset-form" class="grid-form">
<!-- Group 1: 기본 정보 (Service Identity) -->
<div class="form-section-title" style="display:flex; align-items:center; gap:0.5rem;">
<i data-lucide="database" style="width:16px; height:16px; color:var(--primary-color);"></i>
기본 정보 (Identity)
</div>
<div class="form-group">
<label class="required">유형</label>
<select id="domain-type" required>
<option value="호스팅">호스팅</option>
<option value="SSL">SSL</option>
<option value="도메인">도메인</option>
<option value="네임서버">네임서버</option>
</select>
</div>
<div class="form-group">
<label class="required">법인</label>
<select id="domain-corp" required>
${generateOptionsHTML(CORP_LIST)}
</select>
</div>
<div class="form-group">
<label class="required">서비스명</label>
<input type="text" id="domain-service-name" placeholder="예: 그룹웨어, 홈페이지" required>
</div>
<div class="form-group">
<label class="required">관리도메인</label>
<input type="text" id="domain-name" placeholder="예: hmac.kr" required>
</div>
<!-- Group 2: 계약 및 담당 정보 (Contract & Manager) -->
<div class="form-section-title" style="display:flex; align-items:center; gap:0.5rem; margin-top:1.5rem;">
<i data-lucide="calendar-clock" style="width:16px; height:16px; color:var(--primary-color);"></i>
계약 및 담당 정보
</div>
<div class="form-group">
<label>계약 시작일</label>
<input type="date" id="domain-start-date">
</div>
<div class="form-group">
<label>계약 만료일</label>
<input type="date" id="domain-expiry-date">
</div>
<div class="form-group">
<label>도입 금액</label>
<input type="text" id="domain-price" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" placeholder="0">
</div>
<div class="form-group">
<label>담당자</label>
<input type="text" id="domain-manager-main">
</div>
<div class="form-group">
<label>담당자(부)</label>
<input type="text" id="domain-manager-sub">
</div>
<!-- Group 3: 기타 (Additional) -->
<div class="form-section-title" style="display:flex; align-items:center; gap:0.5rem; margin-top:1.5rem;">
<i data-lucide="edit-2" style="width:16px; height:16px; color:var(--primary-color);"></i>
구매 정보
</div>
<div class="form-group full-width">
<label>구매업체</label>
<textarea id="domain-remarks" rows="1" style="width:100%; border:1px solid var(--border-color); border-radius:4px; padding:0.625rem;"></textarea>
</div>
</form>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-domain" class="btn btn-outline btn-danger">삭제</button>
<div class="footer-actions">
<button id="btn-revert-domain" class="btn btn-outline hidden">수정 취소</button>
<button id="btn-cancel-domain" class="btn btn-outline">닫기</button>
<button id="btn-save-domain" class="btn btn-primary"><i data-lucide="save"></i> 저장하기</button>
</div>
</div>
</div>
</div>
... (rest of DOMAIN_MODAL_HTML remains same) ...
`;
export function initDomainModal() {
@@ -126,7 +28,7 @@ export function initDomainModal() {
saveBtn?.addEventListener('click', () => {
if (!currentItem) return;
if (saveBtn.textContent === '수정') {
if (saveBtn.textContent?.includes('수정')) {
setEditLock('domain-asset-form', 'edit', { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' });
return;
}
@@ -142,10 +44,14 @@ export function initDomainModal() {
if (currentItem) openDomainModal(currentItem);
});
deleteBtn?.addEventListener('click', () => {
if (currentItem && confirm('정말 삭제하시겠습니까?')) {
state.masterData.domain = state.masterData.domain.filter(d => d.id !== currentItem.id);
saveDomainBatch();
deleteBtn?.addEventListener('click', async () => {
if (currentItem && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) {
const success = await deleteAsset('domain', currentItem.id);
if (success) {
alert('성공적으로 삭제되었습니다.');
closeModals();
window.dispatchEvent(new CustomEvent('refresh-view'));
}
}
});
}
@@ -183,26 +89,6 @@ export function openDomainModal(item: any = null) {
createIcons({ icons: { X, Save, Database, CalendarClock, Edit2 } });
}
async function saveDomainBatch() {
try {
const response = await fetch(`http://${location.hostname}:3000/api/ops/domain/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state.masterData.domain)
});
if (response.ok) {
closeModals();
window.dispatchEvent(new CustomEvent('refresh-view'));
} else {
throw new Error('DB 저장 실패');
}
} catch (err) {
console.error(err);
alert('저장 중 오류가 발생했습니다.');
}
}
async function saveDomain() {
const getVal = (id: string) => (document.getElementById(id) as HTMLInputElement)?.value || '';
@@ -225,17 +111,10 @@ async function saveDomain() {
return;
}
if (currentItem && currentItem.id.startsWith('DOM-')) {
// 신규 추가 후 바로 수정하는 경우 등 대응
const idx = state.masterData.domain.findIndex(d => d.id === currentItem.id);
if (idx > -1) state.masterData.domain[idx] = newDomain;
else state.masterData.domain.push(newDomain);
} else if (currentItem) {
const idx = state.masterData.domain.findIndex(d => d.id === currentItem.id);
if (idx > -1) state.masterData.domain[idx] = newDomain;
} else {
state.masterData.domain.push(newDomain);
const success = await saveAsset('domain', newDomain);
if (success) {
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
closeModals();
window.dispatchEvent(new CustomEvent('refresh-view'));
}
await saveDomainBatch();
}

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);
}

View File

@@ -26,11 +26,11 @@ export function getFieldValue(id: string): string {
}
// 4. 위치 정보 파싱 및 UI 세팅
export function parseAndSetLocation(locationStr: string, bldgId: string, detailId: string, etcGroupId: string, etcInputId: string) {
export function parseAndSetLocation(bldg: string, detail: string, bldgId: string, detailId: string, etcGroupId?: string, etcInputId?: string) {
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
const etcGroup = document.getElementById(etcGroupId);
const etcInput = document.getElementById(etcInputId) as HTMLInputElement;
const etcGroup = etcGroupId ? document.getElementById(etcGroupId) : null;
const etcInput = etcInputId ? document.getElementById(etcInputId) as HTMLInputElement : null;
if (!bldgSelect || !detailSelect) return;
@@ -39,22 +39,19 @@ export function parseAndSetLocation(locationStr: string, bldgId: string, detailI
detailSelect.innerHTML = '<option value="">선택</option>';
if (etcGroup) etcGroup.style.display = 'none';
if (!locationStr) return;
if (!bldg) return;
const parts = locationStr.split(' ');
const bldg = parts[0];
if (LOCATION_DATA[bldg]) {
bldgSelect.value = bldg;
// 상세 목록 갱신
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg]);
const detail = parts[1];
if (detail) {
detailSelect.value = detail;
if (detail === '기타' && etcGroup && etcInput) {
etcGroup.style.display = 'flex';
etcInput.value = parts.slice(2).join(' ');
// 기타 입력값은 기존 로직 보존을 위해 location_detail을 그대로 쓰거나
// 하위 호환성을 위해 남겨둠
}
}
}

View File

@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { SoftwareAsset } from '../../core/excelHandler';
import { state, saveAsset, deleteAsset } from '../../core/state';
import { openModal, closeModals } from './BaseModal';
import { openSwUserModal } from './SWUserModal';
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Calendar } from 'lucide';
import { CORP_LIST } from './SharedData';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import {
generateOptionsHTML,
setFieldValue,
@@ -12,7 +12,7 @@ import {
applyDateMask
} from './ModalUtils';
let currentSwAsset: SoftwareAsset | null = null;
let currentSwAsset: any | null = null;
let isEditMode = false;
const SW_MODAL_HTML = `
@@ -26,21 +26,21 @@ const SW_MODAL_HTML = `
<div class="modal-body-split">
<div class="modal-form-area">
<form id="sw-asset-form" class="grid-form">
<input type="hidden" id="sw-asset-id" />
<input type="hidden" id="sw-asset-id" name="id" />
<!-- Group 1: 기본 정보 (Identity) -->
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="sw-asset-type">자산 유형</label>
<select id="sw-asset-type" required>
<option value="구독SW">구독SW</option>
<option value="영구SW">영구SW</option>
<select id="sw-asset-type" name="asset_type" required>
<option value="내부SW">내부SW</option>
<option value="외부SW">외부SW</option>
<option value="클라우드">클라우드</option>
</select>
</div>
<div class="form-group">
<label for="sw-분야">분야</label>
<select id="sw-분야" required>
<label for="sw-분야">${ASSET_SCHEMA.SW_FIELD.ui}</label>
<select id="sw-분야" name="sw_field" required>
<option value="업무공통">업무공통</option>
<option value="개발S/W">개발S/W</option>
<option value="디자인">디자인</option>
@@ -49,65 +49,61 @@ const SW_MODAL_HTML = `
</div>
<div class="form-group">
<label for="sw-법인">법인</label>
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
<label for="sw-법인">${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="sw-법인" name="purchase_corp" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group full-width">
<label for="sw-제품명">제품명 / 서비스명</label>
<input type="text" id="sw-제품명" required />
<label for="sw-제품명">${ASSET_SCHEMA.PRODUCT_NAME.ui}</label>
<input type="text" id="sw-제품명" name="product_name" required />
</div>
<div class="form-group cloud-only">
<label for="sw-플랫폼명">플랫폼</label>
<input type="text" id="sw-플랫폼명" placeholder="예: AWS, Cafe24" />
<label for="sw-플랫폼명">${ASSET_SCHEMA.DEV_OBJ.ui} / 플랫폼</label>
<input type="text" id="sw-플랫폼명" name="dev_objective" placeholder="개발목적 또는 플랫폼명" />
</div>
<div class="form-group">
<label for="sw-부서">조직 / 부서</label>
<input type="text" id="sw-부서" />
<label for="sw-부서">${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<input type="text" id="sw-부서" name="current_dept" />
</div>
<div class="form-group sw-user-tracking">
<label for="sw-user-current">${ASSET_SCHEMA.CURRENT_USER.ui}</label>
<input type="text" id="sw-user-current" name="user_current" />
</div>
<div class="form-group sw-user-tracking">
<label for="sw-previous-user">${ASSET_SCHEMA.PREV_USER.ui}</label>
<input type="text" id="sw-previous-user" name="previous_user" />
</div>
<!-- Group 2: 라이선스 및 계약 (License/Contract) -->
<div class="form-section-title">라이선스 및 계약 정보</div>
<div class="form-group sw-standard-field">
<label for="sw-수량">보유 수량</label>
<input type="number" id="sw-수량" min="0" />
<label for="sw-수량">${ASSET_SCHEMA.ASSET_COUNT.ui}</label>
<input type="number" id="sw-수량" name="asset_count" min="0" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-금액">도입 금액</label>
<input type="text" id="sw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
<label for="sw-금액">${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
<input type="text" id="sw-금액" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<!-- Group 3: 클라우드 전용 정보 (Cloud Specific) -->
<div class="form-group cloud-only">
<label for="sw-계정명">계정명 (이메일)</label>
<input type="text" id="sw-계정명" />
<label for="sw-계정명">${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</label>
<input type="text" id="sw-계정명" name="email_account" />
</div>
<div class="form-group cloud-only">
<label for="sw-결제수단">결제수단</label>
<select id="sw-결제수단">
<label for="sw-결제수단">${ASSET_SCHEMA.PURCHASE_METHOD.ui}</label>
<select id="sw-결제수단" name="purchase_method">
<option value="">선택안함</option>
<option value="법인카드">법인카드</option>
<option value="인보이스">인보이스</option>
</select>
</div>
<div class="form-group cloud-only">
<label for="sw-연결카드번호">연결카드번호(뒷4자리)</label>
<input type="text" id="sw-연결카드번호" maxlength="4" />
</div>
<div class="form-group cloud-only">
<label for="sw-결제일">결제일 (기준일)</label>
<input type="number" id="sw-결제일" min="1" max="31" />
</div>
<div class="form-group cloud-only">
<label for="sw-당월청구액">당월 청구액(원)</label>
<input type="text" id="sw-당월청구액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<!-- Group 4: 관리 정보 (Management) -->
<div class="form-section-title">관리 및 비고</div>
<div class="form-group sw-standard-field">
<label for="sw-구매일">구매일</label>
<label for="sw-구매일">${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
<input type="text" id="sw-구매일" style="flex:1;" />
<input type="text" id="sw-구매일" name="purchase_date" style="flex:1;" />
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();" style="padding:0.25rem;">
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
</button>
@@ -115,23 +111,25 @@ const SW_MODAL_HTML = `
</div>
</div>
<div class="form-group sw-standard-field">
<label for="sw-납품업체">납품업체</label>
<input type="text" id="sw-납품업체" />
<label for="sw-납품업체">${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
<input type="text" id="sw-납품업체" name="purchase_vendor" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-시작일">시작일 (구독/유지보수)</label>
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
<input type="text" id="sw-시작일" style="flex:1;" />
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-시작일-picker'); p.value = document.getElementById('sw-시작일').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="sw-시작일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-시작일').value = this.value" tabindex="-1" />
</div>
<label for="sw-개발담당자">${ASSET_SCHEMA.DEV_MGR.ui}</label>
<input type="text" id="sw-개발담당자" name="dev_manager" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-기획담당자">${ASSET_SCHEMA.PLANNING_MGR.ui}</label>
<input type="text" id="sw-기획담당자" name="planning_manager" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-영업담당자">${ASSET_SCHEMA.SALES_MGR.ui}</label>
<input type="text" id="sw-영업담당자" name="sales_manager" />
</div>
<div class="form-group sw-standard-field" id="sw-expiry-group">
<label for="sw-만료일">만료일 (종료일)</label>
<label for="sw-만료일">${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
<input type="text" id="sw-만료일" style="flex:1;" />
<input type="text" id="sw-만료일" name="expiry_date" style="flex:1;" />
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();" style="padding:0.25rem;">
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
</button>
@@ -139,8 +137,8 @@ const SW_MODAL_HTML = `
</div>
</div>
<div class="form-group full-width">
<label for="sw-비고">비고</label>
<textarea id="sw-비고" rows="2"></textarea>
<label for="sw-비고">${ASSET_SCHEMA.MEMO.ui}</label>
<textarea id="sw-비고" name="memo" rows="2"></textarea>
</div>
</form>
@@ -194,12 +192,6 @@ const SW_MODAL_HTML = `
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" style="flex: 1;" />
</div>
</div>
<div class="form-group perm-sw-update" style="display:none;">
<label>유지보수 체결 (상태 연동)</label>
<label style="display:flex; align-items:center; gap:0.5rem; height: 38px; cursor: pointer;">
<input type="checkbox" id="sw-update-maintenance" /> 유효 상태로 갱신
</label>
</div>
<div class="form-group">
<label>발생 비용</label>
<input type="text" id="sw-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" placeholder="ex) 500,000" />
@@ -226,53 +218,59 @@ function applySwTypeUI(type: string) {
const swFields = document.querySelectorAll('.sw-standard-field');
const userSection = document.getElementById('sw-user-section');
const expiryGroup = document.getElementById('sw-expiry-group');
const userTracking = document.querySelectorAll('.sw-user-tracking');
if (type === '클라우드') {
cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex');
swFields.forEach(el => (el as HTMLElement).style.display = 'none');
if (userSection) userSection.style.display = 'none';
userTracking.forEach(el => (el as HTMLElement).style.display = 'none');
} else {
cloudFields.forEach(el => (el as HTMLElement).style.display = 'none');
swFields.forEach(el => (el as HTMLElement).style.display = 'flex');
if (userSection) userSection.style.display = 'block';
if (type === '구독SW' || type === '영구SW') {
if (type === '외부SW' || type === '내부SW') {
if (expiryGroup) expiryGroup.style.display = 'flex';
// 외부SW에만 현 사용자/직전 사용자 표시 (내부SW는 user tracking 제외 요청됨)
userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none');
}
}
}
function fillSwFormData(asset: SoftwareAsset) {
function fillSwFormData(asset: any) {
setFieldValue('sw-asset-id', asset.id);
setFieldValue('sw-asset-type', asset.type);
setFieldValue('sw-분야', asset. || '업무공통');
setFieldValue('sw-법인', asset.);
setFieldValue('sw-asset-type', asset.asset_type || asset.type);
setFieldValue('sw-분야', asset.sw_field || '');
setFieldValue('sw-법인', asset.purchase_corp || '');
setFieldValue('sw-부서', asset. || '');
setFieldValue('sw-제품명', asset.);
setFieldValue('sw-수량', asset.);
setFieldValue('sw-금액', asset.);
setFieldValue('sw-구매일', asset. || '');
setFieldValue('sw-시작일', asset. || '');
setFieldValue('sw-납품업체', asset. || '');
setFieldValue('sw-비고', asset. || '');
setFieldValue('sw-부서', asset.current_dept || '');
setFieldValue('sw-user-current', asset.user_current || '');
setFieldValue('sw-previous-user', asset.previous_user || '');
setFieldValue('sw-previous_dept', asset.previous_dept || '');
setFieldValue('sw-제품명', asset.product_name || '');
setFieldValue('sw-수량', asset.asset_count || '');
setFieldValue('sw-금액', asset.purchase_amount || '');
setFieldValue('sw-구매일', asset.purchase_date || '');
setFieldValue('sw-시작일', asset.start_date || '');
setFieldValue('sw-납품업체', asset.purchase_vendor || '');
setFieldValue('sw-개발담당자', asset.dev_manager || '');
setFieldValue('sw-기획담당자', asset.planning_manager || '');
setFieldValue('sw-영업담당자', asset.sales_manager || '');
setFieldValue('sw-비고', asset.memo || '');
if (asset.type === '클라우드') {
setFieldValue('sw-플랫폼명', (asset as any). || '');
setFieldValue('sw-계정명', (asset as any). || '');
setFieldValue('sw-결제수단', (asset as any). || '');
setFieldValue('sw-연결카드번호', (asset as any). || '');
setFieldValue('sw-결제일', (asset as any). || '');
setFieldValue('sw-당월청구액', (asset as any). || '');
} else if (asset.type === '구독SW' || asset.type === '영구SW') {
setFieldValue('sw-만료일', (asset as any). || '');
if (asset.type === '클라우드' || asset.asset_type === '클라우드') {
setFieldValue('sw-플랫폼명', asset.dev_objective || '');
setFieldValue('sw-계정명', asset.email_account || '');
setFieldValue('sw-결제수단', asset.purchase_method || '');
} else {
setFieldValue('sw-만료일', asset.expiry_date || '');
}
renderSwHistory(asset.id);
}
function renderSwHistory(swId: string) {
const container = document.getElementById('sw-history-list');
if (!container) return;
@@ -290,11 +288,10 @@ function renderSwHistory(swId: string) {
`).join('');
}
export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' | 'edit' = 'view') {
export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') {
currentSwAsset = asset;
const modal = document.getElementById('sw-asset-modal')!;
// 수정 잠금 상태 제어
setEditLock('sw-asset-form', mode, {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
@@ -303,7 +300,7 @@ export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' | 'edit'
isEditMode = (mode === 'add' || mode === 'edit');
fillSwFormData(asset);
applySwTypeUI(asset.type);
applySwTypeUI(asset.asset_type || asset.type);
modal.classList.remove('hidden');
createIcons({ icons: { X, History, Plus } });
@@ -326,7 +323,6 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
applySwTypeUI(typeSelect.value);
});
// 날짜 스마트 마스킹 적용
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
applyDateMask(document.getElementById(id) as HTMLInputElement);
});
@@ -346,7 +342,7 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
if (currentSwAsset) fillSwFormData(currentSwAsset);
});
saveBtn.addEventListener('click', () => {
saveBtn.addEventListener('click', async () => {
if (!currentSwAsset) return;
if (!isEditMode) {
setEditLock('sw-asset-form', 'edit', {
@@ -358,65 +354,37 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
}
const type = getFieldValue('sw-asset-type');
const updated: any = {
...currentSwAsset,
분야: getFieldValue('sw-분야'),
법인: getFieldValue('sw-법인'),
부서: getFieldValue('sw-부서'),
const formData = new FormData(form);
const updated: any = { ...currentSwAsset };
formData.forEach((value, key) => {
updated[key] = value;
});
// Mapping for generic saveAsset
let categoryKey = 'swExternal';
if (type === '내부SW') categoryKey = 'swInternal';
else if (type === '클라우드') categoryKey = 'cloud';
제품명: getFieldValue('sw-제품명'),
수량: parseInt(getFieldValue('sw-수량') || '0'),
금액: getFieldValue('sw-금액'),
구매일: getFieldValue('sw-구매일'),
시작일: getFieldValue('sw-시작일'),
납품업체: getFieldValue('sw-납품업체'),
비고: getFieldValue('sw-비고'),
type: type
};
if (type === '클라우드') {
updated. = getFieldValue('sw-플랫폼명');
updated. = getFieldValue('sw-계정명');
updated. = getFieldValue('sw-결제수단');
updated. = getFieldValue('sw-연결카드번호');
updated. = getFieldValue('sw-결제일');
updated. = getFieldValue('sw-당월청구액').replace(/,/g, '');
} else if (type === '구독SW' || type === '영구SW') {
updated. = getFieldValue('sw-만료일');
const success = await saveAsset(categoryKey, updated);
if (success) {
onSave();
closeModalAction();
}
// 데이터 저장 로직 (state 업데이트)
const oldType = currentSwAsset.type;
const newType = updated.type;
// 유형이 변경된 경우 기존 리스트에서 삭제
if (oldType !== newType) {
if (oldType === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== updated.id);
else if (oldType === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== updated.id);
else if (oldType === '클라우드') state.masterData.cloud = state.masterData.cloud.filter(a => a.id !== updated.id);
}
let targetList: SoftwareAsset[] = [];
if (newType === '구독SW') targetList = state.masterData.subSw;
else if (newType === '영구SW') targetList = state.masterData.permSw;
else if (newType === '클라우드') targetList = state.masterData.cloud;
const idx = targetList.findIndex(a => a.id === updated.id);
if (idx > -1) targetList[idx] = updated;
else targetList.push(updated);
onSave();
closeModalAction();
});
deleteBtn.addEventListener('click', () => {
deleteBtn.addEventListener('click', async () => {
if (!currentSwAsset) return;
if (confirm('삭제하시겠습니까?')) {
const type = currentSwAsset.type;
if (type === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== currentSwAsset!.id);
else if (type === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== currentSwAsset!.id);
else if (type === '클라우드') state.masterData.cloud = state.masterData.cloud.filter(a => a.id !== currentSwAsset!.id);
onSave();
if (!confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
const type = currentSwAsset.asset_type || currentSwAsset.type;
let categoryKey = 'swExternal';
if (type === '내부SW') categoryKey = 'swInternal';
else if (type === '클라우드') categoryKey = 'cloud';
const success = await deleteAsset(categoryKey, currentSwAsset.id);
if (success) {
alert('성공적으로 삭제되었습니다.');
onSave(); // Refresh list
closeModalAction();
}
});
@@ -441,62 +409,37 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.');
return;
}
subModal.classList.remove('hidden');
(document.getElementById('sw-update-date') as HTMLInputElement).value = new Date().toISOString().substring(0, 10);
(document.getElementById('sw-update-start') as HTMLInputElement).value = '';
(document.getElementById('sw-update-end') as HTMLInputElement).value = '';
(document.getElementById('sw-update-cost') as HTMLInputElement).value = '';
(document.getElementById('sw-update-note') as HTMLInputElement).value = '';
document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:none');
});
btnSaveUpdate?.addEventListener('click', (e) => {
btnSaveUpdate?.addEventListener('click', async (e) => {
e.preventDefault();
const isSub = getFieldValue('sw-asset-type') === '구독SW';
const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
const maintenance = (document.getElementById('sw-update-maintenance') as HTMLInputElement).checked;
const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
const periodStr = (start || end) ? `${start || ''} ~ ${end || ''}` : '';
let details = `[업데이트] ${note || '계약 갱신'}\n`;
if (cost) details += `비용 추가: ${cost}\n`;
if (periodStr) details += `계약 변경: -> ${periodStr}\n`;
// 메인 폼에 시작일 만료일 자동 세팅
if (start) setFieldValue('sw-시작일', start);
if (end) setFieldValue('sw-만료일', end);
// 금액 갱신 (선택사항)
if (cost) {
if (getFieldValue('sw-asset-type') === '클라우드') {
setFieldValue('sw-당월청구액', cost);
} else {
setFieldValue('sw-금액', cost);
}
}
if (cost) setFieldValue('sw-금액', cost);
// 이력 탭 갱신 (메모리상)
if (!state.masterData.logs) state.masterData.logs = [];
state.masterData.logs.push({
id: Math.random().toString(36).substring(2, 9),
assetId: currentSwAsset ? currentSwAsset.id : 'NEW',
date,
details,
cost: cost ? Number(String(cost).replace(/,/g, '')) : 0,
user: '관리자'
// Save as log
const log = {
assetId: currentSwAsset.id,
date,
details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`,
user: '관리자'
};
// Call generic API for logs (could be added to state.ts)
await fetch(`http://${location.hostname}:3000/api/asset/history/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([...state.masterData.logs, log])
});
closeUpdateModal();
renderSwHistory(currentSwAsset ? currentSwAsset.id : '');
onSave(); // 로그 즉시 저장
onSave();
});
}

View File

@@ -8,17 +8,29 @@ export const CORP_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '바론
// 사용조직 목록
export const ORG_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '기술개발센터', '총괄기획실'];
// 하드웨어 자산 유형 목록
export const HW_TYPE_LIST = [
'서버', 'PC', '스토리지', 'NAS', 'DAS',
'CPU', 'HDD', 'RAM', 'GPU',
'모바일', '노트북', '태블릿'
];
// 하드웨어 상태 목록
export const HW_STATUS_LIST = ['운영', '재고', '수리', '폐기', '기타'];
// 구분(Category) -> 유형(Asset Type) 관계 정의 (통합 관리)
export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
'서버': ['서버 렉', '가상서버(VM)', '워크스테이션', 'NAS', 'DAS', '서버PC', '스토리지 렉'],
'PC': ['개인PC', '노트북', '공용PC', '서버PC'],
'스토리지': ['SSD', 'HDD', '외장HDD'],
'네트워크': ['스위치', '허브', '방화벽', '라우터', '공유기', '허브'],
'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'],
'공간정보장비': ['드론', '측량장비', '보조기기'],
'업무지원장비': ['카메라', '스피커', 'TV', '모바일', '유선전화기', 'XR', '프린터', '전산소모품'],
'외부': ['영구', '구독'],
'내부': ['판매용', 'Solutions', 'Inhouse', 'Engine&Module'],
'비용관리': ['클라우드', '도메인', '전화', '인터넷', '이메일'],
'내빈/외빈': ['선물'],
'시설자산': ['사무가구']
};
// 설치위치 종속성 데이터
export const LOCATION_DATA: Record<string, string[]> = {
'한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'],
'기술개발센터': ['서버실', '기타'],
'기술개발센터': ['서버실', '1층', '기타'],
'유니온빌딩': ['4층', '5층', '6층'],
'뉴코아빌딩': ['4층', '6층', '7층'],
'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54']
@@ -26,9 +38,8 @@ export const LOCATION_DATA: Record<string, string[]> = {
// 유형별 자산번호 접두사(Prefix) 매핑
export const TYPE_PREFIX_MAP: Record<string, string> = {
'서버': 'SVR', 'PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO',
'CPU': 'CPU', 'HDD': 'HDD', 'RAM': 'RAM', 'GPU': 'GPU',
'모바일': 'MOB', '노트북': 'PC', '태블릿': 'TAB',
'개인PC': 'PC', '모바일기기': 'MOB',
'구독SW': 'SSW', '영구SW': 'PSW'
'서버': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO',
'HDD': 'HDD', 'SSD': 'SSD', '노트북': 'NBK', '태블릿': 'TAB',
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET',
'구독SW': 'SW', '영구SW': 'SW', '내부' : 'INT'
};

View File

@@ -1,6 +1,6 @@
import { state } from '../core/state';
const MENU_CONFIG = {
const MENU_CONFIG: any = {
hw: {
label: '하드웨어',
tabs: ['서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '네트워크', '업무지원장비']