Merge origin/setting into SW_Table and resolve conflicts

This commit is contained in:
2026-04-23 18:22:34 +09:00
58 changed files with 7495 additions and 2508 deletions

View File

@@ -1,234 +1,125 @@
import { state } from '../../core/state';
import { SoftwareAsset } from '../../core/excelHandler';
import { openModal, closeModals } from './BaseModal';
import { closeModals } from './BaseModal';
import { openSwUserModal } from './SWUserModal';
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Calendar } from 'lucide';
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide';
import { CORP_LIST } from './SharedData';
import {
generateOptionsHTML,
setFieldValue,
getFieldValue,
setEditLock,
applyDateMask
createModalFrameHTML,
autoFillForm,
autoExtractForm
} from './ModalUtils';
let currentSwAsset: SoftwareAsset | null = null;
let isEditMode = false;
const SW_MODAL_HTML = `
<div id="sw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="sw-modal-title">소프트웨어 상세 정보</h2>
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></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" />
<!-- 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>
<option value="클라우드">클라우드</option>
</select>
</div>
<div class="form-group">
<label for="sw-분야">분야</label>
<select id="sw-분야" required>
<option value="업무공통">업무공통</option>
<option value="개발S/W">개발S/W</option>
<option value="디자인">디자인</option>
<option value="설계S/W">설계S/W</option>
</select>
</div>
/**
* 소프트웨어 필드 매핑 (통합 스키마 기반)
* 소프트웨어는 자산번호를 사용하지 않으므로 제거함
*/
const SW_FIELD_MAP: Record<string, string> = {
'법인': ASSET_SCHEMA.CORP.key,
'제품명': ASSET_SCHEMA.PRODUCT.key,
'수량': ASSET_SCHEMA.QTY.key,
'금액': ASSET_SCHEMA.PRICE.key,
'구매일': ASSET_SCHEMA.PURCHASE_YM.key,
'납품업체': ASSET_SCHEMA.VENDOR.key,
'비고': ASSET_SCHEMA.REMARKS.key,
'플랫폼명': ASSET_SCHEMA.PLATFORM.key,
'부서': '부서',
'계정명': ASSET_SCHEMA.ACCOUNT.key,
'결제수단': ASSET_SCHEMA.PAY_METHOD.key,
'연결카드번호': ASSET_SCHEMA.CARD_NUM.key,
'결제일': ASSET_SCHEMA.PAY_DAY.key,
'당월청구액': ASSET_SCHEMA.BILLING.key,
'라이선스유형': ASSET_SCHEMA.LICENSE_TYPE.key,
'만료일': ASSET_SCHEMA.EXPIRY.key,
'라이선스키': ASSET_SCHEMA.LICENSE_KEY.key
};
<div class="form-group">
<label for="sw-법인">법인</label>
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group full-width">
<label for="sw-제품명">제품명 / 서비스명</label>
<input type="text" id="sw-제품명" required />
</div>
<div class="form-group cloud-only">
<label for="sw-플랫폼명">플랫폼명</label>
<input type="text" id="sw-플랫폼명" placeholder="예: AWS, Cafe24" />
</div>
<div class="form-group">
<label for="sw-부서">조직 / 부서</label>
<input type="text" id="sw-부서" />
</div>
<!-- Group 2: 라이선스 및 계약 (License/Contract) -->
<div class="form-section-title">라이선스 및 계약 정보</div>
<div class="form-group sw-standard-field" id="sw-license-type-group">
<label for="sw-라이선스유형">라이선스 유형</label>
<input type="text" id="sw-라이선스유형" />
</div>
<div class="form-group sw-standard-field" id="sw-license-key-group">
<label for="sw-라이선스키">라이선스 키</label>
<input type="text" id="sw-라이선스키" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-수량">보유 수량</label>
<input type="number" id="sw-수량" 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, ',')" />
</div>
<!-- Group 3: 클라우드 전용 정보 (Cloud Specific) -->
<div class="form-group cloud-only">
<label for="sw-계정명">계정명 (이메일)</label>
<input type="text" id="sw-계정명" />
</div>
<div class="form-group cloud-only">
<label for="sw-결제수단">결제수단</label>
<select id="sw-결제수단">
<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>
<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>
</div>
<div class="form-group sw-standard-field">
<label for="sw-납품업체">납품업체</label>
<input type="text" id="sw-납품업체" />
</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>
</div>
<div class="form-group sw-standard-field" id="sw-expiry-group">
<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>
</div>
<div class="form-group full-width">
<label for="sw-비고">비고</label>
<textarea id="sw-비고" rows="2"></textarea>
</div>
</form>
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1.5rem;">
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
<i data-lucide="users" style="width:16px; height:16px; margin-right:4px;"></i> 사용자 관리
</button>
</div>
</div>
<div class="modal-history-area">
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
계약 업데이트 <i data-lucide="refresh-ccw" style="width:14px; height:14px;"></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>
const SW_FORM_HTML = `
<!-- Group 1: 기본 정보 -->
<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>
<option value="클라우드">클라우드</option>
</select>
</div>
<div class="form-group">
<label for="sw-법인">${ASSET_SCHEMA.CORP.ui}</label>
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group full-width">
<label for="sw-제품명">${ASSET_SCHEMA.PRODUCT.ui}</label>
<input type="text" id="sw-제품명" required />
</div>
<div class="form-group cloud-only"><label for="sw-플랫폼명">${ASSET_SCHEMA.PLATFORM.ui}</label><input type="text" id="sw-플랫폼명" placeholder="예: AWS, Cafe24" /></div>
<div class="form-group cloud-only"><label for="sw-부서">담당부서</label><input type="text" id="sw-부서" /></div>
<!-- 계약/유지보수 기간 갱신 및 업데이트 모달 -->
<div id="sw-update-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>계약 업데이트 반영</h2>
<button id="btn-close-sw-update" class="btn-icon"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="grid-form" style="grid-template-columns: 1fr;">
<div class="form-group">
<label>업데이트 일자</label>
<input type="date" id="sw-update-date" />
</div>
<div class="form-group sub-sw-update">
<label>새로운 계약 기간</label>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" style="flex: 1;" />
<span>~</span>
<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" />
</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>
<!-- Group 2: 라이선스 및 계약 -->
<div class="form-section-title">라이선스 및 계약 정보</div>
<div class="form-group sw-standard-field" id="sw-license-type-group"><label for="sw-라이선스유형">${ASSET_SCHEMA.LICENSE_TYPE.ui}</label><input type="text" id="sw-라이선스유형" /></div>
<div class="form-group sw-standard-field" id="sw-license-key-group"><label for="sw-라이선스키">${ASSET_SCHEMA.LICENSE_KEY.ui}</label><input type="text" id="sw-라이선스키" /></div>
<div class="form-group sw-standard-field"><label for="sw-수량">${ASSET_SCHEMA.QTY.ui}</label><input type="number" id="sw-수량" min="0" /></div>
<div class="form-group sw-standard-field"><label for="sw-금액">${ASSET_SCHEMA.PRICE.ui}</label><input type="text" id="sw-금액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
<div class="form-group cloud-only"><label for="sw-계정명">${ASSET_SCHEMA.ACCOUNT.ui}</label><input type="text" id="sw-계정명" /></div>
<div class="form-group cloud-only"><label for="sw-결제수단">${ASSET_SCHEMA.PAY_METHOD.ui}</label><select id="sw-결제수단"><option value="">선택안함</option><option value="법인카드">법인카드</option><option value="인보이스">인보이스</option></select></div>
<div class="form-group cloud-only"><label for="sw-연결카드번호">${ASSET_SCHEMA.CARD_NUM.ui}</label><input type="text" id="sw-연결카드번호" maxlength="4" /></div>
<div class="form-group cloud-only"><label for="sw-결제일">${ASSET_SCHEMA.PAY_DAY.ui}</label><input type="number" id="sw-결제일" min="1" max="31" /></div>
<div class="form-group cloud-only"><label for="sw-당월청구액">${ASSET_SCHEMA.BILLING.ui}</label><input type="text" id="sw-당월청구액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
<!-- Group 4: 관리 정보 -->
<div class="form-section-title">관리 및 비고</div>
<div class="form-group sw-standard-field"><label for="sw-구매일">${ASSET_SCHEMA.PURCHASE_YM.ui}</label><input type="text" id="sw-구매일" placeholder="YYYYMM" maxlength="6" /></div>
<div class="form-group sw-standard-field" id="sw-expiry-group"><label for="sw-만료일">${ASSET_SCHEMA.EXPIRY.ui}</label><input type="text" id="sw-만료일" /></div>
<div class="form-group sw-standard-field"><label for="sw-납품업체">${ASSET_SCHEMA.VENDOR.ui}</label><input type="text" id="sw-납품업체" /></div>
<div class="form-group full-width"><label for="sw-비고">${ASSET_SCHEMA.REMARKS.ui}</label><textarea id="hw-비고" rows="2"></textarea></div>
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem;">
<div class="section-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:600;">사용자 할당 현황</h3>
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">할당 관리 <i data-lucide="plus" style="width:14px; height:14px;"></i></button>
</div>
<div id="sw-assigned-users-summary" class="user-summary-grid"></div>
</div>
`;
function renderSwHistory(swId: string) {
const container = document.getElementById('sw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId).sort((a,b) => new Date(b.date).getTime() - new Date(a.date).getTime());
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.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details.replace(/\n/g, '<br>')}</div>
</div>
`).join('');
}
function renderUserSummary(swId: string) {
const container = document.getElementById('sw-assigned-users-summary');
if (!container) return;
const userMapping = state.masterData.swUsers.find(u => u.sw_id === swId);
if (!userMapping || !userMapping.userData || userMapping.userData.length === 0) {
container.innerHTML = '<div class="empty-summary">할당된 사용자가 없습니다.</div>';
return;
}
container.innerHTML = userMapping.userData.map(u => `
<div class="user-badge-item"><span class="u-name">${u[3] || '이름없음'}</span><span class="u-dept">${u[1] || '부서없음'}</span></div>
`).join('');
}
function applySwTypeUI(type: string) {
const cloudFields = document.querySelectorAll('.cloud-only');
const swFields = document.querySelectorAll('.sw-standard-field');
@@ -245,7 +136,6 @@ function applySwTypeUI(type: string) {
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') {
if (keyGroup) keyGroup.style.display = 'none';
if (typeGroup) typeGroup.style.display = 'flex';
@@ -258,79 +148,40 @@ function applySwTypeUI(type: string) {
}
}
function fillSwFormData(asset: SoftwareAsset) {
setFieldValue('sw-asset-id', asset.id);
setFieldValue('sw-asset-type', asset.type);
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. || '');
setFieldValue('sw-비고', asset. || '');
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') {
setFieldValue('sw-라이선스유형', (asset as any). || '');
setFieldValue('sw-만료일', (asset as any). || '');
} else {
setFieldValue('sw-라이선스키', (asset as any). || '');
}
renderSwHistory(asset.id);
}
function renderSwHistory(swId: string) {
const container = document.getElementById('sw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === 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.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details}</div>
</div>
`).join('');
}
export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' | 'edit' = 'view') {
export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' = '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'
revertBtnId: 'btn-revert-sw-edit',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = (mode === 'add' || mode === 'edit');
fillSwFormData(asset);
isEditMode = (mode === 'add');
autoFillForm('sw', asset, SW_FIELD_MAP);
applySwTypeUI(asset.type);
renderUserSummary(asset.id);
renderSwHistory(asset.id);
modal.classList.remove('hidden');
createIcons({ icons: { X, History, Plus } });
}
export function initSwModal(onSave: () => void, closeModals: () => void) {
export function initSwModal(onSave: () => void, closeModalsCb: () => void) {
if (!document.getElementById('sw-asset-modal')) {
document.body.insertAdjacentHTML('beforeend', SW_MODAL_HTML);
const html = createModalFrameHTML('sw', '소프트웨어 상세 정보', SW_FORM_HTML, {
historyTitle: '업데이트 내역',
addLogBtnId: 'btn-add-sw-log'
});
document.body.insertAdjacentHTML('beforeend', html);
const logModalHTML = `
<div id="sw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header"><h2>${UI_TEXT.ACTION.HISTORY_ADD}</h2><button id="btn-close-sw-log" class="btn-icon"><i data-lucide="x"></i></button></div>
<div class="modal-body"><div class="grid-form" style="grid-template-columns: 1fr;"><div class="form-group"><label>날짜</label><input type="date" id="new-log-date" /></div><div class="form-group"><label>상세 내용</label><textarea id="new-log-details" rows="3"></textarea></div></div></div>
<div class="modal-footer"><div></div><div class="footer-actions"><button id="btn-cancel-sw-log" class="btn btn-outline">${UI_TEXT.ACTION.CANCEL}</button><button id="btn-confirm-sw-log" class="btn btn-primary">추가</button></div></div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', logModalHTML);
}
const form = document.getElementById('sw-asset-form') as HTMLFormElement;
@@ -338,74 +189,51 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
const userAssignBtn = document.getElementById('btn-open-sw-user')!;
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
const userUpdateBtn = document.getElementById('btn-open-sw-update')!;
const logAddBtn = document.getElementById('btn-add-sw-log')!;
const logModal = document.getElementById('sw-log-modal')!;
const typeSelect = document.getElementById('sw-asset-type') as HTMLSelectElement;
typeSelect?.addEventListener('change', () => {
applySwTypeUI(typeSelect.value);
});
// 날짜 스마트 마스킹 적용
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
applyDateMask(document.getElementById(id) as HTMLInputElement);
});
createIcons({ icons: { Calendar } });
const closeModalAction = () => { closeModals(); isEditMode = false; };
const closeModalAction = () => { closeModalsCb(); isEditMode = false; };
document.getElementById('btn-close-sw-modal')?.addEventListener('click', closeModalAction);
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', closeModalAction);
revertBtn.addEventListener('click', () => {
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = false;
if (currentSwAsset) fillSwFormData(currentSwAsset);
if (currentSwAsset) openSwModal(currentSwAsset, 'view');
});
// YYYYMM 입력 제한 로직 (숫자 6자리)
document.getElementById('sw-구매일')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6);
});
saveBtn.addEventListener('click', () => {
if (!currentSwAsset) return;
if (!isEditMode) {
setEditLock('sw-asset-form', 'edit', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
setEditLock('sw-asset-form', 'edit', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = true;
return;
}
const extracted = autoExtractForm('sw', SW_FIELD_MAP);
const updated = { ...currentSwAsset, ...extracted, 수량: parseInt(extracted[ASSET_SCHEMA.QTY.key] || '0') };
const type = getFieldValue('sw-asset-type');
const updated: any = {
...currentSwAsset,
분야: getFieldValue('sw-분야'),
법인: getFieldValue('sw-법인'),
부서: getFieldValue('sw-부서'),
제품명: 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-당월청구액');
} else if (type === '구독SW') {
updated. = getFieldValue('sw-라이선스유형');
updated. = getFieldValue('sw-만료일');
} else {
updated. = getFieldValue('sw-라이선스키');
}
const type = getFieldValue('sw-asset-type') || currentSwAsset.type;
updated.type = type;
// 데이터 저장 로직 (state 업데이트)
const oldType = currentSwAsset.type;
@@ -421,108 +249,44 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
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;
else targetList = (state.masterData as any).cloud || [];
const idx = targetList.findIndex(a => a.id === updated.id);
if (idx > -1) targetList[idx] = updated;
else targetList.push(updated);
if (idx > -1) targetList[idx] = updated; else targetList.push(updated);
onSave();
closeModalAction();
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = false;
});
deleteBtn.addEventListener('click', () => {
if (!currentSwAsset) return;
if (confirm('삭제하시겠습니까?')) {
if (currentSwAsset && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) {
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();
closeModalAction();
onSave(); closeModalAction();
}
});
userAssignBtn.addEventListener('click', () => {
if (currentSwAsset) openSwUserModal(currentSwAsset);
userUpdateBtn.addEventListener('click', () => { if (currentSwAsset) openSwUserModal(currentSwAsset); });
logAddBtn.addEventListener('click', () => {
logModal.classList.remove('hidden');
(document.getElementById('new-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
(document.getElementById('new-log-details') as HTMLTextAreaElement).value = '';
});
// 자산 업데이트(계약 갱신) 모달 로직
const subModal = document.getElementById('sw-update-modal')!;
const btnCloseUpdate = document.getElementById('btn-close-sw-update')!;
const btnCancelUpdate = document.getElementById('btn-cancel-sw-update')!;
const btnSaveUpdate = document.getElementById('btn-save-sw-update')!;
const closeUpdateModal = () => subModal.classList.add('hidden');
btnCloseUpdate?.addEventListener('click', closeUpdateModal);
btnCancelUpdate?.addEventListener('click', closeUpdateModal);
btnOpenUpdate?.addEventListener('click', (e) => {
e.preventDefault();
if (!isEditMode) {
alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.');
return;
}
const isSub = getFieldValue('sw-asset-type') === '구독SW';
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 = '';
if (isSub) {
document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:none');
} else {
document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:none');
document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
(document.getElementById('sw-update-maintenance') as HTMLInputElement).checked = (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked;
}
});
btnSaveUpdate?.addEventListener('click', (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 || (isSub ? '구독 갱신' : '유지보수 갱신')}\n`;
if (cost) details += `비용 추가: ${cost}\n`;
if (isSub) {
if (periodStr) details += `계약 변경: -> ${periodStr}\n`;
// 메인 폼에 시작일 만료일 자동 세팅
if (start) setFieldValue('sw-시작일', start);
if (end) setFieldValue('sw-만료일', end);
} else {
details += `유지보수 상태: -> ${maintenance ? '유효' : '만료'}\n`;
(document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = maintenance;
}
// 금액 갱신 (선택사항)
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,
user: '관리자'
});
closeUpdateModal();
renderSwHistory(currentSwAsset ? currentSwAsset.id : '');
document.getElementById('btn-close-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
document.getElementById('btn-cancel-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
document.getElementById('btn-confirm-sw-log')?.addEventListener('click', () => {
if (!currentSwAsset) return;
const date = (document.getElementById('new-log-date') as HTMLInputElement).value;
const details = (document.getElementById('new-log-details') as HTMLTextAreaElement).value;
if (!date || !details) return;
state.masterData.logs = state.masterData.logs || [];
state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentSwAsset.id, date, user: '담당자', details });
logModal.classList.add('hidden'); renderSwHistory(currentSwAsset.id);
});
}