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:
@@ -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>`;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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을 그대로 쓰거나
|
||||
// 하위 호환성을 위해 남겨둠
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user