merge: main 브랜치의 최신 변경 사항 병합 및 충돌 해결
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { state } from '../../core/state';
|
||||
import { HardwareAsset } from '../../core/excelHandler';
|
||||
import { renderTable } from '../../views/AssetTableView';
|
||||
import { createIcons, Paperclip } from 'lucide';
|
||||
|
||||
let currentAsset: HardwareAsset | null = null;
|
||||
@@ -245,7 +244,7 @@ function fillHwFormData(asset: HardwareAsset) {
|
||||
}
|
||||
}
|
||||
|
||||
export function initHwModal() {
|
||||
export function initHwModal(onSave: () => void, closeModals: () => void) {
|
||||
// HTML 주입
|
||||
if (!document.getElementById('hw-asset-modal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML);
|
||||
@@ -260,7 +259,7 @@ export function initHwModal() {
|
||||
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.add('hidden');
|
||||
closeModals();
|
||||
isEditMode = false;
|
||||
};
|
||||
|
||||
@@ -337,7 +336,7 @@ export function initHwModal() {
|
||||
const idx = state.masterData.hw.findIndex(a => a.id === assetId);
|
||||
if (idx > -1) {
|
||||
state.masterData.hw[idx] = updated;
|
||||
renderTable(document.getElementById('main-content')!);
|
||||
onSave();
|
||||
switchToViewMode();
|
||||
}
|
||||
});
|
||||
@@ -346,7 +345,7 @@ export function initHwModal() {
|
||||
if (!currentAsset) return;
|
||||
if (confirm('정말로 이 자산을 삭제하시겠습니까?')) {
|
||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id);
|
||||
renderTable(document.getElementById('main-content')!);
|
||||
onSave();
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,8 +109,9 @@ const PC_MODAL_HTML = `
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-pc-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-pc-modal" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-pc-asset" class="btn btn-primary">저장</button>
|
||||
<button id="btn-revert-pc-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-close-pc-footer" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-pc-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,15 +124,66 @@ export function initPcModal(renderContent: () => void, closeModals: () => void)
|
||||
}
|
||||
|
||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||
const btnRevertEdit = document.getElementById('btn-revert-pc-edit') as HTMLButtonElement;
|
||||
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
||||
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
||||
const btnCancelPc = document.getElementById('btn-cancel-pc-modal') as HTMLButtonElement;
|
||||
const btnClosePc = document.getElementById('btn-close-pc-modal') as HTMLButtonElement;
|
||||
const btnCloseHeader = document.getElementById('btn-close-pc-modal') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-pc-footer') as HTMLButtonElement;
|
||||
|
||||
btnCancelPc?.addEventListener('click', closeModals);
|
||||
btnClosePc?.addEventListener('click', closeModals);
|
||||
let isEditMode = false;
|
||||
let currentAsset: HardwareAsset | null = null;
|
||||
|
||||
const setEditMode = (edit: boolean) => {
|
||||
isEditMode = edit;
|
||||
if (edit) {
|
||||
pcForm.classList.add('is-edit-mode');
|
||||
pcForm.classList.remove('is-view-mode');
|
||||
btnSavePc.textContent = '저장';
|
||||
btnRevertEdit.classList.remove('hidden');
|
||||
btnCloseFooter.classList.add('hidden');
|
||||
} else {
|
||||
pcForm.classList.add('is-view-mode');
|
||||
pcForm.classList.remove('is-edit-mode');
|
||||
btnSavePc.textContent = '수정';
|
||||
btnRevertEdit.classList.add('hidden');
|
||||
btnCloseFooter.classList.remove('hidden');
|
||||
if (currentAsset) fillFormData(currentAsset);
|
||||
}
|
||||
};
|
||||
|
||||
function fillFormData(asset: HardwareAsset) {
|
||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('pc-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('pc-사용자') as HTMLInputElement).value = asset.사용자 || '';
|
||||
(document.getElementById('pc-위치') as HTMLInputElement).value = asset.위치 || '';
|
||||
(document.getElementById('pc-CPU') as HTMLInputElement).value = asset.CPU || '';
|
||||
(document.getElementById('pc-GPU') as HTMLInputElement).value = asset.GPU || '';
|
||||
(document.getElementById('pc-RAM') as HTMLInputElement).value = asset.RAM || '';
|
||||
(document.getElementById('pc-SSD1') as HTMLInputElement).value = asset.SSD1 || '';
|
||||
(document.getElementById('pc-SSD2') as HTMLInputElement).value = asset.SSD2 || '';
|
||||
(document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || '';
|
||||
(document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || '';
|
||||
(document.getElementById('pc-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||
(document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `첨부: ${asset.품의서명}` : '';
|
||||
}
|
||||
|
||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||
btnCloseHeader?.addEventListener('click', closeModals);
|
||||
btnCloseFooter?.addEventListener('click', closeModals);
|
||||
|
||||
btnSavePc?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!isEditMode) {
|
||||
setEditMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; }
|
||||
|
||||
// ... (저장 로직 유지)
|
||||
e.preventDefault();
|
||||
if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; }
|
||||
|
||||
|
||||
@@ -1,109 +1,231 @@
|
||||
import { state } from '../../core/state';
|
||||
import { SoftwareAsset } from '../../core/excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
import { createIcons, X, History, Plus } from 'lucide';
|
||||
|
||||
const SW_MODAL_HTML = `
|
||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="sw-modal-title">S/W 상세 정보</h2>
|
||||
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="sw-asset-form" class="grid-form">
|
||||
<input type="hidden" id="sw-asset-id" />
|
||||
<input type="hidden" id="sw-asset-type" />
|
||||
<div class="form-group">
|
||||
<label for="sw-분야">분야</label>
|
||||
<select id="sw-분야" required>
|
||||
<option value="업무공통">업무공통</option>
|
||||
<option value="개발S/W">개발S/W</option>
|
||||
<option value="디자인">디자인</option>
|
||||
<option value="설계S/W">설계S/W</option>
|
||||
</select>
|
||||
<div class="modal-body-split">
|
||||
<div class="modal-form-area">
|
||||
<form id="sw-asset-form" class="grid-form">
|
||||
<input type="hidden" id="sw-asset-id" />
|
||||
<input type="hidden" id="sw-asset-type" />
|
||||
<div class="form-group">
|
||||
<label for="sw-분야">분야</label>
|
||||
<select id="sw-분야" required>
|
||||
<option value="업무공통">업무공통</option><option value="개발S/W">개발S/W</option><option value="디자인">디자인</option><option value="설계S/W">설계S/W</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-법인">법인</label>
|
||||
<select id="sw-법인" required>
|
||||
<option value="한맥">한맥 (HM)</option><option value="삼안 (SM)">삼안 (SM)</option><option value="바론 (BR)">바론 (BR)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-부서">부서</label>
|
||||
<input type="text" id="sw-부서" placeholder="ex) 경영지원팀" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-구매일">구매일</label>
|
||||
<input type="date" id="sw-구매일" />
|
||||
</div>
|
||||
<div class="form-group" id="sw-구독일-group" style="grid-column: span 2;">
|
||||
<label>구독 기간</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="date" id="sw-구독일-시작" style="flex: 1;" />
|
||||
<span>~</span>
|
||||
<input type="date" id="sw-구독일-종료" style="flex: 1;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="sw-유지보수-group" style="display:none;">
|
||||
<label for="sw-유지보수여부">유지보수 여부</label>
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; height: 38px; cursor: pointer;">
|
||||
<input type="checkbox" id="sw-유지보수여부" /> 대상 여부
|
||||
</label>
|
||||
</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">
|
||||
<label for="sw-수량">수량 (보유량)</label>
|
||||
<input type="number" id="sw-수량" min="1" value="1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-계정명">계정명</label>
|
||||
<input type="text" id="sw-계정명" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-납품업체">납품업체</label>
|
||||
<input type="text" id="sw-납품업체" />
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label for="sw-비고">비고</label>
|
||||
<input type="text" id="sw-비고" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-법인">법인</label>
|
||||
<select id="sw-법인" required>
|
||||
<option value="한맥">한맥 (HM)</option>
|
||||
<option value="삼안 (SM)">삼안 (SM)</option>
|
||||
<option value="바론 (BR)">바론 (BR)</option>
|
||||
</select>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm"><i data-lucide="plus" style="width:14px;height:14px;"></i> 업데이트 추가</button>
|
||||
</div>
|
||||
<div id="sw-history-list" class="history-timeline">
|
||||
<div class="empty-history">내역이 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-부서">부서</label>
|
||||
<input type="text" id="sw-부서" placeholder="ex) 경영지원팀" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-제품명">제품명</label>
|
||||
<input type="text" id="sw-제품명" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-구매일">구매일</label>
|
||||
<input type="text" id="sw-구매일" placeholder="ex) 2024-01-01" />
|
||||
</div>
|
||||
<div class="form-group" id="sw-구독일-group">
|
||||
<label for="sw-구독일">구독일(시작~끝)</label>
|
||||
<input type="text" id="sw-구독일" placeholder="ex) 2024-01-01 ~ 2024-12-31" />
|
||||
</div>
|
||||
<div class="form-group" id="sw-유지보수-group" style="display:none;">
|
||||
<label for="sw-유지보수여부">유지보수 여부</label>
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; height: 38px; cursor: pointer;">
|
||||
<input type="checkbox" id="sw-유지보수여부" /> 대상 여부
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-금액">금액</label>
|
||||
<input type="text" id="sw-금액" placeholder="ex) 1,000,000" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\d))/g, ',')" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-수량">수량 (보유량)</label>
|
||||
<input type="number" id="sw-수량" min="1" value="1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-계정명">계정명</label>
|
||||
<input type="text" id="sw-계정명" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-납품업체">납품업체</label>
|
||||
<input type="text" id="sw-납품업체" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sw-비고">비고</label>
|
||||
<input type="text" id="sw-비고" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-revert-sw-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-close-sw-footer" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-sw-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sw-update-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
<div class="modal-header">
|
||||
<h2>계약 업데이트 반영</h2>
|
||||
<button id="btn-close-sw-update" class="btn-icon"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-form" style="grid-template-columns: 1fr;">
|
||||
<div class="form-group">
|
||||
<label>업데이트 일자</label>
|
||||
<input type="date" id="sw-update-date" />
|
||||
</div>
|
||||
<div class="form-group sub-sw-update">
|
||||
<label>새로운 구독 기간</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="date" id="sw-update-start" style="flex: 1;" />
|
||||
<span>~</span>
|
||||
<input type="date" id="sw-update-end" style="flex: 1;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group perm-sw-update" style="display:none;">
|
||||
<label>유지보수 체결 (상태 연동)</label>
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; height: 38px; cursor: pointer;">
|
||||
<input type="checkbox" id="sw-update-maintenance" /> 유효 상태로 갱신
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>발생 비용</label>
|
||||
<input type="text" id="sw-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\d))/g, ',')" placeholder="ex) 500,000" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>상세 내용 (메모)</label>
|
||||
<input type="text" id="sw-update-note" placeholder="예: 25년도 구독 연장 결제 완료" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div></div>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-update" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-sw-update" class="btn btn-primary">반영하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export let currentAsset: SoftwareAsset | null = null;
|
||||
export let isEditMode = false;
|
||||
|
||||
export function setEditMode(edit: boolean) {
|
||||
isEditMode = edit;
|
||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
||||
const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
|
||||
const btnRevertEdit = document.getElementById('btn-revert-sw-edit') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-sw-footer') as HTMLButtonElement;
|
||||
|
||||
if (edit) {
|
||||
swForm.classList.add('is-edit-mode');
|
||||
swForm.classList.remove('is-view-mode');
|
||||
btnSaveSw.textContent = '저장';
|
||||
btnRevertEdit.classList.remove('hidden');
|
||||
btnCloseFooter.classList.add('hidden');
|
||||
} else {
|
||||
swForm.classList.add('is-view-mode');
|
||||
swForm.classList.remove('is-edit-mode');
|
||||
btnSaveSw.textContent = '수정';
|
||||
btnRevertEdit.classList.add('hidden');
|
||||
btnCloseFooter.classList.remove('hidden');
|
||||
if (currentAsset) fillFormData(currentAsset);
|
||||
}
|
||||
}
|
||||
|
||||
export function fillFormData(asset: SoftwareAsset) {
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
|
||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = asset.분야 || '업무공통';
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('sw-부서') as HTMLInputElement).value = asset.부서 || '';
|
||||
(document.getElementById('sw-제품명') as HTMLInputElement).value = asset.제품명;
|
||||
(document.getElementById('sw-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
|
||||
if (asset.구독일) {
|
||||
const parts = asset.구독일.split('~');
|
||||
(document.getElementById('sw-구독일-시작') as HTMLInputElement).value = parts[0]?.trim() || '';
|
||||
(document.getElementById('sw-구독일-종료') as HTMLInputElement).value = parts[1]?.trim() || '';
|
||||
} else {
|
||||
(document.getElementById('sw-구독일-시작') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-구독일-종료') as HTMLInputElement).value = '';
|
||||
}
|
||||
|
||||
(document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = !!asset.유지보수여부;
|
||||
(document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||
(document.getElementById('sw-수량') as HTMLInputElement).value = String(asset.수량);
|
||||
(document.getElementById('sw-계정명') as HTMLInputElement).value = asset.계정명 || '';
|
||||
(document.getElementById('sw-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('sw-비고') as HTMLInputElement).value = asset.비고 || '';
|
||||
|
||||
document.getElementById('btn-open-sw-update')!.style.display = 'flex';
|
||||
renderSwHistory(asset.id);
|
||||
}
|
||||
|
||||
export function initSwModal(renderContent: () => void, closeModals: () => void) {
|
||||
if (!document.getElementById('sw-asset-modal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', SW_MODAL_HTML);
|
||||
}
|
||||
|
||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
||||
const btnRevertEdit = document.getElementById('btn-revert-sw-edit') as HTMLButtonElement;
|
||||
const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
|
||||
const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement;
|
||||
const btnCancelSw = document.getElementById('btn-cancel-sw-modal') as HTMLButtonElement;
|
||||
const btnCloseSw = document.getElementById('btn-close-sw-modal') as HTMLButtonElement;
|
||||
const btnCloseHeader = document.getElementById('btn-close-sw-modal') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-sw-footer') as HTMLButtonElement;
|
||||
|
||||
btnCancelSw?.addEventListener('click', closeModals);
|
||||
btnCloseSw?.addEventListener('click', closeModals);
|
||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||
btnCloseHeader?.addEventListener('click', closeModals);
|
||||
btnCloseFooter?.addEventListener('click', closeModals);
|
||||
|
||||
btnSaveSw?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!isEditMode) {
|
||||
setEditMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!swForm.checkValidity()) { swForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
|
||||
const start = (document.getElementById('sw-구독일-시작') as HTMLInputElement).value;
|
||||
const end = (document.getElementById('sw-구독일-종료') as HTMLInputElement).value;
|
||||
const 구독일Str = (start || end) ? `${start || ''} ~ ${end || ''}` : '';
|
||||
|
||||
const newAsset: SoftwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: (document.getElementById('sw-asset-type') as HTMLInputElement).value,
|
||||
@@ -112,7 +234,7 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
||||
부서: (document.getElementById('sw-부서') as HTMLInputElement).value,
|
||||
제품명: (document.getElementById('sw-제품명') as HTMLInputElement).value,
|
||||
구매일: (document.getElementById('sw-구매일') as HTMLInputElement).value,
|
||||
구독일: (document.getElementById('sw-구독일') as HTMLInputElement).value,
|
||||
구독일: 구독일Str,
|
||||
유지보수여부: (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked,
|
||||
금액: (document.getElementById('sw-금액') as HTMLInputElement).value,
|
||||
수량: parseInt((document.getElementById('sw-수량') as HTMLInputElement).value || '1', 10),
|
||||
@@ -141,15 +263,119 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
||||
renderContent();
|
||||
}
|
||||
});
|
||||
|
||||
// Update Sub-modal integration
|
||||
const subModal = document.getElementById('sw-update-modal')!;
|
||||
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
|
||||
const btnCloseUpdate = document.getElementById('btn-close-sw-update')!;
|
||||
const btnCancelUpdate = document.getElementById('btn-cancel-sw-update')!;
|
||||
const btnSaveUpdate = document.getElementById('btn-save-sw-update')!;
|
||||
|
||||
const closeUpdateModal = () => subModal.classList.add('hidden');
|
||||
btnCloseUpdate?.addEventListener('click', closeUpdateModal);
|
||||
btnCancelUpdate?.addEventListener('click', closeUpdateModal);
|
||||
|
||||
btnOpenUpdate?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const isSub = (document.getElementById('sw-asset-type') as HTMLInputElement).value === '구독SW';
|
||||
subModal.classList.remove('hidden');
|
||||
|
||||
// Set default values
|
||||
(document.getElementById('sw-update-date') as HTMLInputElement).value = new Date().toISOString().substring(0, 10);
|
||||
(document.getElementById('sw-update-start') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-update-end') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-update-cost') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-update-note') as HTMLInputElement).value = '';
|
||||
|
||||
if (isSub) {
|
||||
document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
|
||||
document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:none');
|
||||
} else {
|
||||
document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:none');
|
||||
document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
|
||||
(document.getElementById('sw-update-maintenance') as HTMLInputElement).checked = (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked;
|
||||
}
|
||||
});
|
||||
|
||||
btnSaveUpdate?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
|
||||
if (!id) { alert('자산이 저장되지 않았습니다. 메인 폼을 먼저 저장해주세요.'); return; }
|
||||
|
||||
const isSub = (document.getElementById('sw-asset-type') as HTMLInputElement).value === '구독SW';
|
||||
const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
|
||||
const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
|
||||
const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
|
||||
const maintenance = (document.getElementById('sw-update-maintenance') as HTMLInputElement).checked;
|
||||
const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
|
||||
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
|
||||
|
||||
const periodStr = (start || end) ? `${start || ''} ~ ${end || ''}` : '';
|
||||
|
||||
let details = `[업데이트] ${note || (isSub ? '구독 갱신' : '유지보수 계약')}\n`;
|
||||
if (cost) details += `발생 비용: ${cost}원\n`;
|
||||
|
||||
if (isSub) {
|
||||
if (periodStr) details += `구독 변경: -> ${periodStr}\n`;
|
||||
// Always update main fields if period is provided
|
||||
if (periodStr) {
|
||||
(document.getElementById('sw-구독일-시작') as HTMLInputElement).value = start;
|
||||
(document.getElementById('sw-구독일-종료') as HTMLInputElement).value = end;
|
||||
}
|
||||
} else {
|
||||
details += `유지보수 상태: -> ${maintenance ? '유효' : '없음'}\n`;
|
||||
(document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = maintenance;
|
||||
}
|
||||
|
||||
if (cost) (document.getElementById('sw-금액') as HTMLInputElement).value = cost;
|
||||
|
||||
state.masterData.logs.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
assetId: id,
|
||||
date,
|
||||
details,
|
||||
user: '관리자'
|
||||
});
|
||||
|
||||
closeUpdateModal();
|
||||
renderSwHistory(id);
|
||||
|
||||
// 메인 테이블 리렌더링도 트리거 (뒤에 보일 수 있게)
|
||||
renderContent();
|
||||
});
|
||||
}
|
||||
|
||||
function renderSwHistory(assetId: string) {
|
||||
const historyList = document.getElementById('sw-history-list');
|
||||
if (!historyList) return;
|
||||
|
||||
const logs = state.masterData.logs
|
||||
.filter(l => l.assetId === assetId)
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
if (logs.length === 0) {
|
||||
historyList.innerHTML = '<div class="empty-history">업데이트 내역이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
historyList.innerHTML = logs.map(log => `
|
||||
<div class="history-item">
|
||||
<div class="history-date">${log.date}</div>
|
||||
<div class="history-user">작업자: ${log.user}</div>
|
||||
<div class="history-details">${log.details.replace(/\\n/g, '<br>')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
createIcons({ icons: { X, History, Plus } });
|
||||
}
|
||||
|
||||
export function openSwModal(asset?: SoftwareAsset) {
|
||||
currentAsset = asset || null;
|
||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
||||
|
||||
openModal('sw-asset-modal');
|
||||
swForm.reset();
|
||||
|
||||
|
||||
const subGroup = document.getElementById('sw-구독일-group')!;
|
||||
const permGroup = document.getElementById('sw-유지보수-group')!;
|
||||
if (state.activeSubTab === '구독SW') {
|
||||
@@ -163,28 +389,17 @@ export function openSwModal(asset?: SoftwareAsset) {
|
||||
if (asset) {
|
||||
document.getElementById('sw-modal-title')!.textContent = `${state.activeSubTab} 상세 정보 수정`;
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
|
||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = asset.분야 || '업무공통';
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('sw-부서') as HTMLInputElement).value = asset.부서 || '';
|
||||
(document.getElementById('sw-제품명') as HTMLInputElement).value = asset.제품명;
|
||||
(document.getElementById('sw-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('sw-구독일') as HTMLInputElement).value = asset.구독일 || '';
|
||||
(document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = !!asset.유지보수여부;
|
||||
(document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||
(document.getElementById('sw-수량') as HTMLInputElement).value = String(asset.수량);
|
||||
(document.getElementById('sw-계정명') as HTMLInputElement).value = asset.계정명 || '';
|
||||
(document.getElementById('sw-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('sw-비고') as HTMLInputElement).value = asset.비고 || '';
|
||||
fillFormData(asset);
|
||||
setEditMode(false);
|
||||
} else {
|
||||
document.getElementById('sw-modal-title')!.textContent = `신규 ${state.activeSubTab} 자산 추가`;
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = '업무공통';
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = '한맥';
|
||||
(document.getElementById('sw-부서') as HTMLInputElement).value = '';
|
||||
document.getElementById('btn-open-sw-update')!.style.display = 'none';
|
||||
renderSwHistory('');
|
||||
setEditMode(true);
|
||||
}
|
||||
|
||||
createIcons({ icons: { X, History, Plus } });
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ export function initSwUserModal(renderContent: () => void, closeModals: () => vo
|
||||
btnSaveSwUserMapping?.addEventListener('click', () => {
|
||||
state.masterData.swUsers = state.masterData.swUsers.filter(u => u.swId !== currentSwUserAssetId);
|
||||
state.masterData.swUsers.push(...tempSwUsers);
|
||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
||||
closeModals();
|
||||
renderContent();
|
||||
});
|
||||
|
||||
btnCancelUserEdit?.addEventListener('click', () => document.getElementById('sw-user-edit-modal')?.classList.add('hidden'));
|
||||
btnCloseUserEdit?.addEventListener('click', () => document.getElementById('sw-user-edit-modal')?.classList.add('hidden'));
|
||||
btnCancelUserModal?.addEventListener('click', () => document.getElementById('sw-user-modal')?.classList.add('hidden'));
|
||||
btnCloseUserModal?.addEventListener('click', () => document.getElementById('sw-user-modal')?.classList.add('hidden'));
|
||||
btnCancelUserModal?.addEventListener('click', closeModals);
|
||||
btnCloseUserModal?.addEventListener('click', closeModals);
|
||||
}
|
||||
|
||||
function renderUserList() {
|
||||
|
||||
@@ -29,7 +29,8 @@ const STORAGE_MODAL_HTML = `
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-storage-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-storage-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-revert-storage-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-close-storage-footer" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-storage-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,26 +38,73 @@ const STORAGE_MODAL_HTML = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
export let currentAsset: HardwareAsset | null = null;
|
||||
export let isEditMode = false;
|
||||
|
||||
export function setEditMode(edit: boolean) {
|
||||
isEditMode = edit;
|
||||
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
|
||||
const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement;
|
||||
const btnRevertEdit = document.getElementById('btn-revert-storage-edit') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-storage-footer') as HTMLButtonElement;
|
||||
|
||||
if (edit) {
|
||||
storageForm.classList.add('is-edit-mode');
|
||||
storageForm.classList.remove('is-view-mode');
|
||||
btnSaveStorage.textContent = '저장';
|
||||
btnRevertEdit.classList.remove('hidden');
|
||||
btnCloseFooter.classList.add('hidden');
|
||||
} else {
|
||||
storageForm.classList.add('is-view-mode');
|
||||
storageForm.classList.remove('is-edit-mode');
|
||||
btnSaveStorage.textContent = '수정';
|
||||
btnRevertEdit.classList.add('hidden');
|
||||
btnCloseFooter.classList.remove('hidden');
|
||||
if (currentAsset) fillFormData(currentAsset);
|
||||
}
|
||||
}
|
||||
|
||||
export function fillFormData(asset: HardwareAsset) {
|
||||
(document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('storage-법인') as HTMLInputElement).value = asset.법인;
|
||||
(document.getElementById('storage-유형') as HTMLInputElement).value = asset.storage유형 || 'NAS';
|
||||
(document.getElementById('storage-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('storage-명칭') as HTMLInputElement).value = asset.명칭;
|
||||
(document.getElementById('storage-위치') as HTMLInputElement).value = asset.위치 || '';
|
||||
(document.getElementById('storage-모델명') as HTMLInputElement).value = asset.모델명 || '';
|
||||
(document.getElementById('storage-용량') as HTMLInputElement).value = asset.용량 || '';
|
||||
(document.getElementById('storage-담당자_정') as HTMLInputElement).value = asset.담당자_정 || '';
|
||||
(document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
|
||||
(document.getElementById('storage-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('storage-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||
}
|
||||
|
||||
export function initStorageModal(renderContent: () => void, closeModals: () => void) {
|
||||
if (!document.getElementById('storage-asset-modal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', STORAGE_MODAL_HTML);
|
||||
}
|
||||
|
||||
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
|
||||
const btnRevertEdit = document.getElementById('btn-revert-storage-edit') as HTMLButtonElement;
|
||||
const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement;
|
||||
const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement;
|
||||
const btnCancelStorage = document.getElementById('btn-cancel-storage-modal') as HTMLButtonElement;
|
||||
const btnCloseStorage = document.getElementById('btn-close-storage-modal') as HTMLButtonElement;
|
||||
const btnCloseHeader = document.getElementById('btn-close-storage-modal') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-storage-footer') as HTMLButtonElement;
|
||||
|
||||
btnCancelStorage?.addEventListener('click', closeModals);
|
||||
btnCloseStorage?.addEventListener('click', closeModals);
|
||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||
btnCloseHeader?.addEventListener('click', closeModals);
|
||||
btnCloseFooter?.addEventListener('click', closeModals);
|
||||
|
||||
btnSaveStorage?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!isEditMode) {
|
||||
setEditMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!storageForm.checkValidity()) { storageForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value;
|
||||
|
||||
const newAsset: HardwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: '스토리지',
|
||||
@@ -97,6 +145,7 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v
|
||||
}
|
||||
|
||||
export function openStorageModal(asset?: HardwareAsset) {
|
||||
currentAsset = asset || null;
|
||||
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-storage-asset')!;
|
||||
|
||||
@@ -106,22 +155,12 @@ export function openStorageModal(asset?: HardwareAsset) {
|
||||
if (asset) {
|
||||
document.getElementById('storage-modal-title')!.textContent = '스토리지 상세 정보 수정';
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
(document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('storage-법인') as HTMLInputElement).value = asset.법인;
|
||||
(document.getElementById('storage-유형') as HTMLInputElement).value = asset.storage유형 || 'NAS';
|
||||
(document.getElementById('storage-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('storage-명칭') as HTMLInputElement).value = asset.명칭;
|
||||
(document.getElementById('storage-위치') as HTMLInputElement).value = asset.위치 || '';
|
||||
(document.getElementById('storage-모델명') as HTMLInputElement).value = asset.모델명 || '';
|
||||
(document.getElementById('storage-용량') as HTMLInputElement).value = asset.용량 || '';
|
||||
(document.getElementById('storage-담당자_정') as HTMLInputElement).value = asset.담당자_정 || '';
|
||||
(document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
|
||||
(document.getElementById('storage-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('storage-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||
fillFormData(asset);
|
||||
setEditMode(false);
|
||||
} else {
|
||||
document.getElementById('storage-modal-title')!.textContent = '신규 스토리지 자산 추가';
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('storage-asset-id') as HTMLInputElement).value = '';
|
||||
setEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
80
src/components/Navigation.ts
Normal file
80
src/components/Navigation.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { state } from '../core/state';
|
||||
|
||||
const MENU_CONFIG = {
|
||||
hw: {
|
||||
label: '하드웨어',
|
||||
tabs: ['대시보드', '개인PC', '서버', '스토리지', '전산비품']
|
||||
},
|
||||
sw: {
|
||||
label: '소프트웨어',
|
||||
tabs: ['대시보드', '구독SW', '영구SW']
|
||||
},
|
||||
ops: {
|
||||
label: '운영 서비스',
|
||||
tabs: ['대시보드', '서비스현황', '백업관리', '보안점검']
|
||||
}
|
||||
};
|
||||
|
||||
export function renderNavigation(onTabChange: (tab: string) => void) {
|
||||
const navContainer = document.getElementById('main-nav')!;
|
||||
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
|
||||
|
||||
const render = () => {
|
||||
navContainer.innerHTML = '';
|
||||
|
||||
(Object.keys(MENU_CONFIG) as Array<keyof typeof MENU_CONFIG>).forEach(catKey => {
|
||||
const config = MENU_CONFIG[catKey];
|
||||
const isActive = state.activeCategory === catKey;
|
||||
|
||||
const group = document.createElement('div');
|
||||
group.className = `nav-group ${isActive ? 'active is-showing-shelf' : ''}`;
|
||||
|
||||
// 메인 카테고리 트리거
|
||||
const trigger = document.createElement('div');
|
||||
trigger.className = 'gnb-trigger';
|
||||
trigger.textContent = config.label;
|
||||
|
||||
trigger.addEventListener('click', () => {
|
||||
if (state.activeCategory !== catKey) {
|
||||
state.activeCategory = catKey;
|
||||
state.activeSubTab = '대시보드';
|
||||
if (btnAddAsset) btnAddAsset.classList.add('hidden');
|
||||
render();
|
||||
onTabChange('대시보드');
|
||||
}
|
||||
});
|
||||
group.appendChild(trigger);
|
||||
|
||||
// 하위 탭 선반 (Shelf)
|
||||
const shelf = document.createElement('div');
|
||||
shelf.className = 'lnb-shelf';
|
||||
|
||||
config.tabs.forEach(tab => {
|
||||
const item = document.createElement('div');
|
||||
item.className = `lnb-item ${isActive && state.activeSubTab === tab ? 'active' : ''}`;
|
||||
item.textContent = tab;
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
state.activeCategory = catKey;
|
||||
state.activeSubTab = tab;
|
||||
|
||||
if (btnAddAsset) {
|
||||
if (tab === '대시보드') btnAddAsset.classList.add('hidden');
|
||||
else btnAddAsset.classList.remove('hidden');
|
||||
}
|
||||
|
||||
render();
|
||||
onTabChange(tab);
|
||||
});
|
||||
shelf.appendChild(item);
|
||||
});
|
||||
group.appendChild(shelf);
|
||||
|
||||
// 마우스 오버 시 다른 그룹의 선반은 가리고 내 것만 보여주는 스타일은 CSS에서 처리함
|
||||
navContainer.appendChild(group);
|
||||
});
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { state } from '../core/state';
|
||||
|
||||
export function renderSidebar(onTabChange: (tab: string) => void) {
|
||||
const navItems = document.querySelectorAll('.nav-list li');
|
||||
const titleElement = document.getElementById('current-tab-title') as HTMLHeadingElement;
|
||||
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
// 탭 UI 업데이트
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// 상태 업데이트
|
||||
state.activeCategory = item.getAttribute('data-category') as 'hw' | 'sw';
|
||||
state.activeSubTab = item.getAttribute('data-tab') || '대시보드';
|
||||
|
||||
// 타이틀 업데이트
|
||||
const catName = state.activeCategory === 'hw' ? '하드웨어' : '소프트웨어';
|
||||
if (titleElement) {
|
||||
titleElement.textContent = `${catName} / ${state.activeSubTab}`;
|
||||
}
|
||||
|
||||
// 추가 버튼 노출 여부
|
||||
if (btnAddAsset) {
|
||||
if (state.activeSubTab === '대시보드') {
|
||||
btnAddAsset.classList.add('hidden');
|
||||
} else {
|
||||
btnAddAsset.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 탭 변경 콜백 실행
|
||||
onTabChange(state.activeSubTab);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user