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 { state } from '../../core/state';
|
||||||
import { HardwareAsset } from '../../core/excelHandler';
|
import { HardwareAsset } from '../../core/excelHandler';
|
||||||
import { renderTable } from '../../views/AssetTableView';
|
|
||||||
import { createIcons, Paperclip } from 'lucide';
|
import { createIcons, Paperclip } from 'lucide';
|
||||||
|
|
||||||
let currentAsset: HardwareAsset | null = null;
|
let currentAsset: HardwareAsset | null = null;
|
||||||
|
|||||||
@@ -1,50 +1,48 @@
|
|||||||
import { state } from '../../core/state';
|
import { state } from '../../core/state';
|
||||||
import { SoftwareAsset } from '../../core/excelHandler';
|
import { SoftwareAsset } from '../../core/excelHandler';
|
||||||
import { openModal } from './BaseModal';
|
import { openModal } from './BaseModal';
|
||||||
|
import { createIcons, X, History, Plus } from 'lucide';
|
||||||
|
|
||||||
const SW_MODAL_HTML = `
|
const SW_MODAL_HTML = `
|
||||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content">
|
<div class="modal-content wide">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="sw-modal-title">S/W 상세 정보</h2>
|
<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>
|
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<div class="modal-body-split">
|
||||||
|
<div class="modal-form-area">
|
||||||
<form id="sw-asset-form" class="grid-form">
|
<form id="sw-asset-form" class="grid-form">
|
||||||
<input type="hidden" id="sw-asset-id" />
|
<input type="hidden" id="sw-asset-id" />
|
||||||
<input type="hidden" id="sw-asset-type" />
|
<input type="hidden" id="sw-asset-type" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-분야">분야</label>
|
<label for="sw-분야">분야</label>
|
||||||
<select id="sw-분야" required>
|
<select id="sw-분야" required>
|
||||||
<option value="업무공통">업무공통</option>
|
<option value="업무공통">업무공통</option><option value="개발S/W">개발S/W</option><option value="디자인">디자인</option><option value="설계S/W">설계S/W</option>
|
||||||
<option value="개발S/W">개발S/W</option>
|
|
||||||
<option value="디자인">디자인</option>
|
|
||||||
<option value="설계S/W">설계S/W</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-법인">법인</label>
|
<label for="sw-법인">법인</label>
|
||||||
<select id="sw-법인" required>
|
<select id="sw-법인" required>
|
||||||
<option value="한맥">한맥 (HM)</option>
|
<option value="한맥">한맥 (HM)</option><option value="삼안 (SM)">삼안 (SM)</option><option value="바론 (BR)">바론 (BR)</option>
|
||||||
<option value="삼안 (SM)">삼안 (SM)</option>
|
|
||||||
<option value="바론 (BR)">바론 (BR)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-부서">부서</label>
|
<label for="sw-부서">부서</label>
|
||||||
<input type="text" id="sw-부서" placeholder="ex) 경영지원팀" required />
|
<input type="text" id="sw-부서" placeholder="ex) 경영지원팀" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="sw-제품명">제품명</label>
|
|
||||||
<input type="text" id="sw-제품명" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-구매일">구매일</label>
|
<label for="sw-구매일">구매일</label>
|
||||||
<input type="text" id="sw-구매일" placeholder="ex) 2024-01-01" />
|
<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">
|
|
||||||
<label for="sw-구독일">구독일(시작~끝)</label>
|
|
||||||
<input type="text" id="sw-구독일" placeholder="ex) 2024-01-01 ~ 2024-12-31" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="sw-유지보수-group" style="display:none;">
|
<div class="form-group" id="sw-유지보수-group" style="display:none;">
|
||||||
<label for="sw-유지보수여부">유지보수 여부</label>
|
<label for="sw-유지보수여부">유지보수 여부</label>
|
||||||
@@ -54,7 +52,7 @@ const SW_MODAL_HTML = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-금액">금액</label>
|
<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, ',')" />
|
<input type="text" id="sw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\d))/g, ',')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-수량">수량 (보유량)</label>
|
<label for="sw-수량">수량 (보유량)</label>
|
||||||
@@ -68,12 +66,23 @@ const SW_MODAL_HTML = `
|
|||||||
<label for="sw-납품업체">납품업체</label>
|
<label for="sw-납품업체">납품업체</label>
|
||||||
<input type="text" id="sw-납품업체" />
|
<input type="text" id="sw-납품업체" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group full-width">
|
||||||
<label for="sw-비고">비고</label>
|
<label for="sw-비고">비고</label>
|
||||||
<input type="text" id="sw-비고" />
|
<input type="text" id="sw-비고" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-history-area">
|
||||||
|
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||||
|
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm"><i data-lucide="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>
|
||||||
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
@@ -84,25 +93,64 @@ const SW_MODAL_HTML = `
|
|||||||
</div>
|
</div>
|
||||||
</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 function initSwModal(renderContent: () => void, closeModals: () => void) {
|
export let currentAsset: SoftwareAsset | null = null;
|
||||||
if (!document.getElementById('sw-asset-modal')) {
|
export let isEditMode = false;
|
||||||
document.body.insertAdjacentHTML('beforeend', SW_MODAL_HTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function setEditMode(edit: boolean) {
|
||||||
|
isEditMode = edit;
|
||||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
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 btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
|
||||||
const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement;
|
const btnRevertEdit = document.getElementById('btn-revert-sw-edit') as HTMLButtonElement;
|
||||||
const btnCloseHeader = document.getElementById('btn-close-sw-modal') as HTMLButtonElement;
|
|
||||||
const btnCloseFooter = document.getElementById('btn-close-sw-footer') 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) {
|
if (edit) {
|
||||||
swForm.classList.add('is-edit-mode');
|
swForm.classList.add('is-edit-mode');
|
||||||
swForm.classList.remove('is-view-mode');
|
swForm.classList.remove('is-view-mode');
|
||||||
@@ -117,9 +165,9 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
|||||||
btnCloseFooter.classList.remove('hidden');
|
btnCloseFooter.classList.remove('hidden');
|
||||||
if (currentAsset) fillFormData(currentAsset);
|
if (currentAsset) fillFormData(currentAsset);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function fillFormData(asset: SoftwareAsset) {
|
export function fillFormData(asset: SoftwareAsset) {
|
||||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
|
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
|
||||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
|
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
|
||||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = asset.분야 || '업무공통';
|
(document.getElementById('sw-분야') as HTMLSelectElement).value = asset.분야 || '업무공통';
|
||||||
@@ -127,15 +175,39 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
|||||||
(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).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).checked = !!asset.유지보수여부;
|
||||||
(document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 || '';
|
(document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||||
(document.getElementById('sw-수량') as HTMLInputElement).value = String(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('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 btnCloseHeader = document.getElementById('btn-close-sw-modal') as HTMLButtonElement;
|
||||||
|
const btnCloseFooter = document.getElementById('btn-close-sw-footer') as HTMLButtonElement;
|
||||||
|
|
||||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||||
btnCloseHeader?.addEventListener('click', closeModals);
|
btnCloseHeader?.addEventListener('click', closeModals);
|
||||||
btnCloseFooter?.addEventListener('click', closeModals);
|
btnCloseFooter?.addEventListener('click', closeModals);
|
||||||
@@ -150,6 +222,10 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
|||||||
if (!swForm.checkValidity()) { swForm.reportValidity(); return; }
|
if (!swForm.checkValidity()) { swForm.reportValidity(); return; }
|
||||||
|
|
||||||
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
|
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 = {
|
const newAsset: SoftwareAsset = {
|
||||||
id: id || Math.random().toString(36).substring(2, 9),
|
id: id || Math.random().toString(36).substring(2, 9),
|
||||||
type: (document.getElementById('sw-asset-type') as HTMLInputElement).value,
|
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,
|
||||||
구매일: (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).checked,
|
||||||
금액: (document.getElementById('sw-금액') as HTMLInputElement).value,
|
금액: (document.getElementById('sw-금액') as HTMLInputElement).value,
|
||||||
수량: parseInt((document.getElementById('sw-수량') as HTMLInputElement).value || '1', 10),
|
수량: parseInt((document.getElementById('sw-수량') as HTMLInputElement).value || '1', 10),
|
||||||
@@ -187,6 +263,109 @@ export function initSwModal(renderContent: () => void, closeModals: () => void)
|
|||||||
renderContent();
|
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) {
|
export function openSwModal(asset?: SoftwareAsset) {
|
||||||
@@ -217,6 +396,10 @@ export function openSwModal(asset?: SoftwareAsset) {
|
|||||||
deleteBtn.style.display = 'none';
|
deleteBtn.style.display = 'none';
|
||||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
|
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
|
||||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||||
|
document.getElementById('btn-open-sw-update')!.style.display = 'none';
|
||||||
|
renderSwHistory('');
|
||||||
setEditMode(true);
|
setEditMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createIcons({ icons: { X, History, Plus } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,14 +116,14 @@ export function initSwUserModal(renderContent: () => void, closeModals: () => vo
|
|||||||
btnSaveSwUserMapping?.addEventListener('click', () => {
|
btnSaveSwUserMapping?.addEventListener('click', () => {
|
||||||
state.masterData.swUsers = state.masterData.swUsers.filter(u => u.swId !== currentSwUserAssetId);
|
state.masterData.swUsers = state.masterData.swUsers.filter(u => u.swId !== currentSwUserAssetId);
|
||||||
state.masterData.swUsers.push(...tempSwUsers);
|
state.masterData.swUsers.push(...tempSwUsers);
|
||||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
closeModals();
|
||||||
renderContent();
|
renderContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
btnCancelUserEdit?.addEventListener('click', () => document.getElementById('sw-user-edit-modal')?.classList.add('hidden'));
|
btnCancelUserEdit?.addEventListener('click', () => document.getElementById('sw-user-edit-modal')?.classList.add('hidden'));
|
||||||
btnCloseUserEdit?.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'));
|
btnCancelUserModal?.addEventListener('click', closeModals);
|
||||||
btnCloseUserModal?.addEventListener('click', () => document.getElementById('sw-user-modal')?.classList.add('hidden'));
|
btnCloseUserModal?.addEventListener('click', closeModals);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUserList() {
|
function renderUserList() {
|
||||||
|
|||||||
@@ -38,23 +38,16 @@ const STORAGE_MODAL_HTML = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function initStorageModal(renderContent: () => void, closeModals: () => void) {
|
export let currentAsset: HardwareAsset | null = null;
|
||||||
if (!document.getElementById('storage-asset-modal')) {
|
export let isEditMode = false;
|
||||||
document.body.insertAdjacentHTML('beforeend', STORAGE_MODAL_HTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function setEditMode(edit: boolean) {
|
||||||
|
isEditMode = edit;
|
||||||
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
|
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 btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement;
|
||||||
const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement;
|
const btnRevertEdit = document.getElementById('btn-revert-storage-edit') as HTMLButtonElement;
|
||||||
const btnCloseHeader = document.getElementById('btn-close-storage-modal') as HTMLButtonElement;
|
|
||||||
const btnCloseFooter = document.getElementById('btn-close-storage-footer') 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) {
|
if (edit) {
|
||||||
storageForm.classList.add('is-edit-mode');
|
storageForm.classList.add('is-edit-mode');
|
||||||
storageForm.classList.remove('is-view-mode');
|
storageForm.classList.remove('is-view-mode');
|
||||||
@@ -69,9 +62,9 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v
|
|||||||
btnCloseFooter.classList.remove('hidden');
|
btnCloseFooter.classList.remove('hidden');
|
||||||
if (currentAsset) fillFormData(currentAsset);
|
if (currentAsset) fillFormData(currentAsset);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function fillFormData(asset: HardwareAsset) {
|
export function fillFormData(asset: HardwareAsset) {
|
||||||
(document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id;
|
(document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id;
|
||||||
(document.getElementById('storage-법인') as HTMLInputElement).value = asset.법인;
|
(document.getElementById('storage-법인') as HTMLInputElement).value = asset.법인;
|
||||||
(document.getElementById('storage-유형') as HTMLInputElement).value = asset.storage유형 || 'NAS';
|
(document.getElementById('storage-유형') as HTMLInputElement).value = asset.storage유형 || 'NAS';
|
||||||
@@ -84,8 +77,20 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v
|
|||||||
(document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
|
(document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
|
||||||
(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.금액 || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 btnCloseHeader = document.getElementById('btn-close-storage-modal') as HTMLButtonElement;
|
||||||
|
const btnCloseFooter = document.getElementById('btn-close-storage-footer') as HTMLButtonElement;
|
||||||
|
|
||||||
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
btnRevertEdit?.addEventListener('click', () => setEditMode(false));
|
||||||
btnCloseHeader?.addEventListener('click', closeModals);
|
btnCloseHeader?.addEventListener('click', closeModals);
|
||||||
btnCloseFooter?.addEventListener('click', closeModals);
|
btnCloseFooter?.addEventListener('click', closeModals);
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export interface HardwareAsset {
|
|||||||
서버PW?: string;
|
서버PW?: string;
|
||||||
모니터링?: string;
|
모니터링?: string;
|
||||||
비고?: string;
|
비고?: string;
|
||||||
|
현사용조직?: string;
|
||||||
|
이전사용조직?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -88,10 +90,12 @@ export interface MasterAssetData {
|
|||||||
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
||||||
const SW_TABS = ['구독SW', '영구SW'];
|
const SW_TABS = ['구독SW', '영구SW'];
|
||||||
|
|
||||||
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명'];
|
// 확장된 헤더 (상세 페이지의 모든 필드 포함)
|
||||||
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', '구매일', '금액', '납품업체', '품의서명'];
|
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage1', 'Storage2', 'Storage3', '모니터링', '비고'];
|
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '서버ID', '서버PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', '모니터링', '비고'];
|
||||||
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명'];
|
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
|
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
|
|
||||||
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||||
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||||
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
||||||
@@ -109,16 +113,24 @@ export function downloadTemplate() {
|
|||||||
|
|
||||||
if (tab === '개인PC') {
|
if (tab === '개인PC') {
|
||||||
hd = PC_HEADERS;
|
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 === '서버') {
|
} else if (tab === '서버') {
|
||||||
hd = SERVER_HEADERS;
|
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 === '스토리지') {
|
} else if (tab === '스토리지') {
|
||||||
hd = STORAGE_HEADERS;
|
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 {
|
} else {
|
||||||
hd = HW_HEADERS;
|
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]);
|
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
||||||
@@ -158,27 +170,44 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
if (tab === '개인PC') {
|
if (tab === '개인PC') {
|
||||||
wsData = [
|
wsData = [
|
||||||
PC_HEADERS,
|
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 === '서버') {
|
} else if (tab === '서버') {
|
||||||
wsData = [
|
wsData = [
|
||||||
SERVER_HEADERS,
|
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 === '스토리지') {
|
} else if (tab === '스토리지') {
|
||||||
wsData = [
|
wsData = [
|
||||||
STORAGE_HEADERS,
|
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 {
|
} else {
|
||||||
wsData = [
|
wsData = [
|
||||||
HW_HEADERS,
|
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);
|
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
||||||
@@ -252,11 +281,16 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
명칭: '',
|
명칭: '',
|
||||||
위치: row['위치'] || '',
|
위치: row['위치'] || '',
|
||||||
사용자: 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'] || '',
|
CPU: row['CPU'] || '', GPU: row['GPU'] || '', RAM: row['RAM'] || '',
|
||||||
SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '',
|
SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '',
|
||||||
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||||
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||||
|
비고: row['비고'] || ''
|
||||||
});
|
});
|
||||||
} else if (sheetName === '서버') {
|
} else if (sheetName === '서버') {
|
||||||
hwAssets.push({
|
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['이전사용조직'] || '',
|
||||||
|
위치: row['설치위치'] || row['위치'] || '',
|
||||||
관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
||||||
IP주소: row['IP 주소'] || row['IP주소'] || '', IP2: row['IP2'] || '',
|
IP주소: row['IP 주소'] || row['IP주소'] || '',
|
||||||
원격접속: row['원격접속'] || '', 서버ID: row['서버ID'] || '', 서버PW: row['서버PW'] || '',
|
원격접속: row['원격접속'] || '',
|
||||||
|
서버ID: row['서버ID'] || '',
|
||||||
|
서버PW: row['서버PW'] || '',
|
||||||
모델명: row['모델명'] || '', OS: row['OS'] || '',
|
모델명: row['모델명'] || '', OS: row['OS'] || '',
|
||||||
CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '',
|
CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '',
|
||||||
SSD1: row['Storage1'] || row['SSD1'] || '', SSD2: row['Storage2'] || row['SSD2'] || '', HDD1: row['Storage3'] || row['HDD1'] || '',
|
SSD1: row['SSD1'] || row['Storage1'] || '',
|
||||||
모니터링: row['모니터링'] || '', 비고: row['비고'] || '', storage유형: row['유형'] || '물리',
|
SSD2: row['SSD2'] || row['Storage2'] || '',
|
||||||
|
HDD1: row['HDD1'] || row['Storage3'] || '',
|
||||||
|
모니터링: row['모니터링'] || '',
|
||||||
|
비고: row['비고'] || '',
|
||||||
|
storage유형: row['유형'] || '물리',
|
||||||
MACaddress: '', HW사양: '', 구매일: '', 금액: '', 납품업체: '', 품의서명: '',
|
MACaddress: '', HW사양: '', 구매일: '', 금액: '', 납품업체: '', 품의서명: '',
|
||||||
});
|
});
|
||||||
} else if (sheetName === '스토리지') {
|
} else if (sheetName === '스토리지') {
|
||||||
@@ -285,6 +329,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
||||||
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||||
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||||
|
비고: row['비고'] || ''
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
hwAssets.push({
|
hwAssets.push({
|
||||||
@@ -295,6 +340,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
HW사양: row['HW사양'] || '', OS: row['OS'] || '',
|
HW사양: row['HW사양'] || '', OS: row['OS'] || '',
|
||||||
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||||
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||||
|
비고: row['비고'] || ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ const dummy = generateDummyData();
|
|||||||
// 서버 데이터만 실제 데이터로 교체
|
// 서버 데이터만 실제 데이터로 교체
|
||||||
const mergedHw: HardwareAsset[] = [
|
const mergedHw: HardwareAsset[] = [
|
||||||
...dummy.hw.filter(a => a.type !== '서버'),
|
...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),
|
id: s.id || Math.random().toString(36).substring(2, 9),
|
||||||
type: '서버',
|
type: '서버',
|
||||||
법인: s.법인,
|
법인: s.법인,
|
||||||
@@ -47,7 +49,7 @@ const mergedHw: HardwareAsset[] = [
|
|||||||
서버PW: s.서버PW || '',
|
서버PW: s.서버PW || '',
|
||||||
모니터링: s.모니터링 || '',
|
모니터링: s.모니터링 || '',
|
||||||
비고: s.비고 || ''
|
비고: s.비고 || ''
|
||||||
}))
|
}})
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Initial State ---
|
// --- Initial State ---
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
--text-muted: #6B7280;
|
--text-muted: #6B7280;
|
||||||
--border-color: #E5E7EB;
|
--border-color: #E5E7EB;
|
||||||
--bg-color: #F9FAFB;
|
--bg-color: #F9FAFB;
|
||||||
|
--bg-light: #FAFAFA;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
--white: #FFFFFF;
|
--white: #FFFFFF;
|
||||||
--danger: #dc2626;
|
--danger: #dc2626;
|
||||||
|
|
||||||
|
|||||||
@@ -265,3 +265,78 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.8125rem;
|
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({
|
createIcons({
|
||||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw }
|
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);
|
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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="text-align:center;">No.</th>
|
<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>
|
<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 = '';
|
tbody.innerHTML = '';
|
||||||
if (filtered.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +90,40 @@ export function renderSwList(container: HTMLElement) {
|
|||||||
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
|
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
|
||||||
const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10);
|
const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10);
|
||||||
const avail = qty - assigned;
|
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');
|
const tr = document.createElement('tr');
|
||||||
tr.style.cursor = 'pointer';
|
tr.style.cursor = 'pointer';
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td style="text-align:center;">${idx+1}</td>
|
<td style="text-align:center;">${idx+1}</td>
|
||||||
|
<td style="text-align:center;">${statusHtml}</td>
|
||||||
<td>${asset.분야||''}</td>
|
<td>${asset.분야||''}</td>
|
||||||
<td>${asset.법인}</td>
|
<td>${asset.법인}</td>
|
||||||
<td>${asset.부서||''}</td>
|
<td>${asset.부서||''}</td>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
Reference in New Issue
Block a user