Compare commits
3 Commits
c5c6acea6a
...
6904925146
| Author | SHA1 | Date | |
|---|---|---|---|
| 6904925146 | |||
| 97478dd9bc | |||
| b3f7920176 |
33
docs/issues/issue_sw_modal_refactor.md
Normal file
33
docs/issues/issue_sw_modal_refactor.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# [이슈] S/W 자산 관리 고도화 및 이력 추적 기능 구현
|
||||
|
||||
## 1. 개요
|
||||
소프트웨어 자산의 라이프사이클을 체계적으로 관리하기 위해 상세 정보 모달을 개편하고, 갱신(업데이트) 이력을 추적할 수 있는 기능을 구현하였습니다. 또한, 사용자의 가독성을 위해 상태를 나타내는 자동 뱃지를 도입하고 날짜 입력 편의성을 개선하였습니다.
|
||||
|
||||
## 2. 작업 상세 내용
|
||||
|
||||
### A. S/W 목록(Table) 개선
|
||||
- **상태 자동 계산 시스템 도입**:
|
||||
- 구독 S/W: 만료일 기준 **[사용중] / [만료]** 자동 표시.
|
||||
- 영구 S/W: 유지보수 대상 여부에 따라 **[유효] / [없음]** 표시.
|
||||
- **UI 뱃지 적용**: 테이블 좌측에 상태 뱃지를 추가하여 시각적 인지도를 높임.
|
||||
|
||||
### B. 상세 정보 모달 개편 (`SWModal.ts`)
|
||||
- **2단 분할 레이아웃 적용**: 좌측(기본 정보), 우측(업데이트 타임라인)으로 UI 재설계.
|
||||
- **날짜 입력 필드 개선**:
|
||||
- '구매일' 필드에 캘린더 피커(Calendar Picker) 적용.
|
||||
- '구독 기간' 필드를 **시작일**과 **종료일**로 분리하여 각각 캘린더 적용.
|
||||
- 직접 입력("yyyy-mm-dd") 형식도 동시 지원.
|
||||
|
||||
### C. 계약 업데이트(갱신) 관리 기능
|
||||
- **[업데이트 추가]** 버튼 및 전용 서브 팝업 구현.
|
||||
- 갱신 시 발생하는 비용, 기간 연장, 메모를 기록하여 타임라인(Log)에 누적.
|
||||
- 업데이트 반영 시 메인 자산 정보의 구독 기한 및 누적 금액이 자동으로 최신화되도록 연동.
|
||||
|
||||
## 3. 관련 파일
|
||||
- `src/views/SW_Table.ts`: 테이블 상태 로직 및 뱃지 렌더링.
|
||||
- `src/components/Modal/SWModal.ts`: 모달 UI 및 날짜 처리, 업데이트 로직.
|
||||
- `src/styles/modal.css`: 분할 레이아웃 및 타임라인 스타일.
|
||||
|
||||
## 4. 확인 사항
|
||||
- 엑셀 업로드/다운로드 시 기존 '구독일' 문자열 형식과의 호환성 유지 확인.
|
||||
- 브라우저 테스트를 통한 캘린더 작동 및 테이블 상태 연동 확인 완료.
|
||||
@@ -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;
|
||||
|
||||
@@ -1,78 +1,87 @@
|
||||
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>
|
||||
@@ -84,8 +93,109 @@ const SW_MODAL_HTML = `
|
||||
</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);
|
||||
@@ -98,44 +208,6 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
||||
const btnCloseHeader = document.getElementById('btn-close-sw-modal') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-sw-footer') as HTMLButtonElement;
|
||||
|
||||
let isEditMode = false;
|
||||
let currentAsset: SoftwareAsset | null = null;
|
||||
|
||||
const setEditMode = (edit: boolean) => {
|
||||
isEditMode = edit;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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.구매일 || '';
|
||||
(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.비고 || '';
|
||||
}
|
||||
|
||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||
btnCloseHeader?.addEventListener('click', closeModals);
|
||||
btnCloseFooter?.addEventListener('click', closeModals);
|
||||
@@ -150,6 +222,10 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
||||
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,
|
||||
@@ -158,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),
|
||||
@@ -187,6 +263,109 @@ 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) {
|
||||
@@ -217,6 +396,10 @@ export function openSwModal(asset?: SoftwareAsset) {
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||
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() {
|
||||
|
||||
@@ -38,6 +38,47 @@ 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);
|
||||
@@ -50,42 +91,6 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v
|
||||
const btnCloseHeader = document.getElementById('btn-close-storage-modal') as HTMLButtonElement;
|
||||
const btnCloseFooter = document.getElementById('btn-close-storage-footer') as HTMLButtonElement;
|
||||
|
||||
let isEditMode = false;
|
||||
let currentAsset: HardwareAsset | null = null;
|
||||
|
||||
const setEditMode = (edit: boolean) => {
|
||||
isEditMode = edit;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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.금액 || '';
|
||||
}
|
||||
|
||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||
btnCloseHeader?.addEventListener('click', closeModals);
|
||||
btnCloseFooter?.addEventListener('click', closeModals);
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface HardwareAsset {
|
||||
서버PW?: string;
|
||||
모니터링?: string;
|
||||
비고?: string;
|
||||
현사용조직?: string;
|
||||
이전사용조직?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,10 +90,12 @@ export interface MasterAssetData {
|
||||
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
||||
const SW_TABS = ['구독SW', '영구SW'];
|
||||
|
||||
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명'];
|
||||
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', '구매일', '금액', '납품업체', '품의서명'];
|
||||
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage1', 'Storage2', 'Storage3', '모니터링', '비고'];
|
||||
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명'];
|
||||
// 확장된 헤더 (상세 페이지의 모든 필드 포함)
|
||||
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '서버ID', '서버PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', '모니터링', '비고'];
|
||||
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||
|
||||
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
||||
@@ -109,16 +113,24 @@ export function downloadTemplate() {
|
||||
|
||||
if (tab === '개인PC') {
|
||||
hd = PC_HEADERS;
|
||||
wscols = [{wch:15}, {wch:25}, {wch:15}, {wch:20}, {wch:20}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
wscols = Array(hd.length).fill({wch: 15});
|
||||
wscols[1] = {wch: 25}; // 자산코드
|
||||
wscols[12] = {wch: 30}; // HW사양
|
||||
} else if (tab === '서버') {
|
||||
hd = SERVER_HEADERS;
|
||||
wscols = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}];
|
||||
wscols = Array(hd.length).fill({wch: 15});
|
||||
wscols[3] = {wch: 25}; // 용도
|
||||
wscols[4] = {wch: 30}; // 상세내용
|
||||
} else if (tab === '스토리지') {
|
||||
hd = STORAGE_HEADERS;
|
||||
wscols = [{wch:15}, {wch:15}, {wch:25}, {wch:25}, {wch:20}, {wch:25}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
wscols = Array(hd.length).fill({wch: 15});
|
||||
wscols[2] = {wch: 25}; // 자산코드
|
||||
wscols[3] = {wch: 25}; // 명칭
|
||||
} else {
|
||||
hd = HW_HEADERS;
|
||||
wscols = [{wch:15}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:40}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
wscols = Array(hd.length).fill({wch: 15});
|
||||
wscols[2] = {wch: 25}; // 명칭
|
||||
wscols[7] = {wch: 30}; // HW사양
|
||||
}
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
||||
@@ -158,27 +170,44 @@ export function exportToExcel(masterData: MasterAssetData) {
|
||||
if (tab === '개인PC') {
|
||||
wsData = [
|
||||
PC_HEADERS,
|
||||
...targetAssets.map(a => [a.법인, a.자산코드, a.사용자, a.위치, a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.구매일, a.금액, a.납품업체, a.품의서명])
|
||||
...targetAssets.map(a => [
|
||||
a.법인, a.자산코드, a.사용자, a.위치, a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고
|
||||
])
|
||||
];
|
||||
colsConfig = [{wch:15}, {wch:25}, {wch:15}, {wch:20}, {wch:20}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
colsConfig = Array(PC_HEADERS.length).fill({wch: 15});
|
||||
colsConfig[1] = {wch: 25};
|
||||
colsConfig[12] = {wch: 30};
|
||||
} else if (tab === '서버') {
|
||||
wsData = [
|
||||
SERVER_HEADERS,
|
||||
...targetAssets.map(a => [a.법인, a.자산코드, a.storage유형 || '물리', a.용도 || '', a.위치, a.담당자_정 || '', a.담당자_부 || '', a.IP주소, a.원격접속 || '', a.모델명 || '', a.OS, a.CPU, a.RAM, a.GPU || '', a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a.모니터링 || '', a.비고 || ''])
|
||||
...targetAssets.map(a => [
|
||||
a.법인, a.자산코드, a.storage유형 || '물리', a.용도 || '', a.상세 || '', a.현사용조직 || '', a.이전사용조직 || '', a.위치, a.담당자_정 || '', a.담당자_부 || '',
|
||||
a.IP주소, a.원격접속 || '', a.서버ID || '', a.서버PW || '', a.모델명 || '', a.OS, a.CPU, a.RAM, a.GPU || '', a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a.모니터링 || '', a.비고 || ''
|
||||
])
|
||||
];
|
||||
colsConfig = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}];
|
||||
colsConfig = Array(SERVER_HEADERS.length).fill({wch: 15});
|
||||
colsConfig[3] = {wch: 25};
|
||||
colsConfig[4] = {wch: 30};
|
||||
} else if (tab === '스토리지') {
|
||||
wsData = [
|
||||
STORAGE_HEADERS,
|
||||
...targetAssets.map(a => [a.법인, a.storage유형, a.자산코드, a.명칭, a.위치, a.모델명, a.용량, a.담당자_정, a.담당자_부, a.IP주소, a.MACaddress, a.구매일, a.금액, a.납품업체, a.품의서명])
|
||||
...targetAssets.map(a => [
|
||||
a.법인, a.storage유형, a.자산코드, a.명칭, a.위치, a.모델명, a.용량, a.담당자_정, a.담당자_부, a.IP주소, a.MACaddress, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고
|
||||
])
|
||||
];
|
||||
colsConfig = [{wch:15}, {wch:15}, {wch:25}, {wch:25}, {wch:20}, {wch:25}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
colsConfig = Array(STORAGE_HEADERS.length).fill({wch: 15});
|
||||
colsConfig[2] = {wch: 25};
|
||||
colsConfig[3] = {wch: 25};
|
||||
} else {
|
||||
wsData = [
|
||||
HW_HEADERS,
|
||||
...targetAssets.map(a => [a.법인, a.자산코드, a.명칭, a.위치, a.관리자, a.IP주소, a.MACaddress, a.HW사양, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명])
|
||||
...targetAssets.map(a => [
|
||||
a.법인, a.자산코드, a.명칭, a.위치, a.관리자, a.IP주소, a.MACaddress, a.HW사양, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고
|
||||
])
|
||||
];
|
||||
colsConfig = [{wch:15}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:40}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
colsConfig = Array(HW_HEADERS.length).fill({wch: 15});
|
||||
colsConfig[2] = {wch: 25};
|
||||
colsConfig[7] = {wch: 30};
|
||||
}
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
||||
@@ -252,11 +281,16 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
명칭: '',
|
||||
위치: row['위치'] || '',
|
||||
사용자: row['사용자'] || '',
|
||||
관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '',
|
||||
관리자: '',
|
||||
IP주소: row['IP주소'] || '',
|
||||
MACaddress: '',
|
||||
HW사양: row['HW사양'] || '',
|
||||
OS: row['OS'] || '',
|
||||
CPU: row['CPU'] || '', GPU: row['GPU'] || '', RAM: row['RAM'] || '',
|
||||
SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '',
|
||||
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||
비고: row['비고'] || ''
|
||||
});
|
||||
} else if (sheetName === '서버') {
|
||||
hwAssets.push({
|
||||
@@ -265,14 +299,24 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
법인: row['법인'] || '',
|
||||
자산코드: row['자산번호'] || row['자산코드'] || '',
|
||||
명칭: row['용도'] || row['명칭'] || '',
|
||||
용도: row['용도'] || '', 위치: row['설치위치'] || row['위치'] || '',
|
||||
용도: row['용도'] || '',
|
||||
상세: row['상세내용'] || row['상세'] || '',
|
||||
현사용조직: row['현사용조직'] || '',
|
||||
이전사용조직: row['이전사용조직'] || '',
|
||||
위치: row['설치위치'] || row['위치'] || '',
|
||||
관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
||||
IP주소: row['IP 주소'] || row['IP주소'] || '', IP2: row['IP2'] || '',
|
||||
원격접속: row['원격접속'] || '', 서버ID: row['서버ID'] || '', 서버PW: row['서버PW'] || '',
|
||||
IP주소: row['IP 주소'] || row['IP주소'] || '',
|
||||
원격접속: row['원격접속'] || '',
|
||||
서버ID: row['서버ID'] || '',
|
||||
서버PW: row['서버PW'] || '',
|
||||
모델명: row['모델명'] || '', OS: row['OS'] || '',
|
||||
CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '',
|
||||
SSD1: row['Storage1'] || row['SSD1'] || '', SSD2: row['Storage2'] || row['SSD2'] || '', HDD1: row['Storage3'] || row['HDD1'] || '',
|
||||
모니터링: row['모니터링'] || '', 비고: row['비고'] || '', storage유형: row['유형'] || '물리',
|
||||
SSD1: row['SSD1'] || row['Storage1'] || '',
|
||||
SSD2: row['SSD2'] || row['Storage2'] || '',
|
||||
HDD1: row['HDD1'] || row['Storage3'] || '',
|
||||
모니터링: row['모니터링'] || '',
|
||||
비고: row['비고'] || '',
|
||||
storage유형: row['유형'] || '물리',
|
||||
MACaddress: '', HW사양: '', 구매일: '', 금액: '', 납품업체: '', 품의서명: '',
|
||||
});
|
||||
} else if (sheetName === '스토리지') {
|
||||
@@ -285,6 +329,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
||||
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||
비고: row['비고'] || ''
|
||||
});
|
||||
} else {
|
||||
hwAssets.push({
|
||||
@@ -295,6 +340,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
HW사양: row['HW사양'] || '', OS: row['OS'] || '',
|
||||
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||
비고: row['비고'] || ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,7 +14,9 @@ const dummy = generateDummyData();
|
||||
// 서버 데이터만 실제 데이터로 교체
|
||||
const mergedHw: HardwareAsset[] = [
|
||||
...dummy.hw.filter(a => a.type !== '서버'),
|
||||
...realServerData.map(s => ({
|
||||
...realServerData.map((serverData: any) => {
|
||||
const s = serverData;
|
||||
return {
|
||||
id: s.id || Math.random().toString(36).substring(2, 9),
|
||||
type: '서버',
|
||||
법인: s.법인,
|
||||
@@ -47,7 +49,7 @@ const mergedHw: HardwareAsset[] = [
|
||||
서버PW: s.서버PW || '',
|
||||
모니터링: s.모니터링 || '',
|
||||
비고: s.비고 || ''
|
||||
}))
|
||||
}})
|
||||
];
|
||||
|
||||
// --- Initial State ---
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
--text-muted: #6B7280;
|
||||
--border-color: #E5E7EB;
|
||||
--bg-color: #F9FAFB;
|
||||
--bg-light: #FAFAFA;
|
||||
--sidebar-bg: #ffffff;
|
||||
--white: #FFFFFF;
|
||||
--danger: #dc2626;
|
||||
|
||||
|
||||
@@ -265,3 +265,78 @@
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Dashboard Detail Modal Table Fixed Header */
|
||||
#dashboard-detail-modal .modal-body {
|
||||
overflow-y: auto;
|
||||
max-height: calc(80vh - 120px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 모달 내부의 table-container 기존 전역 스타일 무력화 */
|
||||
#dashboard-detail-modal .table-container {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#dashboard-detail-modal table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
#dashboard-detail-modal thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg-light);
|
||||
z-index: 10;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
box-shadow: none;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#dashboard-detail-modal tbody td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-main);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#dashboard-detail-modal tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#dashboard-detail-modal tbody tr:hover {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
|
||||
/* 뱃지 스타일 (대시보드 상세 목록용) */
|
||||
#dashboard-detail-modal .badge {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#dashboard-detail-modal .badge-outline {
|
||||
border: 1px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
#dashboard-detail-modal .badge-sw {
|
||||
border: 1px solid #3b82f6;
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export function renderTable(mainContent: HTMLElement) {
|
||||
createIcons({
|
||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw }
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('❌ Error rendering table view:', err);
|
||||
mainContent.innerHTML = `<div style="padding:2rem; color:var(--danger);">목록을 불러오는 중 오류가 발생했습니다: ${err.message}</div>`;
|
||||
mainContent.innerHTML = `<div style="padding:2rem; color:var(--danger);">목록을 불러오는 중 오류가 발생했습니다: ${err?.message || err}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function renderSwList(container: HTMLElement) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center;">No.</th>
|
||||
<th style="text-align:center;">상태</th>
|
||||
<th style="text-align:center;">분야</th>
|
||||
<th style="text-align:center;">법인</th>
|
||||
<th style="text-align:center;">부서</th>
|
||||
@@ -81,7 +82,7 @@ export function renderSwList(container: HTMLElement) {
|
||||
|
||||
tbody.innerHTML = '';
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="${isSub ? 11 : 10}" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="${isSub ? 12 : 11}" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,11 +90,40 @@ export function renderSwList(container: HTMLElement) {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
|
||||
const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10);
|
||||
const avail = qty - assigned;
|
||||
|
||||
let statusHtml = '';
|
||||
if (isSub) {
|
||||
let isExpired = false;
|
||||
if (asset.구독일) {
|
||||
const parts = asset.구독일.split('~');
|
||||
const endDateStr = parts[parts.length - 1].trim().replace(/\./g, '-');
|
||||
const endDate = new Date(endDateStr);
|
||||
if (!isNaN(endDate.getTime())) {
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
if (endDate < new Date()) {
|
||||
isExpired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isExpired) {
|
||||
statusHtml = `<span style="background: var(--danger, #ef4444); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">만료</span>`;
|
||||
} else {
|
||||
statusHtml = `<span style="background: var(--primary-color, #1E5149); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">사용중</span>`;
|
||||
}
|
||||
} else {
|
||||
if (asset.유지보수여부) {
|
||||
statusHtml = `<span style="background: #3b82f6; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">유효</span>`;
|
||||
} else {
|
||||
statusHtml = `<span style="background: #6b7280; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">없음</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="text-align:center;">${idx+1}</td>
|
||||
<td style="text-align:center;">${statusHtml}</td>
|
||||
<td>${asset.분야||''}</td>
|
||||
<td>${asset.법인}</td>
|
||||
<td>${asset.부서||''}</td>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
|
||||
Reference in New Issue
Block a user