Compare commits
3 Commits
PC_Table
...
d94be9a494
| Author | SHA1 | Date | |
|---|---|---|---|
| d94be9a494 | |||
| 157330b06d | |||
| e4914ee66d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
node_modules/
|
||||
.gemini/
|
||||
.gemini
|
||||
.env
|
||||
dist/
|
||||
*.log
|
||||
|
||||
568
index.html
568
index.html
@@ -6,8 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ITAM 자산관리 ERP</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" />
|
||||
<link rel="stylesheet" href="/src/styles/common.css" />
|
||||
<link rel="stylesheet" href="/src/styles/modal.css" />
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
|
||||
</head>
|
||||
@@ -47,15 +46,14 @@
|
||||
<h2 id="current-tab-title">하드웨어 / 대시보드</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<!-- 엑셀 컨트롤러 묶음 -->
|
||||
<button id="btn-download-template" class="btn btn-outline" title="초기 데이터 입력을 위한 전체 엑셀 템플릿 다운로드">
|
||||
<button id="btn-download-template" class="btn btn-outline" title="통합 양식 다운로드">
|
||||
<i data-lucide="download"></i> 통합 양식 다운로드
|
||||
</button>
|
||||
<label for="excel-upload" class="btn btn-outline" title="작성된 엑셀 파일 일괄 업로드">
|
||||
<label for="excel-upload" class="btn btn-outline" title="엑셀 파일 업로드">
|
||||
<i data-lucide="upload"></i> 엑셀 업로드
|
||||
</label>
|
||||
<input type="file" id="excel-upload" accept=".xlsx, .xls" style="display: none;" />
|
||||
<button id="btn-export-excel" class="btn btn-primary" title="마스터 데이터 전체를 엑셀로 저장">
|
||||
<button id="btn-export-excel" class="btn btn-primary" title="일괄 엑셀 저장">
|
||||
<i data-lucide="file-spreadsheet"></i> 일괄 엑셀 저장
|
||||
</button>
|
||||
<button id="btn-add-asset" class="btn btn-primary hidden">
|
||||
@@ -65,106 +63,145 @@
|
||||
</header>
|
||||
|
||||
<main class="content-area" id="main-content">
|
||||
<!-- 대시보드 뷰, 또는 데이터 테이블 뷰가 JavaScript로 주입됩니다 -->
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HW Asset Modal -->
|
||||
<div id="hw-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" style="max-width: 800px; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h2 id="hw-modal-title">자산 상세 정보</h2>
|
||||
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
||||
<form id="hw-asset-form" class="grid-form">
|
||||
<input type="hidden" id="hw-asset-id" />
|
||||
<input type="hidden" id="hw-asset-type" /> <!-- 개인PC, 서버 등 저장용 -->
|
||||
<input type="hidden" id="hw-asset-type" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-법인">법인</label>
|
||||
<input type="text" id="hw-법인" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="hw-비품유형-group" style="display:none;">
|
||||
<label for="hw-비품유형">비품유형</label>
|
||||
<select id="hw-비품유형" class="form-control" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px;">
|
||||
<option value="노트북">노트북</option>
|
||||
<option value="태블릿">태블릿</option>
|
||||
<option value="휴대폰">휴대폰</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-자산코드">자산코드</label>
|
||||
<label for="hw-자산코드">자산번호/코드</label>
|
||||
<input type="text" id="hw-자산코드" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-명칭">명칭</label>
|
||||
<input type="text" id="hw-명칭" required />
|
||||
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-용도">용도</label>
|
||||
<input type="text" id="hw-용도" />
|
||||
</div>
|
||||
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-상세">상세</label>
|
||||
<input type="text" id="hw-상세" />
|
||||
</div>
|
||||
|
||||
<div class="form-group non-server">
|
||||
<label for="hw-명칭">명칭</label>
|
||||
<input type="text" id="hw-명칭" />
|
||||
</div>
|
||||
<div class="form-group" id="hw-비품유형-group" style="display:none;">
|
||||
<label for="hw-비품유형">비품유형</label>
|
||||
<select id="hw-비품유형" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px;">
|
||||
<option value="노트북">노트북</option><option value="태블릿">태블릿</option><option value="휴대폰">휴대폰</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-위치">위치</label>
|
||||
<label for="hw-위치">설치위치</label>
|
||||
<input type="text" id="hw-위치" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-관리자">관리자</label>
|
||||
<input type="text" id="hw-관리자" />
|
||||
<label for="hw-담당자_정">담당자(정)</label>
|
||||
<input type="text" id="hw-담당자_정" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-IP주소">IP주소</label>
|
||||
<label for="hw-담당자_부">담당자(부)</label>
|
||||
<input type="text" id="hw-담당자_부" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-IP주소">IP 주소 1</label>
|
||||
<input type="text" id="hw-IP주소" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-MACaddress">MAC address</label>
|
||||
<input type="text" id="hw-MACaddress" />
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-IP2">IP 주소 2</label>
|
||||
<input type="text" id="hw-IP2" />
|
||||
</div>
|
||||
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-원격접속">원격 도구</label>
|
||||
<input type="text" id="hw-원격접속" />
|
||||
</div>
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-서버ID">서버 ID</label>
|
||||
<input type="text" id="hw-서버ID" />
|
||||
</div>
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-서버PW">서버 PW</label>
|
||||
<input type="text" id="hw-서버PW" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-모델명">모델명</label>
|
||||
<input type="text" id="hw-모델명" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-OS">OS</label>
|
||||
<input type="text" id="hw-OS" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-CPU">CPU</label>
|
||||
<input type="text" id="hw-CPU" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-RAM">RAM</label>
|
||||
<input type="text" id="hw-RAM" />
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label for="hw-HW사양">H/W 사양</label>
|
||||
<div class="form-group">
|
||||
<label for="hw-SSD1">Storage 1</label>
|
||||
<input type="text" id="hw-SSD1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hw-SSD2">Storage 2</label>
|
||||
<input type="text" id="hw-SSD2" />
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width non-server">
|
||||
<label for="hw-HW사양">H/W 사양 상세</label>
|
||||
<textarea id="hw-HW사양" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group non-server">
|
||||
<label for="hw-구매일">구매일</label>
|
||||
<input type="text" id="hw-구매일" placeholder="ex) 2024-01-01" />
|
||||
<input type="text" id="hw-구매일" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group non-server">
|
||||
<label for="hw-금액">금액</label>
|
||||
<input type="text" id="hw-금액" placeholder="ex) 1,000,000" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||
<input type="text" id="hw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-납품업체">납품업체</label>
|
||||
<input type="text" id="hw-납품업체" />
|
||||
<div class="form-group server-only">
|
||||
<label for="hw-비고">비고</label>
|
||||
<input type="text" id="hw-비고" />
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label style="font-size:0.875rem;">품의서 (파일)</label>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<input type="file" id="hw-품의서" style="font-size:0.875rem;" />
|
||||
<input type="file" id="hw-품의서" />
|
||||
<span id="hw-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-hw-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<button id="btn-delete-hw-asset" class="btn btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-hw-modal" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-hw-asset" class="btn btn-primary">저장</button>
|
||||
<button id="btn-cancel-hw-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-hw-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,224 +209,75 @@
|
||||
|
||||
<!-- PC Asset Modal -->
|
||||
<div id="pc-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h2 id="pc-modal-title">개인PC 상세 정보</h2>
|
||||
<button id="btn-close-pc-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body-split">
|
||||
<div class="modal-form-area">
|
||||
<form id="pc-asset-form" class="grid-form">
|
||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
||||
<form id="pc-asset-form" class="grid-form">
|
||||
<input type="hidden" id="pc-asset-id" />
|
||||
<input type="hidden" id="pc-asset-type" value="개인PC" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-법인">법인</label>
|
||||
<select id="pc-법인" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
||||
<option value="한맥">한맥 (HM)</option>
|
||||
<option value="삼안">삼안 (SM)</option>
|
||||
<option value="바론">바론 (BR)</option>
|
||||
<select id="pc-법인" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px;">
|
||||
<option value="한맥">한맥 (HM)</option><option value="삼안">삼안 (SM)</option><option value="바론">바론 (BR)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-자산코드">자산코드</label>
|
||||
<input type="text" id="pc-자산코드" placeholder="ex) HM-PC-2018-001" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-사용자">사용자</label>
|
||||
<input type="text" id="pc-사용자" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-위치">위치</label>
|
||||
<input type="text" id="pc-위치" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-CPU">CPU</label>
|
||||
<input type="text" id="pc-CPU" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-GPU">GPU</label>
|
||||
<input type="text" id="pc-GPU" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-RAM">RAM</label>
|
||||
<input type="text" id="pc-RAM" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-SSD1">SSD1</label>
|
||||
<input type="text" id="pc-SSD1" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-SSD2">SSD2</label>
|
||||
<input type="text" id="pc-SSD2" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-HDD1">HDD1</label>
|
||||
<input type="text" id="pc-HDD1" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-HDD2">HDD2</label>
|
||||
<input type="text" id="pc-HDD2" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-구매일">구매일</label>
|
||||
<input type="text" id="pc-구매일" placeholder="ex) 2024-01-01" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pc-금액">금액</label>
|
||||
<input type="text" id="pc-금액" 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="pc-납품업체">납품업체</label>
|
||||
<input type="text" id="pc-납품업체" />
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label style="font-size:0.875rem;">품의서 (파일)</label>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<input type="file" id="pc-품의서" style="font-size:0.875rem;" />
|
||||
<span id="pc-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label for="pc-자산코드">자산코드</label><input type="text" id="pc-자산코드" required /></div>
|
||||
<div class="form-group"><label for="pc-사용자">사용자</label><input type="text" id="pc-사용자" required /></div>
|
||||
<div class="form-group"><label for="pc-위치">위치</label><input type="text" id="pc-위치" /></div>
|
||||
<div class="form-group"><label for="pc-CPU">CPU</label><input type="text" id="pc-CPU" /></div>
|
||||
<div class="form-group"><label for="pc-GPU">GPU</label><input type="text" id="pc-GPU" /></div>
|
||||
<div class="form-group"><label for="pc-RAM">RAM</label><input type="text" id="pc-RAM" /></div>
|
||||
<div class="form-group"><label for="pc-SSD1">SSD1</label><input type="text" id="pc-SSD1" /></div>
|
||||
<div class="form-group"><label for="pc-SSD2">SSD2</label><input type="text" id="pc-SSD2" /></div>
|
||||
<div class="form-group"><label for="pc-HDD1">HDD1</label><input type="text" id="pc-HDD1" /></div>
|
||||
<div class="form-group"><label for="pc-HDD2">HDD2</label><input type="text" id="pc-HDD2" /></div>
|
||||
<div class="form-group"><label for="pc-구매일">구매일</label><input type="text" id="pc-구매일" /></div>
|
||||
<div class="form-group"><label for="pc-금액">금액</label><input type="text" id="pc-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" /></div>
|
||||
<div class="form-group"><label for="pc-납품업체">납품업체</label><input type="text" id="pc-납품업체" /></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 수정 이력</h3>
|
||||
</div>
|
||||
<div id="pc-history-list" class="history-timeline">
|
||||
<div class="empty-history">이력이 없습니다.</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-pc-asset" class="btn btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-pc-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-pc-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-pc-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-pc-modal" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-pc-asset" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Asset Modal -->
|
||||
<div id="storage-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h2 id="storage-modal-title">스토리지 상세 정보</h2>
|
||||
<button id="btn-close-storage-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
||||
<form id="storage-asset-form" class="grid-form">
|
||||
<input type="hidden" id="storage-asset-id" />
|
||||
<input type="hidden" id="storage-asset-type" value="스토리지" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-법인">법인</label>
|
||||
<select id="storage-법인" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
||||
<option value="한맥">한맥 (HM)</option>
|
||||
<option value="삼안">삼안 (SM)</option>
|
||||
<option value="바론">바론 (BR)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-유형">유형</label>
|
||||
<select id="storage-유형" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
||||
<option value="NAS">NAS</option>
|
||||
<option value="DAS">DAS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-자산코드">자산코드</label>
|
||||
<input type="text" id="storage-자산코드" placeholder="ex) HM-NAS-2024-001" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-명칭">명칭</label>
|
||||
<input type="text" id="storage-명칭" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-위치">위치</label>
|
||||
<input type="text" id="storage-위치" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-모델명">모델명</label>
|
||||
<input type="text" id="storage-모델명" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-용량">용량</label>
|
||||
<input type="text" id="storage-용량" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-담당자_정">담당자(정)</label>
|
||||
<input type="text" id="storage-담당자_정" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-담당자_부">담당자(부)</label>
|
||||
<input type="text" id="storage-담당자_부" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-IP주소">IP주소</label>
|
||||
<input type="text" id="storage-IP주소" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-MAC주소">MAC 주소</label>
|
||||
<input type="text" id="storage-MAC주소" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-구매일">구매일</label>
|
||||
<input type="text" id="storage-구매일" placeholder="ex) 2024-01-01" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storage-금액">금액</label>
|
||||
<input type="text" id="storage-금액" 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="storage-납품업체">납품업체</label>
|
||||
<input type="text" id="storage-납품업체" />
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label style="font-size:0.875rem;">품의서 (파일)</label>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<input type="file" id="storage-품의서" style="font-size:0.875rem;" />
|
||||
<span id="storage-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label for="storage-법인">법인</label><input type="text" id="storage-법인" required /></div>
|
||||
<div class="form-group"><label for="storage-유형">유형</label><input type="text" id="storage-유형" required /></div>
|
||||
<div class="form-group"><label for="storage-자산코드">자산코드</label><input type="text" id="storage-자산코드" required /></div>
|
||||
<div class="form-group"><label for="storage-명칭">명칭</label><input type="text" id="storage-명칭" required /></div>
|
||||
<div class="form-group"><label for="storage-위치">위치</label><input type="text" id="storage-위치" /></div>
|
||||
<div class="form-group"><label for="storage-모델명">모델명</label><input type="text" id="storage-모델명" /></div>
|
||||
<div class="form-group"><label for="storage-용량">용량</label><input type="text" id="storage-용량" /></div>
|
||||
<div class="form-group"><label for="storage-담당자_정">담당자(정)</label><input type="text" id="storage-담당자_정" /></div>
|
||||
<div class="form-group"><label for="storage-IP주소">IP주소</label><input type="text" id="storage-IP주소" /></div>
|
||||
<div class="form-group"><label for="storage-구매일">구매일</label><input type="text" id="storage-구매일" /></div>
|
||||
<div class="form-group"><label for="storage-금액">금액</label><input type="text" id="storage-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" /></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-storage-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<button id="btn-delete-storage-asset" class="btn btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-storage-modal" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-storage-asset" class="btn btn-primary">저장</button>
|
||||
<button id="btn-cancel-storage-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-storage-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,93 +285,28 @@
|
||||
|
||||
<!-- SW Asset Modal -->
|
||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h2 id="sw-modal-title">S/W 상세 정보</h2>
|
||||
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
||||
<form id="sw-asset-form" class="grid-form">
|
||||
<input type="hidden" id="sw-asset-id" />
|
||||
<input type="hidden" id="sw-asset-type" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sw-분야">분야</label>
|
||||
<select id="sw-분야" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
||||
<option value="업무공통">업무공통</option>
|
||||
<option value="개발S/W">개발S/W</option>
|
||||
<option value="디자인">디자인</option>
|
||||
<option value="설계S/W">설계S/W</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sw-법인">법인</label>
|
||||
<select id="sw-법인" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
||||
<option value="한맥">한맥 (HM)</option>
|
||||
<option value="삼안">삼안 (SM)</option>
|
||||
<option value="바론">바론 (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="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;">
|
||||
<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" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;" />
|
||||
</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>
|
||||
<input type="hidden" id="sw-asset-id" /><input type="hidden" id="sw-asset-type" />
|
||||
<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-제품명" required /></div>
|
||||
<div class="form-group"><label for="sw-구매일">구매일</label><input type="text" id="sw-구매일" /></div>
|
||||
<div class="form-group" id="sw-구독일-group"><label for="sw-구독일">구독일</label><input type="text" id="sw-구독일" /></div>
|
||||
<div class="form-group"><label for="sw-금액">금액</label><input type="text" id="sw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" /></div>
|
||||
<div class="form-group"><label for="sw-수량">수량</label><input type="number" id="sw-수량" min="1" /></div>
|
||||
<div class="form-group"><label for="sw-비고">비고</label><input type="text" id="sw-비고" /></div>
|
||||
</form>
|
||||
</div>
|
||||
<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-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-modal" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-sw-asset" class="btn btn-primary">저장</button>
|
||||
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-sw-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,120 +314,25 @@
|
||||
|
||||
<!-- SW User Management Modal -->
|
||||
<div id="sw-user-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h2 id="sw-user-modal-title">S/W 할당 사용자 목록</h2>
|
||||
<button id="btn-close-sw-user-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
||||
<input type="hidden" id="sw-user-asset-id" />
|
||||
|
||||
<div style="text-align: right; margin-bottom: 0.5rem;">
|
||||
<button type="button" id="btn-open-add-user" class="btn btn-primary" style="padding: 0.25rem 1rem;"><i data-lucide="plus"></i> 새 사용자 추가</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container" style="max-height: 250px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 4px; margin-top:0;">
|
||||
<table style="width:100%; border-collapse: collapse; font-size:0.875rem;">
|
||||
<thead style="position: sticky; top: 0; background: var(--bg-light); z-index: 10;">
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<th style="padding:0.5rem; text-align:left;">법인</th>
|
||||
<th style="padding:0.5rem; text-align:left;">부서/팀</th>
|
||||
<th style="padding:0.5rem; text-align:left;">직위</th>
|
||||
<th style="padding:0.5rem; text-align:left;">이름</th>
|
||||
<th style="padding:0.5rem; text-align:center;">사용기간</th>
|
||||
<th style="padding:0.5rem; text-align:center;">첨부파일</th>
|
||||
<th style="padding:0.5rem; text-align:center;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="user-list-body">
|
||||
<!-- Users will be injected here -->
|
||||
</tbody>
|
||||
<div class="table-container">
|
||||
<table style="width:100%;">
|
||||
<thead><tr><th>법인</th><th>부서/팀</th><th>직위</th><th>이름</th><th>사용기간</th><th>관리</th></tr></thead>
|
||||
<tbody id="user-list-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content: flex-end;">
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-user-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-sw-user-mapping" class="btn btn-primary">변경사항 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SW User Form Modal (Add / Edit) -->
|
||||
<div id="sw-user-edit-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="sw-user-edit-modal-title">사용자 추가</h2>
|
||||
<button id="btn-close-sw-user-edit-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit-user-idx" value="-1" />
|
||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="new-user-법인" style="font-size:0.875rem;">법인</label>
|
||||
<select id="new-user-법인" class="form-control" style="width:100%; padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<option value="한맥">한맥</option><option value="삼안">삼안</option><option value="바론">바론</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-user-부서" style="font-size:0.875rem;">부서</label>
|
||||
<input type="text" id="new-user-부서" placeholder="ex: 기술부" style="width:100%; padding:0.5rem; border:1px solid var(--border); border-radius:4px;" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-user-팀" style="font-size:0.875rem;">팀</label>
|
||||
<input type="text" id="new-user-팀" placeholder="ex: 개발1팀" style="width:100%; padding:0.5rem; border:1px solid var(--border); border-radius:4px;" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-user-직위" style="font-size:0.875rem;">직위</label>
|
||||
<input type="text" id="new-user-직위" placeholder="ex: 대리" style="width:100%; padding:0.5rem; border:1px solid var(--border); border-radius:4px;" />
|
||||
</div>
|
||||
<div class="form-group" style="grid-column: span 2;">
|
||||
<label for="new-user-이름" style="font-size:0.875rem;">이름 <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" id="new-user-이름" placeholder="ex: 홍길동" style="width:100%; padding:0.5rem; border:1px solid var(--border); border-radius:4px;" required />
|
||||
</div>
|
||||
<div class="form-group" style="grid-column: span 2;">
|
||||
<label for="new-user-사용기간" style="font-size:0.875rem;">사용기간</label>
|
||||
<input type="text" id="new-user-사용기간" placeholder="ex: 2024.01~12" style="width:100%; padding:0.5rem; border:1px solid var(--border); border-radius:4px;" />
|
||||
</div>
|
||||
<div class="form-group" style="grid-column: span 2;">
|
||||
<label style="font-size:0.875rem;">신청서 (파일)</label>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<input type="file" id="new-user-신청서" style="font-size:0.875rem;" />
|
||||
<span id="new-user-신청서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content: flex-end;">
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-user-edit" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-edit-user" class="btn btn-primary">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Detail Modal -->
|
||||
<div id="dashboard-detail-modal" class="modal-overlay hidden" style="z-index: 1200;">
|
||||
<div class="modal-content" style="max-width: 1000px; max-height: 80vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h2 id="dashboard-detail-modal-title">상세 자산 목록</h2>
|
||||
<button id="btn-close-dashboard-detail" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: 0;">
|
||||
<div class="table-container" style="box-shadow: none; border-radius: 0;">
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th>
|
||||
<th>위치</th><th>담당/사용자</th><th>구매일</th><th>금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dashboard-detail-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button id="btn-cancel-sw-user-modal" class="btn btn-outline">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,27 @@ import { state } from '../../state';
|
||||
import { HardwareAsset } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* 폼의 모든 입력 필드를 활성화/비활성화 처리
|
||||
*/
|
||||
function setFormReadOnly(form: HTMLFormElement, isReadOnly: boolean) {
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
if (input.type === 'file') {
|
||||
(input as HTMLElement).style.display = isReadOnly ? 'none' : 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReadOnly) {
|
||||
(input as HTMLElement).setAttribute('readonly', 'true');
|
||||
if (input.tagName === 'SELECT') (input as HTMLSelectElement).disabled = true;
|
||||
} else {
|
||||
(input as HTMLElement).removeAttribute('readonly');
|
||||
if (input.tagName === 'SELECT') (input as HTMLSelectElement).disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하드웨어(서버, 전산비품 등) 모달 초기화 및 로직 제어
|
||||
*/
|
||||
@@ -10,33 +31,56 @@ export function initHWModal(renderContent: () => void, closeModals: () => void)
|
||||
const btnSaveHw = document.getElementById('btn-save-hw-asset') as HTMLButtonElement;
|
||||
const btnDeleteHw = document.getElementById('btn-delete-hw-asset') as HTMLButtonElement;
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
// 저장/수정 버튼 통합 이벤트
|
||||
btnSaveHw?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 현재 버튼이 '수정' 상태인 경우
|
||||
if (btnSaveHw.textContent === '수정') {
|
||||
setFormReadOnly(hwForm, false);
|
||||
btnSaveHw.textContent = '저장';
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 버튼이 '저장' 상태인 경우 (실제 저장 로직)
|
||||
if (!hwForm.checkValidity()) { hwForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
|
||||
const type = (document.getElementById('hw-asset-type') as HTMLInputElement).value;
|
||||
const fileInput = document.getElementById('hw-품의서') as HTMLInputElement;
|
||||
const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('hw-품의서명') as HTMLElement).innerText.replace('📎', '');
|
||||
|
||||
const newAsset: HardwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: (document.getElementById('hw-asset-type') as HTMLInputElement).value,
|
||||
type: type,
|
||||
법인: (document.getElementById('hw-법인') as HTMLInputElement).value,
|
||||
자산코드: (document.getElementById('hw-자산코드') as HTMLInputElement).value,
|
||||
명칭: (document.getElementById('hw-명칭') as HTMLInputElement).value,
|
||||
명칭: (document.getElementById('hw-명칭') as HTMLInputElement).value || '',
|
||||
위치: (document.getElementById('hw-위치') as HTMLInputElement).value,
|
||||
관리자: (document.getElementById('hw-관리자') as HTMLInputElement).value,
|
||||
관리자: (document.getElementById('hw-담당자_정') as HTMLInputElement).value,
|
||||
담당자_정: (document.getElementById('hw-담당자_정') as HTMLInputElement).value,
|
||||
담당자_부: (document.getElementById('hw-담당자_부') as HTMLInputElement).value,
|
||||
IP주소: (document.getElementById('hw-IP주소') as HTMLInputElement).value,
|
||||
IP2: (document.getElementById('hw-IP2') as HTMLInputElement).value,
|
||||
MACaddress: (document.getElementById('hw-MACaddress') as HTMLInputElement).value,
|
||||
OS: (document.getElementById('hw-OS') as HTMLInputElement).value,
|
||||
CPU: (document.getElementById('hw-CPU') as HTMLInputElement).value,
|
||||
RAM: (document.getElementById('hw-RAM') as HTMLInputElement).value,
|
||||
SSD1: (document.getElementById('hw-SSD1') as HTMLInputElement).value,
|
||||
SSD2: (document.getElementById('hw-SSD2') as HTMLInputElement).value,
|
||||
HW사양: (document.getElementById('hw-HW사양') as HTMLTextAreaElement).value,
|
||||
구매일: (document.getElementById('hw-구매일') as HTMLInputElement).value,
|
||||
금액: (document.getElementById('hw-금액') as HTMLInputElement).value,
|
||||
납품업체: (document.getElementById('hw-납품업체') as HTMLInputElement).value,
|
||||
용도: (document.getElementById('hw-용도') as HTMLInputElement).value,
|
||||
상세: (document.getElementById('hw-상세') as HTMLInputElement).value,
|
||||
원격접속: (document.getElementById('hw-원격접속') as HTMLInputElement).value,
|
||||
서버ID: (document.getElementById('hw-서버ID') as HTMLInputElement).value,
|
||||
서버PW: (document.getElementById('hw-서버PW') as HTMLInputElement).value,
|
||||
비고: (document.getElementById('hw-비고') as HTMLInputElement).value,
|
||||
품의서명,
|
||||
비품유형: (document.getElementById('hw-asset-type') as HTMLInputElement).value === '전산비품'
|
||||
? (document.getElementById('hw-비품유형') as HTMLSelectElement).value : undefined
|
||||
비품유형: type === '전산비품' ? (document.getElementById('hw-비품유형') as HTMLSelectElement).value : undefined,
|
||||
storage유형: (document.getElementById('hw-용도') as HTMLInputElement).value
|
||||
};
|
||||
|
||||
if (id) {
|
||||
@@ -64,49 +108,75 @@ export function initHWModal(renderContent: () => void, closeModals: () => void)
|
||||
|
||||
/**
|
||||
* 하드웨어 상세 모달 열기
|
||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
||||
*/
|
||||
export function openHwModal(asset?: HardwareAsset) {
|
||||
const hwModal = document.getElementById('hw-asset-modal') as HTMLDivElement;
|
||||
const hwForm = document.getElementById('hw-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
|
||||
const btnSaveHw = document.getElementById('btn-save-hw-asset') as HTMLButtonElement;
|
||||
const currentType = asset ? asset.type : state.activeSubTab;
|
||||
|
||||
openModal('hw-asset-modal');
|
||||
hwForm.reset();
|
||||
|
||||
// 타입에 따른 필드 노출 제어
|
||||
const serverFields = document.querySelectorAll('.server-only');
|
||||
const nonServerFields = document.querySelectorAll('.non-server');
|
||||
|
||||
if (currentType === '서버') {
|
||||
serverFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
nonServerFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
} else {
|
||||
serverFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
nonServerFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
}
|
||||
|
||||
if (asset) {
|
||||
document.getElementById('hw-modal-title')!.textContent = '자산 상세 정보 수정';
|
||||
document.getElementById('hw-modal-title')!.textContent = `${currentType} 상세 정보 수정`;
|
||||
deleteBtn.style.display = 'block';
|
||||
btnSaveHw.textContent = '수정';
|
||||
setFormReadOnly(hwForm, true); // 수정 시 초기 상태는 읽기 전용
|
||||
|
||||
(document.getElementById('hw-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('hw-asset-type') as HTMLInputElement).value = asset.type;
|
||||
(document.getElementById('hw-법인') as HTMLInputElement).value = asset.법인;
|
||||
(document.getElementById('hw-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('hw-명칭') as HTMLInputElement).value = asset.명칭;
|
||||
(document.getElementById('hw-위치') as HTMLInputElement).value = asset.위치;
|
||||
(document.getElementById('hw-관리자') as HTMLInputElement).value = asset.관리자;
|
||||
(document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소;
|
||||
(document.getElementById('hw-MACaddress') as HTMLInputElement).value = asset.MACaddress;
|
||||
(document.getElementById('hw-OS') as HTMLInputElement).value = asset.OS;
|
||||
(document.getElementById('hw-HW사양') as HTMLTextAreaElement).value = asset.HW사양;
|
||||
(document.getElementById('hw-법인') as HTMLInputElement).value = asset.법인 || '';
|
||||
(document.getElementById('hw-자산코드') as HTMLInputElement).value = asset.자산코드 || '';
|
||||
(document.getElementById('hw-명칭') as HTMLInputElement).value = asset.명칭 || '';
|
||||
(document.getElementById('hw-위치') as HTMLInputElement).value = asset.위치 || '';
|
||||
(document.getElementById('hw-담당자_정') as HTMLInputElement).value = asset.담당자_정 || asset.관리자 || '';
|
||||
(document.getElementById('hw-담당자_부') as HTMLInputElement).value = asset.담당자_부 || '';
|
||||
(document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
|
||||
(document.getElementById('hw-IP2') as HTMLInputElement).value = asset.IP2 || '';
|
||||
(document.getElementById('hw-MACaddress') as HTMLInputElement).value = asset.MACaddress || '';
|
||||
(document.getElementById('hw-OS') as HTMLInputElement).value = asset.OS || '';
|
||||
(document.getElementById('hw-CPU') as HTMLInputElement).value = asset.CPU || '';
|
||||
(document.getElementById('hw-RAM') as HTMLInputElement).value = asset.RAM || '';
|
||||
(document.getElementById('hw-SSD1') as HTMLInputElement).value = asset.SSD1 || '';
|
||||
(document.getElementById('hw-SSD2') as HTMLInputElement).value = asset.SSD2 || '';
|
||||
(document.getElementById('hw-HW사양') as HTMLTextAreaElement).value = asset.HW사양 || '';
|
||||
(document.getElementById('hw-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('hw-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
||||
(document.getElementById('hw-금액') as HTMLInputElement).value = asset.금액 || '';
|
||||
(document.getElementById('hw-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('hw-용도') as HTMLInputElement).value = asset.용도 || asset.storage유형 || '';
|
||||
(document.getElementById('hw-상세') as HTMLInputElement).value = asset.상세 || '';
|
||||
(document.getElementById('hw-원격접속') as HTMLInputElement).value = asset.원격접속 || '';
|
||||
(document.getElementById('hw-서버ID') as HTMLInputElement).value = asset.서버ID || '';
|
||||
(document.getElementById('hw-서버PW') as HTMLInputElement).value = asset.서버PW || '';
|
||||
(document.getElementById('hw-비고') as HTMLInputElement).value = asset.비고 || '';
|
||||
(document.getElementById('hw-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
||||
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset.비품유형 || '노트북';
|
||||
|
||||
if (currentType === '전산비품') {
|
||||
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset.비품유형 || '노트북';
|
||||
}
|
||||
} else {
|
||||
document.getElementById('hw-modal-title')!.textContent = `새 ${state.activeSubTab} 자산 추가`;
|
||||
document.getElementById('hw-modal-title')!.textContent = `새 ${currentType} 자산 추가`;
|
||||
deleteBtn.style.display = 'none';
|
||||
btnSaveHw.textContent = '저장';
|
||||
setFormReadOnly(hwForm, false); // 신규 등록 시 편집 가능 상태
|
||||
|
||||
(document.getElementById('hw-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('hw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||
(document.getElementById('hw-asset-type') as HTMLInputElement).value = currentType;
|
||||
(document.getElementById('hw-품의서명') as HTMLElement).innerText = '';
|
||||
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = '노트북';
|
||||
}
|
||||
|
||||
// 전산비품일 경우 유형 선택 필드 노출
|
||||
if (state.activeSubTab === '전산비품') {
|
||||
document.getElementById('hw-비품유형-group')!.style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('hw-비품유형-group')!.style.display = 'none';
|
||||
}
|
||||
document.getElementById('hw-비품유형-group')!.style.display = (currentType === '전산비품') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { state } from '../../state';
|
||||
import { HardwareAsset, HardwareLog } from '../../excelHandler';
|
||||
import { HardwareAsset } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* 개인PC 모달 초기화 및 로직 제어
|
||||
*/
|
||||
export function initPCModal(renderContent: () => void, closeModals: () => void) {
|
||||
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
|
||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
||||
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
||||
@@ -47,27 +48,7 @@ export function initPCModal(renderContent: () => void, closeModals: () => void)
|
||||
|
||||
if (id) {
|
||||
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
||||
if(idx !== -1) {
|
||||
const oldAsset = state.masterData.hw[idx];
|
||||
const changes = getChangeDetails(oldAsset, newAsset);
|
||||
|
||||
if (changes) {
|
||||
// 로그인 기능이 없으므로 '관리자'가 로그인한 것으로 가정
|
||||
const modifier = '관리자';
|
||||
|
||||
const log: HardwareLog = {
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
assetId: id,
|
||||
date: new Date().toLocaleString(),
|
||||
details: changes,
|
||||
user: modifier
|
||||
};
|
||||
|
||||
state.masterData.logs.push(log);
|
||||
}
|
||||
|
||||
state.masterData.hw[idx] = newAsset;
|
||||
}
|
||||
if(idx !== -1) state.masterData.hw[idx] = newAsset;
|
||||
} else {
|
||||
state.masterData.hw.push(newAsset);
|
||||
}
|
||||
@@ -82,7 +63,6 @@ export function initPCModal(renderContent: () => void, closeModals: () => void)
|
||||
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
|
||||
if (confirm('삭제하시겠습니까?')) {
|
||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
||||
// 관련 로그도 삭제할지 여부는 정책에 따라 (여기서는 유지)
|
||||
closeModals();
|
||||
renderContent();
|
||||
}
|
||||
@@ -91,11 +71,12 @@ export function initPCModal(renderContent: () => void, closeModals: () => void)
|
||||
|
||||
/**
|
||||
* 개인PC 상세 모달 열기
|
||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
||||
*/
|
||||
export function openPcModal(asset?: HardwareAsset) {
|
||||
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
|
||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-pc-asset')!;
|
||||
const historyArea = document.querySelector('.modal-history-area') as HTMLElement;
|
||||
|
||||
openModal('pc-asset-modal');
|
||||
pcForm.reset();
|
||||
@@ -103,7 +84,6 @@ export function openPcModal(asset?: HardwareAsset) {
|
||||
if (asset) {
|
||||
document.getElementById('pc-modal-title')!.textContent = '개인PC 상세 정보 수정';
|
||||
deleteBtn.style.display = 'block';
|
||||
if (historyArea) historyArea.style.display = 'flex';
|
||||
|
||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인;
|
||||
@@ -118,78 +98,14 @@ export function openPcModal(asset?: HardwareAsset) {
|
||||
(document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || '';
|
||||
(document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || '';
|
||||
(document.getElementById('pc-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 ? asset.금액.replace(/,/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',') : '';
|
||||
(document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
||||
(document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
||||
|
||||
renderHistory(asset.id);
|
||||
} else {
|
||||
document.getElementById('pc-modal-title')!.textContent = '새 개인PC 자산 추가';
|
||||
deleteBtn.style.display = 'none';
|
||||
if (historyArea) historyArea.style.display = 'none'; // 신규 시 이력 숨김
|
||||
|
||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥';
|
||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 변경 사항 감지 및 문자열 생성
|
||||
*/
|
||||
function getChangeDetails(oldAsset: HardwareAsset, newAsset: HardwareAsset): string {
|
||||
const changes: string[] = [];
|
||||
const fields = [
|
||||
{ key: '법인', label: '법인' },
|
||||
{ key: '자산코드', label: '자산코드' },
|
||||
{ key: '사용자', label: '사용자' },
|
||||
{ key: '위치', label: '위치' },
|
||||
{ key: 'CPU', label: 'CPU' },
|
||||
{ key: 'GPU', label: 'GPU' },
|
||||
{ key: 'RAM', label: 'RAM' },
|
||||
{ key: 'SSD1', label: 'SSD1' },
|
||||
{ key: 'SSD2', label: 'SSD2' },
|
||||
{ key: 'HDD1', label: 'HDD1' },
|
||||
{ key: 'HDD2', label: 'HDD2' },
|
||||
{ key: '구매일', label: '구매일' },
|
||||
{ key: '금액', label: '금액' },
|
||||
{ key: '납품업체', label: '납품업체' },
|
||||
{ key: '품의서명', label: '품의서' },
|
||||
];
|
||||
|
||||
fields.forEach(field => {
|
||||
const oldVal = (oldAsset as any)[field.key] || '';
|
||||
const newVal = (newAsset as any)[field.key] || '';
|
||||
|
||||
if (oldVal !== newVal) {
|
||||
changes.push(`${field.label}: ${oldVal || '없음'} → ${newVal || '없음'}`);
|
||||
}
|
||||
});
|
||||
|
||||
return changes.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 이력 리스트 렌더링
|
||||
*/
|
||||
function renderHistory(assetId: string) {
|
||||
const historyList = document.getElementById('pc-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('');
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@ export function initSWModal(renderContent: () => void, closeModals: () => void)
|
||||
const newAsset: SoftwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: (document.getElementById('sw-asset-type') as HTMLInputElement).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('sw-구매일') as HTMLInputElement).value,
|
||||
구독일: (document.getElementById('sw-구독일') as HTMLInputElement).value,
|
||||
@@ -84,9 +82,7 @@ export function openSwModal(asset?: SoftwareAsset) {
|
||||
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
|
||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = asset.분야 || '업무공통';
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('sw-부서') as HTMLInputElement).value = asset.부서 || '';
|
||||
(document.getElementById('sw-제품명') as HTMLInputElement).value = asset.제품명;
|
||||
(document.getElementById('sw-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('sw-구독일') as HTMLInputElement).value = asset.구독일 || '';
|
||||
@@ -101,8 +97,6 @@ export function openSwModal(asset?: SoftwareAsset) {
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||
(document.getElementById('sw-분야') as HTMLSelectElement).value = '업무공통';
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = '한맥';
|
||||
(document.getElementById('sw-부서') as HTMLInputElement).value = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function generateDummyData(): MasterAssetData {
|
||||
|
||||
// 1. 개인PC 50개
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
|
||||
const purchaseYear = Math.floor(Math.random() * 8) + 2017; // 2017~2024
|
||||
hw.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
type: '개인PC',
|
||||
@@ -43,7 +43,7 @@ export function generateDummyData(): MasterAssetData {
|
||||
HDD1: rand(['-', '1TB', '2TB']),
|
||||
HDD2: '',
|
||||
구매일: randDate(purchaseYear, purchaseYear),
|
||||
금액: String(Math.floor(Math.random()*100 + 50) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','),
|
||||
금액: String(Math.floor(Math.random()*100 + 50) * 10000).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','),
|
||||
납품업체: rand(['다나와', '컴퓨존', '오피스디포']),
|
||||
품의서명: '',
|
||||
관리자: '', IP주소: '', MACaddress: '', OS: '', HW사양: ''
|
||||
@@ -52,18 +52,32 @@ export function generateDummyData(): MasterAssetData {
|
||||
|
||||
// 2. 서버 20개
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
|
||||
const purchaseYear = Math.floor(Math.random() * 8) + 2017; // 2017~2024
|
||||
hw.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
type: '서버',
|
||||
법인: rand(corps),
|
||||
자산코드: `HM-SV-${purchaseYear}-${String(i).padStart(3, '0')}`,
|
||||
명칭: `웹/DB 서버 #${i}`,
|
||||
위치: 'IDC / 전산실',
|
||||
관리자: randUser(),
|
||||
용도: rand(['웹 서버', 'DB 서버', '백업 서버', '개발 서버']),
|
||||
storage유형: rand(['물리', 'VM']),
|
||||
위치: rand(['IDC 1센터', 'IDC 2센터', '본사 전산실']),
|
||||
관리자: rand(users),
|
||||
담당자_정: rand(users),
|
||||
담당자_부: rand(users),
|
||||
IP주소: `192.168.10.${i}`,
|
||||
원격접속: `ssh://192.168.10.${i}:22`,
|
||||
MACaddress: '00:11:22:33:44:' + String(i).padStart(2, '0'),
|
||||
OS: rand(['Windows Server 2019', 'Ubuntu 22.04 LTS', 'CentOS 7']),
|
||||
모델명: rand(['Dell PowerEdge R740', 'HP ProLiant DL380', 'Lenovo ThinkSystem']),
|
||||
CPU: rand(['Xeon Silver 4210', 'Xeon Gold 6248', 'EPYC 7702']),
|
||||
RAM: rand(['64GB', '128GB', '256GB']),
|
||||
GPU: rand(['-', 'RTX A4000', 'Tesla V100']),
|
||||
SSD1: rand(['512GB SSD', '1TB NVMe']),
|
||||
SSD2: rand(['-', '1TB SSD', '2TB SSD']),
|
||||
HDD1: rand(['-', '4TB HDD', '8TB HDD']),
|
||||
모니터링: rand(['Zabbix', 'Grafana', 'PRTG']),
|
||||
비고: i % 5 === 0 ? '정기 점검 대상' : '-',
|
||||
HW사양: 'Xeon 16Core, 64GB RAM',
|
||||
구매일: randDate(purchaseYear, purchaseYear),
|
||||
금액: '5,000,000',
|
||||
@@ -74,7 +88,7 @@ export function generateDummyData(): MasterAssetData {
|
||||
|
||||
// 3. 스토리지 20개
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
|
||||
const purchaseYear = Math.floor(Math.random() * 8) + 2017; // 2017~2024
|
||||
hw.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
type: '스토리지',
|
||||
@@ -105,7 +119,7 @@ export function generateDummyData(): MasterAssetData {
|
||||
];
|
||||
equips.forEach((eq) => {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const purchaseYear = Math.floor(Math.random() * 8) + 2019; // 2019~2026
|
||||
const purchaseYear = Math.floor(Math.random() * 6) + 2019; // 2019~2024
|
||||
hw.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
type: '전산비품',
|
||||
@@ -127,7 +141,6 @@ export function generateDummyData(): MasterAssetData {
|
||||
// 5. 구독형 S/W 40개
|
||||
for (let i = 1; i <= 40; i++) {
|
||||
const swId = Math.random().toString(36).substring(2, 9);
|
||||
const purchaseYear = Math.random() < 0.3 ? 2026 : 2024;
|
||||
|
||||
let isExpiring = Math.random() < 0.25;
|
||||
let endDt = new Date();
|
||||
@@ -141,19 +154,16 @@ export function generateDummyData(): MasterAssetData {
|
||||
sw.push({
|
||||
id: swId,
|
||||
type: '구독SW',
|
||||
분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']),
|
||||
법인: rand(corps),
|
||||
부서: rand(depts),
|
||||
제품명: rand(['Adobe CC All Apps', 'Microsoft 365', 'Slack Pro', 'Notion Team']),
|
||||
구매일: `${purchaseYear}-01-01`,
|
||||
구독일: `${purchaseYear}.01.01 ~ ${endStr}`,
|
||||
금액: String(Math.floor(Math.random() * 100 + 10) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','),
|
||||
구매일: '2024-01-01',
|
||||
구독일: `2024.01.01 ~ ${endStr}`,
|
||||
금액: '600,000',
|
||||
수량: Math.floor(Math.random() * 5) + 3, // 3~7
|
||||
계정명: `user${i}@hm.com`,
|
||||
납품업체: '총판',
|
||||
비고: '연간구독'
|
||||
});
|
||||
// ... rest unchanged
|
||||
const assignCount = Math.floor(Math.random() * 2) + 1;
|
||||
for (let j=0; j<assignCount; j++) {
|
||||
swUsers.push({
|
||||
@@ -186,9 +196,7 @@ export function generateDummyData(): MasterAssetData {
|
||||
sw.push({
|
||||
id: swId,
|
||||
type: '영구SW',
|
||||
분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']),
|
||||
법인: rand(corps),
|
||||
부서: rand(depts),
|
||||
제품명: rand(['AutoCAD 2024', 'Windows 10 Pro', '한컴오피스 2022', 'Visual Studio 2022']),
|
||||
구매일: '2020-05-15',
|
||||
유지보수여부: true,
|
||||
|
||||
@@ -28,16 +28,20 @@ export interface HardwareAsset {
|
||||
담당자_부?: string;
|
||||
구매일?: string;
|
||||
금액?: string;
|
||||
납품업체?: string;
|
||||
품의서명?: string;
|
||||
}
|
||||
납품업체: string;
|
||||
품의서명: string;
|
||||
용도?: string;
|
||||
상세?: string;
|
||||
원격접속?: string;
|
||||
모니터링?: string;
|
||||
비고?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface SoftwareAsset {
|
||||
id: string;
|
||||
type: string; // '구독SW', '영구SW'
|
||||
분야?: string;
|
||||
법인: string;
|
||||
부서?: string;
|
||||
제품명: string;
|
||||
구매일: string;
|
||||
구독일?: string;
|
||||
@@ -61,19 +65,10 @@ export interface SWUser {
|
||||
신청서명: string;
|
||||
}
|
||||
|
||||
export interface HardwareLog {
|
||||
id: string;
|
||||
assetId: string;
|
||||
date: string;
|
||||
details: string;
|
||||
user: string;
|
||||
}
|
||||
|
||||
export interface MasterAssetData {
|
||||
hw: HardwareAsset[];
|
||||
sw: SoftwareAsset[];
|
||||
swUsers: SWUser[];
|
||||
logs: HardwareLog[];
|
||||
}
|
||||
|
||||
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
||||
@@ -81,11 +76,11 @@ const SW_TABS = ['구독SW', '영구SW'];
|
||||
|
||||
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명'];
|
||||
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', '구매일', '금액', '납품업체', '품의서명'];
|
||||
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage1', 'Storage2', 'Storage3', '모니터링', '비고'];
|
||||
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명'];
|
||||
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||
const SUB_SW_HEADERS = ['ID', '법인', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||
const PERM_SW_HEADERS = ['ID', '법인', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
||||
const HISTORY_HEADERS = ['id', 'assetId', 'date', 'details', 'user'];
|
||||
|
||||
/**
|
||||
* 템플릿 엑셀 다중 시트로 다운로드
|
||||
@@ -95,26 +90,33 @@ export function downloadTemplate() {
|
||||
|
||||
// HW 탭들 생성
|
||||
HW_TABS.forEach(tab => {
|
||||
let hd = HW_HEADERS;
|
||||
let wscols: any[] = [];
|
||||
|
||||
if (tab === '개인PC') {
|
||||
const ws = XLSX.utils.aoa_to_sheet([PC_HEADERS]);
|
||||
ws['!cols'] = [{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}];
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
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}];
|
||||
} else if (tab === '서버') {
|
||||
hd = SERVER_HEADERS;
|
||||
wscols = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}];
|
||||
} else if (tab === '스토리지') {
|
||||
const ws = XLSX.utils.aoa_to_sheet([STORAGE_HEADERS]);
|
||||
ws['!cols'] = [{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}];
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
hd = STORAGE_HEADERS;
|
||||
wscols = [{wch:15}, {wch:15}, {wch:25}, {wch:25}, {wch:20}, {wch:25}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
} else {
|
||||
const ws = XLSX.utils.aoa_to_sheet([HW_HEADERS]);
|
||||
ws['!cols'] = [{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}];
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
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}];
|
||||
}
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
||||
ws['!cols'] = wscols;
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
});
|
||||
|
||||
// SW 탭들 생성
|
||||
SW_TABS.forEach(tab => {
|
||||
let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS;
|
||||
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
||||
ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
|
||||
ws['!cols'] = [{wch:15}, {wch:15}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
});
|
||||
|
||||
@@ -122,11 +124,6 @@ export function downloadTemplate() {
|
||||
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
||||
|
||||
// History 시트 (템플릿에도 포함)
|
||||
const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]);
|
||||
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
|
||||
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
|
||||
|
||||
XLSX.writeFile(wb, 'itam_assets_template.xlsx');
|
||||
}
|
||||
|
||||
@@ -148,6 +145,12 @@ export function exportToExcel(masterData: MasterAssetData) {
|
||||
...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.품의서명])
|
||||
];
|
||||
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}];
|
||||
} else if (tab === '서버') {
|
||||
wsData = [
|
||||
SERVER_HEADERS,
|
||||
...targetAssets.map(a => [a.법인, a.자산코드, a.storage유형 || '물리', a.용도 || '', a.위치, a.담당자_정 || '', a.담당자_부 || '', a.IP주소, a.원격접속 || '', a.모델명 || '', a.OS, a.CPU, a.RAM, a.GPU, a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a.모니터링 || '', a.비고 || ''])
|
||||
];
|
||||
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}];
|
||||
} else if (tab === '스토리지') {
|
||||
wsData = [
|
||||
STORAGE_HEADERS,
|
||||
@@ -174,16 +177,16 @@ export function exportToExcel(masterData: MasterAssetData) {
|
||||
if (tab === '구독SW') {
|
||||
wsData = [
|
||||
SUB_SW_HEADERS,
|
||||
...targetAssets.map(a => [a.id, a.분야||'', a.법인, a.부서||'', a.제품명, a.구매일, a.구독일, a.금액, a.수량, a.계정명, a.납품업체, a.비고])
|
||||
...targetAssets.map(a => [a.id, a.법인, a.제품명, a.구매일, a.구독일, a.금액, a.수량, a.계정명, a.납품업체, a.비고])
|
||||
];
|
||||
} else {
|
||||
wsData = [
|
||||
PERM_SW_HEADERS,
|
||||
...targetAssets.map(a => [a.id, a.분야||'', a.법인, a.부서||'', a.제품명, a.구매일, a.유지보수여부 ? 'Y' : 'N', a.금액, a.수량, a.계정명, a.납품업체, a.비고])
|
||||
...targetAssets.map(a => [a.id, a.법인, a.제품명, a.구매일, a.유지보수여부 ? 'Y' : 'N', a.금액, a.수량, a.계정명, a.납품업체, a.비고])
|
||||
];
|
||||
}
|
||||
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
||||
ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
|
||||
ws['!cols'] = [{wch:15}, {wch:15}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
});
|
||||
|
||||
@@ -195,15 +198,6 @@ export function exportToExcel(masterData: MasterAssetData) {
|
||||
const swUserWs = XLSX.utils.aoa_to_sheet(swUserWsData);
|
||||
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
||||
|
||||
// History
|
||||
const historyWsData = [
|
||||
HISTORY_HEADERS,
|
||||
...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user])
|
||||
];
|
||||
const historyWs = XLSX.utils.aoa_to_sheet(historyWsData);
|
||||
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
|
||||
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
|
||||
|
||||
const dateStr = new Date().toISOString().split('T')[0];
|
||||
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
|
||||
@@ -224,7 +218,6 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
const hwAssets: HardwareAsset[] = [];
|
||||
const swAssets: SoftwareAsset[] = [];
|
||||
const swUsers: SWUser[] = [];
|
||||
const logs: HardwareLog[] = [];
|
||||
|
||||
workbook.SheetNames.forEach(sheetName => {
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
@@ -258,6 +251,38 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
납품업체: row['납품업체'] || '',
|
||||
품의서명: row['품의서명'] || '',
|
||||
});
|
||||
} else if (sheetName === '서버') {
|
||||
hwAssets.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
type: sheetName,
|
||||
법인: row['법인'] || '',
|
||||
자산코드: row['자산번호'] || row['자산코드'] || '',
|
||||
명칭: row['용도'] || row['명칭'] || '',
|
||||
용도: row['용도'] || '',
|
||||
위치: row['설치위치'] || row['위치'] || '',
|
||||
관리자: row['담당자(정)'] || '',
|
||||
담당자_정: row['담당자(정)'] || '',
|
||||
담당자_부: row['담당자(부)'] || '',
|
||||
IP주소: row['IP 주소'] || row['IP주소'] || '',
|
||||
원격접속: row['원격접속'] || '',
|
||||
모델명: row['모델명'] || '',
|
||||
OS: row['OS'] || '',
|
||||
CPU: row['CPU'] || '',
|
||||
RAM: row['RAM'] || '',
|
||||
GPU: row['GPU'] || '',
|
||||
SSD1: row['Storage1'] || row['SSD1'] || '',
|
||||
SSD2: row['Storage2'] || row['SSD2'] || '',
|
||||
HDD1: row['Storage3'] || row['HDD1'] || '',
|
||||
모니터링: row['모니터링'] || '',
|
||||
비고: row['비고'] || '',
|
||||
storage유형: row['유형'] || '물리',
|
||||
MACaddress: '',
|
||||
HW사양: '',
|
||||
구매일: '',
|
||||
금액: '',
|
||||
납품업체: '',
|
||||
품의서명: '',
|
||||
});
|
||||
} else if (sheetName === '스토리지') {
|
||||
hwAssets.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
@@ -308,9 +333,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
swAssets.push({
|
||||
id: row['ID'] ? String(row['ID']) : Math.random().toString(36).substring(2, 9),
|
||||
type: sheetName,
|
||||
분야: row['분야'] || '',
|
||||
법인: row['법인'] || '',
|
||||
부서: row['부서'] || '',
|
||||
제품명: row['제품명'] || '',
|
||||
구매일: row['구매일'] || '',
|
||||
구독일: row['구독일'] || '',
|
||||
@@ -339,21 +362,9 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (sheetName === 'History') {
|
||||
json.forEach(row => {
|
||||
logs.push({
|
||||
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
|
||||
assetId: row['assetId'] ? String(row['assetId']) : '',
|
||||
date: row['date'] || '',
|
||||
details: row['details'] || '',
|
||||
user: row['user'] || '',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve({ hw: hwAssets, sw: swAssets, swUsers, logs });
|
||||
resolve({ hw: hwAssets, sw: swAssets, swUsers });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History } from 'lucide';
|
||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } from 'lucide';
|
||||
import { downloadTemplate, exportToExcel, parseExcel } from './excelHandler';
|
||||
import { state } from './state';
|
||||
import { initSidebar } from './components/Sidebar';
|
||||
@@ -22,7 +22,7 @@ const btnExport = document.getElementById('btn-export-excel') as HTMLButtonEleme
|
||||
|
||||
// Initialize Icons
|
||||
createIcons({
|
||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History }
|
||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 }
|
||||
});
|
||||
|
||||
// Initialize Components
|
||||
|
||||
1624
src/realServerData.ts
Normal file
1624
src/realServerData.ts
Normal file
File diff suppressed because it is too large
Load Diff
28
src/state.ts
28
src/state.ts
@@ -1,5 +1,6 @@
|
||||
import { MasterAssetData } from './excelHandler';
|
||||
import { generateDummyData } from './dummyDataGenerator';
|
||||
import { realServerData } from './realServerData';
|
||||
|
||||
// --- State Definitions ---
|
||||
export interface AppState {
|
||||
@@ -9,11 +10,34 @@ export interface AppState {
|
||||
activeCharts: any[];
|
||||
}
|
||||
|
||||
const dummy = generateDummyData();
|
||||
// 서버 데이터만 실제 데이터로 교체
|
||||
const mergedHw = [
|
||||
...dummy.hw.filter(a => a.type !== '서버'),
|
||||
...realServerData.map(s => ({
|
||||
...s,
|
||||
type: '서버',
|
||||
관리자: s.담당자_정 || '홍길동',
|
||||
담당자_정: s.담당자_정 || '홍길동',
|
||||
담당자_부: s.담당자_부 || '김철수',
|
||||
MACaddress: s.MACaddress || '',
|
||||
HW사양: s.HW사양 || '',
|
||||
구매일: s.구매일 || '',
|
||||
금액: s.금액 || '',
|
||||
납품업체: s.납품업체 || '',
|
||||
품의서명: s.품의서명 || '',
|
||||
원격접속: s.원격접속 || '',
|
||||
서버ID: s.서버ID || '',
|
||||
서버PW: s.서버PW || '',
|
||||
비고: s.비고 || ''
|
||||
}))
|
||||
];
|
||||
|
||||
// --- Initial State ---
|
||||
export const state: AppState = {
|
||||
masterData: {
|
||||
...generateDummyData(),
|
||||
logs: [] // 초기 로그 배열 추가
|
||||
...dummy,
|
||||
hw: mergedHw
|
||||
},
|
||||
activeCategory: 'hw',
|
||||
activeSubTab: '대시보드',
|
||||
|
||||
@@ -240,12 +240,21 @@ body {
|
||||
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background-color: var(--border-color);
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@@ -270,12 +279,15 @@ body {
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
overflow: auto; /* 가로/세로 스크롤 허용 */
|
||||
max-height: calc(100vh - 180px); /* 화면 높이에 맞춰 제한 (가로 스크롤바 노출용) */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-collapse: separate; /* sticky border 유지를 위해 separate 설정 */
|
||||
border-spacing: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -290,6 +302,10 @@ th {
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
background-color: #FAFAFA;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: inset 0 -1px 0 var(--border-color); /* sticky 시 경계선 유지 */
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -300,66 +316,55 @@ tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background-color: var(--bg-color); }
|
||||
.empty-row td { text-align: center; padding: 3rem; color: var(--text-muted); }
|
||||
|
||||
.sw-table td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Search Filter Bar */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
background-color: var(--white);
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.search-item.flex-1 {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.search-item label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.search-item input,
|
||||
.search-item select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.search-item input:focus,
|
||||
.search-item select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1);
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
height: 36px;
|
||||
padding: 0 1rem;
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-overlay:not(.hidden) { opacity: 1; visibility: visible; }
|
||||
.modal-content {
|
||||
background-color: var(--white);
|
||||
width: 100%; max-width: 600px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.modal-overlay:not(.hidden) .modal-content { transform: translateY(0); }
|
||||
.modal-header {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-header h2 { font-size: 1.125rem; font-weight: 500; }
|
||||
.modal-body { padding: 1.5rem; }
|
||||
.grid-form { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
.form-group.full-width { grid-column: span 2; }
|
||||
.form-group label { font-size: 0.875rem; font-weight: 500; }
|
||||
.form-group input, .form-group textarea {
|
||||
padding: 0.625rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit; font-size: 0.875rem;
|
||||
outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.form-group input:focus, .form-group textarea:focus { border-color: var(--primary-color); }
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem; border-top: 1px solid var(--border-color);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.footer-actions { display: flex; gap: 0.5rem; }
|
||||
@@ -1,161 +0,0 @@
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-overlay:not(.hidden) { opacity: 1; visibility: visible; }
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--white);
|
||||
width: 100%; max-width: 600px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-overlay:not(.hidden) .modal-content { transform: translateY(0); }
|
||||
|
||||
.modal-header {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 { font-size: 1.125rem; font-weight: 500; }
|
||||
|
||||
.modal-body { padding: 1.5rem; }
|
||||
|
||||
.grid-form { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
|
||||
.form-group.full-width { grid-column: span 2; }
|
||||
|
||||
.form-group label { font-size: 0.875rem; font-weight: 500; }
|
||||
|
||||
.form-group input, .form-group textarea {
|
||||
padding: 0.625rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit; font-size: 0.875rem;
|
||||
outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus, .form-group textarea:focus { border-color: var(--primary-color); }
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem; border-top: 1px solid var(--border-color);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
|
||||
.footer-actions { display: flex; gap: 0.5rem; }
|
||||
|
||||
/* Wide Modal for History */
|
||||
.modal-content.wide {
|
||||
max-width: 950px;
|
||||
}
|
||||
|
||||
.modal-body-split {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
.modal-form-area {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.modal-history-area {
|
||||
flex: 0.8;
|
||||
border-left: 1px solid var(--border-color);
|
||||
padding-left: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.history-header h3 {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.history-timeline {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
position: relative;
|
||||
padding-left: 1.25rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-left: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.history-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--white);
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.history-user {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.history-details {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-main);
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { openSwUserModal } from '../components/Modal/SWUserModal';
|
||||
*/
|
||||
export function renderTable(mainContent: HTMLElement) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'view-container'; // 배경과 테두리가 없는 투명한 컨테이너
|
||||
container.className = 'table-container';
|
||||
const table = document.createElement('table');
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
@@ -30,11 +30,8 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
||||
const list = state.masterData.hw.filter(a => a.type === state.activeSubTab);
|
||||
|
||||
if (state.activeSubTab === '개인PC') {
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산코드</th><th>사용자</th><th>위치</th><th>CPU</th><th>GPU</th><th>RAM</th><th>SSD1</th><th>SSD2</th><th>HDD1</th><th>HDD2</th><th>구매일</th><th>금액</th><th>납품업체</th><th>품의서</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||
tableWrapper.appendChild(table);
|
||||
container.appendChild(tableWrapper);
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="17">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
@@ -47,11 +44,8 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else if (state.activeSubTab === '스토리지') {
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
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>`;
|
||||
tableWrapper.appendChild(table);
|
||||
container.appendChild(tableWrapper);
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="13">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
@@ -64,138 +58,124 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
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>`;
|
||||
tableWrapper.appendChild(table);
|
||||
container.appendChild(tableWrapper);
|
||||
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>`;
|
||||
} 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>`;
|
||||
}
|
||||
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="10">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
const colCount = state.activeSubTab === '서버' ? 15 : (state.activeSubTab === '전산비품' ? 11 : 10);
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="${colCount}">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
|
||||
list.forEach((asset, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
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-outline btn-edit">수정</button></td>`;
|
||||
|
||||
const formatInline = (v: any) => String(v || '').replace(/\n/g, ' / ').trim();
|
||||
const getBadge = (text: string, bgColor: string) =>
|
||||
`<span style="background:${bgColor}; color:white; font-size:10px; padding:1px 4px; border-radius:3px; font-weight:700; margin-right:4px; display:inline-block; line-height:1.2;">${text}</span>`;
|
||||
|
||||
if (state.activeSubTab === '서버') {
|
||||
const mainManager = asset.담당자_정 || '';
|
||||
const subManager = asset.담당자_부 || '';
|
||||
|
||||
// 담당자 배지화
|
||||
const managerHtml = [
|
||||
mainManager ? `${getBadge('정', '#1E5149')} ${mainManager}` : '',
|
||||
subManager ? `${getBadge('부', '#9CA3AF')} ${subManager}` : ''
|
||||
].filter(v => v !== '').join(' / ');
|
||||
|
||||
// 원격접속 배지화
|
||||
const tools = (asset.원격접속 || '').split('\n');
|
||||
const ids = (asset.서버ID || '').split('\n');
|
||||
const pws = (asset.서버PW || '').split('\n');
|
||||
const maxLen = Math.max(tools.length, ids.length, pws.length);
|
||||
|
||||
let remoteItems = [];
|
||||
for(let i=0; i<maxLen; i++) {
|
||||
let toolName = tools[i] || '접속';
|
||||
let badgeColor = '#3B82F6'; // 기본 파랑 (Remote)
|
||||
if (toolName.toLowerCase().includes('any')) badgeColor = '#EF4444'; // Anydesk 빨강
|
||||
if (toolName.toLowerCase().includes('chrome')) badgeColor = '#F59E0B'; // Chrome 노랑
|
||||
|
||||
let item = `${getBadge(toolName, badgeColor)}`;
|
||||
if (ids[i] || pws[i]) {
|
||||
item += ` (${ids[i] || '-'}/${pws[i] || '-'})`;
|
||||
}
|
||||
remoteItems.push(item);
|
||||
}
|
||||
const remoteHtml = remoteItems.join(' / ');
|
||||
|
||||
// IP 및 Storage (기존 유지)
|
||||
const ipInfo = [asset.IP주소, asset.IP2].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>
|
||||
`;
|
||||
} 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-outline btn-edit">수정</button></td>`;
|
||||
}
|
||||
|
||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openHwModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) {
|
||||
const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab);
|
||||
const isSub = state.activeSubTab === '구독SW';
|
||||
|
||||
// 0. Container 준비 (조회 바 + 테이블)
|
||||
container.innerHTML = '';
|
||||
|
||||
// 1. 조회 바 (Filter Bar) 생성
|
||||
const filterBar = document.createElement('div');
|
||||
filterBar.className = 'search-bar';
|
||||
filterBar.innerHTML = `
|
||||
<div class="search-item flex-1">
|
||||
<label>통합 검색 (제품명/부서)</label>
|
||||
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>분야</label>
|
||||
<select id="filter-field">
|
||||
<option value="">전체 분야</option>
|
||||
<option value="업무공통">업무공통</option>
|
||||
<option value="개발S/W">개발S/W</option>
|
||||
<option value="디자인">디자인</option>
|
||||
<option value="설계S/W">설계S/W</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>법인</label>
|
||||
<select id="filter-corp">
|
||||
<option value="">전체 법인</option>
|
||||
<option value="한맥">한맥</option>
|
||||
<option value="삼안">삼안</option>
|
||||
<option value="바론">바론</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="btn-reset-filters" class="btn btn-outline btn-reset" title="검색 조건 초기화">
|
||||
<i data-lucide="refresh-ccw" style="width:14px; height:14px;"></i> 필터 초기화
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(filterBar);
|
||||
|
||||
// 2. 테이블 기본 구조 생성
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
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>`;
|
||||
|
||||
tableWrapper.appendChild(table);
|
||||
container.appendChild(tableWrapper);
|
||||
const list = state.masterData.sw.filter(a => a.type === state.activeSubTab);
|
||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>제품명</th><th>구매일</th><th>수량</th><th>사용가능</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
|
||||
// 3. 필터링 및 테이블 업데이트 로직
|
||||
const updateTable = () => {
|
||||
const keyword = (document.getElementById('filter-keyword') as HTMLInputElement).value.toLowerCase().trim();
|
||||
const field = (document.getElementById('filter-field') as HTMLSelectElement).value;
|
||||
const corp = (document.getElementById('filter-corp') as HTMLSelectElement).value;
|
||||
|
||||
const filtered = fullList.filter(asset => {
|
||||
const matchKeyword = !keyword ||
|
||||
(asset.제품명 || '').toLowerCase().includes(keyword) ||
|
||||
(asset.부서 || '').toLowerCase().includes(keyword);
|
||||
const matchField = !field || asset.분야 === field;
|
||||
const matchCorp = !corp || asset.법인 === corp;
|
||||
return matchKeyword && matchField && matchCorp;
|
||||
});
|
||||
|
||||
tbody.innerHTML = '';
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="${isSub ? 11 : 10}" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach((asset, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
|
||||
const avail = (typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10)) - assigned;
|
||||
const tr = document.createElement('tr');
|
||||
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>${asset.수량}</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.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset));
|
||||
tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// 버튼 내 아이콘 다시 그리기
|
||||
createIcons({
|
||||
icons: { Edit2, Users, RefreshCcw: CalendarClock } // RefreshCcw는 아래 버튼용
|
||||
});
|
||||
// 초기화 버튼 아이콘은 별도로
|
||||
createIcons({
|
||||
scope: filterBar
|
||||
});
|
||||
};
|
||||
|
||||
// 4. 이벤트 바인딩
|
||||
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
|
||||
const fieldSelect = document.getElementById('filter-field') as HTMLSelectElement;
|
||||
const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement;
|
||||
const resetBtn = document.getElementById('btn-reset-filters') as HTMLButtonElement;
|
||||
|
||||
keywordInput.addEventListener('input', updateTable);
|
||||
fieldSelect.addEventListener('change', updateTable);
|
||||
corpSelect.addEventListener('change', updateTable);
|
||||
|
||||
resetBtn.addEventListener('click', () => {
|
||||
keywordInput.value = '';
|
||||
fieldSelect.value = '';
|
||||
corpSelect.value = '';
|
||||
updateTable();
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="7">정보가 없습니다.</td></tr>`; return; }
|
||||
list.forEach((asset, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
|
||||
const avail = asset.수량 - assigned;
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.제품명}</td><td>${asset.구매일||''}</td><td>${asset.수량}</td><td><strong style="color: ${avail > 0 ? 'var(--primary)' : 'var(--danger)'}">${avail}</strong></td><td style="display:flex; gap:0.25rem;"><button class="btn-outline btn-edit">수정</button><button class="btn-outline btn-users"><i data-lucide="users" style="width:14px; height:14px;"></i></button></td>`;
|
||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset));
|
||||
tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// 초기 실행
|
||||
updateTable();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { state } from '../state';
|
||||
import { HardwareAsset, SoftwareAsset } from '../excelHandler';
|
||||
|
||||
declare var Chart: any;
|
||||
|
||||
/**
|
||||
* 대시보드 렌더링 메인 함수
|
||||
*/
|
||||
@@ -10,9 +8,7 @@ export function renderDashboard(mainContent: HTMLElement) {
|
||||
mainContent.innerHTML = '';
|
||||
|
||||
// 기존 차트 리소스 해제
|
||||
state.activeCharts.forEach(c => {
|
||||
if (c && typeof c.destroy === 'function') c.destroy();
|
||||
});
|
||||
state.activeCharts.forEach(c => c.destroy());
|
||||
state.activeCharts = [];
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
@@ -124,20 +120,9 @@ function renderSwDashboard(container: HTMLElement) {
|
||||
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
||||
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
||||
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
const corps = ['한맥', '삼안', '바론'];
|
||||
const categories = ['업무공통', '개발S/W', '디자인', '설계S/W'];
|
||||
|
||||
const costByCorp: Record<string, number> = { '한맥': 0, '삼안': 0, '바론': 0 };
|
||||
const costByCat: Record<string, number> = {};
|
||||
categories.forEach(c => costByCat[c] = 0);
|
||||
|
||||
state.masterData.sw.forEach(sw => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
||||
const priceStr = sw.금액 ? sw.금액.replace(/,/g, '') : '0';
|
||||
const price = parseInt(priceStr, 10) || 0;
|
||||
|
||||
if (sw.type === '구독SW') {
|
||||
subQty += qty; subUsed += assigned; subTotal++;
|
||||
if (isSWExpiring(sw)) subExp++;
|
||||
@@ -145,12 +130,6 @@ function renderSwDashboard(container: HTMLElement) {
|
||||
permQty += qty; permUsed += assigned; permTotal++;
|
||||
if (isSWExpiring(sw)) permExp++;
|
||||
}
|
||||
|
||||
// 오늘이 속해있는 년도(2026)의 사용 금액 합계
|
||||
if (sw.구매일 && sw.구매일.startsWith(currentYear)) {
|
||||
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
||||
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
||||
}
|
||||
});
|
||||
|
||||
const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0;
|
||||
@@ -181,127 +160,24 @@ function renderSwDashboard(container: HTMLElement) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="sub-exp" style="padding: 1.25rem 1.5rem; flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer;">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정</span>
|
||||
<span style="font-size:0.75rem; color:#bfbfbf; background:#f9f9f9; padding:2px 6px; border-radius:4px;">30일 이내</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1.25rem;">
|
||||
전체 ${subTotal}개 제품 중 ${subExp}개 만료 예정
|
||||
</div>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${subExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'};">${subExp}개</div>
|
||||
</div>
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 64px; height: 64px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 1rem; color:var(--text-muted); font-weight:600;">${subExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="perm-exp" style="padding: 1.25rem 1.5rem; flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer;">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정</span>
|
||||
<span style="font-size:0.75rem; color:#bfbfbf; background:#f9f9f9; padding:2px 6px; border-radius:4px;">30일 이내</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1.25rem;">
|
||||
전체 ${permTotal}개 제품 중 ${permExp}개 만료 예정
|
||||
</div>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${permExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'};">${permExp}개</div>
|
||||
</div>
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 64px; height: 64px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 1rem; color:var(--text-muted); font-weight:600;">${permExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; color: var(--text-main);">${currentYear}년 소프트웨어 도입 비용</h3>
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card" style="padding: 1.5rem;">
|
||||
<h4 style="margin: 0 0 1rem 0; font-size: 0.9375rem; color: var(--text-main);">법인별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-cost-corp" style="max-height: 250px;"></canvas>
|
||||
<div class="dashboard-card" data-action="sub-exp" style="padding: 1.25rem 1.5rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted);">${subExp}개 만료 예정</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0);"></div>
|
||||
</div>
|
||||
<div class="dashboard-card" style="padding: 1.5rem;">
|
||||
<h4 style="margin: 0 0 1rem 0; font-size: 0.9375rem; color: var(--text-main);">분야별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-cost-cat" style="max-height: 250px;"></canvas>
|
||||
<div class="dashboard-card" data-action="perm-exp" style="padding: 1.25rem 1.5rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted);">${permExp}개 만료 예정</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0);"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 차트 생성
|
||||
setTimeout(() => {
|
||||
const ctxCorp = (document.getElementById('chart-cost-corp') as HTMLCanvasElement)?.getContext('2d');
|
||||
const ctxCat = (document.getElementById('chart-cost-cat') as HTMLCanvasElement)?.getContext('2d');
|
||||
|
||||
if (ctxCorp && typeof Chart !== 'undefined') {
|
||||
const chartCorp = new Chart(ctxCorp, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: corps,
|
||||
datasets: [{
|
||||
label: '도입 금액',
|
||||
data: corps.map(c => costByCorp[c]),
|
||||
backgroundColor: '#3b82f6',
|
||||
borderRadius: 4,
|
||||
barThickness: 20 // 막대 두께 줄임
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { callback: (v: any) => v.toLocaleString() },
|
||||
grid: { display: false } // 가로줄 삭제
|
||||
},
|
||||
x: {
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
state.activeCharts.push(chartCorp);
|
||||
}
|
||||
|
||||
if (ctxCat && typeof Chart !== 'undefined') {
|
||||
const chartCat = new Chart(ctxCat, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: categories,
|
||||
datasets: [{
|
||||
label: '도입 금액',
|
||||
data: categories.map(c => costByCat[c]),
|
||||
backgroundColor: '#10b981',
|
||||
borderRadius: 4,
|
||||
barThickness: 20 // 막대 두께 줄임
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { callback: (v: any) => v.toLocaleString() },
|
||||
grid: { display: false } // 가로줄 삭제
|
||||
},
|
||||
x: {
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
state.activeCharts.push(chartCat);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// 클릭 이벤트 바인딩
|
||||
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => {
|
||||
openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW'));
|
||||
@@ -409,5 +285,3 @@ function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user