feat(sw): S/W 자산 관리 고도화 (이력 관리, 자동 상태 뱃지, 날짜 픽커 및 필드 분할 적용)
This commit is contained in:
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,7 +1,7 @@
|
|||||||
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 { renderSWTable } from '../../views/SW_Table';
|
||||||
import { createIcons, Paperclip } from 'lucide';
|
import { createIcons, X, Paperclip } from 'lucide';
|
||||||
|
|
||||||
let currentAsset: HardwareAsset | null = null;
|
let currentAsset: HardwareAsset | null = null;
|
||||||
let isEditMode = false;
|
let isEditMode = false;
|
||||||
@@ -319,7 +319,7 @@ export function initHwModal() {
|
|||||||
const idx = state.masterData.hw.findIndex(a => a.id === assetId);
|
const idx = state.masterData.hw.findIndex(a => a.id === assetId);
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
state.masterData.hw[idx] = updated;
|
state.masterData.hw[idx] = updated;
|
||||||
renderTable(document.getElementById('main-content')!);
|
renderSWTable(document.getElementById('main-content')!);
|
||||||
switchToViewMode();
|
switchToViewMode();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -328,7 +328,7 @@ export function initHwModal() {
|
|||||||
if (!currentAsset) return;
|
if (!currentAsset) return;
|
||||||
if (confirm('정말로 이 자산을 삭제하시겠습니까?')) {
|
if (confirm('정말로 이 자산을 삭제하시겠습니까?')) {
|
||||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id);
|
state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id);
|
||||||
renderTable(document.getElementById('main-content')!);
|
renderSWTable(document.getElementById('main-content')!);
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,78 +1,87 @@
|
|||||||
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">
|
||||||
<form id="sw-asset-form" class="grid-form">
|
<div class="modal-body-split">
|
||||||
<input type="hidden" id="sw-asset-id" />
|
<div class="modal-form-area">
|
||||||
<input type="hidden" id="sw-asset-type" />
|
<form id="sw-asset-form" class="grid-form">
|
||||||
<div class="form-group">
|
<input type="hidden" id="sw-asset-id" />
|
||||||
<label for="sw-분야">분야</label>
|
<input type="hidden" id="sw-asset-type" />
|
||||||
<select id="sw-분야" required>
|
<div class="form-group">
|
||||||
<option value="업무공통">업무공통</option>
|
<label for="sw-분야">분야</label>
|
||||||
<option value="개발S/W">개발S/W</option>
|
<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>
|
</select>
|
||||||
</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>
|
||||||
<div class="form-group">
|
<div class="modal-history-area">
|
||||||
<label for="sw-법인">법인</label>
|
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
<select id="sw-법인" required>
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||||
<option value="한맥">한맥 (HM)</option>
|
<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>
|
||||||
<option value="삼안 (SM)">삼안 (SM)</option>
|
</div>
|
||||||
<option value="바론 (BR)">바론 (BR)</option>
|
<div id="sw-history-list" class="history-timeline">
|
||||||
</select>
|
<div class="empty-history">내역이 없습니다.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<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">
|
<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>
|
||||||
@@ -83,6 +92,52 @@ 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 function initSwModal(renderContent: () => void, closeModals: () => void) {
|
||||||
@@ -104,6 +159,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,
|
||||||
@@ -112,7 +171,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),
|
||||||
@@ -141,6 +200,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) {
|
||||||
@@ -171,13 +333,24 @@ export function openSwModal(asset?: SoftwareAsset) {
|
|||||||
(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);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('sw-modal-title')!.textContent = `신규 ${state.activeSubTab} 자산 추가`;
|
document.getElementById('sw-modal-title')!.textContent = `신규 ${state.activeSubTab} 자산 추가`;
|
||||||
deleteBtn.style.display = 'none';
|
deleteBtn.style.display = 'none';
|
||||||
@@ -186,5 +359,10 @@ export function openSwModal(asset?: SoftwareAsset) {
|
|||||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = '업무공통';
|
(document.getElementById('sw-분야') as HTMLSelectElement).value = '업무공통';
|
||||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = '한맥';
|
(document.getElementById('sw-법인') as HTMLSelectElement).value = '한맥';
|
||||||
(document.getElementById('sw-부서') as HTMLInputElement).value = '';
|
(document.getElementById('sw-부서') as HTMLInputElement).value = '';
|
||||||
|
|
||||||
|
document.getElementById('btn-open-sw-update')!.style.display = 'none';
|
||||||
|
renderSwHistory('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
|||||||
17
src/main.ts
17
src/main.ts
@@ -1,7 +1,7 @@
|
|||||||
import { state } from './core/state';
|
import { state } from './core/state';
|
||||||
import { renderSidebar } from './components/Sidebar';
|
import { renderSidebar } from './components/Sidebar';
|
||||||
import { renderDashboard } from './views/DashboardView';
|
import { renderDashboard } from './views/DashboardView';
|
||||||
import { renderTable } from './views/AssetTableView';
|
import { renderSWTable } from './views/SW_Table';
|
||||||
import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset } from './core/excelHandler';
|
import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset } from './core/excelHandler';
|
||||||
import { initPcModal } from './components/Modal/PCModal';
|
import { initPcModal } from './components/Modal/PCModal';
|
||||||
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
||||||
@@ -9,6 +9,7 @@ import { initStorageModal } from './components/Modal/StorageModal';
|
|||||||
import { initSwModal } from './components/Modal/SWModal';
|
import { initSwModal } from './components/Modal/SWModal';
|
||||||
import { initSwUserModal } from './components/Modal/SWUserModal';
|
import { initSwUserModal } from './components/Modal/SWUserModal';
|
||||||
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
||||||
|
import { initBaseModal } from './components/Modal/BaseModal';
|
||||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide';
|
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide';
|
||||||
|
|
||||||
// --- App Initialization ---
|
// --- App Initialization ---
|
||||||
@@ -25,7 +26,7 @@ function initApp() {
|
|||||||
renderDashboard(mainContent);
|
renderDashboard(mainContent);
|
||||||
document.getElementById('btn-add-asset')?.classList.add('hidden');
|
document.getElementById('btn-add-asset')?.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
renderTable(mainContent);
|
renderSWTable(mainContent);
|
||||||
document.getElementById('btn-add-asset')?.classList.remove('hidden');
|
document.getElementById('btn-add-asset')?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
// 상단 타이틀 업데이트
|
// 상단 타이틀 업데이트
|
||||||
@@ -37,11 +38,13 @@ function initApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 3. 모달 초기화 (HTML 주입 및 이벤트 바인딩)
|
// 3. 모달 초기화 (HTML 주입 및 이벤트 바인딩)
|
||||||
initPcModal(() => renderTable(mainContent), () => {});
|
const { closeAllModals } = initBaseModal();
|
||||||
|
|
||||||
|
initPcModal(() => renderSWTable(mainContent), closeAllModals);
|
||||||
initHwModal();
|
initHwModal();
|
||||||
initStorageModal(() => renderTable(mainContent), () => {});
|
initStorageModal(() => renderSWTable(mainContent), closeAllModals);
|
||||||
initSwModal(() => renderTable(mainContent), () => {});
|
initSwModal(() => renderSWTable(mainContent), closeAllModals);
|
||||||
initSwUserModal(() => renderTable(mainContent), () => {});
|
initSwUserModal(() => renderSWTable(mainContent), closeAllModals);
|
||||||
initDashboardDetailModal();
|
initDashboardDetailModal();
|
||||||
|
|
||||||
// 4. 전역 버튼 이벤트 바인딩
|
// 4. 전역 버튼 이벤트 바인딩
|
||||||
@@ -54,7 +57,7 @@ function initApp() {
|
|||||||
if (file) {
|
if (file) {
|
||||||
const data = await parseExcel(file);
|
const data = await parseExcel(file);
|
||||||
state.masterData = data;
|
state.masterData = data;
|
||||||
renderTable(mainContent);
|
renderSWTable(mainContent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
--text-muted: #6B7280;
|
--text-muted: #6B7280;
|
||||||
--border-color: #E5E7EB;
|
--border-color: #E5E7EB;
|
||||||
--bg-color: #F9FAFB;
|
--bg-color: #F9FAFB;
|
||||||
|
--bg-light: #FAFAFA;
|
||||||
--sidebar-bg: #ffffff;
|
--sidebar-bg: #ffffff;
|
||||||
--white: #FFFFFF;
|
--white: #FFFFFF;
|
||||||
--danger: #dc2626;
|
--danger: #dc2626;
|
||||||
|
|||||||
@@ -257,3 +257,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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { openSwUserModal } from '../components/Modal/SWUserModal';
|
|||||||
/**
|
/**
|
||||||
* 자산 목록 테이블 렌더링 메인 함수
|
* 자산 목록 테이블 렌더링 메인 함수
|
||||||
*/
|
*/
|
||||||
export function renderTable(mainContent: HTMLElement) {
|
export function renderSWTable(mainContent: HTMLElement) {
|
||||||
mainContent.innerHTML = '';
|
mainContent.innerHTML = '';
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'view-container';
|
container.className = 'view-container';
|
||||||
@@ -46,32 +46,51 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
} else if (state.activeSubTab === '스토리지') {
|
} else if (state.activeSubTab === '스토리지') {
|
||||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>유형</th><th>자산코드</th><th>명칭</th><th>위치</th><th>모델명</th><th>용량</th><th>담당자(정)</th><th>IP주소</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산코드</th><th>명칭</th><th>위치</th><th>모델명</th><th>용량</th><th>담당자(정)</th><th>IP주소</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||||
tableWrapper.appendChild(table);
|
tableWrapper.appendChild(table);
|
||||||
container.appendChild(tableWrapper);
|
container.appendChild(tableWrapper);
|
||||||
mainContent.appendChild(container);
|
mainContent.appendChild(container);
|
||||||
const tbody = document.getElementById('dynamic-tbody')!;
|
const tbody = document.getElementById('dynamic-tbody')!;
|
||||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="13">등록된 자산이 없습니다.</td></tr>`; return; }
|
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="12">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||||
list.forEach((asset, idx) => {
|
list.forEach((asset, idx) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.style.cursor = 'pointer';
|
tr.style.cursor = 'pointer';
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.storage유형||''}</td><td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치||''}</td><td>${asset.모델명||''}</td><td>${asset.용량||''}</td><td>${asset.담당자_정||''}</td><td>${asset.IP주소||''}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치||''}</td><td>${asset.모델명||''}</td><td>${asset.용량||''}</td><td>${asset.담당자_정||''}</td><td>${asset.IP주소||''}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); });
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 서버 또는 전산비품
|
// 서버 또는 전산비품
|
||||||
if (state.activeSubTab === '서버') {
|
if (state.activeSubTab === '서버') {
|
||||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산번호</th><th>유형</th><th>용도</th><th>상세</th><th>설치위치</th><th>담당자</th><th>IP 주소</th><th>원격접속</th><th>모델명</th><th>OS</th><th>CPU</th><th>RAM</th><th>Storage</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
table.innerHTML = `
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>법인</th>
|
||||||
|
<th>자산번호</th>
|
||||||
|
<th>용도</th>
|
||||||
|
<th>상세</th>
|
||||||
|
<th>설치위치</th>
|
||||||
|
<th>담당자</th>
|
||||||
|
<th>IP 주소</th>
|
||||||
|
<th>원격접속</th>
|
||||||
|
<th>모델명</th>
|
||||||
|
<th>OS</th>
|
||||||
|
<th>CPU</th>
|
||||||
|
<th>RAM</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="dynamic-tbody"></tbody>`;
|
||||||
} else {
|
} else {
|
||||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th>${state.activeSubTab === '전산비품' ? '<th>유형</th>' : ''}<th>자산코드</th><th>명칭</th><th>위치</th><th>관리자</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산코드</th><th>명칭</th><th>위치</th><th>관리자</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
tableWrapper.appendChild(table);
|
tableWrapper.appendChild(table);
|
||||||
container.appendChild(tableWrapper);
|
container.appendChild(tableWrapper);
|
||||||
mainContent.appendChild(container);
|
mainContent.appendChild(container);
|
||||||
const tbody = document.getElementById('dynamic-tbody')!;
|
const tbody = document.getElementById('dynamic-tbody')!;
|
||||||
const colCount = state.activeSubTab === '서버' ? 15 : (state.activeSubTab === '전산비품' ? 11 : 10);
|
const colCount = state.activeSubTab === '서버' ? 14 : 9;
|
||||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="${colCount}">등록된 자산이 없습니다.</td></tr>`; return; }
|
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="${colCount}">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||||
|
|
||||||
list.forEach((asset, idx) => {
|
list.forEach((asset, idx) => {
|
||||||
@@ -102,10 +121,25 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
const ipInfo = [asset.IP주소, asset.IP2].filter(v => v && v !== '').join(' / ');
|
const ipInfo = [asset.IP주소, asset.IP2].filter(v => v && v !== '').join(' / ');
|
||||||
const storageInfo = [asset.SSD1, asset.SSD2].filter(v => v && v !== '').join(' / ');
|
const storageInfo = [asset.SSD1, asset.SSD2].filter(v => v && v !== '').join(' / ');
|
||||||
|
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td class="text-nowrap">${formatInline(asset.법인)}</td><td class="text-nowrap">${formatInline(asset.자산코드)}</td><td class="text-nowrap">${formatInline(asset.storage유형)}</td><td class="text-nowrap">${formatInline(asset.용도)}</td><td class="text-nowrap">${formatInline(asset.상세)}</td><td class="text-nowrap">${formatInline(asset.위치)}</td><td class="text-nowrap">${managerHtml}</td><td class="text-nowrap">${formatInline(ipInfo)}</td><td class="text-nowrap">${remoteHtml}</td><td class="text-nowrap">${formatInline(asset.모델명)}</td><td class="text-nowrap">${formatInline(asset.OS)}</td><td class="text-nowrap">${formatInline(asset.CPU)}</td><td class="text-nowrap">${formatInline(asset.RAM)}</td><td class="text-nowrap">${formatInline(storageInfo)}</td>`;
|
tr.innerHTML = `
|
||||||
|
<td>${idx+1}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.법인)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.자산코드)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.용도)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.상세)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.위치)}</td>
|
||||||
|
<td class="text-nowrap">${managerHtml}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(ipInfo)}</td>
|
||||||
|
<td class="text-nowrap">${remoteHtml}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.모델명)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.OS)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.CPU)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(asset.RAM)}</td>
|
||||||
|
<td class="text-nowrap">${formatInline(storageInfo)}</td>
|
||||||
|
`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
||||||
} else {
|
} else {
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td>${state.activeSubTab === '전산비품' ? `<td>${asset.비품유형||'-'}</td>` : ''}<td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치}</td><td>${asset.관리자}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치}</td><td>${asset.관리자}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
||||||
}
|
}
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
@@ -125,7 +159,7 @@ function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
const tableWrapper = document.createElement('div');
|
const tableWrapper = document.createElement('div');
|
||||||
tableWrapper.className = 'table-container';
|
tableWrapper.className = 'table-container';
|
||||||
table.classList.add('sw-table');
|
table.classList.add('sw-table');
|
||||||
table.innerHTML = `<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><th style="text-align:center;">구매일</th>${isSub ? '<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></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
table.innerHTML = `<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><th style="text-align:center;">제품명</th><th style="text-align:center;">구매일</th>${isSub ? '<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></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||||
tableWrapper.appendChild(table);
|
tableWrapper.appendChild(table);
|
||||||
container.appendChild(tableWrapper);
|
container.appendChild(tableWrapper);
|
||||||
mainContent.appendChild(container);
|
mainContent.appendChild(container);
|
||||||
@@ -143,16 +177,45 @@ function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
});
|
});
|
||||||
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;
|
||||||
}
|
}
|
||||||
filtered.forEach((asset, idx) => {
|
filtered.forEach((asset, idx) => {
|
||||||
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())) {
|
||||||
|
// Set end date to end of day to be lenient
|
||||||
|
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 = `<td>${idx+1}</td><td>${asset.분야||''}</td><td>${asset.법인}</td><td>${asset.부서||''}</td><td>${asset.제품명}</td><td>${asset.구매일||''}</td>${isSub ? `<td>${asset.구독일||''}</td>` : ''}<td>${asset.금액||'0'}</td><td>${qty}</td><td><strong style="color: ${avail > 0 ? 'var(--primary-color)' : 'var(--danger)'}">${avail}</strong></td><td style="display:flex; justify-content:center; align-items:center; gap:0.5rem;"><button type="button" class="btn-icon btn-edit" title="수정" style="color: var(--text-muted);"><i data-lucide="edit-2" style="width:18px; height:18px;"></i></button><button type="button" class="btn-icon btn-users" title="사용자 관리" style="color: var(--primary-color);"><i data-lucide="users" style="width:18px; height:18px;"></i></button></td>`;
|
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><td>${asset.제품명}</td><td>${asset.구매일||''}</td>${isSub ? `<td>${asset.구독일||''}</td>` : ''}<td>${asset.금액||'0'}</td><td style="text-align:center;">${qty}</td><td style="text-align:center;"><strong style="color: ${avail > 0 ? 'var(--primary-color)' : 'var(--danger)'}">${avail}</strong></td><td style="display:flex; justify-content:center; align-items:center; gap:0.5rem;"><button type="button" class="btn-icon btn-edit" title="수정" style="color: var(--text-muted);"><i data-lucide="edit-2" style="width:18px; height:18px;"></i></button><button type="button" class="btn-icon btn-users" title="사용자 관리" style="color: var(--primary-color);"><i data-lucide="users" style="width:18px; height:18px;"></i></button></td>`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
|
||||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset));
|
tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset));
|
||||||
tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset));
|
tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset));
|
||||||
Reference in New Issue
Block a user