feat: 서버 상세 모달 그룹화 및 전역 UI/UX 통일, 조회/수정 모드 구현
주요 변경 사항: - 서버 자산 상세 정보 4개 그룹(Identity, Connectivity, Specs, Operation)으로 최적화 - 모달 내 조회/수정 모드 전환 및 수정 강조색(#FF3D00) 적용 - 모든 모달의 버튼 사이즈 및 폰트 스타일 가이드 준수 통일 - 수정 취소(Revert) 기능 및 누락된 대시보드 상세 모달 추가 - TypeScript 타입 오류 및 런타임 렌더링 결함 긴급 복구
This commit is contained in:
140
debug_excel.json
Normal file
140
debug_excel.json
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"서버 관리대장(기술개발센터).xlsx": [
|
||||||
|
[
|
||||||
|
"마천사무실"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"실사진",
|
||||||
|
"구성",
|
||||||
|
"자산번호",
|
||||||
|
"명칭(주기)",
|
||||||
|
"상세",
|
||||||
|
"유형",
|
||||||
|
"담당자",
|
||||||
|
null,
|
||||||
|
"IP",
|
||||||
|
"원격접속",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"모델명",
|
||||||
|
"OS",
|
||||||
|
"CPU",
|
||||||
|
"RAM",
|
||||||
|
"GPU",
|
||||||
|
"Storage1",
|
||||||
|
"Storage2",
|
||||||
|
"Storage3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"정",
|
||||||
|
"부",
|
||||||
|
null,
|
||||||
|
"접속도구",
|
||||||
|
"아이디",
|
||||||
|
"비밀번호"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
"GSIM NAS",
|
||||||
|
"팀 내부 자료 저장 , 정사영상 및 지도 데이터 저장 , Gitea 및 Git 내장 NAS",
|
||||||
|
"NAS",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"Synology DS923+"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
"그래픽스개발팀\r\n데이터 백업 NAS",
|
||||||
|
"그래픽스 개발팀 데이터 백업용 NAS",
|
||||||
|
"NAS",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"Synology DS923+"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"서버 관리대장(한맥빌딩).xlsx": [
|
||||||
|
[
|
||||||
|
"한맥빌딩(MDF 실)"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"실사진",
|
||||||
|
"구성",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"서버번호",
|
||||||
|
"명칭(주기)",
|
||||||
|
"유형",
|
||||||
|
"IP",
|
||||||
|
"모델명",
|
||||||
|
"용도",
|
||||||
|
"담당자",
|
||||||
|
"OS",
|
||||||
|
"CPU",
|
||||||
|
"RAM",
|
||||||
|
"GPU",
|
||||||
|
"Storage1",
|
||||||
|
"Storage2",
|
||||||
|
"Storage3"
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
"NAS 2",
|
||||||
|
"NAS",
|
||||||
|
"192.168.9.23",
|
||||||
|
"DS414j",
|
||||||
|
"한라 기업부설연구소 공용 NAS",
|
||||||
|
"이준하 차장"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"NAS2",
|
||||||
|
"NAS 1\r\n(DS224+)",
|
||||||
|
"NAS4",
|
||||||
|
"NAS 5\r\n(DS923+)",
|
||||||
|
"NAS 6\r\n(DS923+)",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
"NAS 1",
|
||||||
|
"NAS",
|
||||||
|
"192.168.9.32",
|
||||||
|
"DS224+",
|
||||||
|
"한라 사업지원, 경영지원, 업무, 안전품질, 운영 공용 NAS",
|
||||||
|
"이준하 차장"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
182
index.html
182
index.html
@@ -70,16 +70,17 @@
|
|||||||
|
|
||||||
<!-- HW Asset Modal -->
|
<!-- HW Asset Modal -->
|
||||||
<div id="hw-asset-modal" class="modal-overlay hidden">
|
<div id="hw-asset-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content" style="max-width: 800px; max-height: 90vh; display: flex; flex-direction: column;">
|
<div class="modal-content wide">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="hw-modal-title">자산 상세 정보</h2>
|
<h2 id="hw-modal-title">자산 상세 정보</h2>
|
||||||
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
<div class="modal-body">
|
||||||
<form id="hw-asset-form" class="grid-form">
|
<form id="hw-asset-form" class="grid-form">
|
||||||
<input type="hidden" id="hw-asset-id" />
|
<input type="hidden" id="hw-asset-id" />
|
||||||
<input type="hidden" id="hw-asset-type" />
|
<input type="hidden" id="hw-asset-type" />
|
||||||
|
|
||||||
|
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-법인">법인</label>
|
<label for="hw-법인">법인</label>
|
||||||
<input type="text" id="hw-법인" required />
|
<input type="text" id="hw-법인" required />
|
||||||
@@ -88,42 +89,25 @@
|
|||||||
<label for="hw-자산코드">자산번호/코드</label>
|
<label for="hw-자산코드">자산번호/코드</label>
|
||||||
<input type="text" id="hw-자산코드" required />
|
<input type="text" id="hw-자산코드" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group server-only">
|
<div class="form-group server-only">
|
||||||
<label for="hw-용도">용도</label>
|
<label for="hw-용도">용도</label>
|
||||||
<input type="text" id="hw-용도" />
|
<input type="text" id="hw-용도" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group server-only">
|
<div class="form-group server-only">
|
||||||
<label for="hw-상세">상세</label>
|
<label for="hw-상세">상세 내용</label>
|
||||||
<input type="text" id="hw-상세" />
|
<input type="text" id="hw-상세" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group non-server">
|
<div class="form-group non-server">
|
||||||
<label for="hw-명칭">명칭</label>
|
<label for="hw-명칭">명칭</label>
|
||||||
<input type="text" id="hw-명칭" />
|
<input type="text" id="hw-명칭" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="hw-비품유형-group" style="display:none;">
|
<div class="form-group full-width server-only">
|
||||||
<label for="hw-비품유형">비품유형</label>
|
<label for="hw-비고">비고</label>
|
||||||
<select id="hw-비품유형" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px;">
|
<input type="text" id="hw-비고" />
|
||||||
<option value="노트북">노트북</option><option value="태블릿">태블릿</option><option value="휴대폰">휴대폰</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-section-title server-only">네트워크 정보 (Connectivity)</div>
|
||||||
<label for="hw-위치">설치위치</label>
|
<div class="form-group server-only">
|
||||||
<input type="text" id="hw-위치" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="hw-담당자_정">담당자(정)</label>
|
|
||||||
<input type="text" id="hw-담당자_정" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="hw-담당자_부">담당자(부)</label>
|
|
||||||
<input type="text" id="hw-담당자_부" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="hw-IP주소">IP 주소 1</label>
|
<label for="hw-IP주소">IP 주소 1</label>
|
||||||
<input type="text" id="hw-IP주소" />
|
<input type="text" id="hw-IP주소" />
|
||||||
</div>
|
</div>
|
||||||
@@ -131,9 +115,8 @@
|
|||||||
<label for="hw-IP2">IP 주소 2</label>
|
<label for="hw-IP2">IP 주소 2</label>
|
||||||
<input type="text" id="hw-IP2" />
|
<input type="text" id="hw-IP2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group server-only">
|
<div class="form-group server-only">
|
||||||
<label for="hw-원격접속">원격 도구</label>
|
<label for="hw-원격접속">원격 도구 (Anydesk/Chrome 등)</label>
|
||||||
<input type="text" id="hw-원격접속" />
|
<input type="text" id="hw-원격접속" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group server-only">
|
<div class="form-group server-only">
|
||||||
@@ -144,53 +127,74 @@
|
|||||||
<label for="hw-서버PW">서버 PW</label>
|
<label for="hw-서버PW">서버 PW</label>
|
||||||
<input type="text" id="hw-서버PW" />
|
<input type="text" id="hw-서버PW" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group non-server" id="hw-IP주소-group">
|
||||||
|
<label for="hw-IP주소-non-server">IP 주소</label>
|
||||||
|
<input type="text" id="hw-IP주소-non-server" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section-title">시스템 사양 (Specifications)</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-모델명">모델명</label>
|
<label for="hw-모델명">모델명</label>
|
||||||
<input type="text" id="hw-모델명" />
|
<input type="text" id="hw-모델명" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-OS">OS</label>
|
<label for="hw-OS">운영체제 (OS)</label>
|
||||||
<input type="text" id="hw-OS" />
|
<input type="text" id="hw-OS" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-CPU">CPU</label>
|
<label for="hw-CPU">CPU 사양</label>
|
||||||
<input type="text" id="hw-CPU" />
|
<input type="text" id="hw-CPU" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-RAM">RAM</label>
|
<label for="hw-RAM">RAM 용량</label>
|
||||||
<input type="text" id="hw-RAM" />
|
<input type="text" id="hw-RAM" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-SSD1">Storage 1</label>
|
<label for="hw-SSD1">Storage 1 (SSD/HDD)</label>
|
||||||
<input type="text" id="hw-SSD1" />
|
<input type="text" id="hw-SSD1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hw-SSD2">Storage 2</label>
|
<label for="hw-SSD2">Storage 2 (SSD/HDD)</label>
|
||||||
<input type="text" id="hw-SSD2" />
|
<input type="text" id="hw-SSD2" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group server-only">
|
||||||
|
<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-비품유형">
|
||||||
|
<option value="노트북">노트북</option><option value="태블릿">태블릿</option><option value="휴대폰">휴대폰</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group full-width non-server">
|
<div class="form-group full-width non-server">
|
||||||
<label for="hw-HW사양">H/W 사양 상세</label>
|
<label for="hw-HW사양">H/W 사양 상세</label>
|
||||||
<textarea id="hw-HW사양" rows="2"></textarea>
|
<textarea id="hw-HW사양" rows="2"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section-title">관리 및 운영 (Operation)</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hw-위치">설치위치</label>
|
||||||
|
<input type="text" id="hw-위치" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hw-담당자_정">담당자 (정)</label>
|
||||||
|
<input type="text" id="hw-담당자_정" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hw-담당자_부">담당자 (부)</label>
|
||||||
|
<input type="text" id="hw-담당자_부" />
|
||||||
|
</div>
|
||||||
<div class="form-group non-server">
|
<div class="form-group non-server">
|
||||||
<label for="hw-구매일">구매일</label>
|
<label for="hw-구매일">구매일</label>
|
||||||
<input type="text" id="hw-구매일" />
|
<input type="text" id="hw-구매일" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group non-server">
|
<div class="form-group non-server">
|
||||||
<label for="hw-금액">금액</label>
|
<label for="hw-금액">금액</label>
|
||||||
<input type="text" id="hw-금액" 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>
|
||||||
<div class="form-group server-only">
|
|
||||||
<label for="hw-비고">비고</label>
|
|
||||||
<input type="text" id="hw-비고" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label style="font-size:0.875rem;">품의서 (파일)</label>
|
<label>품의서 (파일 증빙)</label>
|
||||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||||
<input type="file" id="hw-품의서" />
|
<input type="file" id="hw-품의서" />
|
||||||
<span id="hw-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
<span id="hw-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||||
@@ -199,8 +203,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="btn-delete-hw-asset" class="btn btn-danger">삭제</button>
|
<button id="btn-delete-hw-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
|
<button id="btn-revert-hw-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||||
<button id="btn-cancel-hw-modal" class="btn btn-outline">닫기</button>
|
<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-save-hw-asset" class="btn btn-primary">수정</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,80 +228,66 @@
|
|||||||
<input type="hidden" id="pc-asset-type" value="개인PC" />
|
<input type="hidden" id="pc-asset-type" value="개인PC" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-법인">법인</label>
|
<label for="pc-법인">법인</label>
|
||||||
<select id="pc-법인" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px;">
|
<select id="pc-법인" required>
|
||||||
<option value="한맥">한맥 (HM)</option><option value="삼안">삼안 (SM)</option><option value="바론">바론 (BR)</option>
|
<option value="한맥">한맥 (HM)</option><option value="삼안">삼안 (SM)</option><option value="바론">바론 (BR)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-자산코드">자산코드</label>
|
<label for="pc-자산코드">자산코드</label>
|
||||||
<input type="text" id="pc-자산코드" placeholder="ex) HM-PC-2018-001" required />
|
<input type="text" id="pc-자산코드" placeholder="ex) HM-PC-2018-001" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-사용자">사용자</label>
|
<label for="pc-사용자">사용자</label>
|
||||||
<input type="text" id="pc-사용자" required />
|
<input type="text" id="pc-사용자" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-위치">위치</label>
|
<label for="pc-위치">위치</label>
|
||||||
<input type="text" id="pc-위치" />
|
<input type="text" id="pc-위치" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-CPU">CPU</label>
|
<label for="pc-CPU">CPU</label>
|
||||||
<input type="text" id="pc-CPU" />
|
<input type="text" id="pc-CPU" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-GPU">GPU</label>
|
<label for="pc-GPU">GPU</label>
|
||||||
<input type="text" id="pc-GPU" />
|
<input type="text" id="pc-GPU" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-RAM">RAM</label>
|
<label for="pc-RAM">RAM</label>
|
||||||
<input type="text" id="pc-RAM" />
|
<input type="text" id="pc-RAM" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-SSD1">SSD1</label>
|
<label for="pc-SSD1">SSD1</label>
|
||||||
<input type="text" id="pc-SSD1" />
|
<input type="text" id="pc-SSD1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-SSD2">SSD2</label>
|
<label for="pc-SSD2">SSD2</label>
|
||||||
<input type="text" id="pc-SSD2" />
|
<input type="text" id="pc-SSD2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-HDD1">HDD1</label>
|
<label for="pc-HDD1">HDD1</label>
|
||||||
<input type="text" id="pc-HDD1" />
|
<input type="text" id="pc-HDD1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-HDD2">HDD2</label>
|
<label for="pc-HDD2">HDD2</label>
|
||||||
<input type="text" id="pc-HDD2" />
|
<input type="text" id="pc-HDD2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-구매일">구매일</label>
|
<label for="pc-구매일">구매일</label>
|
||||||
<input type="text" id="pc-구매일" placeholder="ex) 2024-01-01" />
|
<input type="text" id="pc-구매일" placeholder="ex) 2024-01-01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-금액">금액</label>
|
<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, ',')" />
|
<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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-납품업체">납품업체</label>
|
<label for="pc-납품업체">납품업체</label>
|
||||||
<input type="text" id="pc-납품업체" />
|
<input type="text" id="pc-납품업체" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label style="font-size:0.875rem;">품의서 (파일)</label>
|
<label>품의서 (파일)</label>
|
||||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||||
<input type="file" id="pc-품의서" style="font-size:0.875rem;" />
|
<input type="file" id="pc-품의서" />
|
||||||
<span id="pc-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
<span id="pc-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,12 +315,12 @@
|
|||||||
|
|
||||||
<!-- Storage Asset Modal -->
|
<!-- Storage Asset Modal -->
|
||||||
<div id="storage-asset-modal" class="modal-overlay hidden">
|
<div id="storage-asset-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="storage-modal-title">스토리지 상세 정보</h2>
|
<h2 id="storage-modal-title">스토리지 상세 정보</h2>
|
||||||
<button id="btn-close-storage-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
<button id="btn-close-storage-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
<div class="modal-body">
|
||||||
<form id="storage-asset-form" class="grid-form">
|
<form id="storage-asset-form" class="grid-form">
|
||||||
<input type="hidden" id="storage-asset-id" />
|
<input type="hidden" id="storage-asset-id" />
|
||||||
<input type="hidden" id="storage-asset-type" value="스토리지" />
|
<input type="hidden" id="storage-asset-type" value="스토리지" />
|
||||||
@@ -343,11 +334,11 @@
|
|||||||
<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-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-구매일" /></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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="btn-delete-storage-asset" class="btn btn-danger">삭제</button>
|
<button id="btn-delete-storage-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
<button id="btn-cancel-storage-modal" class="btn btn-outline">닫기</button>
|
<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-save-storage-asset" class="btn btn-primary">수정</button>
|
||||||
@@ -358,82 +349,70 @@
|
|||||||
|
|
||||||
<!-- SW Asset Modal -->
|
<!-- SW Asset Modal -->
|
||||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="sw-modal-title">S/W 상세 정보</h2>
|
<h2 id="sw-modal-title">S/W 상세 정보</h2>
|
||||||
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
<div class="modal-body">
|
||||||
<form id="sw-asset-form" class="grid-form">
|
<form id="sw-asset-form" class="grid-form">
|
||||||
<input type="hidden" id="sw-asset-id" />
|
<input type="hidden" id="sw-asset-id" />
|
||||||
<input type="hidden" id="sw-asset-type" />
|
<input type="hidden" id="sw-asset-type" />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-분야">분야</label>
|
<label for="sw-분야">분야</label>
|
||||||
<select id="sw-분야" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
<select id="sw-분야" required>
|
||||||
<option value="업무공통">업무공통</option>
|
<option value="업무공통">업무공통</option>
|
||||||
<option value="개발S/W">개발S/W</option>
|
<option value="개발S/W">개발S/W</option>
|
||||||
<option value="디자인">디자인</option>
|
<option value="디자인">디자인</option>
|
||||||
<option value="설계S/W">설계S/W</option>
|
<option value="설계S/W">설계S/W</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-법인">법인</label>
|
<label for="sw-법인">법인</label>
|
||||||
<select id="sw-법인" required style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: inherit; font-size: 0.875rem;">
|
<select id="sw-법인" required>
|
||||||
<option value="한맥">한맥 (HM)</option>
|
<option value="한맥">한맥 (HM)</option>
|
||||||
<option value="삼안">삼안 (SM)</option>
|
<option value="삼안">삼안 (SM)</option>
|
||||||
<option value="바론">바론 (BR)</option>
|
<option value="바론">바론 (BR)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-부서">부서</label>
|
<label for="sw-부서">부서</label>
|
||||||
<input type="text" id="sw-부서" placeholder="ex) 경영지원팀" required />
|
<input type="text" id="sw-부서" placeholder="ex) 경영지원팀" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-제품명">제품명</label>
|
<label for="sw-제품명">제품명</label>
|
||||||
<input type="text" id="sw-제품명" required />
|
<input type="text" id="sw-제품명" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-구매일">구매일</label>
|
<label for="sw-구매일">구매일</label>
|
||||||
<input type="text" id="sw-구매일" placeholder="ex) 2024-01-01" />
|
<input type="text" id="sw-구매일" placeholder="ex) 2024-01-01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="sw-구독일-group">
|
<div class="form-group" id="sw-구독일-group">
|
||||||
<label for="sw-구독일">구독일(시작~끝)</label>
|
<label for="sw-구독일">구독일(시작~끝)</label>
|
||||||
<input type="text" id="sw-구독일" placeholder="ex) 2024-01-01 ~ 2024-12-31" />
|
<input type="text" id="sw-구독일" placeholder="ex) 2024-01-01 ~ 2024-12-31" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="sw-유지보수-group" style="display:none;">
|
<div class="form-group" id="sw-유지보수-group" style="display:none;">
|
||||||
<label for="sw-유지보수여부">유지보수 여부</label>
|
<label for="sw-유지보수여부">유지보수 여부</label>
|
||||||
<label style="display:flex; align-items:center; gap:0.5rem; height: 38px;">
|
<label style="display:flex; align-items:center; gap:0.5rem; height: 38px; cursor: pointer;">
|
||||||
<input type="checkbox" id="sw-유지보수여부" /> 대상 여부
|
<input type="checkbox" id="sw-유지보수여부" /> 대상 여부
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-금액">금액</label>
|
<label for="sw-금액">금액</label>
|
||||||
<input type="text" id="sw-금액" placeholder="ex) 1,000,000" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
<input type="text" id="sw-금액" placeholder="ex) 1,000,000" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-수량">수량 (보유량)</label>
|
<label for="sw-수량">수량 (보유량)</label>
|
||||||
<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;" />
|
<input type="number" id="sw-수량" min="1" value="1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-계정명">계정명</label>
|
<label for="sw-계정명">계정명</label>
|
||||||
<input type="text" id="sw-계정명" />
|
<input type="text" id="sw-계정명" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-납품업체">납품업체</label>
|
<label for="sw-납품업체">납품업체</label>
|
||||||
<input type="text" id="sw-납품업체" />
|
<input type="text" id="sw-납품업체" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-비고">비고</label>
|
<label for="sw-비고">비고</label>
|
||||||
<input type="text" id="sw-비고" />
|
<input type="text" id="sw-비고" />
|
||||||
@@ -441,7 +420,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="btn-delete-sw-asset" class="btn btn-danger">삭제</button>
|
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
|
<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-save-sw-asset" class="btn btn-primary">수정</button>
|
||||||
@@ -452,28 +431,53 @@
|
|||||||
|
|
||||||
<!-- SW User Management Modal -->
|
<!-- SW User Management Modal -->
|
||||||
<div id="sw-user-modal" class="modal-overlay hidden">
|
<div id="sw-user-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content" style="max-width: 600px; max-height: 90vh; display: flex; flex-direction: column;">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="sw-user-modal-title">S/W 할당 사용자 목록</h2>
|
<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>
|
<button id="btn-close-sw-user-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow-y: auto; flex: 1;">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="sw-user-asset-id" />
|
<input type="hidden" id="sw-user-asset-id" />
|
||||||
<div style="text-align: right; margin-bottom: 0.5rem;">
|
<div style="text-align: right; margin-bottom: 0.75rem;">
|
||||||
<button type="button" id="btn-open-add-user" class="btn btn-primary" style="padding: 0.25rem 1rem;"><i data-lucide="plus"></i> 새 사용자 추가</button>
|
<button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm" style="padding: 0.375rem 0.75rem;"><i data-lucide="plus"></i> 사용자 추가</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table style="width:100%;">
|
<table style="width:100%;">
|
||||||
<thead><tr><th>법인</th><th>부서/팀</th><th>직위</th><th>이름</th><th>사용기간</th><th>관리</th></tr></thead>
|
<thead><tr><th>법인</th><th>부서/팀</th><th>직위</th><th>이름</th><th>사용기간</th><th style="text-align:center;">관리</th></tr></thead>
|
||||||
<tbody id="user-list-body"></tbody>
|
<tbody id="user-list-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" style="justify-content: flex-end;">
|
<div class="modal-footer">
|
||||||
|
<div></div>
|
||||||
|
<div class="footer-actions">
|
||||||
<button id="btn-cancel-sw-user-modal" class="btn btn-outline">닫기</button>
|
<button id="btn-cancel-sw-user-modal" class="btn btn-outline">닫기</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Detail Modal -->
|
||||||
|
<div id="dashboard-detail-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content wide" style="max-width: 1000px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="dashboard-detail-modal-title">상세 목록</h2>
|
||||||
|
<button id="btn-close-dashboard-detail-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="table-container">
|
||||||
|
<table style="width:100%;">
|
||||||
|
<thead></thead>
|
||||||
|
<tbody id="dashboard-detail-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-cancel-dashboard-detail-modal" class="btn btn-outline">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,182 +1,192 @@
|
|||||||
import { state } from '../../state';
|
import { state } from '../../state';
|
||||||
import { HardwareAsset } from '../../excelHandler';
|
import { HardwareAsset } from '../../excelHandler';
|
||||||
import { openModal } from './BaseModal';
|
import { renderTable } from '../../views/AssetTableView';
|
||||||
|
import { createIcons, Paperclip } from 'lucide';
|
||||||
|
|
||||||
/**
|
let currentAsset: HardwareAsset | null = null;
|
||||||
* 폼의 모든 입력 필드를 활성화/비활성화 처리
|
let isEditMode = false;
|
||||||
*/
|
|
||||||
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) {
|
export function openHwModal(asset: HardwareAsset) {
|
||||||
(input as HTMLElement).setAttribute('readonly', 'true');
|
currentAsset = asset;
|
||||||
if (input.tagName === 'SELECT') (input as HTMLSelectElement).disabled = true;
|
isEditMode = false; // 항상 조회 모드로 시작
|
||||||
} else {
|
|
||||||
(input as HTMLElement).removeAttribute('readonly');
|
const modal = document.getElementById('hw-asset-modal')!;
|
||||||
if (input.tagName === 'SELECT') (input as HTMLSelectElement).disabled = false;
|
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
|
||||||
}
|
const saveBtn = document.getElementById('btn-save-hw-asset')!;
|
||||||
});
|
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
form.classList.remove('is-edit-mode');
|
||||||
|
form.classList.add('is-view-mode');
|
||||||
|
saveBtn.textContent = '수정';
|
||||||
|
revertBtn.classList.add('hidden'); // 조회 모드에서는 숨김
|
||||||
|
|
||||||
|
// 데이터 채우기 함수 호출 (수정 취소 시에도 재사용)
|
||||||
|
fillHwFormData(asset);
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
createIcons({ icons: { Paperclip } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 하드웨어(서버, 전산비품 등) 모달 초기화 및 로직 제어
|
* 폼 필드에 자산 데이터 채우기
|
||||||
*/
|
*/
|
||||||
export function initHWModal(renderContent: () => void, closeModals: () => void) {
|
function fillHwFormData(asset: HardwareAsset) {
|
||||||
const hwForm = document.getElementById('hw-asset-form') as HTMLFormElement;
|
|
||||||
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: 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,
|
|
||||||
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,
|
|
||||||
품의서명,
|
|
||||||
비품유형: type === '전산비품' ? (document.getElementById('hw-비품유형') as HTMLSelectElement).value : undefined,
|
|
||||||
storage유형: (document.getElementById('hw-용도') as HTMLInputElement).value
|
|
||||||
};
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
|
||||||
if(idx !== -1) state.masterData.hw[idx] = newAsset;
|
|
||||||
} else {
|
|
||||||
state.masterData.hw.push(newAsset);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModals();
|
|
||||||
renderContent();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 삭제 버튼 이벤트
|
|
||||||
btnDeleteHw?.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const id = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
|
|
||||||
if (confirm('삭제하시겠습니까?')) {
|
|
||||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
|
||||||
closeModals();
|
|
||||||
renderContent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 하드웨어 상세 모달 열기
|
|
||||||
*/
|
|
||||||
export function openHwModal(asset?: HardwareAsset) {
|
|
||||||
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 = `${currentType} 상세 정보 수정`;
|
|
||||||
deleteBtn.style.display = 'block';
|
|
||||||
btnSaveHw.textContent = '수정';
|
|
||||||
setFormReadOnly(hwForm, true); // 수정 시 초기 상태는 읽기 전용
|
|
||||||
|
|
||||||
(document.getElementById('hw-asset-id') as HTMLInputElement).value = asset.id;
|
(document.getElementById('hw-asset-id') as HTMLInputElement).value = asset.id;
|
||||||
(document.getElementById('hw-asset-type') as HTMLInputElement).value = asset.type;
|
(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-위치') 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-OS') as HTMLInputElement).value = asset.OS || '';
|
||||||
(document.getElementById('hw-CPU') as HTMLInputElement).value = asset.CPU || '';
|
(document.getElementById('hw-CPU') as HTMLInputElement).value = asset.CPU || '';
|
||||||
(document.getElementById('hw-RAM') as HTMLInputElement).value = asset.RAM || '';
|
(document.getElementById('hw-RAM') as HTMLInputElement).value = asset.RAM || '';
|
||||||
(document.getElementById('hw-SSD1') as HTMLInputElement).value = asset.SSD1 || '';
|
(document.getElementById('hw-SSD1') as HTMLInputElement).value = asset.SSD1 || '';
|
||||||
(document.getElementById('hw-SSD2') as HTMLInputElement).value = asset.SSD2 || '';
|
(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.담당자_정 || asset.관리자 || '';
|
||||||
|
(document.getElementById('hw-담당자_부') as HTMLInputElement).value = asset.담당자_부 || '';
|
||||||
|
(document.getElementById('hw-품의서명') as HTMLElement).textContent = asset.품의서명 || '';
|
||||||
|
|
||||||
|
const serverOnly = document.querySelectorAll('.server-only');
|
||||||
|
const nonServer = document.querySelectorAll('.non-server');
|
||||||
|
const equipGroup = document.getElementById('hw-비품유형-group')!;
|
||||||
|
|
||||||
|
if (asset.type === '서버') {
|
||||||
|
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||||
|
nonServer.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
equipGroup.style.display = 'none';
|
||||||
|
|
||||||
|
(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-IP2') as HTMLInputElement).value = (asset as any).IP2 || '';
|
||||||
|
(document.getElementById('hw-원격접속') as HTMLInputElement).value = asset.원격접속 || '';
|
||||||
|
(document.getElementById('hw-서버ID') as HTMLInputElement).value = (asset as any).서버ID || '';
|
||||||
|
(document.getElementById('hw-서버PW') as HTMLInputElement).value = (asset as any).서버PW || '';
|
||||||
|
(document.getElementById('hw-모니터링') as HTMLInputElement).value = asset.모니터링 || '';
|
||||||
|
} else {
|
||||||
|
serverOnly.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||||
|
|
||||||
|
(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-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
(document.getElementById('hw-HW사양') as HTMLTextAreaElement).value = asset.HW사양 || '';
|
||||||
(document.getElementById('hw-용도') as HTMLInputElement).value = asset.용도 || asset.storage유형 || '';
|
(document.getElementById('hw-IP주소-non-server') as HTMLInputElement).value = asset.IP주소 || '';
|
||||||
(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.품의서명}` : '';
|
|
||||||
|
|
||||||
if (currentType === '전산비품') {
|
if (asset.type === '전산비품') {
|
||||||
|
equipGroup.style.display = 'flex';
|
||||||
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset.비품유형 || '노트북';
|
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset.비품유형 || '노트북';
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('hw-modal-title')!.textContent = `새 ${currentType} 자산 추가`;
|
equipGroup.style.display = 'none';
|
||||||
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 = currentType;
|
|
||||||
(document.getElementById('hw-품의서명') as HTMLElement).innerText = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('hw-비품유형-group')!.style.display = (currentType === '전산비품') ? 'block' : 'none';
|
export function initHwModal() {
|
||||||
|
const modal = document.getElementById('hw-asset-modal')!;
|
||||||
|
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
|
||||||
|
const closeBtn = document.getElementById('btn-close-hw-modal')!;
|
||||||
|
const cancelBtn = document.getElementById('btn-cancel-hw-modal')!;
|
||||||
|
const saveBtn = document.getElementById('btn-save-hw-asset')!;
|
||||||
|
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
|
||||||
|
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
isEditMode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchToViewMode = () => {
|
||||||
|
isEditMode = false;
|
||||||
|
form.classList.remove('is-edit-mode');
|
||||||
|
form.classList.add('is-view-mode');
|
||||||
|
saveBtn.textContent = '수정';
|
||||||
|
revertBtn.classList.add('hidden');
|
||||||
|
if (currentAsset) fillHwFormData(currentAsset); // 원래 데이터로 복구
|
||||||
|
};
|
||||||
|
|
||||||
|
closeBtn.addEventListener('click', closeModal);
|
||||||
|
cancelBtn.addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 수정 취소 버튼 이벤트
|
||||||
|
revertBtn.addEventListener('click', () => {
|
||||||
|
switchToViewMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', () => {
|
||||||
|
if (!currentAsset) return;
|
||||||
|
|
||||||
|
if (!isEditMode) {
|
||||||
|
isEditMode = true;
|
||||||
|
form.classList.remove('is-view-mode');
|
||||||
|
form.classList.add('is-edit-mode');
|
||||||
|
saveBtn.textContent = '저장';
|
||||||
|
revertBtn.classList.remove('hidden'); // 수정 모드에서 취소 버튼 표시
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실제 저장 로직
|
||||||
|
const assetId = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
|
||||||
|
const type = (document.getElementById('hw-asset-type') as HTMLInputElement).value;
|
||||||
|
|
||||||
|
const updated: HardwareAsset = {
|
||||||
|
...currentAsset,
|
||||||
|
법인: (document.getElementById('hw-법인') as HTMLInputElement).value,
|
||||||
|
자산코드: (document.getElementById('hw-자산코드') as HTMLInputElement).value,
|
||||||
|
위치: (document.getElementById('hw-위치') as HTMLInputElement).value,
|
||||||
|
모델명: (document.getElementById('hw-모델명') 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,
|
||||||
|
담당자_정: (document.getElementById('hw-담당자_정') as HTMLInputElement).value,
|
||||||
|
관리자: (document.getElementById('hw-담당자_정') as HTMLInputElement).value,
|
||||||
|
담당자_부: (document.getElementById('hw-담당자_부') as HTMLInputElement).value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === '서버') {
|
||||||
|
updated.용도 = (document.getElementById('hw-용도') as HTMLInputElement).value;
|
||||||
|
updated.상세 = (document.getElementById('hw-상세') as HTMLInputElement).value;
|
||||||
|
updated.비고 = (document.getElementById('hw-비고') as HTMLInputElement).value;
|
||||||
|
updated.IP주소 = (document.getElementById('hw-IP주소') as HTMLInputElement).value;
|
||||||
|
(updated as any).IP2 = (document.getElementById('hw-IP2') as HTMLInputElement).value;
|
||||||
|
updated.원격접속 = (document.getElementById('hw-원격접속') as HTMLInputElement).value;
|
||||||
|
(updated as any).서버ID = (document.getElementById('hw-서버ID') as HTMLInputElement).value;
|
||||||
|
(updated as any).서버PW = (document.getElementById('hw-서버PW') as HTMLInputElement).value;
|
||||||
|
updated.모니터링 = (document.getElementById('hw-모니터링') as HTMLInputElement).value;
|
||||||
|
} else {
|
||||||
|
updated.명칭 = (document.getElementById('hw-명칭') as HTMLInputElement).value;
|
||||||
|
updated.구매일 = (document.getElementById('hw-구매일') as HTMLInputElement).value;
|
||||||
|
updated.금액 = (document.getElementById('hw-금액') as HTMLInputElement).value;
|
||||||
|
updated.HW사양 = (document.getElementById('hw-HW사양') as HTMLTextAreaElement).value;
|
||||||
|
updated.IP주소 = (document.getElementById('hw-IP주소-non-server') as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (type === '전산비품') {
|
||||||
|
updated.비품유형 = (document.getElementById('hw-비품유형') as HTMLSelectElement).value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = state.masterData.hw.findIndex(a => a.id === assetId);
|
||||||
|
if (idx > -1) {
|
||||||
|
state.masterData.hw[idx] = updated;
|
||||||
|
renderTable(document.getElementById('main-content')!);
|
||||||
|
switchToViewMode(); // 저장 후 다시 조회 모드로
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener('click', () => {
|
||||||
|
if (!currentAsset) return;
|
||||||
|
if (confirm('정말로 이 자산을 삭제하시겠습니까?')) {
|
||||||
|
state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id);
|
||||||
|
renderTable(document.getElementById('main-content')!);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { openModal } from './BaseModal';
|
|||||||
/**
|
/**
|
||||||
* 개인PC 모달 초기화 및 로직 제어
|
* 개인PC 모달 초기화 및 로직 제어
|
||||||
*/
|
*/
|
||||||
export function initPCModal(renderContent: () => void, closeModals: () => void) {
|
export function initPcModal(renderContent: () => void, closeModals: () => void) {
|
||||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||||
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
||||||
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { openModal } from './BaseModal';
|
|||||||
/**
|
/**
|
||||||
* 소프트웨어 모달 초기화 및 로직 제어
|
* 소프트웨어 모달 초기화 및 로직 제어
|
||||||
*/
|
*/
|
||||||
export function initSWModal(renderContent: () => void, closeModals: () => void) {
|
export function initSwModal(renderContent: () => void, closeModals: () => void) {
|
||||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
||||||
const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
|
const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
|
||||||
const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement;
|
const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ let tempSwUsers: SWUser[] = [];
|
|||||||
/**
|
/**
|
||||||
* 소프트웨어 사용자 할당 모달 초기화
|
* 소프트웨어 사용자 할당 모달 초기화
|
||||||
*/
|
*/
|
||||||
export function initSWUserModal(renderContent: () => void, closeModals: () => void) {
|
export function initSwUserModal(renderContent: () => void, closeModals: () => void) {
|
||||||
const btnOpenAddUser = document.getElementById('btn-open-add-user');
|
const btnOpenAddUser = document.getElementById('btn-open-add-user');
|
||||||
const btnSaveEditUser = document.getElementById('btn-save-edit-user');
|
const btnSaveEditUser = document.getElementById('btn-save-edit-user');
|
||||||
const btnSaveSwUserMapping = document.getElementById('btn-save-sw-user-mapping');
|
const btnSaveSwUserMapping = document.getElementById('btn-save-sw-user-mapping');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
|
|
||||||
export function initSidebar(renderContent: () => void) {
|
export function renderSidebar(onTabChange: (tab: string) => void) {
|
||||||
const navItems = document.querySelectorAll('.nav-list li');
|
const navItems = document.querySelectorAll('.nav-list li');
|
||||||
const titleElement = document.getElementById('current-tab-title') as HTMLHeadingElement;
|
const titleElement = document.getElementById('current-tab-title') as HTMLHeadingElement;
|
||||||
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
|
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
|
||||||
@@ -15,13 +15,13 @@ export function initSidebar(renderContent: () => void) {
|
|||||||
state.activeCategory = item.getAttribute('data-category') as 'hw' | 'sw';
|
state.activeCategory = item.getAttribute('data-category') as 'hw' | 'sw';
|
||||||
state.activeSubTab = item.getAttribute('data-tab') || '대시보드';
|
state.activeSubTab = item.getAttribute('data-tab') || '대시보드';
|
||||||
|
|
||||||
// 타이틀 업데이트 (Deep Green 포인트 컬러 유지)
|
// 타이틀 업데이트
|
||||||
const catName = state.activeCategory === 'hw' ? '하드웨어' : '소프트웨어';
|
const catName = state.activeCategory === 'hw' ? '하드웨어' : '소프트웨어';
|
||||||
if (titleElement) {
|
if (titleElement) {
|
||||||
titleElement.textContent = `${catName} / ${state.activeSubTab}`;
|
titleElement.textContent = `${catName} / ${state.activeSubTab}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 추가 버튼 노출 여부 (대시보드에서는 숨김)
|
// 추가 버튼 노출 여부
|
||||||
if (btnAddAsset) {
|
if (btnAddAsset) {
|
||||||
if (state.activeSubTab === '대시보드') {
|
if (state.activeSubTab === '대시보드') {
|
||||||
btnAddAsset.classList.add('hidden');
|
btnAddAsset.classList.add('hidden');
|
||||||
@@ -30,8 +30,8 @@ export function initSidebar(renderContent: () => void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 화면 리렌더링
|
// 탭 변경 콜백 실행
|
||||||
renderContent();
|
onTabChange(state.activeSubTab);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,5 +228,5 @@ export function generateDummyData(): MasterAssetData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { hw, sw, swUsers };
|
return { hw, sw, swUsers, logs: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface HardwareAsset {
|
|||||||
위치: string;
|
위치: string;
|
||||||
관리자: string;
|
관리자: string;
|
||||||
IP주소: string;
|
IP주소: string;
|
||||||
|
IP2?: string;
|
||||||
MACaddress: string;
|
MACaddress: string;
|
||||||
HW사양: string;
|
HW사양: string;
|
||||||
OS: string;
|
OS: string;
|
||||||
@@ -33,6 +34,8 @@ export interface HardwareAsset {
|
|||||||
용도?: string;
|
용도?: string;
|
||||||
상세?: string;
|
상세?: string;
|
||||||
원격접속?: string;
|
원격접속?: string;
|
||||||
|
서버ID?: string;
|
||||||
|
서버PW?: string;
|
||||||
모니터링?: string;
|
모니터링?: string;
|
||||||
비고?: string;
|
비고?: string;
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,6 @@ const HISTORY_HEADERS = ['id', 'assetId', 'date', 'details', 'user'];
|
|||||||
export function downloadTemplate() {
|
export function downloadTemplate() {
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
// HW 탭들 생성
|
|
||||||
HW_TABS.forEach(tab => {
|
HW_TABS.forEach(tab => {
|
||||||
let hd = HW_HEADERS;
|
let hd = HW_HEADERS;
|
||||||
let wscols: any[] = [];
|
let wscols: any[] = [];
|
||||||
@@ -124,7 +126,6 @@ export function downloadTemplate() {
|
|||||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
// SW 탭들 생성
|
|
||||||
SW_TABS.forEach(tab => {
|
SW_TABS.forEach(tab => {
|
||||||
let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS;
|
let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS;
|
||||||
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
||||||
@@ -136,7 +137,6 @@ export function downloadTemplate() {
|
|||||||
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
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_사용자');
|
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
||||||
|
|
||||||
// History 시트 (템플릿에도 포함)
|
|
||||||
const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]);
|
const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]);
|
||||||
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
|
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
|
||||||
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
|
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
|
||||||
@@ -150,7 +150,6 @@ export function downloadTemplate() {
|
|||||||
export function exportToExcel(masterData: MasterAssetData) {
|
export function exportToExcel(masterData: MasterAssetData) {
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
// HW
|
|
||||||
HW_TABS.forEach(tab => {
|
HW_TABS.forEach(tab => {
|
||||||
const targetAssets = masterData.hw.filter(a => a.type === tab);
|
const targetAssets = masterData.hw.filter(a => a.type === tab);
|
||||||
let wsData;
|
let wsData;
|
||||||
@@ -165,7 +164,7 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
} else if (tab === '서버') {
|
} else if (tab === '서버') {
|
||||||
wsData = [
|
wsData = [
|
||||||
SERVER_HEADERS,
|
SERVER_HEADERS,
|
||||||
...targetAssets.map(a => [a.법인, a.자산코드, a.storage유형 || '물리', a.용도 || '', a.위치, a.담당자_정 || '', a.담당자_부 || '', a.IP주소, a.원격접속 || '', a.모델명 || '', a.OS, a.CPU, a.RAM, a.GPU, a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a.모니터링 || '', a.비고 || ''])
|
...targetAssets.map(a => [a.법인, a.자산코드, a.storage유형 || '물리', a.용도 || '', a.위치, a.담당자_정 || '', a.담당자_부 || '', a.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}];
|
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 === '스토리지') {
|
} else if (tab === '스토리지') {
|
||||||
@@ -187,7 +186,6 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
// SW
|
|
||||||
SW_TABS.forEach(tab => {
|
SW_TABS.forEach(tab => {
|
||||||
const targetAssets = masterData.sw.filter(a => a.type === tab);
|
const targetAssets = masterData.sw.filter(a => a.type === tab);
|
||||||
let wsData;
|
let wsData;
|
||||||
@@ -207,7 +205,6 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
// SW_사용자
|
|
||||||
const swUserWsData = [
|
const swUserWsData = [
|
||||||
SW_USER_HEADERS,
|
SW_USER_HEADERS,
|
||||||
...masterData.swUsers.map(u => [u.id, u.swId, u.법인, u.부서, u.팀, u.직위, u.이름, u.사용기간, u.신청서명])
|
...masterData.swUsers.map(u => [u.id, u.swId, u.법인, u.부서, u.팀, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||||
@@ -216,7 +213,6 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
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_사용자');
|
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
||||||
|
|
||||||
// History
|
|
||||||
const historyWsData = [
|
const historyWsData = [
|
||||||
HISTORY_HEADERS,
|
HISTORY_HEADERS,
|
||||||
...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user])
|
...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user])
|
||||||
@@ -229,18 +225,13 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
|
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 업로드된 다중 시트 엑셀을 파싱하여 Master Data 구성
|
|
||||||
*/
|
|
||||||
export async function parseExcel(file: File): Promise<MasterAssetData> {
|
export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const data = e.target?.result;
|
const data = e.target?.result;
|
||||||
const workbook = XLSX.read(data, { type: 'binary' });
|
const workbook = XLSX.read(data, { type: 'binary' });
|
||||||
|
|
||||||
const hwAssets: HardwareAsset[] = [];
|
const hwAssets: HardwareAsset[] = [];
|
||||||
const swAssets: SoftwareAsset[] = [];
|
const swAssets: SoftwareAsset[] = [];
|
||||||
const swUsers: SWUser[] = [];
|
const swUsers: SWUser[] = [];
|
||||||
@@ -258,25 +249,14 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
type: sheetName,
|
type: sheetName,
|
||||||
법인: row['법인'] || '',
|
법인: row['법인'] || '',
|
||||||
자산코드: row['자산코드'] || '',
|
자산코드: row['자산코드'] || '',
|
||||||
명칭: '', // 개인PC는 명칭 미사용
|
명칭: '',
|
||||||
위치: row['위치'] || '',
|
위치: row['위치'] || '',
|
||||||
사용자: row['사용자'] || '',
|
사용자: row['사용자'] || '',
|
||||||
관리자: '',
|
관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '',
|
||||||
IP주소: '',
|
CPU: row['CPU'] || '', GPU: row['GPU'] || '', RAM: row['RAM'] || '',
|
||||||
MACaddress: '',
|
SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '',
|
||||||
HW사양: '',
|
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||||
OS: '',
|
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||||
CPU: row['CPU'] || '',
|
|
||||||
GPU: row['GPU'] || '',
|
|
||||||
RAM: row['RAM'] || '',
|
|
||||||
SSD1: row['SSD1'] || '',
|
|
||||||
SSD2: row['SSD2'] || '',
|
|
||||||
HDD1: row['HDD1'] || '',
|
|
||||||
HDD2: row['HDD2'] || '',
|
|
||||||
구매일: row['구매일'] || '',
|
|
||||||
금액: row['금액'] ? String(row['금액']) : '',
|
|
||||||
납품업체: row['납품업체'] || '',
|
|
||||||
품의서명: row['품의서명'] || '',
|
|
||||||
});
|
});
|
||||||
} else if (sheetName === '서버') {
|
} else if (sheetName === '서버') {
|
||||||
hwAssets.push({
|
hwAssets.push({
|
||||||
@@ -285,71 +265,36 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
법인: row['법인'] || '',
|
법인: row['법인'] || '',
|
||||||
자산코드: row['자산번호'] || row['자산코드'] || '',
|
자산코드: row['자산번호'] || row['자산코드'] || '',
|
||||||
명칭: row['용도'] || row['명칭'] || '',
|
명칭: row['용도'] || row['명칭'] || '',
|
||||||
용도: row['용도'] || '',
|
용도: row['용도'] || '', 위치: row['설치위치'] || row['위치'] || '',
|
||||||
위치: row['설치위치'] || row['위치'] || '',
|
관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
||||||
관리자: row['담당자(정)'] || '',
|
IP주소: row['IP 주소'] || row['IP주소'] || '', IP2: row['IP2'] || '',
|
||||||
담당자_정: row['담당자(정)'] || '',
|
원격접속: row['원격접속'] || '', 서버ID: row['서버ID'] || '', 서버PW: row['서버PW'] || '',
|
||||||
담당자_부: row['담당자(부)'] || '',
|
모델명: row['모델명'] || '', OS: row['OS'] || '',
|
||||||
IP주소: row['IP 주소'] || row['IP주소'] || '',
|
CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '',
|
||||||
원격접속: row['원격접속'] || '',
|
SSD1: row['Storage1'] || row['SSD1'] || '', SSD2: row['Storage2'] || row['SSD2'] || '', HDD1: row['Storage3'] || row['HDD1'] || '',
|
||||||
모델명: row['모델명'] || '',
|
모니터링: row['모니터링'] || '', 비고: row['비고'] || '', storage유형: row['유형'] || '물리',
|
||||||
OS: row['OS'] || '',
|
MACaddress: '', HW사양: '', 구매일: '', 금액: '', 납품업체: '', 품의서명: '',
|
||||||
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 === '스토리지') {
|
} else if (sheetName === '스토리지') {
|
||||||
hwAssets.push({
|
hwAssets.push({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: sheetName,
|
type: sheetName,
|
||||||
법인: row['법인'] || '',
|
법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '',
|
||||||
자산코드: row['자산코드'] || '',
|
관리자: '', IP주소: row['IP주소'] || '', MACaddress: row['MAC주소'] || '', HW사양: '', OS: '',
|
||||||
명칭: row['명칭'] || '',
|
storage유형: row['유형'] || '', 모델명: row['모델명'] || '', 용량: row['용량'] || '',
|
||||||
위치: row['위치'] || '',
|
담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
|
||||||
관리자: '',
|
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||||
IP주소: row['IP주소'] || '',
|
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||||
MACaddress: row['MAC주소'] || row['MACaddress'] || row['MAC address'] || '',
|
|
||||||
HW사양: '',
|
|
||||||
OS: '',
|
|
||||||
storage유형: row['유형'] || '',
|
|
||||||
모델명: row['모델명'] || '',
|
|
||||||
용량: row['용량'] || '',
|
|
||||||
담당자_정: row['담당자(정)'] || '',
|
|
||||||
담당자_부: row['담당자(부)'] || '',
|
|
||||||
구매일: row['구매일'] || '',
|
|
||||||
금액: row['금액'] ? String(row['금액']) : '',
|
|
||||||
납품업체: row['납품업체'] || '',
|
|
||||||
품의서명: row['품의서명'] || '',
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
hwAssets.push({
|
hwAssets.push({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: sheetName,
|
type: sheetName,
|
||||||
법인: row['법인'] || '',
|
법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '',
|
||||||
자산코드: row['자산코드'] || '',
|
관리자: row['관리자'] || '', IP주소: row['IP주소'] || '', MACaddress: row['MACaddress'] || '',
|
||||||
명칭: row['명칭'] || '',
|
HW사양: row['HW사양'] || '', OS: row['OS'] || '',
|
||||||
위치: row['위치'] || '',
|
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
|
||||||
관리자: row['관리자'] || '',
|
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
|
||||||
IP주소: row['IP주소'] || '',
|
|
||||||
MACaddress: row['MACaddress'] || row['MAC address'] || '',
|
|
||||||
HW사양: row['HW사양'] || row['H/W 사양'] || '',
|
|
||||||
OS: row['OS'] || '',
|
|
||||||
구매일: row['구매일'] || '',
|
|
||||||
금액: row['금액'] ? String(row['금액']) : '',
|
|
||||||
납품업체: row['납품업체'] || '',
|
|
||||||
품의서명: row['품의서명'] || '',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -359,19 +304,10 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
json.forEach(row => {
|
json.forEach(row => {
|
||||||
swAssets.push({
|
swAssets.push({
|
||||||
id: row['ID'] ? String(row['ID']) : Math.random().toString(36).substring(2, 9),
|
id: row['ID'] ? String(row['ID']) : Math.random().toString(36).substring(2, 9),
|
||||||
type: sheetName,
|
type: sheetName, 분야: row['분야'] || '', 법인: row['법인'] || '', 부서: row['부서'] || '', 제품명: row['제품명'] || '',
|
||||||
분야: row['분야'] || '',
|
구매일: row['구매일'] || '', 구독일: row['구독일'] || '', 유지보수여부: row['유지보수여부'] === 'Y' || row['유지보수여부'] === true,
|
||||||
법인: row['법인'] || '',
|
금액: row['금액'] ? String(row['금액']) : '', 수량: parseInt(row['수량'] || '1', 10),
|
||||||
부서: row['부서'] || '',
|
계정명: row['계정명'] || '', 납품업체: row['납품업체'] || '', 비고: row['비고'] || '',
|
||||||
제품명: row['제품명'] || '',
|
|
||||||
구매일: row['구매일'] || '',
|
|
||||||
구독일: row['구독일'] || '',
|
|
||||||
유지보수여부: row['유지보수여부'] === 'Y' || row['유지보수여부'] === true,
|
|
||||||
금액: row['금액'] ? String(row['금액']) : '',
|
|
||||||
수량: parseInt(row['수량'] || '1', 10),
|
|
||||||
계정명: row['계정명'] || '',
|
|
||||||
납품업체: row['납품업체'] || '',
|
|
||||||
비고: row['비고'] || '',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -380,14 +316,9 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
json.forEach(row => {
|
json.forEach(row => {
|
||||||
swUsers.push({
|
swUsers.push({
|
||||||
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
|
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
|
||||||
swId: row['swId'] ? String(row['swId']) : '',
|
swId: row['swId'] ? String(row['swId']) : '', 법인: row['법인'] || '', 부서: row['부서'] || '',
|
||||||
법인: row['법인'] || '',
|
팀: row['팀'] || '', 직위: row['직위'] || '', 이름: row['이름'] || '',
|
||||||
부서: row['부서'] || '',
|
사용기간: row['사용기간'] || '', 신청서명: row['신청서명'] || '',
|
||||||
팀: row['팀'] || '',
|
|
||||||
직위: row['직위'] || '',
|
|
||||||
이름: row['이름'] || '',
|
|
||||||
사용기간: row['사용기간'] || '',
|
|
||||||
신청서명: row['신청서명'] || '',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -397,20 +328,16 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
logs.push({
|
logs.push({
|
||||||
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
|
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
|
||||||
assetId: row['assetId'] ? String(row['assetId']) : '',
|
assetId: row['assetId'] ? String(row['assetId']) : '',
|
||||||
date: row['date'] || '',
|
date: row['date'] || '', details: row['details'] || '', user: row['user'] || '',
|
||||||
details: row['details'] || '',
|
|
||||||
user: row['user'] || '',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve({ hw: hwAssets, sw: swAssets, swUsers, logs });
|
resolve({ hw: hwAssets, sw: swAssets, swUsers, logs });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = (err) => reject(err);
|
reader.onerror = (err) => reject(err);
|
||||||
reader.readAsBinaryString(file);
|
reader.readAsBinaryString(file);
|
||||||
});
|
});
|
||||||
|
|||||||
161
src/main.ts
161
src/main.ts
@@ -1,88 +1,95 @@
|
|||||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History } from 'lucide';
|
|
||||||
import { downloadTemplate, exportToExcel, parseExcel } from './excelHandler';
|
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import { initSidebar } from './components/Sidebar';
|
import { renderSidebar } from './components/Sidebar';
|
||||||
import { initBaseModal } from './components/Modal/BaseModal';
|
|
||||||
import { initPCModal, openPcModal } from './components/Modal/PCModal';
|
|
||||||
import { initHWModal, openHwModal } from './components/Modal/HWModal';
|
|
||||||
import { initStorageModal, openStorageModal } from './components/Modal/StorageModal';
|
|
||||||
import { initSWModal, openSwModal } from './components/Modal/SWModal';
|
|
||||||
import { initSWUserModal, openSwUserModal } from './components/Modal/SWUserModal';
|
|
||||||
import { renderDashboard } from './views/DashboardView';
|
import { renderDashboard } from './views/DashboardView';
|
||||||
import { renderTable } from './views/AssetTableView';
|
import { renderTable } from './views/AssetTableView';
|
||||||
|
import { downloadTemplate, exportToExcel, parseExcel } from './excelHandler';
|
||||||
|
import { initPcModal } from './components/Modal/PCModal';
|
||||||
|
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
||||||
|
import { initStorageModal } from './components/Modal/StorageModal';
|
||||||
|
import { initSwModal } from './components/Modal/SWModal';
|
||||||
|
import { initSwUserModal } from './components/Modal/SWUserModal';
|
||||||
|
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide';
|
||||||
|
|
||||||
declare var Chart: any;
|
// --- App Initialization ---
|
||||||
|
function initApp() {
|
||||||
|
const mainContent = document.getElementById('main-content')!;
|
||||||
|
|
||||||
// --- DOM Elements ---
|
// 1. 초기 뷰 렌더링 (대시보드)
|
||||||
const mainContent = document.getElementById('main-content') as HTMLElement;
|
|
||||||
const uploadInput = document.getElementById('excel-upload') as HTMLInputElement;
|
|
||||||
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
|
|
||||||
const btnDownloadTemp = document.getElementById('btn-download-template') as HTMLButtonElement;
|
|
||||||
const btnExport = document.getElementById('btn-export-excel') as HTMLButtonElement;
|
|
||||||
|
|
||||||
// Initialize Icons
|
|
||||||
createIcons({
|
|
||||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize Components
|
|
||||||
const { closeAllModals } = initBaseModal();
|
|
||||||
initSidebar(renderContent);
|
|
||||||
initPCModal(renderContent, closeAllModals);
|
|
||||||
initHWModal(renderContent, closeAllModals);
|
|
||||||
initStorageModal(renderContent, closeAllModals);
|
|
||||||
initSWModal(renderContent, closeAllModals);
|
|
||||||
initSWUserModal(renderContent, closeAllModals);
|
|
||||||
|
|
||||||
// Dashboard Detail 닫기 버튼 (공통 처리용)
|
|
||||||
const btnCloseDashboardDetail = document.getElementById('btn-close-dashboard-detail') as HTMLButtonElement;
|
|
||||||
btnCloseDashboardDetail?.addEventListener('click', () => {
|
|
||||||
document.getElementById('dashboard-detail-modal')?.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Element Button
|
|
||||||
btnAddAsset?.addEventListener('click', () => {
|
|
||||||
if (state.activeSubTab === '대시보드') return;
|
|
||||||
if (state.activeCategory === 'hw') {
|
|
||||||
if (state.activeSubTab === '개인PC') openPcModal();
|
|
||||||
else if (state.activeSubTab === '스토리지') openStorageModal();
|
|
||||||
else openHwModal();
|
|
||||||
} else {
|
|
||||||
openSwModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Excel Controls ---
|
|
||||||
btnDownloadTemp?.addEventListener('click', () => downloadTemplate());
|
|
||||||
btnExport?.addEventListener('click', () => exportToExcel(state.masterData));
|
|
||||||
|
|
||||||
uploadInput?.addEventListener('change', async (e) => {
|
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
try {
|
|
||||||
state.masterData = await parseExcel(file);
|
|
||||||
renderContent();
|
|
||||||
alert('모든 시트 데이터 연동 완료');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
alert('엑셀 파싱 오류: 템플릿 양식이 다르거나 파일이 손상되었습니다.');
|
|
||||||
} finally {
|
|
||||||
uploadInput.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전역 렌더링 함수 - 각 컴포넌트에서 호출하여 화면을 갱신합니다.
|
|
||||||
*/
|
|
||||||
function renderContent() {
|
|
||||||
mainContent.innerHTML = ''; // 화면 초기화
|
|
||||||
|
|
||||||
if (state.activeSubTab === '대시보드') {
|
|
||||||
renderDashboard(mainContent);
|
renderDashboard(mainContent);
|
||||||
|
|
||||||
|
// 2. 사이드바 초기화
|
||||||
|
renderSidebar((tab) => {
|
||||||
|
if (tab === '대시보드') {
|
||||||
|
renderDashboard(mainContent);
|
||||||
|
document.getElementById('btn-add-asset')?.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
renderTable(mainContent);
|
renderTable(mainContent);
|
||||||
|
document.getElementById('btn-add-asset')?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
// 상단 타이틀 업데이트
|
||||||
|
const titleEl = document.getElementById('current-tab-title')!;
|
||||||
|
titleEl.textContent = `${state.activeCategory === 'hw' ? '하드웨어' : '소프트웨어'} / ${state.activeSubTab}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 모달 초기화
|
||||||
|
initPcModal();
|
||||||
|
initHwModal();
|
||||||
|
initStorageModal();
|
||||||
|
initSwModal();
|
||||||
|
initSwUserModal(() => renderTable(mainContent), () => {});
|
||||||
|
initDashboardDetailModal();
|
||||||
|
|
||||||
|
// 4. 전역 버튼 이벤트 바인딩
|
||||||
|
document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate());
|
||||||
|
document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData));
|
||||||
|
|
||||||
|
const uploadInput = document.getElementById('excel-upload') as HTMLInputElement;
|
||||||
|
uploadInput?.addEventListener('change', async (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const data = await parseExcel(file);
|
||||||
|
state.masterData = data;
|
||||||
|
renderTable(mainContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-add-asset')?.addEventListener('click', () => {
|
||||||
|
if (state.activeSubTab === '서버' || state.activeSubTab === '전산비품' || state.activeSubTab === '스토리지') {
|
||||||
|
const newAsset = {
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
type: state.activeSubTab,
|
||||||
|
법인: '한맥',
|
||||||
|
자산코드: '',
|
||||||
|
명칭: '',
|
||||||
|
위치: '',
|
||||||
|
관리자: '',
|
||||||
|
IP주소: '',
|
||||||
|
MACaddress: '',
|
||||||
|
HW사양: '',
|
||||||
|
OS: '',
|
||||||
|
납품업체: '',
|
||||||
|
품의서명: ''
|
||||||
|
};
|
||||||
|
openHwModal(newAsset);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 전역 아이콘 초기화
|
||||||
|
createIcons({
|
||||||
|
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial Render
|
function initDashboardDetailModal() {
|
||||||
renderContent();
|
const modal = document.getElementById('dashboard-detail-modal')!;
|
||||||
|
const closeBtn = document.getElementById('btn-close-dashboard-detail-modal')!;
|
||||||
|
const cancelBtn = document.getElementById('btn-cancel-dashboard-detail-modal')!;
|
||||||
|
|
||||||
|
const closeModal = () => modal.classList.add('hidden');
|
||||||
|
closeBtn.addEventListener('click', closeModal);
|
||||||
|
cancelBtn.addEventListener('click', closeModal);
|
||||||
|
modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the app
|
||||||
|
document.addEventListener('DOMContentLoaded', initApp);
|
||||||
|
|||||||
13
src/server_data.json
Normal file
13
src/server_data.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"법인": "(주)회사1",
|
||||||
|
"자산코드": "ASSET-100",
|
||||||
|
"명칭": "서버 모델A",
|
||||||
|
"위치": "본사 1층",
|
||||||
|
"관리자": "관리자A",
|
||||||
|
"IP주소": "192.168.0.1",
|
||||||
|
"MACaddress": "00:00:00:00:00:01",
|
||||||
|
"HW사양": "Core i7, 16GB RAM",
|
||||||
|
"OS": "Windows 10"
|
||||||
|
}
|
||||||
|
]
|
||||||
25
src/state.ts
25
src/state.ts
@@ -1,4 +1,4 @@
|
|||||||
import { MasterAssetData } from './excelHandler';
|
import { MasterAssetData, HardwareAsset } from './excelHandler';
|
||||||
import { generateDummyData } from './dummyDataGenerator';
|
import { generateDummyData } from './dummyDataGenerator';
|
||||||
import { realServerData } from './realServerData';
|
import { realServerData } from './realServerData';
|
||||||
|
|
||||||
@@ -12,23 +12,40 @@ export interface AppState {
|
|||||||
|
|
||||||
const dummy = generateDummyData();
|
const dummy = generateDummyData();
|
||||||
// 서버 데이터만 실제 데이터로 교체
|
// 서버 데이터만 실제 데이터로 교체
|
||||||
const mergedHw = [
|
const mergedHw: HardwareAsset[] = [
|
||||||
...dummy.hw.filter(a => a.type !== '서버'),
|
...dummy.hw.filter(a => a.type !== '서버'),
|
||||||
...realServerData.map(s => ({
|
...realServerData.map(s => ({
|
||||||
...s,
|
id: s.id || Math.random().toString(36).substring(2, 9),
|
||||||
type: '서버',
|
type: '서버',
|
||||||
|
법인: s.법인,
|
||||||
|
자산코드: s.자산코드,
|
||||||
|
명칭: s.용도 || '',
|
||||||
|
위치: s.위치,
|
||||||
관리자: s.담당자_정 || '홍길동',
|
관리자: s.담당자_정 || '홍길동',
|
||||||
담당자_정: s.담당자_정 || '홍길동',
|
담당자_정: s.담당자_정 || '홍길동',
|
||||||
담당자_부: s.담당자_부 || '김철수',
|
담당자_부: s.담당자_부 || '김철수',
|
||||||
|
IP주소: s.IP주소,
|
||||||
|
IP2: s.IP2 || '',
|
||||||
MACaddress: s.MACaddress || '',
|
MACaddress: s.MACaddress || '',
|
||||||
HW사양: s.HW사양 || '',
|
HW사양: s.HW사양 || '',
|
||||||
|
OS: s.OS,
|
||||||
|
CPU: s.CPU,
|
||||||
|
RAM: s.RAM,
|
||||||
|
SSD1: s.SSD1,
|
||||||
|
SSD2: s.SSD2,
|
||||||
|
HDD1: s.HDD1,
|
||||||
|
storage유형: s.storage유형,
|
||||||
|
모델명: s.모델명,
|
||||||
구매일: s.구매일 || '',
|
구매일: s.구매일 || '',
|
||||||
금액: s.금액 || '',
|
금액: s.금액 || '',
|
||||||
납품업체: s.납품업체 || '',
|
납품업체: s.납품업체 || '',
|
||||||
품의서명: s.품의서명 || '',
|
품의서명: s.품의서명 || '',
|
||||||
|
용도: s.용도,
|
||||||
|
상세: s.상세,
|
||||||
원격접속: s.원격접속 || '',
|
원격접속: s.원격접속 || '',
|
||||||
서버ID: s.서버ID || '',
|
서버ID: s.서버ID || '',
|
||||||
서버PW: s.서버PW || '',
|
서버PW: s.서버PW || '',
|
||||||
|
모니터링: s.모니터링 || '',
|
||||||
비고: s.비고 || ''
|
비고: s.비고 || ''
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
@@ -38,7 +55,7 @@ export const state: AppState = {
|
|||||||
masterData: {
|
masterData: {
|
||||||
...dummy,
|
...dummy,
|
||||||
hw: mergedHw,
|
hw: mergedHw,
|
||||||
logs: [] // 공동작업자의 수정 이력 배열 추가
|
logs: [] // MasterAssetData 인터페이스에 맞게 추가
|
||||||
},
|
},
|
||||||
activeCategory: 'hw',
|
activeCategory: 'hw',
|
||||||
activeSubTab: '대시보드',
|
activeSubTab: '대시보드',
|
||||||
|
|||||||
@@ -215,14 +215,22 @@ body {
|
|||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1.25rem; /* 표준 사이즈로 통일 */
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
min-width: 80px; /* 버튼의 최소 너비 확보 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
min-width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn i { width: 16px; height: 16px; }
|
.btn i { width: 16px; height: 16px; }
|
||||||
@@ -242,9 +250,6 @@ body {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,3 +384,7 @@ tbody tr:hover { background-color: var(--bg-color); }
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,12 +16,16 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
width: 100%; max-width: 600px;
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 90vh;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay:not(.hidden) .modal-content { transform: translateY(0); }
|
.modal-overlay:not(.hidden) .modal-content { transform: translateY(0); }
|
||||||
@@ -33,38 +37,133 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h2 { font-size: 1.125rem; font-weight: 500; }
|
.modal-header h2 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body { padding: 1.5rem; }
|
.modal-header .btn-icon {
|
||||||
|
color: var(--white);
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-form { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
.modal-header .btn-icon:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group { display: flex; flex-direction: column; gap: 0.375rem; }
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group.full-width { grid-column: span 2; }
|
.grid-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group label { font-size: 0.875rem; font-weight: 500; }
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group input, .form-group textarea {
|
.form-group.full-width {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Title for Grouping */
|
||||||
|
.form-section-title {
|
||||||
|
grid-column: span 2;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding: 1.5rem 0 0.5rem 0; /* 패딩 조정 */
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Readonly/Edit Mode Interaction */
|
||||||
|
.grid-form.is-view-mode input,
|
||||||
|
.grid-form.is-view-mode select,
|
||||||
|
.grid-form.is-view-mode textarea {
|
||||||
|
border-color: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-form.is-edit-mode input,
|
||||||
|
.grid-form.is-edit-mode select,
|
||||||
|
.grid-form.is-edit-mode textarea {
|
||||||
|
color: #FF3D00; /* 수정 시 글자색 변경 */
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-form.is-edit-mode input:focus,
|
||||||
|
.grid-form.is-edit-mode select:focus,
|
||||||
|
.grid-form.is-edit-mode textarea:focus {
|
||||||
|
border-color: #FF3D00;
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 61, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section-title:first-child {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
padding: 0.625rem;
|
padding: 0.625rem;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: inherit; font-size: 0.875rem;
|
font-family: inherit;
|
||||||
outline: none; transition: border-color 0.2s;
|
font-size: 0.875rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus, .form-group textarea:focus { border-color: var(--primary-color); }
|
.form-group input:focus,
|
||||||
|
.form-group select:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
padding: 1rem 1.5rem; border-top: 1px solid var(--border-color);
|
padding: 1rem 1.5rem;
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FAFAFA;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-actions { display: flex; gap: 0.5rem; }
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Wide Modal for History */
|
/* Wide Modal for History/Detail */
|
||||||
.modal-content.wide {
|
.modal-content.wide {
|
||||||
max-width: 950px;
|
max-width: 950px;
|
||||||
}
|
}
|
||||||
@@ -158,4 +257,3 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { openSwUserModal } from '../components/Modal/SWUserModal';
|
|||||||
* 자산 목록 테이블 렌더링 메인 함수
|
* 자산 목록 테이블 렌더링 메인 함수
|
||||||
*/
|
*/
|
||||||
export function renderTable(mainContent: HTMLElement) {
|
export function renderTable(mainContent: HTMLElement) {
|
||||||
|
mainContent.innerHTML = ''; // 기존 내용 삭제 (중요)
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'view-container';
|
container.className = 'view-container';
|
||||||
const table = document.createElement('table');
|
const table = document.createElement('table');
|
||||||
@@ -41,7 +42,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
list.forEach((asset, idx) => {
|
list.forEach((asset, idx) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.style.cursor = 'pointer';
|
tr.style.cursor = 'pointer';
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.자산코드}</td><td>${asset.사용자||''}</td><td>${asset.위치||''}</td><td>${asset.CPU||''}</td><td>${asset.GPU||''}</td><td>${asset.RAM||''}</td><td>${asset.SSD1||'-'}</td><td>${asset.SSD2||'-'}</td><td>${asset.HDD1||'-'}</td><td>${asset.HDD2||'-'}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td>${asset.납품업체||''}</td><td style="text-align:center;">${asset.품의서명 ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
|
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.자산코드}</td><td>${asset.사용자||''}</td><td>${asset.위치||''}</td><td>${asset.CPU||''}</td><td>${asset.GPU||''}</td><td>${asset.RAM||''}</td><td>${asset.SSD1||'-'}</td><td>${asset.SSD2||'-'}</td><td>${asset.HDD1||'-'}</td><td>${asset.HDD2||'-'}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td>${asset.납품업체||''}</td><td style="text-align:center;">${asset.품의서명 ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); });
|
||||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openPcModal(asset));
|
tr.querySelector('.btn-edit')?.addEventListener('click', () => openPcModal(asset));
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
@@ -56,7 +57,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
list.forEach((asset, idx) => {
|
list.forEach((asset, idx) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.style.cursor = 'pointer';
|
tr.style.cursor = 'pointer';
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.storage유형||''}</td><td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치||''}</td><td>${asset.모델명||''}</td><td>${asset.용량||''}</td><td>${asset.담당자_정||''}</td><td>${asset.IP주소||''}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
|
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.storage유형||''}</td><td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치||''}</td><td>${asset.모델명||''}</td><td>${asset.용량||''}</td><td>${asset.담당자_정||''}</td><td>${asset.IP주소||''}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); });
|
||||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openStorageModal(asset));
|
tr.querySelector('.btn-edit')?.addEventListener('click', () => openStorageModal(asset));
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
@@ -158,7 +159,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
`;
|
`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
||||||
} else {
|
} else {
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td>${state.activeSubTab === '전산비품' ? `<td>${asset.비품유형||'-'}</td>` : ''}<td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치}</td><td>${asset.관리자}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
|
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td>${state.activeSubTab === '전산비품' ? `<td>${asset.비품유형||'-'}</td>` : ''}<td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치}</td><td>${asset.관리자}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
||||||
}
|
}
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
@@ -255,10 +256,6 @@ function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainCont
|
|||||||
createIcons({
|
createIcons({
|
||||||
icons: { Edit2, Users, RefreshCcw }
|
icons: { Edit2, Users, RefreshCcw }
|
||||||
});
|
});
|
||||||
// 초기화 버튼 아이콘은 별도로
|
|
||||||
createIcons({
|
|
||||||
scope: filterBar
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. 이벤트 바인딩
|
// 4. 이벤트 바인딩
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ declare var Chart: any;
|
|||||||
* 대시보드 렌더링 메인 함수
|
* 대시보드 렌더링 메인 함수
|
||||||
*/
|
*/
|
||||||
export function renderDashboard(mainContent: HTMLElement) {
|
export function renderDashboard(mainContent: HTMLElement) {
|
||||||
|
if (!mainContent) return;
|
||||||
mainContent.innerHTML = '';
|
mainContent.innerHTML = '';
|
||||||
|
|
||||||
// 기존 차트 리소스 해제
|
// 기존 차트 리소스 해제
|
||||||
|
if (state.activeCharts) {
|
||||||
state.activeCharts.forEach(c => {
|
state.activeCharts.forEach(c => {
|
||||||
if (c && typeof c.destroy === 'function') c.destroy();
|
if (c && typeof c.destroy === 'function') c.destroy();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
state.activeCharts = [];
|
state.activeCharts = [];
|
||||||
|
|
||||||
if (state.activeCategory === 'hw') {
|
if (state.activeCategory === 'hw') {
|
||||||
@@ -41,7 +44,6 @@ function renderHwDashboard(container: HTMLElement) {
|
|||||||
else groups[a.type].normal.push(a);
|
else groups[a.type].normal.push(a);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 사용현황 카드 생성
|
|
||||||
let usageCards = '';
|
let usageCards = '';
|
||||||
types.forEach((t, i) => {
|
types.forEach((t, i) => {
|
||||||
const total = groups[t].idle.length + groups[t].active.length;
|
const total = groups[t].idle.length + groups[t].active.length;
|
||||||
@@ -69,7 +71,6 @@ function renderHwDashboard(container: HTMLElement) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 노후화 카드 생성
|
|
||||||
let agedCards = '';
|
let agedCards = '';
|
||||||
types.forEach((t, i) => {
|
types.forEach((t, i) => {
|
||||||
const total = groups[t].aged.length + groups[t].normal.length;
|
const total = groups[t].aged.length + groups[t].normal.length;
|
||||||
@@ -104,7 +105,6 @@ function renderHwDashboard(container: HTMLElement) {
|
|||||||
<div class="dashboard-layout-2col">${agedCards}</div>
|
<div class="dashboard-layout-2col">${agedCards}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 클릭 이벤트 바인딩
|
|
||||||
container.querySelectorAll('[data-action="idle"]').forEach(card => {
|
container.querySelectorAll('[data-action="idle"]').forEach(card => {
|
||||||
card.addEventListener('click', () => {
|
card.addEventListener('click', () => {
|
||||||
const t = card.getAttribute('data-type')!;
|
const t = card.getAttribute('data-type')!;
|
||||||
@@ -135,7 +135,7 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
state.masterData.sw.forEach(sw => {
|
state.masterData.sw.forEach(sw => {
|
||||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||||
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
||||||
const priceStr = sw.금액 ? sw.금액.replace(/,/g, '') : '0';
|
const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0';
|
||||||
const price = parseInt(priceStr, 10) || 0;
|
const price = parseInt(priceStr, 10) || 0;
|
||||||
|
|
||||||
if (sw.type === '구독SW') {
|
if (sw.type === '구독SW') {
|
||||||
@@ -146,7 +146,6 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
if (isSWExpiring(sw)) permExp++;
|
if (isSWExpiring(sw)) permExp++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 오늘이 속해있는 년도(2026)의 사용 금액 합계
|
|
||||||
if (sw.구매일 && sw.구매일.startsWith(currentYear)) {
|
if (sw.구매일 && sw.구매일.startsWith(currentYear)) {
|
||||||
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
||||||
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
||||||
@@ -232,7 +231,6 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 차트 생성
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const ctxCorp = (document.getElementById('chart-cost-corp') as HTMLCanvasElement)?.getContext('2d');
|
const ctxCorp = (document.getElementById('chart-cost-corp') as HTMLCanvasElement)?.getContext('2d');
|
||||||
const ctxCat = (document.getElementById('chart-cost-cat') as HTMLCanvasElement)?.getContext('2d');
|
const ctxCat = (document.getElementById('chart-cost-cat') as HTMLCanvasElement)?.getContext('2d');
|
||||||
@@ -247,7 +245,7 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
data: corps.map(c => costByCorp[c]),
|
data: corps.map(c => costByCorp[c]),
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: '#3b82f6',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
barThickness: 20 // 막대 두께 줄임
|
barThickness: 20
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -258,11 +256,9 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
ticks: { callback: (v: any) => v.toLocaleString() },
|
ticks: { callback: (v: any) => v.toLocaleString() },
|
||||||
grid: { display: false } // 가로줄 삭제
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
grid: { display: false }
|
grid: { display: false }
|
||||||
}
|
},
|
||||||
|
x: { grid: { display: false } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -279,7 +275,7 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
data: categories.map(c => costByCat[c]),
|
data: categories.map(c => costByCat[c]),
|
||||||
backgroundColor: '#10b981',
|
backgroundColor: '#10b981',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
barThickness: 20 // 막대 두께 줄임
|
barThickness: 20
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -290,11 +286,9 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
ticks: { callback: (v: any) => v.toLocaleString() },
|
ticks: { callback: (v: any) => v.toLocaleString() },
|
||||||
grid: { display: false } // 가로줄 삭제
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
grid: { display: false }
|
grid: { display: false }
|
||||||
}
|
},
|
||||||
|
x: { grid: { display: false } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -302,7 +296,6 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// 클릭 이벤트 바인딩
|
|
||||||
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => {
|
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => {
|
||||||
openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW'));
|
openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW'));
|
||||||
});
|
});
|
||||||
@@ -317,7 +310,6 @@ function renderSwDashboard(container: HTMLElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 공통 헬퍼 함수들 ---
|
|
||||||
function isHwIdle(a: HardwareAsset) {
|
function isHwIdle(a: HardwareAsset) {
|
||||||
if (a.type === '개인PC') return !a.사용자 || a.사용자.trim() === '' || a.사용자.trim() === '-';
|
if (a.type === '개인PC') return !a.사용자 || a.사용자.trim() === '' || a.사용자.trim() === '-';
|
||||||
if (a.type === '스토리지') return !a.담당자_정 || a.담당자_정.trim() === '' || a.담당자_정.trim() === '-';
|
if (a.type === '스토리지') return !a.담당자_정 || a.담당자_정.trim() === '' || a.담당자_정.trim() === '-';
|
||||||
@@ -326,7 +318,11 @@ function isHwIdle(a: HardwareAsset) {
|
|||||||
|
|
||||||
function getHwAgeYears(a: HardwareAsset) {
|
function getHwAgeYears(a: HardwareAsset) {
|
||||||
if (!a.구매일) return 0;
|
if (!a.구매일) return 0;
|
||||||
return (Date.now() - new Date(a.구매일).getTime()) / (1000 * 60 * 60 * 24 * 365.25);
|
try {
|
||||||
|
const buyDate = new Date(a.구매일.replace(/\./g, '-'));
|
||||||
|
if (isNaN(buyDate.getTime())) return 0;
|
||||||
|
return (Date.now() - buyDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25);
|
||||||
|
} catch { return 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSWExpiring(sw: SoftwareAsset) {
|
function isSWExpiring(sw: SoftwareAsset) {
|
||||||
@@ -339,29 +335,30 @@ function isSWExpiring(sw: SoftwareAsset) {
|
|||||||
return diffDays >= 0 && diffDays <= 30;
|
return diffDays >= 0 && diffDays <= 30;
|
||||||
}
|
}
|
||||||
} else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) {
|
} else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) {
|
||||||
|
try {
|
||||||
const endStr = sw.비고.split('~')[1].trim();
|
const endStr = sw.비고.split('~')[1].trim();
|
||||||
const endMs = new Date(endStr.replace(/\./g, '-')).getTime();
|
const endMs = new Date(endStr.replace(/\./g, '-')).getTime();
|
||||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||||
return diffDays >= 0 && diffDays <= 30;
|
return diffDays >= 0 && diffDays <= 30;
|
||||||
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 대시보드 상세 모달 제어 (main.ts의 함수 재사용 또는 이동 필요) ---
|
|
||||||
// 일단 main.ts에 있는 함수를 전역에서 가져와 쓸 수 없으므로, 여기서 직접 정의하거나 main.ts에서 export 해야 합니다.
|
|
||||||
// 구조 개선을 위해 main.ts에서 이 함수들도 DashboardView로 옮기는 것이 좋습니다.
|
|
||||||
|
|
||||||
function openDashboardDetail(title: string, list: HardwareAsset[]) {
|
function openDashboardDetail(title: string, list: HardwareAsset[]) {
|
||||||
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
|
const modal = document.getElementById('dashboard-detail-modal');
|
||||||
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
|
if (!modal) return;
|
||||||
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
|
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||||
const thead = tbody.closest('table')!.querySelector('thead')!;
|
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||||
|
if (!titleEl || !tbody) return;
|
||||||
|
const thead = tbody.closest('table')?.querySelector('thead');
|
||||||
|
if (!thead) return;
|
||||||
|
|
||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매일</th><th>금액</th></tr>`;
|
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매일</th><th>금액</th></tr>`;
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;">해당 조건의 자산이 없습니다.</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
|
||||||
} else {
|
} else {
|
||||||
list.forEach((asset, idx) => {
|
list.forEach((asset, idx) => {
|
||||||
let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-';
|
let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-';
|
||||||
@@ -375,10 +372,13 @@ function openDashboardDetail(title: string, list: HardwareAsset[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
|
function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||||
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
|
const modal = document.getElementById('dashboard-detail-modal');
|
||||||
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
|
if (!modal) return;
|
||||||
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
|
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||||
const thead = tbody.closest('table')!.querySelector('thead')!;
|
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||||
|
if (!titleEl || !tbody) return;
|
||||||
|
const thead = tbody.closest('table')?.querySelector('thead');
|
||||||
|
if (!thead) return;
|
||||||
|
|
||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>법인</th><th>제품명</th><th>수량</th><th>금액</th></tr>`;
|
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>법인</th><th>제품명</th><th>수량</th><th>금액</th></tr>`;
|
||||||
@@ -392,10 +392,13 @@ function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
||||||
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
|
const modal = document.getElementById('dashboard-detail-modal');
|
||||||
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
|
if (!modal) return;
|
||||||
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
|
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||||
const thead = tbody.closest('table')!.querySelector('thead')!;
|
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||||
|
if (!titleEl || !tbody) return;
|
||||||
|
const thead = tbody.closest('table')?.querySelector('thead');
|
||||||
|
if (!thead) return;
|
||||||
|
|
||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
||||||
@@ -409,5 +412,3 @@ function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
|||||||
});
|
});
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user