Merge latest main with optimized multi-branch features into server_dashboard
This commit is contained in:
@@ -1,45 +1,42 @@
|
||||
/**
|
||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
|
||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모달 모듈입니다.
|
||||
*/
|
||||
export function initBaseModal() {
|
||||
const closeAllModals = () => {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
};
|
||||
|
||||
export function closeModals() {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => modal.classList.add('hidden'));
|
||||
}
|
||||
|
||||
export function initBaseModal() {
|
||||
// ESC 키로 닫기
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeAllModals();
|
||||
if (e.key === 'Escape') closeModals();
|
||||
});
|
||||
|
||||
// 배경(Overlay) 및 닫기 버튼 클릭 시 닫기 (이벤트 위임)
|
||||
// 배경(Overlay) 및 닫기/취소 버튼 클릭 시 닫기
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 1. 오버레이 클릭 시 닫기
|
||||
if (target.classList.contains('modal-overlay')) {
|
||||
closeAllModals();
|
||||
closeModals();
|
||||
}
|
||||
|
||||
// 2. 닫기 아이콘(data-lucide="x") 또는 닫기/취소 버튼 클릭 시 닫기
|
||||
// 버튼 ID가 btn-close- 또는 btn-cancel-로 시작하는 경우 대응
|
||||
// 2. 닫기 또는 취소 버튼 클릭 시 닫기 (이름 패턴 기준)
|
||||
const btn = target.closest('button');
|
||||
if (btn && (btn.id.startsWith('btn-close-') || btn.id.startsWith('btn-cancel-'))) {
|
||||
closeAllModals();
|
||||
closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
return { closeAllModals };
|
||||
return { closeAllModals: closeModals };
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 모달을 엽니다.
|
||||
* @param modalId 모달 엘리먼트의 ID
|
||||
* 특정 모달 열기 (기존의 classList 조작을 정형화)
|
||||
*/
|
||||
export function openModal(modalId: string) {
|
||||
const modal = document.getElementById(modalId);
|
||||
export function openModal(id: string) {
|
||||
const modal = document.getElementById(id);
|
||||
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.swId === sw.id).length;
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === 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);
|
||||
|
||||
@@ -131,6 +131,7 @@ const HW_FORM_HTML = `
|
||||
<div class="form-group full-width"><label for="hw-비고">비고</label><textarea id="hw-비고" rows="2"></textarea></div>
|
||||
`;
|
||||
|
||||
<<<<<<< HEAD
|
||||
function renderHwHistory(assetId: string) {
|
||||
const container = document.getElementById('hw-history-list');
|
||||
if (!container) return;
|
||||
@@ -143,6 +144,27 @@ function renderHwHistory(assetId: string) {
|
||||
<div class="history-details">${l.details.replace(/\n/g, '<br>')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
=======
|
||||
export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' | 'edit' = 'view') {
|
||||
currentAsset = asset;
|
||||
const modal = document.getElementById('hw-asset-modal')!;
|
||||
|
||||
// 1. 잠금 상태 통합 제어 (데이터 유무가 아닌 호출 mode에만 의존)
|
||||
setEditLock('hw-asset-form', mode, {
|
||||
saveBtnId: 'btn-save-hw-asset',
|
||||
revertBtnId: 'btn-revert-hw-edit',
|
||||
generateBtnId: 'btn-generate-hw-code'
|
||||
});
|
||||
|
||||
isEditMode = (mode === 'add' || mode === 'edit');
|
||||
|
||||
// 2. 데이터 바인딩
|
||||
fillHwFormData(asset);
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
applyTypeSpecificUI(asset.type);
|
||||
createIcons({ icons: { Paperclip } });
|
||||
>>>>>>> origin/SW_Table
|
||||
}
|
||||
|
||||
function applyTypeSpecificUI(type: string) {
|
||||
|
||||
@@ -1,213 +1,180 @@
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
import { createIcons, Save, Edit2, RotateCcw } from 'lucide';
|
||||
|
||||
/**
|
||||
* 모달 조작 및 UI 생성을 위한 공통 유틸리티
|
||||
*/
|
||||
// 공통 옵션 생성 함수
|
||||
export const generateOptionsHTML = (options: string[]) =>
|
||||
options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
|
||||
|
||||
// 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) {
|
||||
el.value = value || '';
|
||||
if (el.type === 'checkbox') (el as HTMLInputElement).checked = !!value;
|
||||
else el.value = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 안전하게 폼 필드 값 읽기
|
||||
// 필드 값 가져오기 유틸리티
|
||||
export function getFieldValue(id: string): string {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
return el ? el.value : '';
|
||||
if (!el) return '';
|
||||
if (el.type === 'checkbox') return (el as HTMLInputElement).checked ? 'Y' : 'N';
|
||||
return el.value || '';
|
||||
}
|
||||
|
||||
// 4. 위치 정보 파싱 및 UI 세팅
|
||||
// 폼 자동 채우기
|
||||
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 } });
|
||||
}
|
||||
|
||||
// 위치 정보 파싱 및 설정
|
||||
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(' ');
|
||||
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(' ');
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 위치 종속성(Cascade) 이벤트 바인딩
|
||||
// 위치 정보 취합
|
||||
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';
|
||||
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;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
bldgSelect.addEventListener('change', () => {
|
||||
bldgSelect?.addEventListener('change', () => {
|
||||
const bldg = bldgSelect.value;
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg] || []);
|
||||
const details = LOCATION_DATA[bldg] || [];
|
||||
detailSelect.innerHTML = `<option value="">선택</option>` + generateOptionsHTML(details) + `<option value="기타">직접 입력(기타)</option>`;
|
||||
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';
|
||||
});
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 모달 프레임 HTML 생성 (2열 그리드 표준 레이아웃)
|
||||
export function createModalFrameHTML(id: string, title: string, formHTML: string, options: { historyTitle?: string, addLogBtnId?: string }) {
|
||||
return `
|
||||
<div id="${idPrefix}-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div id="${id}-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2 id="${idPrefix}-modal-title">${title}</h2>
|
||||
<button id="btn-close-${idPrefix}-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
<h2 id="${id}-modal-title">${title}</h2>
|
||||
<button id="btn-close-${id}-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="${idPrefix}-asset-form" class="grid-form">
|
||||
<input type="hidden" id="${idPrefix}-asset-id" />
|
||||
<input type="hidden" id="${idPrefix}-asset-type" />
|
||||
${formContent}
|
||||
<form id="${id}-asset-form" class="grid-form">
|
||||
<input type="hidden" id="${id}-asset-id" />
|
||||
<input type="hidden" id="${id}-asset-type" />
|
||||
${formHTML}
|
||||
</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}" 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 || 'btn-add-log'}" class="btn btn-outline btn-sm">
|
||||
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="${idPrefix}-history-list" class="history-timeline"></div>
|
||||
<div id="${id}-history-list" class="history-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-${idPrefix}-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<button id="btn-delete-${id}-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<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>
|
||||
<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>
|
||||
</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,9 +1,8 @@
|
||||
import { state } from '../../core/state';
|
||||
import { state, saveSoftwareAsset, deleteSoftwareAsset } from '../../core/state';
|
||||
import { SoftwareAsset } from '../../core/excelHandler';
|
||||
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 { openModal, closeModals } from './BaseModal';
|
||||
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, UserPlus } from 'lucide';
|
||||
import { CORP_LIST, ORG_LIST } from './SharedData';
|
||||
import {
|
||||
generateOptionsHTML,
|
||||
setFieldValue,
|
||||
@@ -13,273 +12,198 @@ import {
|
||||
autoFillForm,
|
||||
autoExtractForm
|
||||
} from './ModalUtils';
|
||||
import { openSwUserModal } from './SWUserModal';
|
||||
|
||||
let currentSwAsset: SoftwareAsset | null = null;
|
||||
let currentAsset: SoftwareAsset | null = null;
|
||||
let isEditMode = false;
|
||||
|
||||
const SW_FIELD_MAP: Record<string, string> = {
|
||||
'유형': 'type',
|
||||
'법인': '법인',
|
||||
'자산번호': '자산번호',
|
||||
'제품명': '제품명',
|
||||
'부서': '부서',
|
||||
'제품명': '소프트웨어명',
|
||||
'구매일': '구매일',
|
||||
'만료일': '만료일',
|
||||
'수량': '수량',
|
||||
'금액': '금액',
|
||||
'구매일': '구매일',
|
||||
'납품업체': '납품업체',
|
||||
'비고': '비고',
|
||||
'플랫폼명': '플랫폼명',
|
||||
'부서': '부서',
|
||||
'계정명': '계정명',
|
||||
'결제수단': '결제수단',
|
||||
'연결카드번호': '연결카드번호',
|
||||
'결제일': '결제일',
|
||||
'당월청구액': '당월청구액',
|
||||
'라이선스유형': '라이선스유형',
|
||||
'만료일': '만료일',
|
||||
'라이선스키': '라이선스키'
|
||||
'자산번호': '자산번호'
|
||||
};
|
||||
|
||||
const SW_FORM_HTML = `
|
||||
<!-- Group 1: 기본 정보 -->
|
||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label for="sw-법인">구매법인</label>
|
||||
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||
<select id="sw-법인">${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label for="sw-자산번호">자산번호</label>
|
||||
<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 class="form-group">
|
||||
<label for="sw-부서">관리부서</label>
|
||||
<select id="sw-부서">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label for="sw-제품명">제품명 / 서비스명</label>
|
||||
<div class="form-group">
|
||||
<label for="sw-제품명">제품명 (S/W명)</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-group">
|
||||
<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>
|
||||
<div class="form-group full-width">
|
||||
<label for="sw-비고">비고 (특이사항)</label>
|
||||
<textarea id="sw-비고" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 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 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>
|
||||
</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>';
|
||||
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;
|
||||
}
|
||||
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>
|
||||
|
||||
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>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
createIcons({ icons: { X } });
|
||||
|
||||
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'
|
||||
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);
|
||||
});
|
||||
});
|
||||
isEditMode = (mode === 'add');
|
||||
}
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
isEditMode = false;
|
||||
autoFillForm('sw', asset, SW_FIELD_MAP);
|
||||
applySwTypeUI(asset.type);
|
||||
renderUserSummary(asset.id);
|
||||
renderSwHistory(asset.id);
|
||||
|
||||
const subOnly = document.querySelectorAll('.sw-sub-only');
|
||||
subOnly.forEach(el => (el as HTMLElement).style.display = asset.type === '구독SW' ? 'block' : 'none');
|
||||
|
||||
renderSwUsers(asset.id);
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
createIcons({ icons: { X, History, Plus } });
|
||||
createIcons({ icons: { X, Save, Edit2, RotateCcw, UserPlus } });
|
||||
}
|
||||
|
||||
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 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-유형') as HTMLSelectElement;
|
||||
|
||||
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');
|
||||
typeSelect?.addEventListener('change', () => {
|
||||
const subOnly = document.querySelectorAll('.sw-sub-only');
|
||||
subOnly.forEach(el => (el as HTMLElement).style.display = typeSelect.value === '구독SW' ? 'block' : 'none');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
const handleClose = () => { closeModalsCb(); isEditMode = false; };
|
||||
document.getElementById('btn-close-sw-modal')?.addEventListener('click', handleClose);
|
||||
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', handleClose);
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
if (!currentSwAsset) return;
|
||||
if (!currentAsset) return;
|
||||
if (!isEditMode) {
|
||||
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'
|
||||
});
|
||||
setEditLock('sw-asset-form', 'edit', { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit' });
|
||||
isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const extracted = autoExtractForm('sw', SW_FIELD_MAP);
|
||||
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);
|
||||
const updated = { ...currentAsset, ...extracted };
|
||||
|
||||
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 (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();
|
||||
if (currentAsset && confirm('이 소프트웨어 자산을 삭제하시겠습니까?')) {
|
||||
deleteSoftwareAsset(currentAsset.id, currentAsset.type);
|
||||
onSave();
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
document.getElementById('btn-add-sw-user')?.addEventListener('click', () => {
|
||||
if (!currentAsset) return;
|
||||
openSwUserModal(currentAsset.id, () => renderSwUsers(currentAsset!.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 } from 'lucide';
|
||||
import { createIcons, Edit2, X, Paperclip, Calendar } from 'lucide';
|
||||
import { CORP_LIST, ORG_LIST } from './SharedData';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
|
||||
|
||||
let currentSwUserAsset: SoftwareAsset | null = null;
|
||||
let tempSwUsers: SWUser[] = [];
|
||||
let tempSwUsers: any[] = [];
|
||||
|
||||
const SW_USER_MODAL_HTML = `
|
||||
<div id="sw-user-modal" class="modal-overlay hidden">
|
||||
@@ -74,8 +74,24 @@ const SW_USER_MODAL_HTML = `
|
||||
<input type="text" id="new-user-이름" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용기간</label>
|
||||
<input type="text" id="new-user-사용기간" placeholder="ex) 2024-01-01 ~ 2024-12-31" />
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>신청서 (증빙)</label>
|
||||
@@ -105,7 +121,9 @@ export function openSwUserModal(asset: SoftwareAsset) {
|
||||
|
||||
// 기존 사용자 데이터 복사 (원본 보호를 위해 temp 사용)
|
||||
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
||||
tempSwUsers = existingMapping ? JSON.parse(JSON.stringify(existingMapping.userDataList || [])) : [];
|
||||
tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
||||
법인: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
||||
})) : [];
|
||||
|
||||
renderUserList();
|
||||
modal.classList.remove('hidden');
|
||||
@@ -124,7 +142,7 @@ function renderUserList() {
|
||||
tempSwUsers.forEach((user, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${user.구매법인 || user.법인 || ''}</td>
|
||||
<td>${user.법인 || ''}</td>
|
||||
<td>${user.부서 || ''}</td>
|
||||
<td>${user.직위 || ''}</td>
|
||||
<td>${user.이름 || ''}</td>
|
||||
@@ -169,11 +187,20 @@ function openUserEditSubModal(idx: number = -1) {
|
||||
|
||||
if (idx > -1) {
|
||||
const user = tempSwUsers[idx];
|
||||
setFieldValue('new-user-법인', user.구매법인 || user.법인);
|
||||
setFieldValue('new-user-법인', user.법인);
|
||||
setFieldValue('new-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-종료일', '');
|
||||
}
|
||||
} else {
|
||||
setFieldValue('new-user-법인', currentSwUserAsset?.법인);
|
||||
}
|
||||
@@ -190,6 +217,12 @@ 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', () => {
|
||||
@@ -203,7 +236,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,
|
||||
userDataList: tempSwUsers
|
||||
userData: tempSwUsers.map(u => [u.법인, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||
};
|
||||
|
||||
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
||||
@@ -233,11 +266,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