399 lines
20 KiB
TypeScript
399 lines
20 KiB
TypeScript
import { state, saveAsset, deleteAsset } from '../../core/state';
|
|
import { BaseModal } from './BaseModal';
|
|
import { openSwUserModal } from './SWUserModal';
|
|
import { createIcons, History, Plus, X, Save, RotateCcw, Calendar, Users } from 'lucide';
|
|
import { CORP_LIST } from './SharedData';
|
|
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
|
import { API_BASE_URL } from '../../core/utils';
|
|
import {
|
|
generateOptionsHTML,
|
|
setFieldValue,
|
|
getFieldValue,
|
|
applyDateMask
|
|
} from './ModalUtils';
|
|
|
|
class SwAssetModal extends BaseModal {
|
|
constructor() {
|
|
super('sw', '소프트웨어 상세 정보');
|
|
}
|
|
|
|
protected renderFrameHTML(): string {
|
|
return `
|
|
<div id="sw-asset-modal" class="modal-overlay hidden">
|
|
<div class="modal-content wide">
|
|
<div class="modal-header">
|
|
<div class="header-left">
|
|
<h2 id="sw-modal-title" class="modal-title">${this.title}</h2>
|
|
<div id="sw-header-identity" class="header-identity"></div>
|
|
</div>
|
|
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<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" name="id" />
|
|
|
|
<div class="form-section-title">기본 정보 (Identity)</div>
|
|
<div class="form-group">
|
|
<label>자산 유형</label>
|
|
<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>${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>
|
|
<option value="설계S/W">설계S/W</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>${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>${ASSET_SCHEMA.PRODUCT_NAME.ui}</label>
|
|
<input type="text" id="sw-제품명" name="product_name" required />
|
|
</div>
|
|
<div class="form-group cloud-only">
|
|
<label>${ASSET_SCHEMA.DEV_OBJ.ui} / 플랫폼</label>
|
|
<input type="text" id="sw-플랫폼명" name="dev_objective" placeholder="개발목적 또는 플랫폼명" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
|
<input type="text" id="sw-부서" name="current_dept" />
|
|
</div>
|
|
<div class="form-group sw-user-tracking">
|
|
<label>${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>${ASSET_SCHEMA.PREV_USER.ui}</label>
|
|
<input type="text" id="sw-previous-user" name="previous_user" />
|
|
</div>
|
|
|
|
<div class="form-section-title">라이선스 및 계약 정보</div>
|
|
<div class="form-group sw-standard-field">
|
|
<label>${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>${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>
|
|
|
|
<div class="form-group cloud-only">
|
|
<label>${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</label>
|
|
<input type="text" id="sw-계정명" name="email_account" />
|
|
</div>
|
|
<div class="form-group cloud-only">
|
|
<label>${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-section-title">관리 및 비고</div>
|
|
<div class="form-group sw-standard-field">
|
|
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="sw-구매일" name="purchase_date" />
|
|
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();">
|
|
<i data-lucide="calendar"></i>
|
|
</button>
|
|
<input type="date" id="sw-구매일-picker" class="hidden-picker" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group sw-standard-field">
|
|
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
|
<input type="text" id="sw-납품업체" name="purchase_vendor" />
|
|
</div>
|
|
<div class="form-group sw-standard-field">
|
|
<label>${ASSET_SCHEMA.DEV_MGR.ui}</label>
|
|
<input type="text" id="sw-개발담당자" name="dev_manager" />
|
|
</div>
|
|
<div class="form-group sw-standard-field">
|
|
<label>${ASSET_SCHEMA.PLANNING_MGR.ui}</label>
|
|
<input type="text" id="sw-기획담당자" name="planning_manager" />
|
|
</div>
|
|
<div class="form-group sw-standard-field">
|
|
<label>${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>${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="sw-만료일" name="expiry_date" />
|
|
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();">
|
|
<i data-lucide="calendar"></i>
|
|
</button>
|
|
<input type="date" id="sw-만료일-picker" class="hidden-picker" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group full-width">
|
|
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
|
<textarea id="sw-비고" name="memo" rows="2"></textarea>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="sw-user-section" class="user-management-section">
|
|
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
|
|
<i data-lucide="users"></i> 사용자 관리
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-history-area">
|
|
<div class="history-header">
|
|
<h3><i data-lucide="history"></i> 업데이트 내역</h3>
|
|
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
|
계약 업데이트 <i data-lucide="rotate-ccw"></i>
|
|
</button>
|
|
</div>
|
|
<div id="sw-history-list" class="history-timeline"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
|
<div class="footer-actions">
|
|
<button id="btn-revert-sw-edit" class="btn btn-outline hidden">수정 취소</button>
|
|
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
|
|
<button id="btn-save-sw-asset" class="btn btn-primary">수정</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 계약 업데이트 서브 모달 -->
|
|
<div id="sw-update-modal" class="modal-overlay hidden sub-modal">
|
|
<div class="modal-content narrow">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">계약 업데이트 반영</h2>
|
|
<button id="btn-close-sw-update" class="btn-icon">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="grid-form vertical-form">
|
|
<div class="form-group">
|
|
<label>업데이트 일자</label>
|
|
<input type="date" id="sw-update-date" />
|
|
</div>
|
|
<div class="form-group sub-sw-update">
|
|
<label>새로운 계약 기간</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" />
|
|
<span>~</span>
|
|
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" />
|
|
</div>
|
|
</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" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>상세 내용 (메모)</label>
|
|
<input type="text" id="sw-update-note" placeholder="예: 25년도 구독 연장 결제 완료" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<div></div>
|
|
<div class="footer-actions">
|
|
<button id="btn-cancel-sw-update" class="btn btn-outline">취소</button>
|
|
<button id="btn-save-sw-update" class="btn btn-primary">반영하기</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<style>
|
|
.hidden-picker {
|
|
position: absolute;
|
|
width: 0;
|
|
height: 0;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
`;
|
|
}
|
|
|
|
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
|
const saveBtn = document.getElementById('btn-save-sw-asset')!;
|
|
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
|
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
|
const typeSelect = document.getElementById('sw-asset-type') as HTMLSelectElement;
|
|
const userAssignBtn = document.getElementById('btn-open-sw-user')!;
|
|
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
|
|
|
|
typeSelect?.addEventListener('change', () => this.applySwTypeUI(typeSelect.value));
|
|
|
|
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
|
|
const el = document.getElementById(id) as HTMLInputElement;
|
|
if (el) applyDateMask(el);
|
|
});
|
|
|
|
userAssignBtn.addEventListener('click', () => {
|
|
if (this.currentAsset) openSwUserModal(this.currentAsset);
|
|
});
|
|
|
|
const subModal = document.getElementById('sw-update-modal')!;
|
|
const closeUpdate = () => subModal.classList.add('hidden');
|
|
document.getElementById('btn-close-sw-update')?.addEventListener('click', closeUpdate);
|
|
document.getElementById('btn-cancel-sw-update')?.addEventListener('click', closeUpdate);
|
|
|
|
btnOpenUpdate?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (!this.isEditMode) { alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.'); return; }
|
|
subModal.classList.remove('hidden');
|
|
});
|
|
|
|
document.getElementById('btn-save-sw-update')?.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
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 cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
|
|
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
|
|
|
|
if (start) setFieldValue('sw-시작일', start);
|
|
if (end) setFieldValue('sw-만료일', end);
|
|
if (cost) setFieldValue('sw-금액', cost);
|
|
|
|
const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' };
|
|
await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify([...state.masterData.logs, log])
|
|
});
|
|
|
|
closeUpdate(); onSave();
|
|
});
|
|
|
|
revertBtn.addEventListener('click', () => {
|
|
this.setEditLockMode('view');
|
|
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
|
});
|
|
|
|
saveBtn.addEventListener('click', async () => {
|
|
if (!this.currentAsset) return;
|
|
if (!this.isEditMode) { this.setEditLockMode('edit'); this.isEditMode = true; return; }
|
|
|
|
const type = getFieldValue('sw-asset-type');
|
|
const formData = new FormData(this.formEl!);
|
|
const updated = { ...this.currentAsset };
|
|
formData.forEach((value, key) => { updated[key] = value; });
|
|
|
|
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
|
if (await saveAsset(categoryKey, updated)) { onSave(); this.close(); closeModals(); }
|
|
});
|
|
|
|
deleteBtn.addEventListener('click', async () => {
|
|
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
|
const type = this.currentAsset.asset_type || this.currentAsset.type;
|
|
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
|
if (await deleteAsset(categoryKey, this.currentAsset.id)) {
|
|
alert('성공적으로 삭제되었습니다.'); onSave(); this.close(); closeModals();
|
|
}
|
|
});
|
|
|
|
createIcons({ icons: { History, Plus, Save, Calendar, Users, RotateCcw } });
|
|
}
|
|
|
|
protected fillFormData(asset: any): void {
|
|
setFieldValue('sw-asset-id', asset.id);
|
|
setFieldValue('sw-asset-type', asset.asset_type || asset.type);
|
|
setFieldValue('sw-분야', asset.sw_field || '');
|
|
setFieldValue('sw-법인', asset.purchase_corp || '');
|
|
setFieldValue('sw-부서', asset.current_dept || '');
|
|
setFieldValue('sw-user-current', asset.user_current || '');
|
|
setFieldValue('sw-previous-user', asset.previous_user || '');
|
|
setFieldValue('sw-제품명', asset.product_name || '');
|
|
setFieldValue('sw-수량', asset.asset_count || '');
|
|
setFieldValue('sw-금액', asset.purchase_amount || '');
|
|
setFieldValue('sw-구매일', asset.purchase_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 === '클라우드' || asset.asset_type === '클라우드') {
|
|
setFieldValue('sw-플랫폼명', asset.dev_objective || '');
|
|
setFieldValue('sw-계정명', asset.email_account || '');
|
|
setFieldValue('sw-결제수단', asset.purchase_method || '');
|
|
} else {
|
|
setFieldValue('sw-만료일', asset.expiry_date || '');
|
|
}
|
|
|
|
this.renderHistory(asset.id);
|
|
this.updateHeaderIdentity(asset);
|
|
}
|
|
|
|
protected onAfterOpen(asset: any, mode: string): void {
|
|
this.applySwTypeUI(asset.asset_type || asset.type);
|
|
this.updateHeaderIdentity(asset);
|
|
}
|
|
|
|
private updateHeaderIdentity(asset: any) {
|
|
const container = document.getElementById('sw-header-identity');
|
|
if (!container) return;
|
|
|
|
if (this.currentMode === 'add') {
|
|
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
|
return;
|
|
}
|
|
|
|
const type = getFieldValue('sw-asset-type') || asset.asset_type || asset.type || '';
|
|
const name = getFieldValue('sw-제품명') || asset.product_name || '';
|
|
const corp = getFieldValue('sw-법인') || asset.purchase_corp || '';
|
|
|
|
container.innerHTML = `
|
|
<span class="asset-code-title">${name}</span>
|
|
<span class="service-type-badge">${corp}</span>
|
|
<span class="asset-type-label">${type}</span>
|
|
`;
|
|
}
|
|
|
|
private applySwTypeUI(type: string) {
|
|
const cloudFields = document.querySelectorAll('.cloud-only');
|
|
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 (expiryGroup) expiryGroup.style.display = 'flex';
|
|
userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none');
|
|
}
|
|
}
|
|
}
|
|
|
|
private renderHistory(swId: string) {
|
|
const container = document.getElementById('sw-history-list');
|
|
if (!container) return;
|
|
const logs = (state.masterData.logs || []).filter(l => l.asset_id === swId);
|
|
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>'; return; }
|
|
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
|
|
}
|
|
}
|
|
|
|
export const swModal = new SwAssetModal();
|
|
export function initSwModal(onSave: () => void, closeModals: () => void) { swModal.init(onSave, closeModals); }
|
|
export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') { swModal.open(asset, mode); }
|