feat: restore database from backup and fix date formatting in restore script
This commit is contained in:
@@ -1,42 +1,34 @@
|
||||
/**
|
||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모달 모듈입니다.
|
||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
|
||||
*/
|
||||
|
||||
export function closeModals() {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => modal.classList.add('hidden'));
|
||||
}
|
||||
|
||||
export function initBaseModal() {
|
||||
const closeAllModals = () => {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => modal.classList.add('hidden'));
|
||||
};
|
||||
|
||||
// ESC 키로 닫기
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeModals();
|
||||
if (e.key === 'Escape') closeAllModals();
|
||||
});
|
||||
|
||||
// 배경(Overlay) 및 닫기/취소 버튼 클릭 시 닫기
|
||||
// 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현)
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 1. 오버레이 클릭 시 닫기
|
||||
if (target.classList.contains('modal-overlay')) {
|
||||
closeModals();
|
||||
}
|
||||
|
||||
// 2. 닫기 또는 취소 버튼 클릭 시 닫기 (이름 패턴 기준)
|
||||
const btn = target.closest('button');
|
||||
if (btn && (btn.id.startsWith('btn-close-') || btn.id.startsWith('btn-cancel-'))) {
|
||||
closeModals();
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
|
||||
return { closeAllModals: closeModals };
|
||||
return { closeAllModals };
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 모달 열기 (기존의 classList 조작을 정형화)
|
||||
* 특정 모달을 엽니다.
|
||||
* @param modalId 모달 엘리먼트의 ID
|
||||
*/
|
||||
export function openModal(id: string) {
|
||||
const modal = document.getElementById(id);
|
||||
export function openModal(modalId: string) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
||||
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${assigned}</td><td>${Number(sw.수량) - assigned}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
|
||||
@@ -19,23 +19,23 @@ import {
|
||||
let currentAsset: HardwareAsset | null = null;
|
||||
let isEditMode = false;
|
||||
|
||||
const STATUS_LIST = ['사용중', '보관중', '수리중', '폐기예정', '기타'];
|
||||
const STATUS_LIST = ['대여중', '보관중', '수리중', '기타'];
|
||||
|
||||
// 필드 ID ↔ 데이터 Key 매핑
|
||||
// 필드 ID ↔ 데이터 Key 매핑 (유지보수 시 이 부분만 수정)
|
||||
const HW_FIELD_MAP: Record<string, string> = {
|
||||
'유형': 'type',
|
||||
'법인': '법인',
|
||||
'자산코드': '자산코드',
|
||||
'현사용조직': '실사용조직',
|
||||
'현사용조직': '현사용조직',
|
||||
'이전사용조직': '이전사용조직',
|
||||
'상세용도': '서버용도',
|
||||
'상세용도': '상세용도',
|
||||
'모델명': '모델명',
|
||||
'명칭': '명칭',
|
||||
'보관위치': '보관위치',
|
||||
'현재상태': '상태',
|
||||
'현재상태': '현재상태',
|
||||
'IP주소': 'IP주소',
|
||||
'IP2': 'IP2',
|
||||
'원격접속': '원격방법',
|
||||
'원격접속': '원격접속',
|
||||
'서버ID': '서버ID',
|
||||
'서버PW': '서버PW',
|
||||
'모니터링': '모니터링',
|
||||
@@ -45,14 +45,16 @@ const HW_FIELD_MAP: Record<string, string> = {
|
||||
'SSD1': 'SSD1',
|
||||
'SSD2': 'SSD2',
|
||||
'HW사양': 'HW사양',
|
||||
'담당자_정': '관리조직',
|
||||
'구매일': '도입일',
|
||||
'금액': '구매가',
|
||||
'담당자_정': '담당자_정',
|
||||
'담당자_부': '담당자_부',
|
||||
'구매일': '구매연월',
|
||||
'금액': '금액',
|
||||
'비고': '비고',
|
||||
'사용자': '사용자'
|
||||
};
|
||||
|
||||
const HW_FORM_HTML = `
|
||||
<!-- Group 1: 기본 정보 -->
|
||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-법인">구매법인</label>
|
||||
@@ -70,7 +72,7 @@ const HW_FORM_HTML = `
|
||||
<input type="text" id="hw-사용자" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-현사용조직">실사용조직</label>
|
||||
<label for="hw-현사용조직">현 사용조직</label>
|
||||
<select id="hw-현사용조직">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group" id="hw-이전사용조직-group">
|
||||
@@ -78,17 +80,15 @@ const HW_FORM_HTML = `
|
||||
<input type="text" id="hw-이전사용조직" readonly />
|
||||
</div>
|
||||
<div class="form-group" id="hw-유형-group">
|
||||
<label for="hw-유형">자산구분</label>
|
||||
<label for="hw-유형">유형</label>
|
||||
<select id="hw-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group" id="hw-상세용도-group" style="display:none;">
|
||||
<label for="hw-상세용도">서버용도</label>
|
||||
<div class="form-group" id="hw-상세용도-group">
|
||||
<label for="hw-상세용도">상세유형</label>
|
||||
<select id="hw-상세용도">
|
||||
<option value="">선택하세요</option>
|
||||
<option value="DB서버">DB 서버</option>
|
||||
<option value="웹서버">웹 서버</option>
|
||||
<option value="어플리케이션">어플리케이션</option>
|
||||
<option value="가상화">가상화 호스트</option>
|
||||
<option value="">선택</option>
|
||||
<option value="서버">서버</option>
|
||||
<option value="개인PC">개인PC</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -105,29 +105,37 @@ const HW_FORM_HTML = `
|
||||
<div class="form-section-title server-only" id="hw-network-title">네트워크 정보 (Connectivity)</div>
|
||||
<div class="form-group server-only" id="hw-ip-group"><label for="hw-IP주소">IP 주소 1</label><input type="text" id="hw-IP주소" /></div>
|
||||
<div class="form-group server-only" id="hw-ip2-group"><label for="hw-IP2">IP 주소 2</label><input type="text" id="hw-IP2" /></div>
|
||||
<div class="form-group server-only" id="hw-remote-group"><label for="hw-원격접속">원격 방법</label><input type="text" id="hw-원격접속" /></div>
|
||||
<div class="form-group server-only" id="hw-remote-group"><label for="hw-원격접속">원격 도구</label><input type="text" id="hw-원격접속" /></div>
|
||||
<div class="form-group server-only" id="hw-server-id-group"><label for="hw-서버ID">서버 ID</label><input type="text" id="hw-서버ID" /></div>
|
||||
<div class="form-group server-only" id="hw-server-pw-group"><label for="hw-서버PW">서버 PW</label><input type="password" id="hw-서버PW" /></div>
|
||||
<div class="form-group server-only" id="hw-server-pw-group"><label for="hw-서버PW">서버 PW</label><input type="text" id="hw-서버PW" /></div>
|
||||
<div class="form-group non-server" id="hw-ip-non-server-group"><label for="hw-IP주소-non-server">IP 주소</label><input type="text" id="hw-IP주소-non-server" /></div>
|
||||
|
||||
<div class="form-section-title" id="hw-spec-title">하드웨어 사양 (Specifications)</div>
|
||||
<div class="form-section-title" id="hw-spec-title">시스템 사양 (Specifications)</div>
|
||||
<div class="form-group" id="hw-model-group"><label for="hw-모델명">모델명</label><input type="text" id="hw-모델명" /></div>
|
||||
<div class="form-group" id="hw-os-group"><label for="hw-OS">운영체제 (OS)</label><input type="text" id="hw-OS" /></div>
|
||||
<div class="form-group" id="hw-cpu-group"><label for="hw-CPU">CPU 사양</label><input type="text" id="hw-CPU" /></div>
|
||||
<div class="form-group" id="hw-ram-group"><label for="hw-RAM">RAM 용량</label><input type="text" id="hw-RAM" /></div>
|
||||
<div class="form-group" id="hw-ssd1-group"><label for="hw-SSD1">Storage 1 (SSD/HDD)</label><input type="text" id="hw-SSD1" /></div>
|
||||
<div class="form-group" id="hw-ssd2-group"><label for="hw-SSD2">Storage 2 (SSD/HDD)</label><input type="text" id="hw-SSD2" /></div>
|
||||
<div class="form-group server-only" id="hw-monitoring-group"><label for="hw-모니터링">모니터링 유무</label><input type="text" id="hw-모니터링" /></div>
|
||||
<div class="form-group server-only" id="hw-monitoring-group"><label for="hw-모니터링">모니터링 여부</label><input type="text" id="hw-모니터링" /></div>
|
||||
<div class="form-group full-width non-server" id="hw-hwspec-group"><label for="hw-HW사양">사양 상세</label><textarea id="hw-HW사양" rows="2"></textarea></div>
|
||||
|
||||
<div class="form-section-title" id="hw-loc-title">설치 위치 및 관리</div>
|
||||
<div class="form-group loc-standard"><label for="hw-위치-빌딩">설치장소 (대분류)</label><select id="hw-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select></div>
|
||||
<div class="form-group loc-standard"><label for="hw-위치-상세">세부 설치 위치</label><select id="hw-위치-상세"><option value="">선택</option></select></div>
|
||||
<div class="form-group loc-standard"><label for="hw-위치-빌딩">설치위치 (건물)</label><select id="hw-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select></div>
|
||||
<div class="form-group loc-standard"><label for="hw-위치-상세">상세 위치</label><select id="hw-위치-상세"><option value="">선택</option></select></div>
|
||||
<div class="form-group" id="hw-위치-기타-group" style="display:none;"><label for="hw-위치-기타">직접 입력 (기타)</label><input type="text" id="hw-위치-기타" /></div>
|
||||
<div class="form-group"><label for="hw-담당자_정">자산관리조직</label><input type="text" id="hw-담당자_정" readonly /></div>
|
||||
<div class="form-group"><label for="hw-구매일">도입일</label><input type="text" id="hw-구매일" /></div>
|
||||
<div class="form-group"><label for="hw-금액">구매가</label><input type="text" id="hw-금액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
|
||||
<div class="form-group"><label for="hw-담당자_정">담당자(정)</label><input type="text" id="hw-담당자_정" /></div>
|
||||
<div class="form-group"><label for="hw-담당자_부">담당자(부)</label><input type="text" id="hw-담당자_부" /></div>
|
||||
<div class="form-group"><label for="hw-구매일">구매연월</label><input type="text" id="hw-구매일" placeholder="YYYYMM" maxlength="6" /></div>
|
||||
<div class="form-group"><label for="hw-금액">금액</label><input type="text" id="hw-금액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
|
||||
<div class="form-group full-width"><label for="hw-비고">비고</label><textarea id="hw-비고" rows="2"></textarea></div>
|
||||
<div class="form-group full-width">
|
||||
<label>품의서 (파일 증빙)</label>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<input type="file" id="hw-품의서" />
|
||||
<span id="hw-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function renderHwHistory(assetId: string) {
|
||||
@@ -169,39 +177,68 @@ function applyTypeSpecificUI(type: string) {
|
||||
const opOnly = document.querySelectorAll('.op-only');
|
||||
const standardLoc = document.querySelectorAll('.loc-standard');
|
||||
|
||||
// 초기화
|
||||
serverOnly.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
nonServer.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
opOnly.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; });
|
||||
|
||||
const isServer = upperType.includes('서버') || upperType.includes('SERVER');
|
||||
const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement;
|
||||
const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement;
|
||||
const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement;
|
||||
if (osLabel) osLabel.innerText = '운영체제 (OS)';
|
||||
if (ramLabel) ramLabel.innerText = 'RAM 용량';
|
||||
if (modelLabel) modelLabel.innerText = '모델명';
|
||||
|
||||
const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t));
|
||||
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품');
|
||||
const isOpType = isMobileGroup || isEquipGroup;
|
||||
const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북';
|
||||
|
||||
if (isServer) {
|
||||
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex';
|
||||
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
|
||||
if (groups.specTitle) groups.specTitle.style.display = 'flex';
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
} else if (isPcType) {
|
||||
if (groups.user) groups.user.style.display = 'flex';
|
||||
if (groups.specTitle) groups.specTitle.style.display = 'flex';
|
||||
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
} else if (isOpType) {
|
||||
if (groups.opTitle) groups.opTitle.style.display = isOpType ? 'flex' : 'none';
|
||||
|
||||
if (isOpType) {
|
||||
opOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
standardLoc.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
if (groups.specTitle) groups.specTitle.style.display = 'flex';
|
||||
if (groups.model) groups.model.style.display = 'flex';
|
||||
if (groups.hwSpec) groups.hwSpec.style.display = 'flex';
|
||||
} else {
|
||||
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
|
||||
if (['CPU', 'GPU'].some(t => upperType.includes(t))) {
|
||||
if (groups.os && osLabel) { osLabel.innerText = '출시연월'; groups.os.style.display = 'flex'; }
|
||||
} else if (['RAM', 'HDD'].some(t => upperType.includes(t))) {
|
||||
if (groups.ram && ramLabel) { ramLabel.innerText = '용량'; groups.ram.style.display = 'flex'; }
|
||||
if (upperType.includes('HDD') && modelLabel) modelLabel.innerText = 'S/N';
|
||||
} else {
|
||||
if (groups.hwSpec) groups.hwSpec.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
else if (isPcType) {
|
||||
if (groups.user) groups.user.style.display = 'flex';
|
||||
if (groups.specTitle) groups.specTitle.style.display = 'flex';
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
|
||||
// 노트북은 상세유형 선택창 숨김
|
||||
if (upperType === '노트북') {
|
||||
if (groups.detailPurpose) groups.detailPurpose.style.display = 'none';
|
||||
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
} else {
|
||||
if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex';
|
||||
if (detailPurpose === '서버') {
|
||||
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
} else {
|
||||
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
|
||||
if (groups.specTitle) groups.specTitle.style.display = 'flex';
|
||||
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +254,12 @@ export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view')
|
||||
});
|
||||
|
||||
isEditMode = (mode === 'add');
|
||||
|
||||
// 데이터 채우기 (자동 매핑)
|
||||
autoFillForm('hw', asset, HW_FIELD_MAP);
|
||||
setFieldValue('hw-명칭', asset.명칭 || asset.모델명);
|
||||
if (!asset.구매연월 && asset.구매일) setFieldValue('hw-구매일', asset.구매일);
|
||||
|
||||
parseAndSetLocation(asset.위치, 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
|
||||
applyTypeSpecificUI(asset.type);
|
||||
renderHwHistory(asset.id);
|
||||
@@ -229,11 +271,12 @@ export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view')
|
||||
export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
if (!document.getElementById('hw-asset-modal')) {
|
||||
const html = createModalFrameHTML('hw', '자산 상세 정보', HW_FORM_HTML, {
|
||||
historyTitle: '유지보수 및 변경 이력',
|
||||
historyTitle: '분출 및 변경 이력',
|
||||
addLogBtnId: 'btn-add-hw-log'
|
||||
});
|
||||
document.body.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// 이력 추가 모달 HTML도 함께 추가
|
||||
const logModalHTML = `
|
||||
<div id="hw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
@@ -241,7 +284,7 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
<div class="modal-body">
|
||||
<div class="grid-form" style="grid-template-columns: 1fr;">
|
||||
<div class="form-group"><label>날짜</label><input type="date" id="new-hw-log-date" /></div>
|
||||
<div class="form-group"><label>내용</label><textarea id="new-hw-log-details" rows="3" placeholder="예: [유지보수] 먼지 제거, [부품교체] 배터리 교체 등"></textarea></div>
|
||||
<div class="form-group"><label>변경/분출 내용</label><textarea id="new-hw-log-details" rows="3" placeholder="예: [분출] 기술팀 홍길동, [수리] 배터리 교체 등"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><div></div><div class="footer-actions"><button id="btn-cancel-hw-log" class="btn btn-outline">취소</button><button id="btn-confirm-hw-log" class="btn btn-primary">추가</button></div></div>
|
||||
@@ -251,6 +294,7 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
document.body.insertAdjacentHTML('beforeend', logModalHTML);
|
||||
}
|
||||
|
||||
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
|
||||
const saveBtn = document.getElementById('btn-save-hw-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
|
||||
@@ -285,7 +329,7 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
const purchaseDate = getFieldValue('hw-구매일');
|
||||
const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC';
|
||||
const dateStr = purchaseDate.replace(/[^0-9]/g, '');
|
||||
if (dateStr.length < 6) { alert('올바른 도입일(YYYYMM)을 입력해주세요.'); return; }
|
||||
if (dateStr.length < 6) { alert('올바른 구매연월(YYYYMM)을 입력해주세요.'); return; }
|
||||
const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`;
|
||||
try {
|
||||
const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`);
|
||||
@@ -294,6 +338,19 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
} catch (err) { alert('자산번호 생성에 실패했습니다.'); }
|
||||
});
|
||||
|
||||
// YYYYMM 입력 제한 로직 (숫자 6자리)
|
||||
['hw-구매일', 'hw-OS'].forEach(id => {
|
||||
const el = document.getElementById(id) as HTMLInputElement;
|
||||
el?.addEventListener('input', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const label = document.querySelector(`label[for="${id}"]`) as HTMLElement;
|
||||
// OS 필드의 경우 라벨이 '출시연월'일 때만 숫자 제한 적용
|
||||
if (id === 'hw-OS' && label?.innerText !== '출시연월') return;
|
||||
|
||||
target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6);
|
||||
});
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
if (!currentAsset) return;
|
||||
if (!isEditMode) {
|
||||
@@ -308,52 +365,94 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터 추출 (자동 매핑)
|
||||
const extracted = autoExtractForm('hw', HW_FIELD_MAP);
|
||||
|
||||
if (!extracted.자산코드) {
|
||||
alert('자산번호가 없습니다. [생성] 버튼을 눌러 자산번호를 부여해주세요.');
|
||||
alert('자산번호가 없습니다. [생성] 버튼을 눌러 자산번호를 먼저 부여해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const diffLogs: string[] = [];
|
||||
const compareFields = [
|
||||
{ key: '실사용조직', label: '실사용조직' },
|
||||
{ key: '위치', label: '위치' },
|
||||
{ key: '상태', label: '상태' },
|
||||
{ key: 'IP주소', label: 'IP' }
|
||||
];
|
||||
const upperType = (extracted.type || '').toUpperCase();
|
||||
const isOpType = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품') || ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t));
|
||||
|
||||
const currentLocation = currentAsset.위치 || '';
|
||||
const newLocation = getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타');
|
||||
// --- 자동 변경 이력 생성 로직 ---
|
||||
// 모든 하드웨어 유형에 대해 자동 로깅 적용
|
||||
if (HW_TYPE_LIST.includes(extracted.type) || extracted.type === '개인PC') {
|
||||
const diffLogs: string[] = [];
|
||||
const compareFields = [
|
||||
{ key: '현사용조직', label: '현사용조직' },
|
||||
{ key: '위치', label: '설치위치' },
|
||||
{ key: '관리자', label: '담당자' },
|
||||
{ key: '현재상태', label: '상태' },
|
||||
{ key: 'IP주소', label: 'IP' },
|
||||
{ key: '상세용도', label: '상세유형' },
|
||||
{ key: '모델명', label: '모델명' }
|
||||
];
|
||||
|
||||
compareFields.forEach(f => {
|
||||
let oldVal = (currentAsset as any)[f.key] || '';
|
||||
let newVal = (extracted as any)[f.key] || '';
|
||||
if (f.key === '위치') { oldVal = currentLocation; newVal = newLocation; }
|
||||
if (f.key === 'IP주소') { newVal = getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'); }
|
||||
const currentIp = currentAsset.IP주소 || '';
|
||||
const newIp = getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server');
|
||||
const currentLocation = currentAsset.위치 || '';
|
||||
const newLocation = isOpType ? extracted.보관위치 : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타');
|
||||
|
||||
if (oldVal !== newVal) {
|
||||
diffLogs.push(`${f.label}: ${oldVal || '(없음)'} → ${newVal || '(없음)'}`);
|
||||
}
|
||||
});
|
||||
compareFields.forEach(f => {
|
||||
let oldVal = '';
|
||||
let newVal = '';
|
||||
|
||||
if (diffLogs.length > 0) {
|
||||
state.masterData.logs = state.masterData.logs || [];
|
||||
state.masterData.logs.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
assetId: currentAsset.id,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
user: '관리자',
|
||||
details: diffLogs.join('\n')
|
||||
if (f.key === 'IP주소') {
|
||||
oldVal = currentIp;
|
||||
newVal = newIp;
|
||||
} else if (f.key === '위치') {
|
||||
oldVal = currentLocation;
|
||||
newVal = newLocation;
|
||||
} else if (f.key === '관리자') {
|
||||
oldVal = currentAsset.담당자_정 || '';
|
||||
newVal = extracted.담당자_정 || '';
|
||||
} else if (f.key === '상세용도') {
|
||||
oldVal = currentAsset.상세용도 || '';
|
||||
// 비 PC 자산은 유형을 상세유형으로 간주
|
||||
newVal = (extracted.type !== 'PC' && extracted.type !== '개인PC') ? extracted.type : (extracted.상세용도 || '');
|
||||
} else {
|
||||
oldVal = (currentAsset as any)[f.key] || '';
|
||||
newVal = extracted[f.key] || '';
|
||||
}
|
||||
|
||||
if (oldVal !== newVal) {
|
||||
diffLogs.push(`${f.label}: ${oldVal || '(없음)'} → ${newVal || '(없음)'}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (diffLogs.length > 0) {
|
||||
state.masterData.logs = state.masterData.logs || [];
|
||||
state.masterData.logs.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
assetId: currentAsset.id,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
user: '관리자',
|
||||
details: diffLogs.join('\n')
|
||||
});
|
||||
}
|
||||
}
|
||||
// ----------------------------
|
||||
|
||||
const updated: any = {
|
||||
...currentAsset,
|
||||
...extracted,
|
||||
IP주소: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'),
|
||||
위치: newLocation
|
||||
관리자: extracted.담당자_정,
|
||||
위치: isOpType ? extracted.보관위치 : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타')
|
||||
};
|
||||
|
||||
// 현 사용조직 변경 시 이전 사용조직 자동 업데이트
|
||||
if (currentAsset.현사용조직 && currentAsset.현사용조직 !== extracted.현사용조직) {
|
||||
updated.이전사용조직 = currentAsset.현사용조직;
|
||||
}
|
||||
|
||||
// 비 PC 자산에 대해 상세유형(상세용도)을 유형과 동기화
|
||||
if (updated.type !== 'PC') {
|
||||
updated.상세용도 = updated.type;
|
||||
}
|
||||
|
||||
saveHardwareAsset(updated);
|
||||
onSave();
|
||||
setEditLock('hw-asset-form', 'view', {
|
||||
|
||||
@@ -1,180 +1,213 @@
|
||||
import { createIcons, Save, Edit2, RotateCcw } from 'lucide';
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
|
||||
// 공통 옵션 생성 함수
|
||||
export const generateOptionsHTML = (options: string[]) =>
|
||||
options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
|
||||
/**
|
||||
* 모달 조작 및 UI 생성을 위한 공통 유틸리티
|
||||
*/
|
||||
|
||||
// 필드 값 설정 유틸리티
|
||||
// 1. Select 박스의 Option HTML 생성
|
||||
export function generateOptionsHTML(list: string[], defaultValue: string = '', includeSelectHint: boolean = true): string {
|
||||
let html = includeSelectHint ? '<option value="">선택</option>' : '';
|
||||
html += list.map(item => `<option value="${item}" ${item === defaultValue ? 'selected' : ''}>${item}</option>`).join('');
|
||||
return html;
|
||||
}
|
||||
|
||||
// 2. 안전하게 폼 필드 값 설정 (Null 에러 방지)
|
||||
export function setFieldValue(id: string, value: any) {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
if (el) {
|
||||
if (el.type === 'checkbox') (el as HTMLInputElement).checked = !!value;
|
||||
else el.value = value || '';
|
||||
el.value = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 필드 값 가져오기 유틸리티
|
||||
// 3. 안전하게 폼 필드 값 읽기
|
||||
export function getFieldValue(id: string): string {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
if (!el) return '';
|
||||
if (el.type === 'checkbox') return (el as HTMLInputElement).checked ? 'Y' : 'N';
|
||||
return el.value || '';
|
||||
return el ? el.value : '';
|
||||
}
|
||||
|
||||
// 폼 자동 채우기
|
||||
export function autoFillForm(prefix: string, data: any, fieldMap: Record<string, string>) {
|
||||
Object.keys(fieldMap).forEach(fieldId => {
|
||||
const dataKey = fieldMap[fieldId];
|
||||
setFieldValue(`${prefix}-${fieldId}`, data[dataKey]);
|
||||
});
|
||||
}
|
||||
|
||||
// 폼 데이터 자동 추출
|
||||
export function autoExtractForm(prefix: string, fieldMap: Record<string, string>): any {
|
||||
const extracted: any = {};
|
||||
Object.keys(fieldMap).forEach(fieldId => {
|
||||
const dataKey = fieldMap[fieldId];
|
||||
extracted[dataKey] = getFieldValue(`${prefix}-${fieldId}`);
|
||||
});
|
||||
return extracted;
|
||||
}
|
||||
|
||||
// 모달 편집 잠금/해제 유틸리티
|
||||
export function setEditLock(formId: string, mode: 'view' | 'edit' | 'add', options: {
|
||||
saveBtnId: string,
|
||||
revertBtnId: string,
|
||||
generateBtnId?: string,
|
||||
addLogBtnId?: string
|
||||
}) {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
if (!form) return;
|
||||
|
||||
const isView = mode === 'view';
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const el = input as HTMLInputElement;
|
||||
if (el.id.includes('자산코드') || el.id.includes('asset-id') || el.classList.contains('is-readonly-field')) {
|
||||
el.readOnly = true;
|
||||
el.disabled = false;
|
||||
} else {
|
||||
if (el.tagName === 'SELECT') (el as HTMLSelectElement).disabled = isView;
|
||||
else (el as HTMLInputElement).readOnly = isView;
|
||||
}
|
||||
});
|
||||
|
||||
const saveBtn = document.getElementById(options.saveBtnId);
|
||||
const revertBtn = document.getElementById(options.revertBtnId);
|
||||
const generateBtn = options.generateBtnId ? document.getElementById(options.generateBtnId) : null;
|
||||
const addLogBtn = options.addLogBtnId ? document.getElementById(options.addLogBtnId) : null;
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.innerHTML = isView
|
||||
? `<i data-lucide="edit-2" style="width:16px; height:16px;"></i> 수정`
|
||||
: `<i data-lucide="save" style="width:16px; height:16px;"></i> 저장`;
|
||||
saveBtn.className = isView ? 'btn btn-primary' : 'btn btn-success';
|
||||
}
|
||||
|
||||
if (revertBtn) revertBtn.classList.toggle('hidden', isView);
|
||||
if (generateBtn) generateBtn.classList.toggle('hidden', isView);
|
||||
if (addLogBtn) addLogBtn.classList.toggle('hidden', isView);
|
||||
|
||||
createIcons({ icons: { Save, Edit2, RotateCcw } });
|
||||
}
|
||||
|
||||
// 위치 정보 파싱 및 설정
|
||||
// 4. 위치 정보 파싱 및 UI 세팅
|
||||
export function parseAndSetLocation(locationStr: 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;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
// 초기화
|
||||
bldgSelect.value = '';
|
||||
detailSelect.innerHTML = '<option value="">선택</option>';
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
|
||||
if (!locationStr) return;
|
||||
|
||||
const parts = locationStr.split(' > ');
|
||||
if (parts.length >= 1) {
|
||||
bldgSelect.value = parts[0];
|
||||
bldgSelect.dispatchEvent(new Event('change'));
|
||||
if (parts.length >= 2) {
|
||||
setTimeout(() => {
|
||||
detailSelect.value = parts[1];
|
||||
if (parts[1] === '기타' && parts[2]) {
|
||||
if (etcGroup) etcGroup.style.display = 'flex';
|
||||
etcInput.value = parts[2];
|
||||
}
|
||||
}, 0);
|
||||
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(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 위치 정보 취합
|
||||
export function getCombinedLocation(bldgId: string, detailId: string, etcId: string): string {
|
||||
const bldg = getFieldValue(bldgId);
|
||||
const detail = getFieldValue(detailId);
|
||||
const etc = getFieldValue(etcId);
|
||||
|
||||
if (!bldg) return '';
|
||||
if (detail === '기타') return `${bldg} > 기타 > ${etc}`;
|
||||
return detail ? `${bldg} > ${detail}` : bldg;
|
||||
}
|
||||
|
||||
// 위치 이벤트 바인딩
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
// 5. 위치 종속성(Cascade) 이벤트 바인딩
|
||||
export function bindLocationEvents(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;
|
||||
|
||||
bldgSelect?.addEventListener('change', () => {
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
bldgSelect.addEventListener('change', () => {
|
||||
const bldg = bldgSelect.value;
|
||||
const details = LOCATION_DATA[bldg] || [];
|
||||
detailSelect.innerHTML = `<option value="">선택</option>` + generateOptionsHTML(details) + `<option value="기타">직접 입력(기타)</option>`;
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg] || []);
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
if (etcInput) etcInput.value = '';
|
||||
});
|
||||
|
||||
detailSelect?.addEventListener('change', () => {
|
||||
if (etcGroup) etcGroup.style.display = detailSelect.value === '기타' ? 'flex' : 'none';
|
||||
detailSelect.addEventListener('change', () => {
|
||||
if (etcGroup) {
|
||||
etcGroup.style.display = detailSelect.value === '기타' ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 모달 프레임 HTML 생성 (2열 그리드 표준 레이아웃)
|
||||
export function createModalFrameHTML(id: string, title: string, formHTML: string, options: { historyTitle?: string, addLogBtnId?: string }) {
|
||||
// 6. 위치 문자열 조합 (저장용)
|
||||
export function getCombinedLocation(bldgId: string, detailId: string, etcInputId: string): string {
|
||||
const bldg = getFieldValue(bldgId);
|
||||
const detail = getFieldValue(detailId);
|
||||
const etc = getFieldValue(etcInputId);
|
||||
|
||||
let combined = bldg;
|
||||
if (detail) combined += ` ${detail}`;
|
||||
if (detail === '기타' && etc) combined += ` ${etc}`;
|
||||
|
||||
return combined.trim();
|
||||
}
|
||||
|
||||
// 7. 조회/수정 모드 UI 통합 제어
|
||||
export function setEditLock(
|
||||
formId: string,
|
||||
mode: 'view' | 'add' | 'edit',
|
||||
options: {
|
||||
saveBtnId: string,
|
||||
revertBtnId: string,
|
||||
generateBtnId?: string,
|
||||
addLogBtnId?: string
|
||||
}
|
||||
) {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
const saveBtn = document.getElementById(options.saveBtnId);
|
||||
const revertBtn = document.getElementById(options.revertBtnId);
|
||||
const generateBtn = options.generateBtnId ? document.getElementById(options.generateBtnId) : null;
|
||||
const addLogBtn = options.addLogBtnId ? document.getElementById(options.addLogBtnId) : null;
|
||||
|
||||
if (!form || !saveBtn || !revertBtn) return;
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
// 편집 모드 활성화
|
||||
form.classList.remove('is-view-mode');
|
||||
form.classList.add('is-edit-mode');
|
||||
saveBtn.textContent = '저장';
|
||||
revertBtn.classList.toggle('hidden', mode === 'add'); // 신규 추가 시에는 취소 버튼 숨김
|
||||
|
||||
// 번호 생성 버튼은 '추가(add)' 시에만 노출
|
||||
if (generateBtn) {
|
||||
generateBtn.style.display = mode === 'add' ? 'flex' : 'none';
|
||||
}
|
||||
// 내역 추가 버튼 노출
|
||||
if (addLogBtn) addLogBtn.style.display = 'flex';
|
||||
} else {
|
||||
// 조회 모드 (잠금)
|
||||
form.classList.remove('is-edit-mode');
|
||||
form.classList.add('is-view-mode');
|
||||
saveBtn.textContent = '수정';
|
||||
revertBtn.classList.add('hidden');
|
||||
|
||||
// 조회 모드에서는 버튼들 숨김
|
||||
if (generateBtn) generateBtn.style.display = 'none';
|
||||
if (addLogBtn) addLogBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 8. 공통 모달 프레임 템플릿 생성
|
||||
* @param idPrefix 필드 ID의 접두사 (예: 'hw', 'sw', 'pc')
|
||||
* @param title 모달 제목
|
||||
* @param formContent 각 모달마다 다른 폼 본문 HTML
|
||||
* @param options 설정 (이력 영역 제목 등)
|
||||
*/
|
||||
export function createModalFrameHTML(
|
||||
idPrefix: string,
|
||||
title: string,
|
||||
formContent: string,
|
||||
options: { historyTitle: string, addLogBtnId: string }
|
||||
): string {
|
||||
return `
|
||||
<div id="${id}-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content modal-lg">
|
||||
<div id="${idPrefix}-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="${id}-modal-title">${title}</h2>
|
||||
<button id="btn-close-${id}-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
<h2 id="${idPrefix}-modal-title">${title}</h2>
|
||||
<button id="btn-close-${idPrefix}-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="${id}-asset-form" class="grid-form">
|
||||
<input type="hidden" id="${id}-asset-id" />
|
||||
<input type="hidden" id="${id}-asset-type" />
|
||||
${formHTML}
|
||||
<form id="${idPrefix}-asset-form" class="grid-form">
|
||||
<input type="hidden" id="${idPrefix}-asset-id" />
|
||||
<input type="hidden" id="${idPrefix}-asset-type" />
|
||||
${formContent}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> ${options.historyTitle || '변경 이력'}</h3>
|
||||
<button type="button" id="${options.addLogBtnId || 'btn-add-log'}" class="btn btn-outline btn-sm">
|
||||
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> ${options.historyTitle}</h3>
|
||||
<button type="button" id="${options.addLogBtnId}" class="btn btn-outline btn-sm">
|
||||
내역 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="${id}-history-list" class="history-timeline"></div>
|
||||
<div id="${idPrefix}-history-list" class="history-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-${id}-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<button id="btn-delete-${idPrefix}-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-${id}-edit" class="btn btn-outline hidden">취소</button>
|
||||
<button id="btn-save-${id}-asset" class="btn btn-primary">수정</button>
|
||||
<button id="btn-cancel-${id}-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-revert-${idPrefix}-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-${idPrefix}-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-${idPrefix}-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 9. 데이터 ↔ 폼 자동 매핑 (유지보수 핵심)
|
||||
*/
|
||||
export function autoFillForm(idPrefix: string, data: any, fieldMap: Record<string, string>) {
|
||||
Object.entries(fieldMap).forEach(([fieldId, dataKey]) => {
|
||||
setFieldValue(`${idPrefix}-${fieldId}`, data[dataKey]);
|
||||
});
|
||||
}
|
||||
|
||||
export function autoExtractForm(idPrefix: string, fieldMap: Record<string, string>): any {
|
||||
const result: any = {};
|
||||
Object.entries(fieldMap).forEach(([fieldId, dataKey]) => {
|
||||
result[dataKey] = getFieldValue(`${idPrefix}-${fieldId}`);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { state, saveSoftwareAsset, deleteSoftwareAsset } from '../../core/state';
|
||||
import { state } from '../../core/state';
|
||||
import { SoftwareAsset } from '../../core/excelHandler';
|
||||
import { openModal, closeModals } from './BaseModal';
|
||||
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, UserPlus } from 'lucide';
|
||||
import { CORP_LIST, ORG_LIST } from './SharedData';
|
||||
import { closeModals } from './BaseModal';
|
||||
import { openSwUserModal } from './SWUserModal';
|
||||
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide';
|
||||
import { CORP_LIST, TYPE_PREFIX_MAP } from './SharedData';
|
||||
import {
|
||||
generateOptionsHTML,
|
||||
setFieldValue,
|
||||
@@ -12,198 +13,273 @@ import {
|
||||
autoFillForm,
|
||||
autoExtractForm
|
||||
} from './ModalUtils';
|
||||
import { openSwUserModal } from './SWUserModal';
|
||||
|
||||
let currentAsset: SoftwareAsset | null = null;
|
||||
let currentSwAsset: SoftwareAsset | null = null;
|
||||
let isEditMode = false;
|
||||
|
||||
const SW_FIELD_MAP: Record<string, string> = {
|
||||
'유형': 'type',
|
||||
'법인': '법인',
|
||||
'부서': '부서',
|
||||
'제품명': '소프트웨어명',
|
||||
'구매일': '구매일',
|
||||
'만료일': '만료일',
|
||||
'자산번호': '자산번호',
|
||||
'제품명': '제품명',
|
||||
'수량': '수량',
|
||||
'금액': '금액',
|
||||
'구매일': '구매일',
|
||||
'납품업체': '납품업체',
|
||||
'비고': '비고',
|
||||
'자산번호': '자산번호'
|
||||
'플랫폼명': '플랫폼명',
|
||||
'부서': '부서',
|
||||
'계정명': '계정명',
|
||||
'결제수단': '결제수단',
|
||||
'연결카드번호': '연결카드번호',
|
||||
'결제일': '결제일',
|
||||
'당월청구액': '당월청구액',
|
||||
'라이선스유형': '라이선스유형',
|
||||
'만료일': '만료일',
|
||||
'라이선스키': '라이선스키'
|
||||
};
|
||||
|
||||
const SW_FORM_HTML = `
|
||||
<div class="form-section-title">소프트웨어 기본 정보</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-유형">라이선스 유형</label>
|
||||
<select id="sw-유형">
|
||||
<option value="구독SW">구독 라이선스</option>
|
||||
<option value="영구SW">영구 라이선스</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Group 1: 기본 정보 -->
|
||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-법인">구매법인</label>
|
||||
<select id="sw-법인">${generateOptionsHTML(CORP_LIST)}</select>
|
||||
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-부서">관리부서</label>
|
||||
<select id="sw-부서">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-제품명">제품명 (S/W명)</label>
|
||||
<input type="text" id="sw-제품명" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group sw-standard-field">
|
||||
<label for="sw-자산번호">자산번호</label>
|
||||
<input type="text" id="sw-자산번호" placeholder="관리번호 입력" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-수량">총 라이선스 수량</label>
|
||||
<input type="number" id="sw-수량" value="1" />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">계약 및 금액</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-구매일">구매일 (계약시작)</label>
|
||||
<input type="date" id="sw-구매일" />
|
||||
</div>
|
||||
<div class="form-group sw-sub-only">
|
||||
<label for="sw-만료일">만료일 (계약종료)</label>
|
||||
<input type="date" id="sw-만료일" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 class="input-with-btn">
|
||||
<input type="text" id="sw-자산번호" readonly class="is-readonly-field" placeholder="번호 생성을 클릭하세요" />
|
||||
<button type="button" id="btn-generate-sw-code" class="btn btn-outline btn-sm">생성</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label for="sw-비고">비고 (특이사항)</label>
|
||||
<textarea id="sw-비고" rows="2"></textarea>
|
||||
<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 cloud-only"><label for="sw-부서">담당부서</label><input type="text" id="sw-부서" /></div>
|
||||
|
||||
<div class="form-section-title">
|
||||
사용자 할당 현황
|
||||
<button type="button" id="btn-add-sw-user" class="btn btn-outline btn-xs" style="margin-left: 0.5rem;">
|
||||
<i data-lucide="user-plus" style="width:12px; height:12px;"></i> 할당 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="full-width">
|
||||
<div id="sw-user-list-container" class="mini-table-container">
|
||||
<table class="itam-table mini">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>부서</th>
|
||||
<th>이름</th>
|
||||
<th>사번</th>
|
||||
<th>사용기간</th>
|
||||
<th>삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sw-user-list-body"></tbody>
|
||||
</table>
|
||||
<!-- Group 2: 라이선스 및 계약 -->
|
||||
<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>
|
||||
|
||||
<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: 관리 정보 -->
|
||||
<div class="form-section-title">관리 및 비고</div>
|
||||
<div class="form-group sw-standard-field"><label for="sw-구매일">구매연월</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-만료일">만료일 (구독)</label><input type="text" id="sw-만료일" /></div>
|
||||
<div class="form-group sw-standard-field"><label for="sw-납품업체">납품업체</label><input type="text" id="sw-납품업체" /></div>
|
||||
<div class="form-group full-width"><label for="sw-비고">비고</label><textarea id="sw-비고" 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 renderSwUsers(swId: string) {
|
||||
const body = document.getElementById('sw-user-list-body');
|
||||
if (!body) return;
|
||||
const users = (state.masterData.swUsers || []).filter(u => u.swId === swId || u.sw_id === swId);
|
||||
|
||||
if (users.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="5" class="empty-row">할당된 사용자가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
body.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.부서 || '-'}</td>
|
||||
<td>${u.이름 || '-'}</td>
|
||||
<td>${u.사번 || '-'}</td>
|
||||
<td><small>${u.사용기간 || '-'}</small></td>
|
||||
<td><button class="btn-icon text-danger btn-delete-user" data-id="${u.id}"><i data-lucide="x" style="width:14px; height:14px;"></i></button></td>
|
||||
</tr>
|
||||
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('');
|
||||
|
||||
createIcons({ icons: { X } });
|
||||
|
||||
body.querySelectorAll('.btn-delete-user').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
state.masterData.swUsers = state.masterData.swUsers.filter(u => u.id !== id);
|
||||
renderSwUsers(swId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function openSwModal(asset: SoftwareAsset) {
|
||||
currentAsset = asset;
|
||||
const modal = document.getElementById('sw-asset-modal')!;
|
||||
|
||||
setEditLock('sw-asset-form', 'view', {
|
||||
saveBtnId: 'btn-save-sw-asset',
|
||||
revertBtnId: 'btn-revert-sw-edit'
|
||||
});
|
||||
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('');
|
||||
}
|
||||
|
||||
isEditMode = false;
|
||||
function applySwTypeUI(type: string) {
|
||||
const cloudFields = document.querySelectorAll('.cloud-only');
|
||||
const swFields = document.querySelectorAll('.sw-standard-field');
|
||||
const userSection = document.getElementById('sw-user-section');
|
||||
const keyGroup = document.getElementById('sw-license-key-group');
|
||||
const typeGroup = document.getElementById('sw-license-type-group');
|
||||
const expiryGroup = document.getElementById('sw-expiry-group');
|
||||
|
||||
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';
|
||||
} 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') {
|
||||
if (keyGroup) keyGroup.style.display = 'none';
|
||||
if (typeGroup) typeGroup.style.display = 'flex';
|
||||
if (expiryGroup) expiryGroup.style.display = 'flex';
|
||||
} else {
|
||||
if (keyGroup) keyGroup.style.display = 'flex';
|
||||
if (typeGroup) typeGroup.style.display = 'none';
|
||||
if (expiryGroup) expiryGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
generateBtnId: 'btn-generate-sw-code',
|
||||
addLogBtnId: 'btn-add-sw-log'
|
||||
});
|
||||
isEditMode = (mode === 'add');
|
||||
autoFillForm('sw', asset, SW_FIELD_MAP);
|
||||
|
||||
const subOnly = document.querySelectorAll('.sw-sub-only');
|
||||
subOnly.forEach(el => (el as HTMLElement).style.display = asset.type === '구독SW' ? 'block' : 'none');
|
||||
|
||||
renderSwUsers(asset.id);
|
||||
|
||||
applySwTypeUI(asset.type);
|
||||
renderUserSummary(asset.id);
|
||||
renderSwHistory(asset.id);
|
||||
modal.classList.remove('hidden');
|
||||
createIcons({ icons: { X, Save, Edit2, RotateCcw, UserPlus } });
|
||||
createIcons({ icons: { X, History, Plus } });
|
||||
}
|
||||
|
||||
export function initSwModal(onSave: () => void, closeModalsCb: () => void) {
|
||||
if (!document.getElementById('sw-asset-modal')) {
|
||||
const html = createModalFrameHTML('sw', '소프트웨어 상세 정보', SW_FORM_HTML, {
|
||||
historyTitle: '라이선스 변경 이력',
|
||||
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>업데이트 내역 추가</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">취소</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;
|
||||
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-유형') as HTMLSelectElement;
|
||||
const userUpdateBtn = document.getElementById('btn-open-sw-update')!;
|
||||
const logAddBtn = document.getElementById('btn-add-sw-log')!;
|
||||
const logModal = document.getElementById('sw-log-modal')!;
|
||||
|
||||
typeSelect?.addEventListener('change', () => {
|
||||
const subOnly = document.querySelectorAll('.sw-sub-only');
|
||||
subOnly.forEach(el => (el as HTMLElement).style.display = typeSelect.value === '구독SW' ? 'block' : 'none');
|
||||
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',
|
||||
generateBtnId: 'btn-generate-sw-code',
|
||||
addLogBtnId: 'btn-add-sw-log'
|
||||
});
|
||||
isEditMode = false;
|
||||
if (currentSwAsset) openSwModal(currentSwAsset, 'view');
|
||||
});
|
||||
|
||||
const handleClose = () => { closeModalsCb(); isEditMode = false; };
|
||||
document.getElementById('btn-close-sw-modal')?.addEventListener('click', handleClose);
|
||||
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', handleClose);
|
||||
document.getElementById('btn-generate-sw-code')?.addEventListener('click', async () => {
|
||||
const typeValue = getFieldValue('sw-asset-type');
|
||||
const purchaseDate = getFieldValue('sw-구매일');
|
||||
const typeCode = TYPE_PREFIX_MAP[typeValue] || 'SW';
|
||||
const dateStr = purchaseDate.replace(/[^0-9]/g, '');
|
||||
if (dateStr.length < 6) { alert('올바른 구매연월(YYYYMM)을 입력해주세요.'); return; }
|
||||
const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`;
|
||||
try {
|
||||
const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`);
|
||||
const data = await res.json();
|
||||
if (data.nextCode) setFieldValue('sw-자산번호', data.nextCode);
|
||||
} catch (err) { alert('자산번호 생성에 실패했습니다.'); }
|
||||
});
|
||||
|
||||
// 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 (!currentAsset) return;
|
||||
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',
|
||||
generateBtnId: 'btn-generate-sw-code',
|
||||
addLogBtnId: 'btn-add-sw-log'
|
||||
});
|
||||
isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const extracted = autoExtractForm('sw', SW_FIELD_MAP);
|
||||
const updated = { ...currentAsset, ...extracted };
|
||||
const updated = { ...currentSwAsset, ...extracted, 수량: parseInt(extracted.수량 || '0') };
|
||||
|
||||
let targetList: SoftwareAsset[] = [];
|
||||
if (updated.type === '구독SW') targetList = state.masterData.subSw;
|
||||
else if (updated.type === '영구SW') targetList = state.masterData.permSw;
|
||||
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);
|
||||
|
||||
saveSoftwareAsset(updated);
|
||||
onSave();
|
||||
setEditLock('sw-asset-form', 'view', {
|
||||
saveBtnId: 'btn-save-sw-asset',
|
||||
revertBtnId: 'btn-revert-sw-edit',
|
||||
generateBtnId: 'btn-generate-sw-code',
|
||||
addLogBtnId: 'btn-add-sw-log'
|
||||
});
|
||||
isEditMode = false;
|
||||
setEditLock('sw-asset-form', 'view', { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit' });
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
if (currentAsset && confirm('이 소프트웨어 자산을 삭제하시겠습니까?')) {
|
||||
deleteSoftwareAsset(currentAsset.id, currentAsset.type);
|
||||
onSave();
|
||||
handleClose();
|
||||
if (currentSwAsset && 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);
|
||||
onSave(); closeModalAction();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-add-sw-user')?.addEventListener('click', () => {
|
||||
if (!currentAsset) return;
|
||||
openSwUserModal(currentAsset.id, () => renderSwUsers(currentAsset!.id));
|
||||
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 = '';
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { state } from '../../core/state';
|
||||
import { SoftwareAsset, SWUser } from '../../core/excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
import { createIcons, Edit2, X, Paperclip, Calendar } from 'lucide';
|
||||
import { createIcons, Edit2, X, Paperclip } from 'lucide';
|
||||
import { CORP_LIST, ORG_LIST } from './SharedData';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||
|
||||
let currentSwUserAsset: SoftwareAsset | null = null;
|
||||
let tempSwUsers: any[] = [];
|
||||
let tempSwUsers: SWUser[] = [];
|
||||
|
||||
const SW_USER_MODAL_HTML = `
|
||||
<div id="sw-user-modal" class="modal-overlay hidden">
|
||||
@@ -74,24 +74,8 @@ const SW_USER_MODAL_HTML = `
|
||||
<input type="text" id="new-user-이름" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용 시작일</label>
|
||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||
<input type="text" id="new-user-시작일" style="flex:1;" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').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="new-user-시작일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용 종료일</label>
|
||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||
<input type="text" id="new-user-종료일" style="flex:1;" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').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="new-user-종료일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
<label>사용기간</label>
|
||||
<input type="text" id="new-user-사용기간" placeholder="ex) 2024-01-01 ~ 2024-12-31" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>신청서 (증빙)</label>
|
||||
@@ -121,9 +105,7 @@ export function openSwUserModal(asset: SoftwareAsset) {
|
||||
|
||||
// 기존 사용자 데이터 복사 (원본 보호를 위해 temp 사용)
|
||||
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
||||
tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
||||
법인: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
||||
})) : [];
|
||||
tempSwUsers = existingMapping ? JSON.parse(JSON.stringify(existingMapping.userDataList || [])) : [];
|
||||
|
||||
renderUserList();
|
||||
modal.classList.remove('hidden');
|
||||
@@ -142,7 +124,7 @@ function renderUserList() {
|
||||
tempSwUsers.forEach((user, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${user.법인 || ''}</td>
|
||||
<td>${user.구매법인 || user.법인 || ''}</td>
|
||||
<td>${user.부서 || ''}</td>
|
||||
<td>${user.직위 || ''}</td>
|
||||
<td>${user.이름 || ''}</td>
|
||||
@@ -187,20 +169,11 @@ function openUserEditSubModal(idx: number = -1) {
|
||||
|
||||
if (idx > -1) {
|
||||
const user = tempSwUsers[idx];
|
||||
setFieldValue('new-user-법인', user.법인);
|
||||
setFieldValue('new-user-법인', user.구매법인 || user.법인);
|
||||
setFieldValue('new-user-부서', user.부서);
|
||||
setFieldValue('new-user-직위', user.직위);
|
||||
setFieldValue('new-user-이름', user.이름);
|
||||
|
||||
// 사용기간 파싱 (yyyy-mm-dd ~ yyyy-mm-dd)
|
||||
if (user.사용기간 && user.사용기간.includes('~')) {
|
||||
const parts = user.사용기간.split('~');
|
||||
setFieldValue('new-user-시작일', parts[0].trim());
|
||||
setFieldValue('new-user-종료일', parts[1].trim());
|
||||
} else {
|
||||
setFieldValue('new-user-시작일', '');
|
||||
setFieldValue('new-user-종료일', '');
|
||||
}
|
||||
setFieldValue('new-user-사용기간', user.사용기간);
|
||||
} else {
|
||||
setFieldValue('new-user-법인', currentSwUserAsset?.법인);
|
||||
}
|
||||
@@ -217,12 +190,6 @@ export function initSwUserModal(onSave: () => void, closeModals: () => void) {
|
||||
const addUserBtn = document.getElementById('btn-open-add-user')!;
|
||||
const confirmUserBtn = document.getElementById('btn-confirm-user-edit')!;
|
||||
|
||||
['new-user-시작일', 'new-user-종료일'].forEach(id => {
|
||||
applyDateMask(document.getElementById(id) as HTMLInputElement);
|
||||
});
|
||||
|
||||
createIcons({ icons: { Calendar } });
|
||||
|
||||
addUserBtn.addEventListener('click', () => openUserEditSubModal());
|
||||
|
||||
confirmUserBtn.addEventListener('click', () => {
|
||||
@@ -236,7 +203,7 @@ export function initSwUserModal(onSave: () => void, closeModals: () => void) {
|
||||
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === currentSwUserAsset!.id);
|
||||
const newMapping = {
|
||||
sw_id: currentSwUserAsset!.id,
|
||||
userData: tempSwUsers.map(u => [u.법인, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||
userDataList: tempSwUsers
|
||||
};
|
||||
|
||||
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
||||
@@ -266,11 +233,11 @@ function saveUserDataToList() {
|
||||
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? tempSwUsers[idx].신청서명 : '');
|
||||
|
||||
const userData: any = {
|
||||
법인: getFieldValue('new-user-법인'),
|
||||
구매법인: getFieldValue('new-user-법인'),
|
||||
부서: getFieldValue('new-user-부서'),
|
||||
직위: getFieldValue('new-user-직위'),
|
||||
이름: getFieldValue('new-user-이름'),
|
||||
사용기간: `${getFieldValue('new-user-시작일')} ~ ${getFieldValue('new-user-종료일')}`,
|
||||
사용기간: getFieldValue('new-user-사용기간'),
|
||||
신청서명
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user